- make.py: 8 типов слайдов (cover/quote/phototop/photoside/stat/list/formula/final), палитры carbon и bone в стиле Shkor Labs; - studio-orchestration, production-lessons — настоящие фото (ч/б); principle-zero, govern-agents — фото, сгенерированные Kling (Nano Banana Pro) по референсам; - тезисы — из 42_Harness_Deck, Shkor_Labs_Deck, Simbiot Whitepaper, COOP_CONTEXT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
294 lines
19 KiB
Python
294 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""Карусель из spec.json → hyperframes-композиция 1080×1350 → PNG-слайды (+ анимированный mp4).
|
||
|
||
python3 video/carousels/make.py video/carousels/<slug> # собрать + снапшоты
|
||
python3 video/carousels/make.py video/carousels/<slug> --render # + mp4
|
||
|
||
spec.json: {"palette": "carbon"|"bone", "handle": "@alexshkor", "slides": [...]}
|
||
Типы слайдов (у каждого опционально "kicker"):
|
||
cover photo, title, byline
|
||
quote text, sub, visual: "chart-down"|"network"|"ring"|null, chips[] (для ring)
|
||
phototop photo, text, sub
|
||
photoside photo, text, sub, dots: true|false
|
||
stat value, label, sub
|
||
list title, items[] (3–4)
|
||
formula parts[] (2–4 слова формулы), sub
|
||
final photo, text, cta
|
||
В text/title/sub фрагмент *так* выделяется акцентом. photo — путь относительно папки карусели.
|
||
"""
|
||
import html
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
REPO = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||
FONTS = os.path.join(REPO, "video", "daily", "fonts")
|
||
ASSETS = os.path.join(REPO, "video", "simbiot-enterprise-en")
|
||
BRAND = os.path.join(REPO, "brand", "shkor-labs")
|
||
HF = ["npx", "--yes", "hyperframes@0.8.168"]
|
||
SLIDE = 3.5
|
||
|
||
PALETTES = {
|
||
"carbon": dict(bg="#050506", bg2="#111113", ink="#ece6da", dim="#a8a297", acc="#c6ff4d", accink="#050506",
|
||
line="rgba(236,230,218,0.14)", em="color:var(--acc);", fade="5,5,6", logo="shkor-labs-logo-dark-bg.svg"),
|
||
"bone": dict(bg="#ece6da", bg2="#e2dbcd", ink="#0b0b0c", dim="#55504a", acc="#0b0b0c", accink="#c6ff4d",
|
||
line="rgba(11,11,12,0.14)", em="background:#c6ff4d; color:#0b0b0c; padding:0 0.12em; box-decoration-break:clone; -webkit-box-decoration-break:clone;",
|
||
fade="236,230,218", logo="shkor-labs-logo-light.svg"),
|
||
}
|
||
|
||
|
||
def esc(s):
|
||
return html.escape(str(s), quote=False)
|
||
|
||
|
||
def rich(s):
|
||
parts = re.split(r"\*([^*]+)\*", str(s))
|
||
out = "".join(f"<em>{esc(p)}</em>" if i % 2 else esc(p) for i, p in enumerate(parts))
|
||
for tag in ("b", "/b", "span", "/span"): # разрешённая разметка в kicker/byline
|
||
out = out.replace(f"<{tag}>", f"<{tag}>")
|
||
return out
|
||
|
||
|
||
CSS = """
|
||
@font-face { font-family: "Fraunces"; font-weight: 400; src: url("fonts/fraunces-400-latin.woff2") format("woff2"); }
|
||
@font-face { font-family: "Fraunces"; font-weight: 700; src: url("fonts/fraunces-700-latin.woff2") format("woff2"); }
|
||
@font-face { font-family: "DM Sans"; font-weight: 400; src: url("fonts/dm-sans-400-latin.woff2") format("woff2"); }
|
||
@font-face { font-family: "DM Sans"; font-weight: 700; src: url("fonts/dm-sans-700-latin.woff2") format("woff2"); }
|
||
@font-face { font-family: "JetBrains Mono"; font-weight: 400; src: url("fonts/jetbrains-mono-400-latin.woff2") format("woff2"); }
|
||
@font-face { font-family: "JetBrains Mono"; font-weight: 700; src: url("fonts/jetbrains-mono-700-latin.woff2") format("woff2"); }
|
||
* { margin:0; padding:0; box-sizing:border-box; }
|
||
html, body { width:1080px; height:1350px; overflow:hidden; background:var(--bg); }
|
||
body { color:var(--ink); font-family:"DM Sans", sans-serif; }
|
||
#root { position:relative; width:100%; height:100%; overflow:hidden; }
|
||
.clip { position:absolute; inset:0; }
|
||
.mono { font-family:"JetBrains Mono", monospace; }
|
||
em { font-style:normal; }
|
||
#bg { background:radial-gradient(900px 1100px at 70% 20%, var(--bg2) 0%, var(--bg) 70%); }
|
||
#bg-grid { position:absolute; inset:0; background-image:linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px); background-size:90px 90px; opacity:0.35; }
|
||
.slide { overflow:hidden; }
|
||
.top { position:absolute; left:64px; right:64px; top:56px; display:flex; justify-content:space-between; align-items:center; z-index:5; }
|
||
.mark { width:56px; height:56px; display:block; }
|
||
.count { font-size:22px; letter-spacing:0.18em; color:var(--dim); background:rgba(var(--fade),0.75); padding:10px 16px; border-radius:20px; }
|
||
.count b { color:var(--ink); }
|
||
.foot { position:absolute; left:64px; right:64px; bottom:54px; display:flex; justify-content:space-between; align-items:center; font-size:22px; letter-spacing:0.14em; color:var(--dim); text-transform:uppercase; z-index:5; }
|
||
.swipe { display:flex; align-items:center; gap:14px; color:var(--ink); font-weight:700; }
|
||
.kick { font-size:24px; letter-spacing:0.26em; color:var(--dim); text-transform:uppercase; margin-bottom:26px; }
|
||
.kick b { color:var(--ink); }
|
||
.body { position:absolute; left:64px; right:64px; z-index:3; }
|
||
.q { font-family:"Fraunces", serif; font-weight:700; letter-spacing:-0.02em; line-height:1.05; }
|
||
.sub { font-size:34px; line-height:1.4; color:var(--dim); margin-top:34px; }
|
||
.photo { position:absolute; overflow:hidden; }
|
||
.photo img { display:block; width:100%; height:100%; object-fit:cover; }
|
||
.fadeb { position:absolute; left:0; right:0; }
|
||
/* cover */
|
||
.cover .photo { inset:0; }
|
||
.cover .fadeb { inset:0; background:linear-gradient(180deg, rgba(var(--fade),0) 0%, rgba(var(--fade),0.1) 30%, rgba(var(--fade),0.9) 52%, rgb(var(--fade)) 66%); }
|
||
.cover .body { bottom:150px; }
|
||
.cover .q { font-size:100px; }
|
||
.byline { margin-top:30px; font-size:30px; } .byline span { color:var(--dim); }
|
||
/* quote */
|
||
.quote .body { top:210px; } .quote .q { font-size:94px; }
|
||
.vis { position:absolute; left:64px; right:64px; bottom:140px; height:420px; }
|
||
/* phototop */
|
||
.phototop .photo { left:0; top:0; width:1080px; height:640px; }
|
||
.phototop .fadeb { top:380px; height:262px; background:linear-gradient(180deg, rgba(var(--fade),0), rgb(var(--fade))); }
|
||
.phototop .body { top:650px; } .phototop .q { font-size:76px; }
|
||
/* photoside */
|
||
.photoside .photo { right:0; top:0; width:520px; height:1350px; }
|
||
.photoside .fadeb { right:0; top:0; width:520px; height:1350px; background:linear-gradient(90deg, rgb(var(--fade)) 0%, rgba(var(--fade),0.25) 45%, rgba(var(--fade),0) 100%); left:auto; }
|
||
.photoside .body { top:250px; right:480px; } .photoside .q { font-size:80px; }
|
||
.dots { display:flex; gap:8px; margin-top:40px; }
|
||
.dots i { display:block; width:46px; height:46px; border-radius:50%; }
|
||
.dots .h { background:var(--ink); } .dots .a { border:4px solid var(--acc); }
|
||
/* stat */
|
||
.stat .body { top:330px; }
|
||
.big { font-family:"Fraunces", serif; font-weight:700; font-size:300px; line-height:0.95; letter-spacing:-0.04em; white-space:nowrap; }
|
||
.big-l { font-size:44px; font-weight:700; margin-top:24px; line-height:1.25; }
|
||
/* list */
|
||
.list .body { top:210px; } .list .q { font-size:76px; }
|
||
.items { margin-top:50px; display:flex; flex-direction:column; gap:22px; }
|
||
.item { display:flex; gap:28px; align-items:baseline; padding:26px 30px; border:2px solid var(--line); border-radius:22px; background:rgba(var(--fade),0.5); }
|
||
.item b { font-family:"JetBrains Mono", monospace; font-size:30px; color:var(--dim); flex:0 0 52px; }
|
||
.item span { font-size:38px; line-height:1.3; }
|
||
/* formula */
|
||
.formula .body { top:300px; }
|
||
.parts { display:flex; flex-direction:column; gap:6px; }
|
||
.part { font-family:"Fraunces", serif; font-weight:700; font-size:120px; line-height:1.02; letter-spacing:-0.02em; }
|
||
.op { font-family:"JetBrains Mono", monospace; font-size:60px; color:var(--dim); margin-left:6px; }
|
||
/* final */
|
||
.final .photo { left:64px; top:170px; width:300px; height:300px; border-radius:50%; border:6px solid var(--acc); }
|
||
.final .body { top:530px; } .final .q { font-size:74px; }
|
||
.cta { margin-top:48px; display:flex; align-items:center; gap:26px; }
|
||
.btn { padding:22px 40px; border-radius:40px; background:var(--acc); color:var(--accink); font-size:34px; font-weight:700; }
|
||
.handle { font-size:32px; }
|
||
.logo { position:absolute; left:64px; bottom:110px; width:380px; }
|
||
"""
|
||
|
||
|
||
def visual(kind, chips):
|
||
if kind == "chart-down":
|
||
return ('<svg class="vis" viewBox="0 0 952 420"><line x1="0" y1="400" x2="952" y2="400" stroke="var(--line)" stroke-width="2"/>'
|
||
'<path class="draw" d="M0 30 C 160 40, 240 130, 360 200 S 600 340, 952 386" fill="none" stroke="var(--acc)" stroke-width="6" stroke-linecap="round"/></svg>')
|
||
if kind == "chart-up":
|
||
return ('<svg class="vis" viewBox="0 0 952 420"><line x1="0" y1="400" x2="952" y2="400" stroke="var(--line)" stroke-width="2"/>'
|
||
'<path class="draw" d="M0 380 C 220 370, 360 330, 520 250 S 800 60, 952 30" fill="none" stroke="var(--acc)" stroke-width="6" stroke-linecap="round"/></svg>')
|
||
if kind == "network":
|
||
pts = [(120, 210), (360, 80), (360, 340), (600, 210), (840, 80), (840, 340)]
|
||
lines = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (3, 5), (1, 4)]
|
||
l = "".join(f'<line class="nl" x1="{pts[a][0]}" y1="{pts[a][1]}" x2="{pts[b][0]}" y2="{pts[b][1]}" stroke="var(--acc)" stroke-opacity="0.6" stroke-width="3"/>' for a, b in lines)
|
||
c = "".join(f'<circle class="nc" cx="{x}" cy="{y}" r="32" fill="{"var(--ink)" if i == 0 else "var(--acc)" if i == 3 else "none"}" stroke="var(--acc)" stroke-width="6"/>' for i, (x, y) in enumerate(pts))
|
||
return f'<svg class="vis" viewBox="0 0 952 420">{l}{c}</svg>'
|
||
if kind == "ring" and chips:
|
||
import math
|
||
n = len(chips)
|
||
out = ['<div class="vis" style="height:520px"><div style="position:absolute;left:206px;top:10px;width:540px;height:500px;border:2px dashed var(--acc);border-radius:50%;opacity:0.6"></div>']
|
||
for i, ch in enumerate(chips):
|
||
ang = -math.pi / 2 + 2 * math.pi * i / n
|
||
x, y = 476 + 270 * math.cos(ang), 260 + 230 * math.sin(ang)
|
||
out.append(f'<div class="chip" style="position:absolute;left:{x - 95:.0f}px;top:{y - 28:.0f}px;width:190px;height:56px;border-radius:28px;'
|
||
f'background:var(--acc);color:var(--accink);font-size:23px;font-weight:700;display:flex;align-items:center;justify-content:center">{esc(ch)}</div>')
|
||
out.append("</div>")
|
||
return "".join(out)
|
||
return ""
|
||
|
||
|
||
def build(spec, d):
|
||
pal = PALETTES[spec.get("palette", "carbon")]
|
||
n = len(spec["slides"])
|
||
total = n * SLIDE
|
||
mark = "shkor-labs-icon-lime.svg"
|
||
sec, js = [], []
|
||
|
||
def IN(sel, at, frm="{ opacity: 0, y: 36 }", dur=0.6):
|
||
js.append(f'tl.fromTo("{sel}", {frm}, {{ opacity: 1, x: 0, y: 0, scale: 1, duration: {dur}, ease: "power3.out" }}, {at:.2f});')
|
||
|
||
for i, s in enumerate(spec["slides"]):
|
||
a, sid, t = i * SLIDE, f"s{i + 1}", s["type"]
|
||
top = (f'<div class="top">{"" if t == "final" else f"<img class=mark src=img/{mark} alt=>"}'
|
||
f'<div class="count mono"><b>{i + 1:02d}</b> / {n:02d}</div></div>')
|
||
if t == "final":
|
||
top = top.replace('class="top"', 'class="top" style="justify-content:flex-end"')
|
||
kick = f'<div class="kick mono" id="{sid}-k">{rich(s["kicker"])}</div>' if s.get("kicker") else ""
|
||
photo = f'<div class="photo" id="{sid}-p"><img src="{esc(s["photo"])}" alt="" style="object-position:{s.get("pos", "50% 25%")}"/></div>' if s.get("photo") else ""
|
||
inner = ""
|
||
if t == "cover":
|
||
fs = 100 if len(s["title"]) <= 40 else 84
|
||
inner = (f'{photo}<div class="fadeb"></div>{top}<div class="body">{kick}<div class="q" id="{sid}-q" style="font-size:{fs}px">{rich(s["title"])}</div>'
|
||
f'<div class="byline" id="{sid}-b">{rich(s.get("byline", ""))}</div></div>'
|
||
f'<div class="foot mono"><span>cil.by</span><span class="swipe">swipe →</span></div>')
|
||
js.append(f'tl.fromTo("#{sid}-p", {{ scale: 1.12 }}, {{ scale: 1, duration: {SLIDE - 0.1}, ease: "power2.out" }}, {a:.2f});')
|
||
IN(f"#{sid}-q", a + 0.4); IN(f"#{sid}-b", a + 1.0, "{ opacity: 0, y: 16 }", 0.5)
|
||
elif t == "quote":
|
||
inner = (f'{top}<div class="body">{kick}<div class="q" id="{sid}-q">{rich(s["text"])}</div>'
|
||
f'<div class="sub" id="{sid}-s">{rich(s.get("sub", ""))}</div></div>{visual(s.get("visual"), s.get("chips"))}')
|
||
IN(f"#{sid}-q", a + 0.2); IN(f"#{sid}-s", a + 0.9, "{ opacity: 0, y: 16 }", 0.5)
|
||
if s.get("visual") in ("chart-down", "chart-up"):
|
||
js.append(f'document.querySelectorAll("#{sid} .draw").forEach(p => {{ const L = p.getTotalLength(); p.style.strokeDasharray = L; p.style.strokeDashoffset = L; }});')
|
||
js.append(f'tl.to("#{sid} .draw", {{ strokeDashoffset: 0, duration: 1.4, ease: "power2.inOut" }}, {a + 0.6:.2f});')
|
||
elif s.get("visual") == "network":
|
||
js.append(f'tl.fromTo("#{sid} .nc", {{ opacity: 0 }}, {{ opacity: 1, duration: 0.3, stagger: 0.1 }}, {a + 0.6:.2f});')
|
||
js.append(f'tl.fromTo("#{sid} .nl", {{ opacity: 0 }}, {{ opacity: 1, duration: 0.3, stagger: 0.08 }}, {a + 1.0:.2f});')
|
||
elif s.get("visual") == "ring":
|
||
js.append(f'tl.fromTo("#{sid} .chip", {{ opacity: 0, scale: 0.6 }}, {{ opacity: 1, scale: 1, duration: 0.4, stagger: 0.1, ease: "back.out(2)" }}, {a + 0.6:.2f});')
|
||
elif t in ("phototop", "photoside"):
|
||
dots = ""
|
||
if s.get("dots"):
|
||
dots = '<div class="dots" id="%s-d"><i class="h"></i><i class="h"></i><i class="h"></i><i class="a"></i><i class="a"></i><i class="a"></i></div>' % sid
|
||
inner = (f'{photo}<div class="fadeb"></div>{top}<div class="body">{kick}<div class="q" id="{sid}-q">{rich(s["text"])}</div>'
|
||
f'{dots}<div class="sub" id="{sid}-s">{rich(s.get("sub", ""))}</div></div>')
|
||
if t == "phototop":
|
||
js.append(f'tl.fromTo("#{sid}-p", {{ scale: 1.1 }}, {{ scale: 1, duration: {SLIDE - 0.1}, ease: "power2.out" }}, {a:.2f});')
|
||
else:
|
||
IN(f"#{sid}-p", a, "{ opacity: 0, x: 80 }", 0.8)
|
||
IN(f"#{sid}-q", a + 0.3); IN(f"#{sid}-s", a + 1.0, "{ opacity: 0, y: 16 }", 0.5)
|
||
if dots:
|
||
js.append(f'tl.fromTo("#{sid}-d i", {{ opacity: 0, scale: 0.4 }}, {{ opacity: 1, scale: 1, duration: 0.3, stagger: 0.1, ease: "back.out(2)" }}, {a + 0.8:.2f});')
|
||
elif t == "stat":
|
||
size = min(300, int(900 / max(1, 0.62 * len(str(s["value"])))))
|
||
inner = (f'{top}<div class="body">{kick}<div class="big" id="{sid}-v" style="font-size:{size}px">{rich(s["value"])}</div>'
|
||
f'<div class="big-l" id="{sid}-l">{rich(s["label"])}</div><div class="sub" id="{sid}-s">{rich(s.get("sub", ""))}</div></div>')
|
||
IN(f"#{sid}-v", a + 0.2, "{ opacity: 0, scale: 0.7 }", 0.7); IN(f"#{sid}-l", a + 0.7); IN(f"#{sid}-s", a + 1.1, "{ opacity: 0, y: 16 }", 0.5)
|
||
elif t == "list":
|
||
items = "".join(f'<div class="item" id="{sid}-i{j}"><b>{j + 1:02d}</b><span>{rich(it)}</span></div>' for j, it in enumerate(s["items"]))
|
||
inner = f'{top}<div class="body">{kick}<div class="q" id="{sid}-q">{rich(s["title"])}</div><div class="items">{items}</div></div>'
|
||
IN(f"#{sid}-q", a + 0.2)
|
||
for j in range(len(s["items"])):
|
||
IN(f"#{sid}-i{j}", a + 0.7 + 0.25 * j, "{ opacity: 0, x: -40 }", 0.45)
|
||
elif t == "formula":
|
||
parts = []
|
||
for j, p in enumerate(s["parts"]):
|
||
if j:
|
||
parts.append(f'<div class="op" id="{sid}-o{j}">×</div>')
|
||
parts.append(f'<div class="part" id="{sid}-p{j}">{rich(p)}</div>')
|
||
inner = f'{top}<div class="body">{kick}<div class="parts">{"".join(parts)}</div><div class="sub" id="{sid}-s">{rich(s.get("sub", ""))}</div></div>'
|
||
for j in range(len(s["parts"])):
|
||
IN(f"#{sid}-p{j}", a + 0.2 + 0.35 * j, "{ opacity: 0, x: -50 }", 0.5)
|
||
IN(f"#{sid}-s", a + 0.4 + 0.35 * len(s["parts"]), "{ opacity: 0, y: 16 }", 0.5)
|
||
elif t == "final":
|
||
inner = (f'{photo}{top}<div class="body"><div class="q" id="{sid}-q">{rich(s["text"])}</div>'
|
||
f'<div class="cta" id="{sid}-c"><div class="btn">{esc(s.get("cta", "Start at cil.by"))}</div>'
|
||
f'<div class="handle mono">{esc(spec.get("handle", "@alexshkor"))}</div></div></div>'
|
||
f'<img class="logo" id="{sid}-logo" src="img/{pal["logo"]}" alt=""/>')
|
||
IN(f"#{sid}-p", a + 0.1, "{ opacity: 0, scale: 0.8 }", 0.6); IN(f"#{sid}-q", a + 0.4)
|
||
IN(f"#{sid}-c", a + 1.1, "{ opacity: 0, y: 16 }", 0.5); IN(f"#{sid}-logo", a + 1.5, "{ opacity: 0 }", 0.6)
|
||
else:
|
||
raise ValueError(t)
|
||
if s.get("kicker"):
|
||
js.append(f'tl.fromTo("#{sid}-k", {{ opacity: 0 }}, {{ opacity: 1, duration: 0.4 }}, {a + 0.1:.2f});')
|
||
sec.append(f'<section id="{sid}" class="clip slide {t}" data-start="{a:.2f}" data-duration="{SLIDE}" data-track-index="1">{inner}</section>')
|
||
|
||
root = ";".join(f"--{k}:{v}" for k, v in pal.items() if k not in ("em", "logo"))
|
||
return f"""<!doctype html>
|
||
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=1080, height=1350" />
|
||
<title>{esc(spec.get("title", "Carousel"))}</title><script src="./gsap.min.js"></script>
|
||
<style>{CSS}
|
||
:root {{ {root} }}
|
||
em {{ {pal["em"]} }}
|
||
</style></head><body>
|
||
<div id="root" data-composition-id="main" data-start="0" data-duration="{total}" data-width="1080" data-height="1350">
|
||
<div id="bg" class="clip" data-start="0" data-duration="{total}" data-track-index="0"><div id="bg-grid"></div></div>
|
||
{"".join(sec)}
|
||
</div>
|
||
<script>
|
||
const tl = gsap.timeline({{ paused: true }});
|
||
tl.fromTo("#bg-grid", {{ y: 0 }}, {{ y: 90, duration: {total}, ease: "none" }}, 0);
|
||
{chr(10).join(js)}
|
||
window.__timelines = window.__timelines || {{}};
|
||
window.__timelines["main"] = tl;
|
||
</script></body></html>
|
||
"""
|
||
|
||
|
||
def main():
|
||
d = os.path.abspath(sys.argv[1])
|
||
spec = json.load(open(os.path.join(d, "spec.json")))
|
||
for sub in ("fonts",):
|
||
if not os.path.exists(os.path.join(d, sub)):
|
||
shutil.copytree(FONTS, os.path.join(d, sub))
|
||
os.makedirs(os.path.join(d, "img"), exist_ok=True)
|
||
for f in ("shkor-labs-icon-lime.svg", "shkor-labs-logo-dark-bg.svg", "shkor-labs-logo-light.svg"):
|
||
shutil.copy(os.path.join(BRAND, f), os.path.join(d, "img", f))
|
||
for f in ("gsap.min.js", "hyperframes.json", "meta.json", "package.json"):
|
||
if not os.path.exists(os.path.join(d, f)):
|
||
shutil.copy(os.path.join(ASSETS, f), d)
|
||
open(os.path.join(d, "index.html"), "w").write(build(spec, d))
|
||
n = len(spec["slides"])
|
||
at = ",".join(f"{(i + 1) * SLIDE - 0.2:.2f}" for i in range(n))
|
||
out = os.path.join(d, "slides")
|
||
shutil.rmtree(out, ignore_errors=True)
|
||
subprocess.run(HF + ["snapshot", "-o", out, "--at", at, "--no-end", "--describe", "false"], cwd=d, check=True,
|
||
stdin=subprocess.DEVNULL, capture_output=True)
|
||
frames = sorted(f for f in os.listdir(out) if f.startswith("frame-"))
|
||
for i, f in enumerate(frames):
|
||
os.rename(os.path.join(out, f), os.path.join(out, f"slide-{i + 1:02d}.png"))
|
||
if "--render" in sys.argv:
|
||
subprocess.run(HF + ["render", "-o", os.path.join(d, "animated.mp4")], cwd=d, check=True, stdin=subprocess.DEVNULL, capture_output=True)
|
||
print(out)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|