Files
flatrender/scripts/seed_remotion_templates.py
T
soroush.asadi 4bac5154ed fix(seed): real per-scene images for every scene (were only colour swatches)
Scenes were seeded with just a scene_color_svg swatch — content.scenes.image /
snapshot_url were empty, so the studio/admin scene previews showed swatches, not
the actual scene. Now every scene gets a real rendered image:
- single-scene templates → their per-aspect thumbnail;
- multi-scene templates → one still per scene, captured at that scene's own frame.

Adds the 39 CharacterStory per-scene stills (13 scenes × 3 aspects), each rendered
at (sceneIndex*90 + 45). LogoMotion3D's single scene now points at its thumbnail.
Verified: DB image/snapshot_url populated, all per-scene images serve 200, and the
stills are distinct per scene (c7 = «یک مانع», kicker ۰۷/۱۳).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:07:48 +03:30

174 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Generates SQL to seed the 10 branded Remotion templates into content.* so they
# appear on the site. Each template -> 1 container + 3 aspect projects, each with
# a scene, Persian text content-elements (bindable -> Remotion props) and shared
# colours (bindable -> colour props) + a per-scene colour-swatch SVG.
#
# Usage: python scripts/seed_remotion_templates.py | docker exec -i fr2-postgres psql -U postgres -d flatrender
import uuid
# Assets are served from the Next app's public/ folder (relative URLs), so this
# works regardless of MinIO/object-store availability.
MINIO = ""
NS = uuid.UUID("11111111-2222-3333-4444-555555555555")
def uid(s): return str(uuid.uuid5(NS, s))
def q(s): return "'" + str(s).replace("'", "''") + "'"
ASPECTS = [("16x9", 1920, 1080, "16:9"), ("1x1", 1080, 1080, "1:1"), ("9x16", 1080, 1920, "9:16")]
CTITLES = {"accentColor": "رنگ اصلی", "secondaryColor": "رنگ دوم", "backgroundColor": "پس‌زمینه", "textColor": "رنگ متن"}
SCENE_SECONDS = 3 # CharacterStory: per-scene duration
# CharacterStory: 13 explainer beats → 26 content fields (title + caption per scene).
# Keys s{N}_title / s{N}_text match the globally-unique Remotion props.
CS_BEATS = [
("داستان شما", "روایت خود را در سیزده صحنه بسازید"),
("معرفی", "شخصیت یا برند خود را معرفی کنید"),
("شروع ماجرا", "همه‌چیز از یک ایده آغاز شد"),
("یک چالش", "اما یک مشکل سر راه پیدا شد"),
("جست‌وجو", "به دنبال یک راه‌حل گشتیم"),
("قدم اول", "اولین قدم را برداشتیم"),
("یک مانع", "همه‌چیز آسان نبود"),
("نقطهٔ عطف", "و سپس همه‌چیز تغییر کرد"),
("ایده", "راه‌حل را پیدا کردیم"),
("اقدام", "دست به کار شدیم"),
("اوج داستان", "بزرگ‌ترین لحظه فرا رسید"),
("نتیجه", "و به هدف رسیدیم"),
("پایان", "همین حالا داستان خود را بسازید"),
]
CS_TEXTS = []
for _i, (_t, _c) in enumerate(CS_BEATS, 1):
CS_TEXTS.append((f"s{_i}_title", f"صحنهٔ {_i} — عنوان", _t))
CS_TEXTS.append((f"s{_i}_text", f"صحنهٔ {_i} — متن", _c))
# id, slug, name(fa), desc(fa), dur, [(textKey,title,value)], (accent,secondary,bg)
T = [
("LogoMotion","fr-logo-motion","موشن لوگو","نمایش حرفه‌ای لوگو و نام برند با درخشش و حرکت",5,
[("brandText","متن لوگو","فلت‌رندر"),("tagline","شعار","موشن، ساده و حرفه‌ای")],("#3ba7ff","#a855f7","#04060f")),
("Opener","fr-opener","تیتراژ آغازین","شروع سینمایی برای ویدیو با عنوان و زیرعنوان",5,
[("kicker","پیش‌متن","تقدیم می‌کند"),("title","عنوان","یک شروع تازه"),("subtitle","زیرعنوان","داستان شما از همین‌جا آغاز می‌شود")],("#22d3ee","#6366f1","#0a0a12")),
("InstaPromo","fr-insta-promo","تبلیغ پیج اینستاگرام","معرفی و تبلیغ صفحهٔ اینستاگرام با دعوت به فالو",5,
[("handle","آیدی پیج","@flatrender"),("headline","عنوان","پیج ما را دنبال کنید"),("subtext","توضیح","هر روز محتوای تازه و الهام‌بخش"),("cta","دکمه","فالو کنید")],("#fb7185","#f59e0b","#140a12")),
("YouTubeIntro","fr-youtube-intro","اینترو کانال یوتیوب","اینترو حرفه‌ای کانال یوتیوب با دکمهٔ سابسکرایب",5,
[("channelName","نام کانال","کانال فلت‌رندر"),("subtitle","زیرعنوان","آموزش، ترفند و انگیزه"),("cta","دکمه","سابسکرایب کنید")],("#ff4d4d","#a855f7","#0c0810")),
("Slideshow","fr-slideshow","اسلایدشو","نمایش پشت‌سرهم چند پیام یا ویژگی به‌صورت اسلاید",9,
[("title","عنوان","چرا فلت‌رندر؟"),("slide1","اسلاید ۱","ساخت ویدیو در چند دقیقه"),("slide2","اسلاید ۲","بدون نیاز به دانش فنی"),("slide3","اسلاید ۳","خروجی با کیفیت حرفه‌ای")],("#34d399","#3b82f6","#060b0a")),
("HappyBirthday","fr-happy-birthday","تولدت مبارک","کارت تبریک تولد با کاغذرنگی و نام شخص",6,
[("greeting","تبریک","تولدت مبارک"),("name","نام","سارا"),("message","پیام","بهترین‌ها را برایت آرزومندیم 🎉")],("#fb7185","#fde047","#140a18")),
("SalePromo","fr-sale-promo","فروش ویژه","بنر تبلیغاتی فروش و تخفیف با دعوت به خرید",5,
[("badge","نشان تخفیف","۵۰٪ تخفیف"),("headline","عنوان","فروش ویژهٔ پایان فصل"),("subtext","توضیح","فقط تا پایان همین هفته"),("cta","دکمه","همین حالا خرید کنید")],("#f59e0b","#fb7185","#120a08")),
("QuoteCard","fr-quote-card","کارت نقل‌قول","نمایش جملهٔ انگیزشی یا نقل‌قول با نام گوینده",6,
[("quote","نقل‌قول","موفقیت، مجموع تلاش‌های کوچکِ هر روز است."),("author","گوینده","فلت‌رندر")],("#22d3ee","#6366f1","#0a0a12")),
("EventInvite","fr-event-invite","دعوت‌نامهٔ رویداد","دعوت‌نامهٔ شیک برای رویداد با تاریخ و مکان",6,
[("kicker","پیش‌متن","دعوت‌نامه"),("eventTitle","عنوان رویداد","همایش سالانهٔ نوآوری"),("date","تاریخ","۱۵ مهر ۱۴۰۳"),("location","مکان","تهران، سالن همایش‌ها"),("cta","دکمه","ثبت‌نام کنید")],("#a855f7","#3ba7ff","#0a0814")),
("Countdown","fr-countdown","شمارش معکوس","شمارش معکوس هیجان‌انگیز برای شروع یک رویداد",8,
[("title","عنوان","شروع رویداد تا"),("startNumber","عدد شروع","5"),("goText","متن پایان","شروع!"),("subtitle","زیرعنوان","آماده‌اید؟")],("#3ba7ff","#22d3ee","#04060f")),
("GlitterReveal","fr-glitter-reveal","نمایش لوگو با غبار درخشان","نمایش جادویی لوگو با ذرات درخشان؛ لوگو و متن قابل ویرایش",6,
[("brandText","نام برند","فلت‌رندر"),("tagline","شعار","موشن، ساده و حرفه‌ای")],("#3ba7ff","#a855f7","#05040e")),
("NowruzGreeting","fr-nowruz","تبریک نوروز","صحنهٔ بهاری نوروز با شخصیت‌های متحرک؛ حاجی‌فیروز، ماهی قرمز و سبزه",8,
[("greeting","متن تبریک","نوروز مبارک"),("subtitle","زیرعنوان","سال نو پیروز و شادمان"),("message","پیام / سال","۱۴۰۶")],("#f5b942","#e23b3b","#1fb6b0")),
("Hero3D","fr-hero-3d","نمایش سه‌بعدی برند","نمایش حرفه‌ای و سه‌بعدی لوگو و برند با نورپردازی و جلوه‌های واقعی",6,
[("brandText","نام برند","فلت‌رندر"),("tagline","شعار","موشن، ساده و حرفه‌ای")],("#3ba7ff","#a855f7","#04060f")),
("Nowruz3D","fr-nowruz-3d","تبریک نوروز سه‌بعدی","صحنهٔ سه‌بعدی نوروز با حاجی‌فیروز، سفرهٔ هفت‌سین و نورپردازی سینمایی",7,
[("greeting","متن تبریک","نوروز مبارک"),("subtitle","زیرعنوان","سال نو پیروز و شادمان"),("message","پیام / سال","۱۴۰۶")],("#f5c542","#e23b3b","#1a1228")),
("Birthday3D","fr-birthday-3d","تولد سه‌بعدی","صحنهٔ سه‌بعدی تولد با کیک و شمع‌های روشن، بادکنک و کاغذرنگی",6,
[("greeting","تبریک","تولدت مبارک"),("name","نام","سارا"),("message","پیام","بهترین‌ها را برایت آرزومندیم 🎉")],("#fb7185","#a855f7","#1a1226")),
("Promo3D","fr-promo-3d","فروش ویژه سه‌بعدی","تبلیغ سه‌بعدی فروش و تخفیف با جعبه‌های هدیه و نورپردازی سینمایی",6,
[("badge","نشان تخفیف","۵۰٪ تخفیف"),("headline","عنوان","فروش ویژهٔ پایان فصل"),("subtext","توضیح","فقط تا پایان همین هفته"),("cta","دکمه","همین حالا خرید کنید")],("#f59e0b","#fb7185","#140e1f")),
("AppShowcase3D","fr-app-showcase","معرفی اپلیکیشن سه‌بعدی","نمایش سه‌بعدی و حرفه‌ای اپلیکیشن روی گوشی پرچم‌دار با نورپردازی استودیویی",6,
[("appName","نام اپلیکیشن","اپلیکیشن شما"),("tagline","شعار","تجربه‌ای روان، سریع و زیبا"),("cta","دکمه","همین حالا دانلود کنید")],("#3b82f6","#8b5cf6","#f4f5f7")),
("CharacterStory","fr-character-story","داستان شخصیتی (۱۳ صحنه)","روایت داستان شما در سیزده صحنهٔ متحرک با شخصیت؛ تصویرسازی مدرن و مینیمال، صحنه‌ها کاملاً قابل ویرایش و انعطاف‌پذیر",39,
CS_TEXTS,("#cf8a76","#6f9d96","#ece4d6")),
("LogoMotion3D","fr-logo-motion-3d","موشن لوگو سه‌بعدی","نمایش سینمایی و سه‌بعدی لوگو با کارت فلزی، پرتوهای نور، درخشش و افکت لنز؛ لوگو و متن قابل ویرایش",5,
[("brandText","نام برند","برند شما"),("tagline","شعار","شعار شما اینجا")],("#38bdf8","#818cf8","#0a0e1a")),
]
# Optional Media (image) content elements per template — these surface in the
# studio as upload/replace fields. key = the Remotion prop the image binds to.
MEDIA = {
"GlitterReveal": [("logoUrl", "لوگو (تصویر دلخواه)")],
"AppShowcase3D": [("screenUrl", "تصویر اپلیکیشن (اسکرین‌شات)")],
"LogoMotion3D": [("logoUrl", "لوگو (تصویر)")],
}
# Per-template text colour (default white for dark backgrounds; dark for light studios).
TEXTCOLORS = {
"AppShowcase3D": "#0f172a",
"CharacterStory": "#2b3a55",
"LogoMotion3D": "#f8fafc",
}
# Templates that ship a distinct preview video PER aspect (so the detail page shows
# the matching render, not the 16:9 cropped). Others reuse the single 16:9 preview.
PERASPECT_VIDEO = {"AppShowcase3D", "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)}
def swatch_svg(colors):
rects = "".join(f'<rect x="{i*50}" y="0" width="50" height="40" fill="{c}"/>' for i, c in enumerate(colors))
return f'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="40">{rects}</svg>'
def icon_svg(hex):
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="{hex}"/></svg>'
out = []
out.append("BEGIN;")
slugs = ",".join(q(t[1]) for t in T)
out.append(f"DELETE FROM content.project_containers WHERE slug IN ({slugs});")
for idx, (tid, slug, name, desc, dur, texts, (accent, sec, bg)) in enumerate(T):
cid = uid("c-" + tid)
thumb16 = f"{MINIO}/template-media/{tid}-16x9.png"
preview = f"{MINIO}/template-media/{tid}.mp4"
txt = TEXTCOLORS.get(tid, "#ffffff")
colors = [("accentColor", accent), ("secondaryColor", sec), ("backgroundColor", bg), ("textColor", txt)]
out.append(
"INSERT INTO content.project_containers (id,tenant_id,slug,name,description,image,demo,full_demo,mini_demo,"
"is_published,is_premium,is_mockup,primary_mode,sort) VALUES ("
f"{q(cid)},NULL,{q(slug)},{q(name)},{q(desc)},{q(thumb16)},{q(preview)},{q(preview)},{q(preview)},"
f"TRUE,FALSE,FALSE,'FLEXIBLE',{idx});")
for (asp, w, h, aspstr) in ASPECTS:
pid = uid(f"p-{tid}-{asp}")
sid = uid(f"s-{tid}-{asp}")
thumb = f"{MINIO}/template-media/{tid}-{asp}.png"
pvideo = f"{MINIO}/template-media/{tid}-{asp}.mp4" if tid in PERASPECT_VIDEO else preview
out.append(
"INSERT INTO content.projects (id,container_id,name,image,full_demo,original_width,original_height,aspect,"
"project_duration_sec,free_fps,choose_mode,resolution,render_engine,render_remotion_comp,is_published,sort) VALUES ("
f"{q(pid)},{q(cid)},{q(aspstr)},{q(thumb)},{q(pvideo)},{w},{h},{q(aspstr)},"
f"{dur},30,'FLEXIBLE','FullHD','Remotion',{q(tid+'-'+asp)},TRUE,0);")
nscenes = MULTISCENE.get(tid, 1)
if nscenes > 1:
# 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
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});")
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:
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);")
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 ("
f"{q(uid(f'ce-{tid}-{asp}-{k}'))},{q(sid)},{q(k)},{q(title)},'Text',{q(val)},{pos},1);")
for mpos, (k, title) in enumerate(MEDIA.get(tid, [])):
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(sid)},{q(k)},{q(title)},'Media','',{len(texts)+mpos},0);")
for si, (k, hexv) in enumerate(colors):
out.append(
"INSERT INTO content.shared_colors (id,project_id,element_key,title,icon,attr_value,default_color,sort) VALUES ("
f"{q(uid(f'sc-{tid}-{asp}-{k}'))},{q(pid)},{q(k)},{q(CTITLES[k])},{q(icon_svg(hexv))},'fill',{q(hexv)},{si});")
out.append("COMMIT;")
out.append("SELECT count(*) AS containers FROM content.project_containers WHERE slug LIKE 'fr-%';")
print("\n".join(out))