feat(seed): per-scene loop video + thumbnail for every scene

Each scene now carries both a still (content.scenes.image) AND a short ~1.5s LOOP
video (content.scenes.demo), so the studio scene cards show a looping preview, not
a static swatch.

- LOOP_SCENES = {CharacterStory, LogoMotion3D}: their scenes get a dedicated
  per-scene loop ({tid}-{asp}-c{n}-loop.mp4 / {tid}-{asp}-loop.mp4); other
  templates fall back to their full preview MP4.
- Renders 42 loops: LogoMotion3D ×3 (frames 30–74) + CharacterStory 13 scenes ×3
  aspects (frames (n-1)*90+20 … +64), each a 45-frame / 1.5s clip mid-scene.
- Seed sets image + snapshot_url + demo per scene; verified all 42 serve 200 and
  the DB wires each scene to its own still + loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-24 21:09:04 +03:30
parent 825f25be55
commit e4fd936953
43 changed files with 11 additions and 5 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -5
View File
@@ -101,6 +101,10 @@ TEXTCOLORS = {
# the matching render, not the 16:9 cropped). Others reuse the single 16:9 preview.
PERASPECT_VIDEO = {"AppShowcase3D", "CharacterStory", "LogoMotion3D"}
# Templates that ship a short ~1.5s LOOP video PER SCENE (scene.demo), so the studio
# scene cards play a looping preview. Others fall back to the full preview MP4.
LOOP_SCENES = {"CharacterStory", "LogoMotion3D"}
# Templates whose content is split across MANY scenes (key c1..cN), one editable
# scene card per beat. value = scene count; texts are assigned 2-per-scene in order.
MULTISCENE = {"CharacterStory": len(CS_BEATS)}
@@ -143,18 +147,20 @@ for idx, (tid, slug, name, desc, dur, texts, (accent, sec, bg)) in enumerate(T):
# one editable scene card per beat; 2 text fields (title+caption) each.
for sc in range(1, nscenes + 1):
skid = uid(f"s-{tid}-{asp}-{sc}")
scimg = f"{MINIO}/template-media/{tid}-{asp}-c{sc}.png" # per-scene rendered still
scimg = f"{MINIO}/template-media/{tid}-{asp}-c{sc}.png" # per-scene still
scdemo = f"{MINIO}/template-media/{tid}-{asp}-c{sc}-loop.mp4" if tid in LOOP_SCENES else pvideo
out.append(
"INSERT INTO content.scenes (id,project_id,key,title,image,snapshot_url,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(skid)},{q(pid)},{q('c'+str(sc))},{q('صحنه '+str(sc))},{q(scimg)},{q(scimg)},{q(swatch_svg([accent,sec,bg,txt]))},{SCENE_SECONDS},{sc-1});")
"INSERT INTO content.scenes (id,project_id,key,title,image,snapshot_url,demo,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(skid)},{q(pid)},{q('c'+str(sc))},{q('صحنه '+str(sc))},{q(scimg)},{q(scimg)},{q(scdemo)},{q(swatch_svg([accent,sec,bg,txt]))},{SCENE_SECONDS},{sc-1});")
for pos, (k, title, val) in enumerate(texts[(sc - 1) * 2: sc * 2]):
out.append(
"INSERT INTO content.scene_content_elements (id,scene_id,key,title,type,default_value,position_in_container,direction_layer_value) VALUES ("
f"{q(uid(f'ce-{tid}-{asp}-{k}'))},{q(skid)},{q(k)},{q(title)},'Text',{q(val)},{pos},1);")
else:
scdemo = f"{MINIO}/template-media/{tid}-{asp}-loop.mp4" if tid in LOOP_SCENES else pvideo
out.append(
"INSERT INTO content.scenes (id,project_id,key,title,image,snapshot_url,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(sid)},{q(pid)},'c1','صحنه ۱',{q(thumb)},{q(thumb)},{q(swatch_svg([accent,sec,bg,txt]))},{dur},0);")
"INSERT INTO content.scenes (id,project_id,key,title,image,snapshot_url,demo,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(sid)},{q(pid)},'c1','صحنه ۱',{q(thumb)},{q(thumb)},{q(scdemo)},{q(swatch_svg([accent,sec,bg,txt]))},{dur},0);")
for pos, (k, title, val) in enumerate(texts):
out.append(
"INSERT INTO content.scene_content_elements (id,scene_id,key,title,type,default_value,position_in_container,direction_layer_value) VALUES ("