"""Сценарий (spec) + тайминги озвучки → index.html композиции hyperframes. Визуальный язык — как у ролика «Simbiot Harness for enterprise» (тёмный фон, золотой акцент, Unbounded/Inter/JetBrains Mono, HUD с главами и прогрессом). Типы сцен (spec["scenes"][i]["type"]): hook line1, strike (слово из line1, зачёркивается), line2 points kicker, headline, items[{title, desc}] (2–4) flow kicker, headline, steps[{title, desc}] (3–4) stat kicker, headline, value, caption compare kicker, headline, left_title, left[], right_title, right[] (2–4) statement kicker, text, sub outro line1, line2, cta В headline/text/line2 можно выделить фрагмент золотом: *так*. """ import html import json import re FONTS_CSS = "\n".join( f'@font-face {{ font-family: "{fam}"; font-weight: {w}; src: url("fonts/{file}-{w}-{sub}.woff2") format("woff2"); unicode-range: {rng}; }}' for fam, file, weights in (("Unbounded", "unbounded", (400, 700)), ("Inter", "inter", (400, 600)), ("JetBrains Mono", "jetbrains-mono", (400, 700))) for w in weights for sub, rng in (("latin", "U+0000-00FF, U+2000-206F, U+2190-21FF, U+2200-22FF"), ("cyrillic", "U+0400-04FF")) ) CSS = """ :root { --bg:#070a10; --ink:#f1f3f7; --dim:#a9b1be; --gold:#f5c46b; --mint:#5cffb0; --red:#ff5d73; --line:rgba(255,255,255,0.08); --card:rgba(16,21,31,0.9); } * { margin:0; padding:0; box-sizing:border-box; } html, body { width:1920px; height:1080px; overflow:hidden; background:var(--bg); } body { color:var(--ink); font-family:"Inter", sans-serif; } #root { position:relative; width:100%; height:100%; overflow:hidden; } .clip { position:absolute; inset:0; } .mono { font-family:"JetBrains Mono", monospace; } .gold { color:var(--gold); } #bg { background:radial-gradient(1400px 900px at 50% 45%, #111827 0%, var(--bg) 70%); } #bg-lines { position:absolute; inset:-300px; background-image:repeating-linear-gradient(115deg, rgba(245,196,107,0.045) 0 1px, transparent 1px 64px); mask-image:radial-gradient(ellipse at center, #000 25%, transparent 75%); } #bg-glow { position:absolute; width:1100px; height:1100px; border-radius:50%; left:410px; top:-700px; background:var(--gold); filter:blur(180px); opacity:0.1; } .hud-row { position:absolute; left:64px; right:64px; display:flex; justify-content:space-between; align-items:center; font-size:18px; letter-spacing:0.14em; color:var(--dim); text-transform:uppercase; } #hud-top { top:44px; } #hud-bot { bottom:52px; } #hud-top b { color:var(--gold); font-weight:700; } .chap { display:flex; gap:34px; } .chap span { display:block; color:#858e9c; } #hud-prog { position:absolute; left:64px; right:64px; bottom:36px; height:3px; background:rgba(255,255,255,0.06); } #hud-prog-fill { width:100%; height:3px; background:var(--gold); transform-origin:left center; } .scene { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:130px 150px; } .in { display:flex; flex-direction:column; align-items:center; width:100%; } .kicker { font-size:22px; letter-spacing:0.32em; color:var(--gold); text-transform:uppercase; margin-bottom:24px; text-align:center; } .h2 { font-family:"Unbounded", sans-serif; font-weight:700; font-size:64px; letter-spacing:-0.02em; line-height:1.1; text-align:center; max-width:1560px; } /* hook */ .hook-l1 { font-family:"Unbounded", sans-serif; font-weight:700; font-size:72px; letter-spacing:-0.02em; text-align:center; max-width:1560px; line-height:1.12; } .hook-l1 .strike-w { position:relative; display:inline-block; } .hook-l1 .strike { position:absolute; left:-8px; right:-8px; top:54%; height:10px; border-radius:5px; background:var(--red); transform-origin:left center; } .hook-l2 { margin-top:46px; font-family:"Unbounded", sans-serif; font-weight:700; font-size:72px; letter-spacing:-0.02em; text-align:center; color:var(--gold); max-width:1560px; line-height:1.12; } .brand { margin-top:60px; font-size:26px; letter-spacing:0.3em; color:var(--dim); } .brand b { color:var(--ink); } /* points */ .list { margin-top:56px; width:1320px; display:flex; flex-direction:column; gap:22px; } .chk { display:flex; align-items:center; gap:30px; min-height:108px; padding:18px 36px; border-radius:20px; background:var(--card); border:1px solid var(--line); } .box { position:relative; flex:0 0 56px; width:56px; height:56px; border-radius:14px; border:2px solid var(--gold); } .box svg { position:absolute; inset:6px; width:40px; height:40px; } .chk-t { font-size:33px; font-weight:600; } .chk-t small { display:block; font-size:23px; color:var(--dim); font-weight:400; margin-top:6px; } /* flow */ .flow { margin-top:70px; display:flex; align-items:stretch; gap:0; } .step { width:360px; padding:30px 30px; border-radius:22px; background:var(--card); border:1px solid rgba(245,196,107,0.3); } .step-n { font-size:20px; color:var(--gold); letter-spacing:0.2em; } .step-t { font-family:"Unbounded", sans-serif; font-weight:700; font-size:30px; margin-top:14px; line-height:1.15; } .step-d { font-size:22px; color:var(--dim); margin-top:12px; line-height:1.35; } .arrow { width:70px; display:flex; align-items:center; justify-content:center; } .arrow svg { width:46px; height:46px; display:block; } /* stat */ .stat { display:flex; align-items:center; gap:90px; margin-top:40px; } .stat-v { font-family:"Unbounded", sans-serif; font-weight:700; font-size:220px; line-height:1; color:var(--gold); white-space:nowrap; } .stat-c { width:640px; font-size:36px; line-height:1.35; border-left:4px solid var(--gold); padding-left:30px; } /* compare */ .cmp { margin-top:60px; display:flex; gap:40px; } .col { width:640px; padding:34px 38px; border-radius:24px; background:var(--card); border:1px solid var(--line); } .col.us { border-color:var(--gold); background:rgba(245,196,107,0.07); } .col-t { font-family:"Unbounded", sans-serif; font-weight:700; font-size:32px; margin-bottom:22px; } .col.them .col-t { color:var(--dim); } .col.us .col-t { color:var(--gold); } .row { display:flex; gap:16px; align-items:baseline; font-size:27px; line-height:1.3; margin-top:16px; } .row i { display:block; flex:0 0 14px; width:14px; height:14px; border-radius:4px; } .col.them .row { color:var(--dim); } .col.them .row i { background:#5a6270; } .col.us .row i { background:var(--gold); } /* statement */ .big { font-family:"Unbounded", sans-serif; font-weight:700; font-size:82px; letter-spacing:-0.02em; line-height:1.12; text-align:center; max-width:1600px; } .big-sub { margin-top:40px; font-size:32px; color:var(--dim); text-align:center; max-width:1300px; line-height:1.4; } .rule { width:760px; height:2px; background:var(--line); margin:56px 0 44px; } /* outro */ .o1 { font-family:"Unbounded", sans-serif; font-weight:700; font-size:80px; letter-spacing:-0.02em; text-align:center; max-width:1600px; } .o2 { font-family:"Unbounded", sans-serif; font-weight:700; font-size:80px; letter-spacing:-0.02em; text-align:center; color:var(--gold); margin-top:10px; max-width:1600px; } .logo { font-family:"Unbounded", sans-serif; font-weight:700; font-size:54px; } .logo span { color:var(--gold); } .cta { margin-top:26px; font-size:32px; color:var(--dim); } .cta b { color:var(--ink); } """ TICK_SVG = ('') ARROW_SVG = ('') def esc(s): return html.escape(str(s), quote=False) def rich(s): """*фрагмент* → золотой span; остальное экранируется.""" parts = re.split(r"\*([^*]+)\*", str(s)) return "".join(f'{esc(p)}' if i % 2 else esc(p) for i, p in enumerate(parts)) def plain(s): return str(s).replace("*", "") def _cues(n, sc): """n моментов появления элементов внутри сцены: по началам фраз диктора.""" starts = [t for t in sc["phrase_starts"] if t > sc["vo"] + 0.6] if len(starts) >= n: # берём последние n фраз (первая фраза обычно — заголовок) return starts[-n:] if len(starts) > n + 1 else starts[:n] span_a, span_b = sc["vo"] + 1.2, sc["vo"] + max(2.0, sc["vo_dur"] * 0.8) return [span_a + (span_b - span_a) * i / max(1, n - 1) for i in range(n)] def build(spec, timing, series="DAILY"): """timing: [{start, dur, vo, vo_dur, phrase_starts[]}] по сценам; total — общая длина.""" scenes = spec["scenes"] total = timing[-1]["start"] + timing[-1]["dur"] body, js = [], [] chapters = [] def pop(sel, at, frm='{ opacity: 0, y: 30 }', d=0.7): js.append(f'tl.fromTo("{sel}", {frm}, {{ opacity: 1, x: 0, y: 0, duration: {d}, ease: "power3.out" }}, {at:.2f});') for i, (s, t) in enumerate(zip(scenes, timing)): sid = f"s{i + 1}" a, vo = t["start"], t["vo"] out_at = a + t["dur"] - 0.5 typ = s["type"] kick = f'
{esc(s.get("kicker", ""))}
' if s.get("kicker") else "" head = f'
{rich(s.get("headline", ""))}
' if s.get("headline") else "" if typ not in ("hook", "outro") and s.get("kicker"): chapters.append((plain(s["kicker"]).split("·")[-1].strip()[:14], a, a + t["dur"])) inner = "" if typ == "hook": l1 = esc(s["line1"]) w = s.get("strike") if w and w in s["line1"]: l1 = esc(s["line1"]).replace(esc(w), f'{esc(w)}', 1) inner = (f'
{l1}
{rich(s["line2"])}
' f'
SIMBIOT HARNESS · {esc(series)}
') c = _cues(2, t) pop(f"#{sid}-l1", a + 0.3) if w and w in s["line1"]: js.append(f'tl.fromTo("#{sid}-strike", {{ scaleX: 0 }}, {{ scaleX: 1, duration: 0.45, ease: "power2.inOut" }}, {max(a + 1.2, c[0] - 0.6):.2f});') js.append(f'tl.to("#{sid}-l1", {{ opacity: 0.45, duration: 0.5 }}, {max(a + 1.8, c[0] - 0.1):.2f});') pop(f"#{sid}-l2", max(a + 1.6, c[0])) js.append(f'tl.fromTo("#{sid}-b", {{ opacity: 0 }}, {{ opacity: 1, duration: 0.6 }}, {min(out_at - 1.2, c[-1] + 0.4):.2f});') elif typ == "points": items = s["items"] rows = "".join(f'
{TICK_SVG}
' f'
{esc(it["title"])}{esc(it.get("desc", ""))}
' for j, it in enumerate(items)) inner = f'{kick}{head}
{rows}
' for j, at in enumerate(_cues(len(items), t)): pop(f"#{sid}-i{j}", at, '{ opacity: 0, x: -50 }', 0.5) js.append(f'tl.to("#{sid}-i{j} .tick", {{ strokeDashoffset: 0, duration: 0.35, ease: "power2.out" }}, {at + 0.35:.2f});') js.append(f'tl.to("#{sid}-b{j}", {{ backgroundColor: "rgba(245,196,107,0.15)", duration: 0.3 }}, {at + 0.35:.2f});') elif typ == "flow": parts = [] for j, st in enumerate(s["steps"]): if j: parts.append(f'
{ARROW_SVG}
') parts.append(f'
{j + 1:02d}
' f'
{esc(st["title"])}
{esc(st.get("desc", ""))}
') inner = f'{kick}{head}
{"".join(parts)}
' for j, at in enumerate(_cues(len(s["steps"]), t)): if j: pop(f"#{sid}-a{j}", at - 0.15, '{ opacity: 0, x: -20 }', 0.35) pop(f"#{sid}-i{j}", at, '{ opacity: 0, y: 40 }', 0.55) elif typ == "stat": inner = (f'{kick}{head}
{esc(s["value"])}
' f'
{rich(s.get("caption", ""))}
') c = _cues(2, t) js.append(f'tl.fromTo("#{sid}-v", {{ opacity: 0, scale: 0.6 }}, {{ opacity: 1, scale: 1, duration: 0.8, ease: "back.out(1.6)" }}, {c[0]:.2f});') pop(f"#{sid}-c", c[1], '{ opacity: 0, x: 40 }', 0.6) elif typ == "compare": def col(cls, title, rows, tag): return (f'
{esc(title)}
' + "".join(f'
{esc(r)}
' for r in rows) + '
') inner = (f'{kick}{head}
{col("them", s["left_title"], s["left"], "l")}' f'{col("us", s["right_title"], s["right"], "r")}
') c = _cues(2, t) pop(f"#{sid}-l", c[0], '{ opacity: 0, x: -50 }', 0.6) pop(f"#{sid}-r", c[1], '{ opacity: 0, x: 50 }', 0.6) js.append(f'tl.fromTo("#{sid}-r", {{ boxShadow: "0 0 0px rgba(245,196,107,0)" }}, {{ boxShadow: "0 0 60px rgba(245,196,107,0.35)", duration: 0.6, yoyo: true, repeat: 3, ease: "sine.inOut" }}, {c[1] + 0.6:.2f});') elif typ == "statement": inner = (f'{kick}
{rich(s["text"])}
' f'
{rich(s.get("sub", ""))}
') c = _cues(2, t) pop(f"#{sid}-t", a + 0.3, '{ opacity: 0, y: 40 }', 0.8) js.append(f'tl.fromTo("#{sid}-r", {{ scaleX: 0 }}, {{ scaleX: 1, duration: 0.8, ease: "power3.inOut" }}, {c[1] - 0.3:.2f});') pop(f"#{sid}-s", c[1]) elif typ == "outro": inner = (f'
{esc(s["line1"])}
{rich(s["line2"])}
' f'
' f'
{rich(s.get("cta", "Let’s talk about a pilot · *cil.by*")).replace("", "").replace("", "")}
') c = _cues(3, t) pop(f"#{sid}-o1", a + 0.3) pop(f"#{sid}-o2", c[0]) js.append(f'tl.fromTo("#{sid}-r", {{ scaleX: 0 }}, {{ scaleX: 1, duration: 0.8, ease: "power3.inOut" }}, {c[1] - 0.4:.2f});') js.append(f'tl.fromTo("#{sid}-logo", {{ opacity: 0, scale: 0.94 }}, {{ opacity: 1, scale: 1, duration: 0.7, ease: "power3.out" }}, {c[1]:.2f});') js.append(f'tl.fromTo("#{sid}-cta", {{ opacity: 0 }}, {{ opacity: 1, duration: 0.7 }}, {c[2]:.2f});') else: raise ValueError(f"unknown scene type {typ}") if typ not in ("hook", "outro", "statement"): if s.get("kicker"): pop(f"#{sid}-k", a + 0.1, '{ opacity: 0, y: 16 }', 0.5) if s.get("headline"): pop(f"#{sid}-h", a + 0.2) elif typ == "statement" and s.get("kicker"): pop(f"#{sid}-k", a + 0.1, '{ opacity: 0, y: 16 }', 0.5) if i < len(scenes) - 1: js.append(f'tl.to("#{sid}-in", {{ opacity: 0, duration: 0.45, ease: "power2.in" }}, {out_at:.2f});') body.append(f'
' f'
{inner}
') audio = [f''] for i, t in enumerate(timing): audio.append(f'') chap_html = "".join(f'{esc(c[0])}' for j, c in enumerate(chapters)) hud_from = timing[1]["start"] if len(timing) > 1 else 0 hud_js = [ f'tl.fromTo("#hud-top, #hud-bot, #hud-prog", {{ opacity: 0 }}, {{ opacity: 1, duration: 0.8 }}, {hud_from:.2f});', f'tl.fromTo("#hud-prog-fill", {{ scaleX: 0 }}, {{ scaleX: 1, duration: {total - hud_from:.2f}, ease: "none" }}, {hud_from:.2f});', ] for j, (_, ca, cb) in enumerate(chapters): hud_js.append(f'tl.to("#c{j}", {{ color: "#f5c46b", duration: 0.3 }}, {ca:.2f});') hud_js.append(f'tl.to("#c{j}", {{ color: "#858e9c", duration: 0.3 }}, {cb:.2f});') return f""" {esc(plain(spec.get("title", "Simbiot Harness")))}
{"".join(body)} {"".join(audio)}
SIMBIOT · {esc(series)}
{esc(spec.get("date_label", ""))}
{chap_html}
cil.by
""" if __name__ == "__main__": import sys spec = json.load(open(sys.argv[1])) timing = json.load(open(sys.argv[2])) print(build(spec, timing))