Initial monorepo — everything BlackRoad in one place
bin/ 230 CLI tools (ask-*, br-*, agent-*, roadid, carpool) scripts/ 99 automation scripts fleet/ Node configs and deployment workers/ Cloudflare Worker sources (roadpay, road-search, squad webhooks) roadc/ RoadC programming language roadnet/ Mesh network (5 APs, WireGuard) operator/ Memory system scripts config/ System configs dotfiles/ Shell configs docs/ Documentation BlackRoad OS — Pave Tomorrow. RoadChain-SHA2048: d1a24f55318d338b RoadChain-Identity: alexa@sovereign RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
This commit is contained in:
887
bin/br-video-create
Executable file
887
bin/br-video-create
Executable file
@@ -0,0 +1,887 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
br-video-create — BlackRoad Animated Video Generator
|
||||
Usage:
|
||||
br-video-create <config.json> [output.html]
|
||||
br-video-create --interactive
|
||||
br-video-create --example
|
||||
|
||||
Reads a JSON config defining scenes and generates a complete
|
||||
animated HTML video following BRAND-LOCK design rules.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# ── BlackRoad Design Tokens ─────────────────────────────────────────────
|
||||
GRADIENT = "linear-gradient(90deg,#FF6B2B,#FF2255,#CC00AA,#8844FF,#4488FF,#00D4FF)"
|
||||
FONTS_URL = "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400&display=swap"
|
||||
|
||||
EXAMPLE_CONFIG = {
|
||||
"title": "My Awesome Video",
|
||||
"aspect": "16:9",
|
||||
"background": "particles",
|
||||
"scenes": [
|
||||
{
|
||||
"type": "title",
|
||||
"pretitle": "INTRODUCING",
|
||||
"title": "Something Amazing",
|
||||
"subtitle": "Built from scratch.",
|
||||
"duration": 4000
|
||||
},
|
||||
{
|
||||
"type": "quote",
|
||||
"text": "The best way to predict the future is to build it.",
|
||||
"attribution": "— ALAN KAY",
|
||||
"duration": 5000
|
||||
},
|
||||
{
|
||||
"type": "stats",
|
||||
"heading": "BY THE NUMBERS",
|
||||
"items": [
|
||||
{"value": "10K", "label": "Users"},
|
||||
{"value": "99.9%", "label": "Uptime"},
|
||||
{"value": "50ms", "label": "Latency"},
|
||||
{"value": "24/7", "label": "Support"}
|
||||
],
|
||||
"duration": 5000
|
||||
},
|
||||
{
|
||||
"type": "bullets",
|
||||
"heading": "Why Us",
|
||||
"items": [
|
||||
"Zero cloud dependency",
|
||||
"Open source everything",
|
||||
"Built on sovereign hardware"
|
||||
],
|
||||
"duration": 5000
|
||||
},
|
||||
{
|
||||
"type": "terminal",
|
||||
"title": "demo@localhost — zsh",
|
||||
"lines": [
|
||||
{"type": "input", "text": "curl api.example.com/status"},
|
||||
{"type": "success", "text": ' {"status": "operational", "uptime": "99.99%"}'},
|
||||
{"type": "input", "text": "deploy --production"},
|
||||
{"type": "info", "text": " Deployed to 3 regions in 2.4s"}
|
||||
],
|
||||
"duration": 7000
|
||||
},
|
||||
{
|
||||
"type": "cta",
|
||||
"title": "Get Started",
|
||||
"subtitle": "example.com",
|
||||
"duration": 4000
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# ── Scene Types ──────────────────────────────────────────────────────────
|
||||
|
||||
def scene_title(s, idx):
|
||||
pretitle = s.get("pretitle", "")
|
||||
title = s.get("title", "Title")
|
||||
subtitle = s.get("subtitle", "")
|
||||
gradient_title = s.get("gradient", True)
|
||||
|
||||
title_class = ' class="grad"' if gradient_title else ''
|
||||
pretitle_html = f'<div class="mono" style="font-size:13px;letter-spacing:.3em;color:rgba(255,255,255,.2);margin-bottom:16px">{esc(pretitle)}</div>' if pretitle else ''
|
||||
subtitle_html = f'<div style="font-size:20px;color:rgba(255,255,255,.3);margin-top:16px;font-weight:300">{esc(subtitle)}</div>' if subtitle else ''
|
||||
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
{pretitle_html}
|
||||
<div style="font-size:64px;font-weight:700;text-align:center;line-height:1.15"{title_class}>{esc(title)}</div>
|
||||
<div class="grad-line" style="width:150px;margin-top:24px"></div>
|
||||
{subtitle_html}
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_subtitle(s, idx):
|
||||
lines = s.get("lines", [s.get("text", "")])
|
||||
size = s.get("size", 28)
|
||||
html_lines = "<br>".join(esc(l) for l in lines)
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<div style="text-align:center;max-width:1000px">
|
||||
<div style="font-size:{size}px;color:rgba(255,255,255,.3);line-height:2">{html_lines}</div>
|
||||
<div class="grad-line" style="width:200px;margin:32px auto"></div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_bigtext(s, idx):
|
||||
text = s.get("text", "")
|
||||
gradient = s.get("gradient", False)
|
||||
size = s.get("size", 64)
|
||||
cls = ' class="grad"' if gradient else ''
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<div style="font-size:{size}px;font-weight:700;text-align:center;line-height:1.15"{cls}>{esc(text)}</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_quote(s, idx):
|
||||
text = s.get("text", "")
|
||||
attr = s.get("attribution", "")
|
||||
attr_html = f'<div class="quote-attr">{esc(attr)}</div>' if attr else ''
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<div class="quote">"{esc(text)}"</div>
|
||||
{attr_html}
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_stats(s, idx):
|
||||
heading = s.get("heading", "")
|
||||
items = s.get("items", [])
|
||||
heading_html = f'<div class="mono" style="font-size:14px;letter-spacing:.2em;color:rgba(255,255,255,.2);margin-bottom:40px">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
cards = []
|
||||
for i, item in enumerate(items):
|
||||
delay = i * 0.2
|
||||
cards.append(f'''<div class="stat-card" style="transition-delay:{delay}s">
|
||||
<div class="stat-value grad">{esc(str(item["value"]))}</div>
|
||||
<div class="stat-label">{esc(item["label"])}</div>
|
||||
</div>''')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="stats">
|
||||
{heading_html}
|
||||
<div class="stats-grid">
|
||||
{"".join(cards)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_bullets(s, idx):
|
||||
heading = s.get("heading", "")
|
||||
items = s.get("items", [])
|
||||
icon = s.get("icon", "▸")
|
||||
heading_html = f'<div style="font-size:36px;font-weight:600;margin-bottom:40px;text-align:center">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
bullets = []
|
||||
for i, item in enumerate(items):
|
||||
delay = i * 0.3
|
||||
bullets.append(f'<div class="bullet-item" style="transition-delay:{delay}s"><span class="bullet-icon grad">{esc(icon)}</span> {esc(item)}</div>')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="bullets">
|
||||
{heading_html}
|
||||
<div class="bullet-list">
|
||||
{"".join(bullets)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_cards(s, idx):
|
||||
heading = s.get("heading", "")
|
||||
items = s.get("items", [])
|
||||
heading_html = f'<div class="mono" style="font-size:14px;letter-spacing:.2em;color:rgba(255,255,255,.2);margin-bottom:40px">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
cards = []
|
||||
for i, item in enumerate(items):
|
||||
delay = i * 0.2
|
||||
icon = item.get("icon", "")
|
||||
name = item.get("name", "")
|
||||
desc = item.get("description", "")
|
||||
status = item.get("status", "")
|
||||
status_cls = "on" if status.lower() in ("online", "active", "ok", "yes") else "off" if status else ""
|
||||
status_html = f'<div class="card-status {status_cls}">{esc(status)}</div>' if status else ''
|
||||
icon_html = f'<div class="card-icon">{icon}</div>' if icon else ''
|
||||
cards.append(f'''<div class="info-card" style="transition-delay:{delay}s">
|
||||
{icon_html}
|
||||
<div class="card-body"><div class="card-name">{esc(name)}</div><div class="card-desc">{esc(desc)}</div></div>
|
||||
{status_html}
|
||||
</div>''')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="cards">
|
||||
{heading_html}
|
||||
<div class="cards-list">
|
||||
{"".join(cards)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_timeline(s, idx):
|
||||
heading = s.get("heading", "THE TIMELINE")
|
||||
items = s.get("items", [])
|
||||
heading_html = f'<div class="mono" style="font-size:14px;letter-spacing:.2em;color:rgba(255,255,255,.2);margin-bottom:40px">{esc(heading)}</div>'
|
||||
|
||||
entries = []
|
||||
for i, item in enumerate(items):
|
||||
delay = i * 0.2
|
||||
date = item.get("date", "")
|
||||
event = item.get("event", "")
|
||||
detail = item.get("detail", "")
|
||||
detail_html = f'<div class="tl-detail">{esc(detail)}</div>' if detail else ''
|
||||
entries.append(f'''<div class="tl-item" style="transition-delay:{delay}s">
|
||||
<div class="tl-date">{esc(date)}</div>
|
||||
<div class="tl-event">{esc(event)}</div>
|
||||
{detail_html}
|
||||
</div>''')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="timeline">
|
||||
{heading_html}
|
||||
<div class="timeline">
|
||||
{"".join(entries)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_terminal(s, idx):
|
||||
title = s.get("title", "terminal")
|
||||
lines = s.get("lines", [])
|
||||
term_id = f"term{idx}"
|
||||
|
||||
lines_json = json.dumps(lines)
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="terminal" data-term="{term_id}" data-lines='{lines_json}'>
|
||||
<div class="term-window">
|
||||
<div class="term-bar">
|
||||
<div class="term-dot r"></div><div class="term-dot y"></div><div class="term-dot g"></div>
|
||||
<div class="term-title">{esc(title)}</div>
|
||||
</div>
|
||||
<div class="term-body" id="{term_id}"></div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_comparison(s, idx):
|
||||
heading = s.get("heading", "")
|
||||
left_title = s.get("left_title", "Before")
|
||||
right_title = s.get("right_title", "After")
|
||||
left = s.get("left", [])
|
||||
right = s.get("right", [])
|
||||
heading_html = f'<div style="font-size:36px;font-weight:600;margin-bottom:40px;text-align:center">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
left_items = "".join(f'<div class="cmp-item bad" style="transition-delay:{i*0.15}s"><span class="cmp-x">✗</span> {esc(x)}</div>' for i, x in enumerate(left))
|
||||
right_items = "".join(f'<div class="cmp-item good" style="transition-delay:{i*0.15+0.3}s"><span class="cmp-check">✓</span> {esc(x)}</div>' for i, x in enumerate(right))
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="comparison">
|
||||
{heading_html}
|
||||
<div class="compare">
|
||||
<div class="compare-col">
|
||||
<div class="compare-head" style="color:rgba(255,255,255,.3)">{esc(left_title)}</div>
|
||||
{left_items}
|
||||
</div>
|
||||
<div class="compare-divider"></div>
|
||||
<div class="compare-col">
|
||||
<div class="compare-head grad">{esc(right_title)}</div>
|
||||
{right_items}
|
||||
</div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_code(s, idx):
|
||||
code = s.get("code", "")
|
||||
lang = s.get("language", "")
|
||||
caption = s.get("caption", "")
|
||||
caption_html = f'<div class="mono" style="font-size:13px;color:rgba(255,255,255,.2);margin-top:16px;letter-spacing:.1em">{esc(caption)}</div>' if caption else ''
|
||||
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<div class="code-block">
|
||||
<div class="code-lang">{esc(lang)}</div>
|
||||
<pre class="code-pre">{esc(code)}</pre>
|
||||
</div>
|
||||
{caption_html}
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_split(s, idx):
|
||||
"""Left text, right visual (big number, icon, or gradient text)"""
|
||||
left_lines = s.get("left", [])
|
||||
right_text = s.get("right", "")
|
||||
right_gradient = s.get("right_gradient", True)
|
||||
|
||||
left_html = "".join(f'<div style="font-size:24px;color:rgba(255,255,255,.4);line-height:2">{esc(l)}</div>' for l in left_lines)
|
||||
right_cls = ' class="grad"' if right_gradient else ''
|
||||
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<div class="split-layout">
|
||||
<div class="split-left">{left_html}</div>
|
||||
<div class="split-right"><div style="font-size:72px;font-weight:700"{right_cls}>{esc(right_text)}</div></div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_flow(s, idx):
|
||||
"""Horizontal pipeline / data flow"""
|
||||
stages = s.get("stages", [])
|
||||
heading = s.get("heading", "")
|
||||
heading_html = f'<div style="font-size:36px;font-weight:600;margin-bottom:48px;text-align:center">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
stage_html = []
|
||||
for i, st in enumerate(stages):
|
||||
delay = i * 0.3
|
||||
icon = st.get("icon", "")
|
||||
label = st.get("label", "")
|
||||
stage_html.append(f'<div class="flow-stage" style="transition-delay:{delay}s"><div class="flow-icon">{icon}</div><div class="flow-label">{esc(label)}</div></div>')
|
||||
if i < len(stages) - 1:
|
||||
stage_html.append('<div class="flow-arrow">→</div>')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="flow">
|
||||
{heading_html}
|
||||
<div class="flow-row">
|
||||
{"".join(stage_html)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_metrics(s, idx):
|
||||
"""Grid of metric tiles"""
|
||||
items = s.get("items", [])
|
||||
heading = s.get("heading", "")
|
||||
heading_html = f'<div class="mono" style="font-size:14px;letter-spacing:.2em;color:rgba(255,255,255,.2);margin-bottom:40px">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
tiles = []
|
||||
for i, item in enumerate(items):
|
||||
delay = i * 0.12
|
||||
tiles.append(f'''<div class="metric-tile" style="transition-delay:{delay}s">
|
||||
<div class="mv">{esc(str(item.get("value","")))}</div>
|
||||
<div class="ml">{esc(item.get("label",""))}</div>
|
||||
</div>''')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="metrics">
|
||||
{heading_html}
|
||||
<div class="metrics-grid">
|
||||
{"".join(tiles)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_image(s, idx):
|
||||
"""Full-screen image or centered image with caption"""
|
||||
src = s.get("src", "")
|
||||
caption = s.get("caption", "")
|
||||
full = s.get("fullscreen", False)
|
||||
caption_html = f'<div style="font-size:18px;color:rgba(255,255,255,.3);margin-top:24px">{esc(caption)}</div>' if caption else ''
|
||||
|
||||
if full:
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<img src="{esc(src)}" style="width:100%;height:100%;object-fit:cover;position:absolute;inset:0">
|
||||
<div style="position:absolute;bottom:80px;left:80px;z-index:2">{caption_html}</div>
|
||||
</div>'''
|
||||
else:
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<img src="{esc(src)}" style="max-width:80%;max-height:70%;border-radius:12px;border:1px solid rgba(255,255,255,.08)">
|
||||
{caption_html}
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_logos(s, idx):
|
||||
"""Row of logos or brand names"""
|
||||
items = s.get("items", [])
|
||||
heading = s.get("heading", "")
|
||||
heading_html = f'<div style="font-size:24px;color:rgba(255,255,255,.3);margin-bottom:40px">{esc(heading)}</div>' if heading else ''
|
||||
|
||||
logos = []
|
||||
for i, item in enumerate(items):
|
||||
delay = i * 0.15
|
||||
if isinstance(item, dict):
|
||||
text = item.get("name", "")
|
||||
icon = item.get("icon", "")
|
||||
logos.append(f'<div class="logo-item" style="transition-delay:{delay}s">{icon}<div>{esc(text)}</div></div>')
|
||||
else:
|
||||
logos.append(f'<div class="logo-item" style="transition-delay:{delay}s"><div>{esc(str(item))}</div></div>')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="logos">
|
||||
{heading_html}
|
||||
<div class="logos-row">
|
||||
{"".join(logos)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_cta(s, idx):
|
||||
title = s.get("title", "")
|
||||
subtitle = s.get("subtitle", "")
|
||||
gradient_title = s.get("gradient", True)
|
||||
links = s.get("links", [])
|
||||
|
||||
title_cls = ' class="grad"' if gradient_title else ''
|
||||
subtitle_html = f'<div class="mono" style="font-size:16px;color:rgba(255,255,255,.2);margin-top:20px;letter-spacing:.2em">{esc(subtitle)}</div>' if subtitle else ''
|
||||
|
||||
links_html = ""
|
||||
if links:
|
||||
link_items = "".join(f'<div class="mono" style="font-size:13px;color:rgba(255,255,255,.2)">{esc(l)}</div>' for l in links)
|
||||
links_html = f'<div style="display:flex;gap:48px;margin-top:48px">{link_items}</div>'
|
||||
|
||||
return f'''<div class="scene" id="s{idx}">
|
||||
<div class="grad-line" style="width:200px;margin-bottom:32px"></div>
|
||||
<div style="font-size:48px;font-weight:700"{title_cls}>{esc(title)}</div>
|
||||
{subtitle_html}
|
||||
<div class="grad-line" style="width:200px;margin-top:32px"></div>
|
||||
{links_html}
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_countdown(s, idx):
|
||||
"""Animated number with label"""
|
||||
target = s.get("target", 100)
|
||||
label = s.get("label", "")
|
||||
prefix = s.get("prefix", "")
|
||||
suffix = s.get("suffix", "")
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="counter" data-target="{target}" data-prefix="{esc(prefix)}" data-suffix="{esc(suffix)}">
|
||||
<div style="text-align:center">
|
||||
<div class="counter-value grad" id="counter{idx}">{esc(prefix)}0{esc(suffix)}</div>
|
||||
<div style="font-size:28px;color:rgba(255,255,255,.4);margin-top:16px">{esc(label)}</div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
def scene_reveal(s, idx):
|
||||
"""Word-by-word reveal of a statement"""
|
||||
words = s.get("text", "").split()
|
||||
gradient_words = s.get("gradient_words", [])
|
||||
|
||||
word_spans = []
|
||||
for i, w in enumerate(words):
|
||||
delay = i * 0.15
|
||||
cls = "reveal-word grad" if w.strip(".,!?") in gradient_words else "reveal-word"
|
||||
word_spans.append(f'<span class="{cls}" style="transition-delay:{delay}s">{esc(w)}</span>')
|
||||
|
||||
return f'''<div class="scene" id="s{idx}" data-anim="reveal">
|
||||
<div class="reveal-text">
|
||||
{" ".join(word_spans)}
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
|
||||
SCENE_RENDERERS = {
|
||||
"title": scene_title,
|
||||
"subtitle": scene_subtitle,
|
||||
"bigtext": scene_bigtext,
|
||||
"quote": scene_quote,
|
||||
"stats": scene_stats,
|
||||
"bullets": scene_bullets,
|
||||
"cards": scene_cards,
|
||||
"timeline": scene_timeline,
|
||||
"terminal": scene_terminal,
|
||||
"comparison": scene_comparison,
|
||||
"code": scene_code,
|
||||
"split": scene_split,
|
||||
"flow": scene_flow,
|
||||
"metrics": scene_metrics,
|
||||
"image": scene_image,
|
||||
"logos": scene_logos,
|
||||
"cta": scene_cta,
|
||||
"countdown": scene_countdown,
|
||||
"reveal": scene_reveal,
|
||||
}
|
||||
|
||||
|
||||
def esc(text):
|
||||
"""Escape HTML entities (minimal — preserve intentional HTML in content)"""
|
||||
if not text:
|
||||
return ""
|
||||
return str(text).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def bg_particles():
|
||||
return '''<canvas id="bgCanvas"></canvas>
|
||||
<script>
|
||||
(function(){
|
||||
const c=document.getElementById('bgCanvas'),x=c.getContext('2d');
|
||||
c.width=parseInt(document.body.style.width||1920);c.height=parseInt(document.body.style.height||1080);
|
||||
const N=50,ps=Array.from({length:N},()=>({x:Math.random()*c.width,y:Math.random()*c.height,vx:(Math.random()-.5)*.6,vy:(Math.random()-.5)*.6,r:Math.random()*2+.5}));
|
||||
const g=x.createLinearGradient(0,0,c.width,0);
|
||||
g.addColorStop(0,'#FF6B2B');g.addColorStop(.2,'#FF2255');g.addColorStop(.4,'#CC00AA');g.addColorStop(.6,'#8844FF');g.addColorStop(.8,'#4488FF');g.addColorStop(1,'#00D4FF');
|
||||
function d(){x.clearRect(0,0,c.width,c.height);
|
||||
ps.forEach(p=>{p.x+=p.vx;p.y+=p.vy;if(p.x<0||p.x>c.width)p.vx*=-1;if(p.y<0||p.y>c.height)p.vy*=-1;x.fillStyle='rgba(255,255,255,.5)';x.beginPath();x.arc(p.x,p.y,p.r,0,Math.PI*2);x.fill()});
|
||||
x.strokeStyle=g;x.lineWidth=.5;x.globalAlpha=.12;
|
||||
for(let i=0;i<N;i++)for(let j=i+1;j<N;j++){const dx=ps[i].x-ps[j].x,dy=ps[i].y-ps[j].y;if(dx*dx+dy*dy<40000){x.beginPath();x.moveTo(ps[i].x,ps[i].y);x.lineTo(ps[j].x,ps[j].y);x.stroke()}}
|
||||
x.globalAlpha=1;requestAnimationFrame(d)}d()})();
|
||||
</script>'''
|
||||
|
||||
|
||||
def bg_mesh():
|
||||
return '''<canvas id="bgCanvas"></canvas>
|
||||
<script>
|
||||
(function(){
|
||||
const c=document.getElementById('bgCanvas'),x=c.getContext('2d');
|
||||
c.width=parseInt(document.body.style.width||1920);c.height=parseInt(document.body.style.height||1080);
|
||||
const N=60,ps=Array.from({length:N},()=>({x:Math.random()*c.width,y:Math.random()*c.height,vx:(Math.random()-.5)*.8,vy:(Math.random()-.5)*.8,r:Math.random()*2+1}));
|
||||
const g=x.createLinearGradient(0,0,c.width,0);
|
||||
g.addColorStop(0,'#FF6B2B');g.addColorStop(.2,'#FF2255');g.addColorStop(.4,'#CC00AA');g.addColorStop(.6,'#8844FF');g.addColorStop(.8,'#4488FF');g.addColorStop(1,'#00D4FF');
|
||||
function d(){x.clearRect(0,0,c.width,c.height);
|
||||
ps.forEach(p=>{p.x+=p.vx;p.y+=p.vy;if(p.x<0||p.x>c.width)p.vx*=-1;if(p.y<0||p.y>c.height)p.vy*=-1;x.fillStyle='#fff';x.beginPath();x.arc(p.x,p.y,p.r,0,Math.PI*2);x.fill()});
|
||||
x.strokeStyle=g;x.lineWidth=.5;x.globalAlpha=.15;
|
||||
for(let i=0;i<N;i++)for(let j=i+1;j<N;j++){const dx=ps[i].x-ps[j].x,dy=ps[i].y-ps[j].y;if(dx*dx+dy*dy<40000){x.beginPath();x.moveTo(ps[i].x,ps[i].y);x.lineTo(ps[j].x,ps[j].y);x.stroke()}}
|
||||
x.globalAlpha=1;requestAnimationFrame(d)}d()})();
|
||||
</script>'''
|
||||
|
||||
|
||||
def bg_grid():
|
||||
return '''<canvas id="bgCanvas"></canvas>
|
||||
<script>
|
||||
(function(){
|
||||
const c=document.getElementById('bgCanvas'),x=c.getContext('2d');
|
||||
c.width=parseInt(document.body.style.width||1920);c.height=parseInt(document.body.style.height||1080);
|
||||
x.strokeStyle='rgba(255,255,255,.03)';x.lineWidth=1;
|
||||
const s=60;
|
||||
for(let i=0;i<=c.width;i+=s){x.beginPath();x.moveTo(i,0);x.lineTo(i,c.height);x.stroke()}
|
||||
for(let j=0;j<=c.height;j+=s){x.beginPath();x.moveTo(0,j);x.lineTo(c.width,j);x.stroke()}
|
||||
// Pulse a random intersection
|
||||
let px=0,py=0;
|
||||
function pulse(){
|
||||
x.clearRect(px-20,py-20,40,40);
|
||||
x.strokeStyle='rgba(255,255,255,.03)';
|
||||
[px,py]=[Math.floor(Math.random()*(c.width/s))*s, Math.floor(Math.random()*(c.height/s))*s];
|
||||
x.fillStyle='rgba(255,255,255,.08)';x.beginPath();x.arc(px,py,3,0,Math.PI*2);x.fill();
|
||||
setTimeout(pulse,800+Math.random()*2000)}
|
||||
pulse()})();
|
||||
</script>'''
|
||||
|
||||
|
||||
def bg_none():
|
||||
return ''
|
||||
|
||||
|
||||
BG_RENDERERS = {
|
||||
"particles": bg_particles,
|
||||
"mesh": bg_mesh,
|
||||
"grid": bg_grid,
|
||||
"none": bg_none,
|
||||
"": bg_none,
|
||||
}
|
||||
|
||||
|
||||
def generate(config):
|
||||
title = config.get("title", "BlackRoad Video")
|
||||
aspect = config.get("aspect", "16:9")
|
||||
bg_type = config.get("background", "none")
|
||||
cinematic = config.get("cinematic_bars", False)
|
||||
scenes = config.get("scenes", [])
|
||||
|
||||
if aspect == "9:16":
|
||||
w, h = 1080, 1920
|
||||
else:
|
||||
w, h = 1920, 1080
|
||||
|
||||
# Render scenes
|
||||
scene_htmls = []
|
||||
durations = []
|
||||
for i, s in enumerate(scenes):
|
||||
stype = s.get("type", "title")
|
||||
renderer = SCENE_RENDERERS.get(stype)
|
||||
if not renderer:
|
||||
print(f"Warning: Unknown scene type '{stype}', skipping", file=sys.stderr)
|
||||
continue
|
||||
scene_htmls.append(renderer(s, i + 1))
|
||||
durations.append(s.get("duration", 4000))
|
||||
|
||||
# Background
|
||||
bg_html = BG_RENDERERS.get(bg_type, bg_none)()
|
||||
|
||||
# Cinematic bars
|
||||
cinema_html = ""
|
||||
if cinematic:
|
||||
cinema_html = '<div class="cinema-top" id="ctop"></div><div class="cinema-bot" id="cbot"></div>'
|
||||
|
||||
durations_js = json.dumps(durations)
|
||||
|
||||
return f'''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width={w},height={h}">
|
||||
<title>{esc(title)}</title>
|
||||
<link href="{FONTS_URL}" rel="stylesheet">
|
||||
<style>
|
||||
*{{margin:0;padding:0;box-sizing:border-box}}
|
||||
html,body{{width:{w}px;height:{h}px;overflow:hidden;background:#000;color:#fff;font-family:'Space Grotesk',sans-serif}}
|
||||
|
||||
/* Scene system */
|
||||
.scene{{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;padding:{80 if aspect=="9:16" else 60}px;opacity:0;transition:opacity .8s;z-index:1}}
|
||||
.scene.active{{opacity:1}}
|
||||
.mono{{font-family:'JetBrains Mono',monospace}}
|
||||
.grad{{background:{GRADIENT};-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}}
|
||||
.grad-line{{height:3px;border-radius:2px;background:{GRADIENT}}}
|
||||
|
||||
/* Canvas background */
|
||||
#bgCanvas{{position:fixed;inset:0;z-index:0;opacity:.4}}
|
||||
|
||||
/* Cinematic bars */
|
||||
.cinema-top,.cinema-bot{{position:fixed;left:0;right:0;background:#000;z-index:100;transition:height 1.5s ease}}
|
||||
.cinema-top{{top:0;height:80px}}.cinema-bot{{bottom:0;height:80px}}
|
||||
.cinema-top.open,.cinema-bot.open{{height:0}}
|
||||
|
||||
/* Quote */
|
||||
.quote{{font-size:{32 if aspect=="9:16" else 36}px;font-weight:300;line-height:1.6;max-width:{800 if aspect=="9:16" else 900}px;text-align:center;font-style:italic;color:rgba(255,255,255,.7)}}
|
||||
.quote-attr{{font-family:'JetBrains Mono',monospace;font-size:14px;color:rgba(255,255,255,.3);margin-top:24px;letter-spacing:.1em}}
|
||||
|
||||
/* Stats */
|
||||
.stats-grid{{display:flex;gap:{32 if aspect=="9:16" else 48}px;flex-wrap:wrap;justify-content:center}}
|
||||
.stat-card{{text-align:center;opacity:0;transform:translateY(20px);transition:all .5s}}
|
||||
.stat-card.visible{{opacity:1;transform:translateY(0)}}
|
||||
.stat-value{{font-size:{56 if aspect=="9:16" else 64}px;font-weight:700}}
|
||||
.stat-label{{font-family:'JetBrains Mono',monospace;font-size:13px;color:rgba(255,255,255,.3);margin-top:8px;letter-spacing:.15em;text-transform:uppercase}}
|
||||
|
||||
/* Bullets */
|
||||
.bullet-list{{max-width:800px;width:100%}}
|
||||
.bullet-item{{font-size:{22 if aspect=="9:16" else 26}px;padding:16px 0;opacity:0;transform:translateX(-20px);transition:all .5s;color:rgba(255,255,255,.6)}}
|
||||
.bullet-item.visible{{opacity:1;transform:translateX(0)}}
|
||||
.bullet-icon{{margin-right:16px;font-weight:700}}
|
||||
|
||||
/* Cards */
|
||||
.cards-list{{width:100%;max-width:{900 if aspect=="9:16" else 1200}px;display:flex;flex-direction:column;gap:16px}}
|
||||
.info-card{{display:flex;align-items:center;gap:20px;padding:24px 28px;border:1px solid rgba(255,255,255,.08);border-radius:12px;opacity:0;transform:translateY(20px);transition:all .5s}}
|
||||
.info-card.visible{{opacity:1;transform:translateY(0)}}
|
||||
.card-icon{{font-size:32px;width:52px;height:52px;display:flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid rgba(255,255,255,.08)}}
|
||||
.card-body{{flex:1}}.card-name{{font-size:20px;font-weight:600}}.card-desc{{font-size:14px;color:rgba(255,255,255,.3);margin-top:4px;font-family:'JetBrains Mono',monospace}}
|
||||
.card-status{{font-size:13px;font-family:'JetBrains Mono',monospace}}.card-status.on{{color:#22c55e}}.card-status.off{{color:#ef4444}}
|
||||
|
||||
/* Timeline */
|
||||
.timeline{{width:{800 if aspect=="9:16" else 1100}px;position:relative;padding-left:40px;border-left:1px solid rgba(255,255,255,.08)}}
|
||||
.tl-item{{padding:16px 0 16px 36px;position:relative;opacity:0;transform:translateX(-20px);transition:all .5s}}
|
||||
.tl-item.visible{{opacity:1;transform:translateX(0)}}
|
||||
.tl-item::before{{content:'';position:absolute;left:-5px;top:24px;width:9px;height:9px;border-radius:50%;background:{GRADIENT}}}
|
||||
.tl-date{{font-family:'JetBrains Mono',monospace;font-size:12px;color:rgba(255,255,255,.3);letter-spacing:.1em}}
|
||||
.tl-event{{font-size:20px;font-weight:500;margin-top:4px}}
|
||||
.tl-detail{{font-size:14px;color:rgba(255,255,255,.3);margin-top:4px}}
|
||||
|
||||
/* Terminal */
|
||||
.term-window{{width:{900 if aspect=="9:16" else 1400}px;background:#0a0a0a;border:1px solid rgba(255,255,255,.1);border-radius:12px;overflow:hidden}}
|
||||
.term-bar{{height:40px;background:rgba(255,255,255,.03);border-bottom:1px solid rgba(255,255,255,.06);display:flex;align-items:center;padding:0 16px;gap:8px}}
|
||||
.term-dot{{width:12px;height:12px;border-radius:50%}}.term-dot.r{{background:#ff5f57}}.term-dot.y{{background:#febc2e}}.term-dot.g{{background:#28c840}}
|
||||
.term-title{{flex:1;text-align:center;font-family:'JetBrains Mono',monospace;font-size:12px;color:rgba(255,255,255,.3)}}
|
||||
.term-body{{padding:24px 28px;font-family:'JetBrains Mono',monospace;font-size:{14 if aspect=="9:16" else 16}px;line-height:1.9;min-height:300px}}
|
||||
.term-body .prompt{{color:#8844FF}}.term-body .cmd{{color:#fff}}.term-body .output{{color:rgba(255,255,255,.5)}}
|
||||
.term-body .success{{color:#22c55e}}.term-body .error{{color:#ef4444}}.term-body .info{{color:#4488FF}}
|
||||
.term-body .pink{{color:#FF2255}}.term-body .dim{{color:rgba(255,255,255,.2)}}
|
||||
.tline{{opacity:0;transform:translateY(5px);transition:all .3s}}.tline.show{{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* Comparison */
|
||||
.compare{{display:flex;gap:48px;align-items:flex-start;max-width:1100px}}
|
||||
.compare-col{{flex:1}}.compare-head{{font-size:20px;font-weight:600;margin-bottom:20px;text-align:center}}
|
||||
.compare-divider{{width:1px;background:rgba(255,255,255,.08);align-self:stretch}}
|
||||
.cmp-item{{padding:12px 0;font-size:18px;opacity:0;transform:translateX(-10px);transition:all .4s}}
|
||||
.cmp-item.visible{{opacity:1;transform:translateX(0)}}
|
||||
.cmp-x{{color:#ef4444;margin-right:12px}}.cmp-check{{color:#22c55e;margin-right:12px}}
|
||||
.cmp-item.bad{{color:rgba(255,255,255,.3);text-decoration:line-through}}
|
||||
|
||||
/* Code block */
|
||||
.code-block{{background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:32px 36px;max-width:1000px;width:100%}}
|
||||
.code-lang{{font-family:'JetBrains Mono',monospace;font-size:11px;color:rgba(255,255,255,.2);margin-bottom:16px;letter-spacing:.1em;text-transform:uppercase}}
|
||||
.code-pre{{font-family:'JetBrains Mono',monospace;font-size:16px;line-height:1.8;color:rgba(255,255,255,.7);white-space:pre-wrap}}
|
||||
|
||||
/* Split */
|
||||
.split-layout{{display:flex;align-items:center;gap:80px;width:100%;max-width:1400px}}
|
||||
.split-left,.split-right{{flex:1}}
|
||||
.split-right{{text-align:center}}
|
||||
|
||||
/* Flow */
|
||||
.flow-row{{display:flex;align-items:center;gap:24px;justify-content:center;flex-wrap:wrap}}
|
||||
.flow-stage{{text-align:center;opacity:0;transform:scale(.8);transition:all .5s}}.flow-stage.visible{{opacity:1;transform:scale(1)}}
|
||||
.flow-icon{{font-size:40px;width:80px;height:80px;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255,255,255,.08);border-radius:16px;margin-bottom:12px}}
|
||||
.flow-label{{font-size:14px;color:rgba(255,255,255,.5);font-family:'JetBrains Mono',monospace}}
|
||||
.flow-arrow{{font-size:28px;color:rgba(255,255,255,.15);margin:0 8px}}
|
||||
|
||||
/* Metrics grid */
|
||||
.metrics-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;max-width:1000px;width:100%}}
|
||||
.metric-tile{{border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:24px;text-align:center;opacity:0;transform:translateY(15px);transition:all .4s}}
|
||||
.metric-tile.visible{{opacity:1;transform:translateY(0)}}
|
||||
.mv{{font-size:28px;font-weight:700}}.ml{{font-family:'JetBrains Mono',monospace;font-size:11px;color:rgba(255,255,255,.3);margin-top:8px;letter-spacing:.1em;text-transform:uppercase}}
|
||||
|
||||
/* Logos */
|
||||
.logos-row{{display:flex;gap:48px;align-items:center;justify-content:center;flex-wrap:wrap}}
|
||||
.logo-item{{text-align:center;font-size:18px;font-weight:500;opacity:0;transform:translateY(10px);transition:all .4s;color:rgba(255,255,255,.4)}}
|
||||
.logo-item.visible{{opacity:1;transform:translateY(0)}}
|
||||
|
||||
/* Counter */
|
||||
.counter-value{{font-size:120px;font-weight:700}}
|
||||
|
||||
/* Reveal */
|
||||
.reveal-text{{font-size:{40 if aspect=="9:16" else 52}px;font-weight:700;text-align:center;line-height:1.4;max-width:1100px}}
|
||||
.reveal-word{{display:inline-block;opacity:0;transform:translateY(15px);transition:all .4s}}.reveal-word.visible{{opacity:1;transform:translateY(0)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{cinema_html}
|
||||
{bg_html}
|
||||
|
||||
{chr(10).join(scene_htmls)}
|
||||
|
||||
<script>
|
||||
// Terminal typing engine
|
||||
function typeTerminal(id, lines, charDelay) {{
|
||||
charDelay = charDelay || 30;
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.innerHTML = '';
|
||||
let totalDelay = 0;
|
||||
lines.forEach(function(line) {{
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tline';
|
||||
if (line.type === 'input') {{
|
||||
totalDelay += 200;
|
||||
const ps = '<span class="prompt">$</span> ';
|
||||
div.innerHTML = ps;
|
||||
el.appendChild(div);
|
||||
setTimeout(function(){{ div.classList.add('show') }}, totalDelay);
|
||||
const chars = line.text.split('');
|
||||
chars.forEach(function(ch, ci) {{
|
||||
totalDelay += charDelay;
|
||||
setTimeout(function() {{
|
||||
div.innerHTML = ps + '<span class="cmd">' + line.text.slice(0, ci + 1) + '</span><span class="dim">_</span>';
|
||||
}}, totalDelay);
|
||||
}});
|
||||
totalDelay += 400;
|
||||
setTimeout(function() {{
|
||||
div.innerHTML = ps + '<span class="cmd">' + line.text + '</span>';
|
||||
}}, totalDelay);
|
||||
}} else {{
|
||||
totalDelay += line.delay || 80;
|
||||
div.innerHTML = '<span class="' + (line.type || 'output') + '">' + line.text + '</span>';
|
||||
el.appendChild(div);
|
||||
setTimeout(function(){{ div.classList.add('show') }}, totalDelay);
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
|
||||
// Scene controller
|
||||
const scenes = document.querySelectorAll('.scene');
|
||||
const durations = {durations_js};
|
||||
let cur = 0;
|
||||
|
||||
function animateScene(el) {{
|
||||
const anim = el.dataset.anim;
|
||||
if (!anim) return;
|
||||
|
||||
if (anim === 'stats') el.querySelectorAll('.stat-card').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*200) }});
|
||||
if (anim === 'bullets') el.querySelectorAll('.bullet-item').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*300) }});
|
||||
if (anim === 'cards') el.querySelectorAll('.info-card').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*200) }});
|
||||
if (anim === 'timeline') el.querySelectorAll('.tl-item').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*300) }});
|
||||
if (anim === 'comparison') el.querySelectorAll('.cmp-item').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*150) }});
|
||||
if (anim === 'flow') el.querySelectorAll('.flow-stage').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*300) }});
|
||||
if (anim === 'metrics') el.querySelectorAll('.metric-tile').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*120) }});
|
||||
if (anim === 'logos') el.querySelectorAll('.logo-item').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*150) }});
|
||||
if (anim === 'reveal') el.querySelectorAll('.reveal-word').forEach(function(c,j){{ setTimeout(function(){{ c.classList.add('visible') }}, j*150) }});
|
||||
|
||||
if (anim === 'terminal') {{
|
||||
const termId = el.dataset.term;
|
||||
try {{ var lines = JSON.parse(el.dataset.lines); typeTerminal(termId, lines); }} catch(e){{}}
|
||||
}}
|
||||
|
||||
if (anim === 'counter') {{
|
||||
const target = parseInt(el.dataset.target) || 100;
|
||||
const prefix = el.dataset.prefix || '';
|
||||
const suffix = el.dataset.suffix || '';
|
||||
const counterEl = el.querySelector('.counter-value');
|
||||
if (!counterEl) return;
|
||||
let current = 0;
|
||||
const step = Math.max(1, Math.floor(target / 60));
|
||||
const iv = setInterval(function() {{
|
||||
current = Math.min(current + step, target);
|
||||
counterEl.textContent = prefix + current.toLocaleString() + suffix;
|
||||
if (current >= target) clearInterval(iv);
|
||||
}}, 25);
|
||||
}}
|
||||
}}
|
||||
|
||||
function show(i) {{
|
||||
if (i >= scenes.length) return;
|
||||
scenes.forEach(function(s) {{ s.classList.remove('active') }});
|
||||
scenes[i].classList.add('active');
|
||||
animateScene(scenes[i]);
|
||||
{"// Cinematic bars" if cinematic else ""}
|
||||
{"if(i===1){document.getElementById('ctop').classList.add('open');document.getElementById('cbot').classList.add('open')}" if cinematic else ""}
|
||||
{"if(i===scenes.length-1){document.getElementById('ctop').classList.remove('open');document.getElementById('cbot').classList.remove('open')}" if cinematic else ""}
|
||||
setTimeout(function(){{ show(i + 1) }}, durations[i]);
|
||||
}}
|
||||
show(0);
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
def print_usage():
|
||||
print("""
|
||||
\033[38;5;205m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m
|
||||
\033[38;5;205m br-video-create — Animated Video Generator\033[0m
|
||||
\033[38;5;205m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m
|
||||
|
||||
\033[38;5;82mUsage:\033[0m
|
||||
br-video-create <config.json> [output.html]
|
||||
br-video-create --example
|
||||
br-video-create --types
|
||||
|
||||
\033[38;5;135mScene Types:\033[0m
|
||||
title Hero title with optional pretitle/subtitle
|
||||
subtitle Multi-line subtitle text
|
||||
bigtext Single big statement
|
||||
quote Quoted text with attribution
|
||||
stats Animated stat counters in a row
|
||||
bullets Bullet point list with stagger
|
||||
cards Info cards with icon/name/desc/status
|
||||
timeline Vertical timeline with dates
|
||||
terminal Typing terminal with colored output
|
||||
comparison Side-by-side before/after
|
||||
code Syntax-highlighted code block
|
||||
split Left text, right big number/word
|
||||
flow Horizontal pipeline stages
|
||||
metrics Grid of metric tiles
|
||||
image Image with optional caption
|
||||
logos Row of brand names/icons
|
||||
cta Call-to-action end card
|
||||
countdown Animated counting number
|
||||
reveal Word-by-word text reveal
|
||||
|
||||
\033[38;5;135mBackgrounds:\033[0m particles, mesh, grid, none
|
||||
|
||||
\033[38;5;135mAspect Ratios:\033[0m 16:9 (1920x1080), 9:16 (1080x1920)
|
||||
|
||||
\033[38;5;69mExample:\033[0m
|
||||
br-video-create --example > my-config.json
|
||||
br-video-create my-config.json my-video.html
|
||||
open my-video.html
|
||||
""")
|
||||
|
||||
|
||||
def print_types():
|
||||
print(json.dumps(list(SCENE_RENDERERS.keys()), indent=2))
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
if not args or args[0] in ("-h", "--help"):
|
||||
print_usage()
|
||||
sys.exit(0)
|
||||
|
||||
if args[0] == "--example":
|
||||
print(json.dumps(EXAMPLE_CONFIG, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
if args[0] == "--types":
|
||||
print_types()
|
||||
sys.exit(0)
|
||||
|
||||
# Read config
|
||||
config_path = args[0]
|
||||
if config_path == "-":
|
||||
config = json.load(sys.stdin)
|
||||
else:
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Generate
|
||||
html = generate(config)
|
||||
|
||||
# Output
|
||||
if len(args) > 1:
|
||||
output_path = args[1]
|
||||
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
f.write(html)
|
||||
print(f"\033[38;5;82m✓ Generated: {output_path}\033[0m")
|
||||
print(f"\033[38;5;69m Scenes: {len(config.get('scenes',[]))}\033[0m")
|
||||
print(f"\033[38;5;69m Aspect: {config.get('aspect','16:9')}\033[0m")
|
||||
print(f"\033[38;5;69m Background: {config.get('background','none')}\033[0m")
|
||||
else:
|
||||
print(html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user