video/daily/daily.py — оркестратор (cron 06:25 UTC, публикация 07:00 UTC = 10:00 Минск): факты только из README/SOUL/коммитов за 4 дня (числа сверяются с источниками), темы не повторяются (state.json в SIMBIOT_HOME/daily-video), hyperframes check с ошибками/вылезающим текстом — без публикации, сбой — уведомление владельцу. Площадки: X/Telegram/LinkedIn (coop/social/publish.py) + FB/IG/YouTube (Buffer). Модели: 9router ocz/claude-sonnet-5 → OpenRouter claude-sonnet-5.5/opus-5.5. video/daily/template.py — 7 типов сцен в стиле enterprise-ролика. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
291 lines
18 KiB
Python
291 lines
18 KiB
Python
"""Сценарий (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 = ('<svg viewBox="0 0 40 40"><path class="tick" d="M8 21 L17 30 L33 11" stroke="#f5c46b" stroke-width="5" '
|
||
'fill="none" stroke-linecap="round" stroke-linejoin="round" /></svg>')
|
||
ARROW_SVG = ('<svg viewBox="0 0 46 46"><path d="M6 23 H38 M26 11 L38 23 L26 35" stroke="#f5c46b" stroke-width="4" '
|
||
'fill="none" stroke-linecap="round" stroke-linejoin="round" /></svg>')
|
||
|
||
|
||
def esc(s):
|
||
return html.escape(str(s), quote=False)
|
||
|
||
|
||
def rich(s):
|
||
"""*фрагмент* → золотой span; остальное экранируется."""
|
||
parts = re.split(r"\*([^*]+)\*", str(s))
|
||
return "".join(f'<span class="gold">{esc(p)}</span>' 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'<div class="kicker mono" id="{sid}-k">{esc(s.get("kicker", ""))}</div>' if s.get("kicker") else ""
|
||
head = f'<div class="h2" id="{sid}-h">{rich(s.get("headline", ""))}</div>' 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'<span class="strike-w">{esc(w)}<span class="strike" id="{sid}-strike"></span></span>', 1)
|
||
inner = (f'<div class="hook-l1" id="{sid}-l1">{l1}</div><div class="hook-l2" id="{sid}-l2">{rich(s["line2"])}</div>'
|
||
f'<div class="brand mono" id="{sid}-b"><b>SIMBIOT HARNESS</b> · {esc(series)}</div>')
|
||
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'<div class="chk" id="{sid}-i{j}"><div class="box" id="{sid}-b{j}">{TICK_SVG}</div>'
|
||
f'<div class="chk-t">{esc(it["title"])}<small>{esc(it.get("desc", ""))}</small></div></div>'
|
||
for j, it in enumerate(items))
|
||
inner = f'{kick}{head}<div class="list">{rows}</div>'
|
||
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'<div class="arrow" id="{sid}-a{j}">{ARROW_SVG}</div>')
|
||
parts.append(f'<div class="step" id="{sid}-i{j}"><div class="step-n mono">{j + 1:02d}</div>'
|
||
f'<div class="step-t">{esc(st["title"])}</div><div class="step-d">{esc(st.get("desc", ""))}</div></div>')
|
||
inner = f'{kick}{head}<div class="flow">{"".join(parts)}</div>'
|
||
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}<div class="stat"><div class="stat-v" id="{sid}-v">{esc(s["value"])}</div>'
|
||
f'<div class="stat-c" id="{sid}-c">{rich(s.get("caption", ""))}</div></div>')
|
||
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'<div class="col {cls}" id="{sid}-{tag}"><div class="col-t">{esc(title)}</div>'
|
||
+ "".join(f'<div class="row"><i></i><span>{esc(r)}</span></div>' for r in rows) + '</div>')
|
||
inner = (f'{kick}{head}<div class="cmp">{col("them", s["left_title"], s["left"], "l")}'
|
||
f'{col("us", s["right_title"], s["right"], "r")}</div>')
|
||
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}<div class="big" id="{sid}-t">{rich(s["text"])}</div><div class="rule" id="{sid}-r"></div>'
|
||
f'<div class="big-sub" id="{sid}-s">{rich(s.get("sub", ""))}</div>')
|
||
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'<div class="o1" id="{sid}-o1">{esc(s["line1"])}</div><div class="o2" id="{sid}-o2">{rich(s["line2"])}</div>'
|
||
f'<div class="rule" id="{sid}-r"></div><div class="logo" id="{sid}-logo">SIMBIOT <span>HARNESS</span></div>'
|
||
f'<div class="cta" id="{sid}-cta">{rich(s.get("cta", "Let’s talk about a pilot · *cil.by*")).replace("<span class=\"gold\">", "<b>").replace("</span>", "</b>")}</div>')
|
||
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'<section id="{sid}" class="clip scene" data-start="{a:.2f}" data-duration="{t["dur"]:.2f}" data-track-index="1">'
|
||
f'<div class="in" id="{sid}-in">{inner}</div></section>')
|
||
|
||
audio = [f'<audio id="bgm" src="assets/bgm/bed.wav" data-start="0" data-duration="{total:.2f}" data-track-index="3" data-volume="0.16"></audio>']
|
||
for i, t in enumerate(timing):
|
||
audio.append(f'<audio id="vo{i + 1}" src="assets/vo/v{i + 1}.wav" data-start="{t["vo"]:.2f}" data-duration="{t["vo_dur"]:.2f}" data-track-index="4" data-volume="1"></audio>')
|
||
|
||
chap_html = "".join(f'<span id="c{j}">{esc(c[0])}</span>' 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"""<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1920, height=1080" />
|
||
<title>{esc(plain(spec.get("title", "Simbiot Harness")))}</title>
|
||
<script src="./gsap.min.js"></script>
|
||
<style>
|
||
{FONTS_CSS}
|
||
{CSS}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root" data-composition-id="main" data-start="0" data-duration="{total:.2f}" data-width="1920" data-height="1080">
|
||
<div id="bg" class="clip" data-start="0" data-duration="{total:.2f}" data-track-index="0"><div id="bg-lines"></div><div id="bg-glow"></div></div>
|
||
{"".join(body)}
|
||
{"".join(audio)}
|
||
<div id="hud" class="clip" data-start="0" data-duration="{total:.2f}" data-track-index="2">
|
||
<div class="hud-row mono" id="hud-top"><div><b>SIMBIOT</b> · {esc(series)}</div><div>{esc(spec.get("date_label", ""))}</div></div>
|
||
<div class="hud-row mono" id="hud-bot"><div class="chap">{chap_html}</div><div>cil.by</div></div>
|
||
<div id="hud-prog"><div id="hud-prog-fill"></div></div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const tl = gsap.timeline({{ paused: true }});
|
||
tl.fromTo("#bg-lines", {{ x: 0 }}, {{ x: 256, duration: {total:.2f}, ease: "none" }}, 0);
|
||
tl.fromTo("#bg-glow", {{ x: 0, y: 0 }}, {{ x: -380, y: 180, duration: {total:.2f}, ease: "sine.inOut" }}, 0);
|
||
document.querySelectorAll(".tick").forEach((p) => {{ const L = p.getTotalLength(); p.style.strokeDasharray = L; p.style.strokeDashoffset = L; }});
|
||
{chr(10).join(" " + l for l in hud_js + js).strip()}
|
||
window.__timelines = window.__timelines || {{}};
|
||
window.__timelines["main"] = tl;
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
spec = json.load(open(sys.argv[1]))
|
||
timing = json.load(open(sys.argv[2]))
|
||
print(build(spec, timing))
|