"""Сценарий (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'