feat(remotion): premium CharacterStory template (13 flexible scenes) + fix detail-page SSR

- CharacterStory: refined flat-illustration character (gradient-shaded sweater,
  modern hair, calm minimal face), muted editorial palette (coral/teal/sand/navy),
  abstract environment (soft depth blobs, ground "stage", sparse particles,
  vignette + grain), scene-number kicker. Verified in 16:9/1:1/9:16 and all poses.
- seed: 13 editable scene cards (c1..c13, keys s{N}_title/s{N}_text) via new
  MULTISCENE path; per-aspect previews; muted defaults.
- assets: 3 thumbnails + 4 preview MP4s vendored into public/template-media.
- fix: load BrandedVideoPlayer (plyr-react) client-only via next/dynamic
  (ssr:false) — plyr touches `document` at import, which was 500-ing every
  template detail page during SSR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-22 16:58:48 +03:30
parent 863b9503b3
commit a3152ee84f
11 changed files with 319 additions and 12 deletions
+54 -11
View File
@@ -16,6 +16,29 @@ 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 = [
@@ -53,6 +76,8 @@ T = [
[("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")),
]
# Optional Media (image) content elements per template — these surface in the
@@ -65,11 +90,16 @@ MEDIA = {
# Per-template text colour (default white for dark backgrounds; dark for light studios).
TEXTCOLORS = {
"AppShowcase3D": "#0f172a",
"CharacterStory": "#2b3a55",
}
# 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"}
PERASPECT_VIDEO = {"AppShowcase3D", "CharacterStory"}
# 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))
@@ -104,17 +134,30 @@ for idx, (tid, slug, name, desc, dur, texts, (accent, sec, bg)) in enumerate(T):
"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);")
out.append(
"INSERT INTO content.scenes (id,project_id,key,title,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(sid)},{q(pid)},'c1','صحنه ۱',{q(swatch_svg([accent,sec,bg,txt]))},{dur},0);")
for pos, (k, title, val) in enumerate(texts):
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}")
out.append(
"INSERT INTO content.scenes (id,project_id,key,title,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(skid)},{q(pid)},{q('c'+str(sc))},{q('صحنه '+str(sc))},{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.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);")
"INSERT INTO content.scenes (id,project_id,key,title,scene_color_svg,default_duration_sec,sort) VALUES ("
f"{q(sid)},{q(pid)},'c1','صحنه ۱',{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 ("