Fix demo rendering and baseline requirements
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.PHONY: setup install install-dev install-gpu test test-contracts lint clean preflight demo
|
||||
.PHONY: setup install install-dev install-gpu test test-contracts lint clean preflight demo demo-list
|
||||
|
||||
# ---- One-command setup ----
|
||||
|
||||
@@ -47,10 +47,10 @@ demo:
|
||||
@echo "==> Rendering zero-key demo videos (no API keys needed)..."
|
||||
@echo " These use only Remotion components — animated charts, text, data viz."
|
||||
@echo ""
|
||||
./render-demo.sh
|
||||
python render_demo.py
|
||||
|
||||
demo-list:
|
||||
@./render-demo.sh --list
|
||||
@python render_demo.py --list
|
||||
|
||||
lint:
|
||||
python -m py_compile tools/base_tool.py
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"theme": "flat-motion-graphics",
|
||||
"cuts": [
|
||||
{
|
||||
"id": "code-hook",
|
||||
"source": "",
|
||||
"type": "hero_title",
|
||||
"in_seconds": 0,
|
||||
"out_seconds": 3.5,
|
||||
"text": "From Code to Screen",
|
||||
"subtitle": "How a small system turns work into a polished release",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "code-tip",
|
||||
"source": "",
|
||||
"type": "callout",
|
||||
"in_seconds": 3.5,
|
||||
"out_seconds": 7.5,
|
||||
"title": "Shipping Rule",
|
||||
"text": "Start with one visible win, then automate the repeatable path.",
|
||||
"callout_type": "tip",
|
||||
"accentColor": "#22D3EE",
|
||||
"backgroundColor": "#0F172A",
|
||||
"color": "#F8FAFC"
|
||||
},
|
||||
{
|
||||
"id": "code-compare",
|
||||
"source": "",
|
||||
"type": "comparison",
|
||||
"in_seconds": 7.5,
|
||||
"out_seconds": 13,
|
||||
"title": "Reusable systems beat one-off edits",
|
||||
"leftLabel": "Manual Fixes",
|
||||
"leftValue": "9 files",
|
||||
"rightLabel": "Shared Pattern",
|
||||
"rightValue": "1 flow",
|
||||
"accentColor": "#A78BFA",
|
||||
"backgroundColor": "#0F172A",
|
||||
"color": "#F8FAFC"
|
||||
},
|
||||
{
|
||||
"id": "code-progress",
|
||||
"source": "",
|
||||
"type": "progress_bar",
|
||||
"in_seconds": 13,
|
||||
"out_seconds": 17.5,
|
||||
"title": "Automation coverage",
|
||||
"progress": 0.82,
|
||||
"progressLabel": "82% of the release path is scripted",
|
||||
"progressColor": "#34D399",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "code-kpis",
|
||||
"source": "",
|
||||
"type": "kpi_grid",
|
||||
"in_seconds": 17.5,
|
||||
"out_seconds": 24,
|
||||
"title": "Release pulse",
|
||||
"chartData": [
|
||||
{ "label": "PRs merged", "value": 14, "change": 18, "suffix": "" },
|
||||
{ "label": "Build minutes", "value": 11, "change": -22, "suffix": " min" },
|
||||
{ "label": "Bugs reopened", "value": 2, "change": -50, "suffix": "" }
|
||||
],
|
||||
"columns": 3,
|
||||
"chartColors": ["#22D3EE", "#34D399", "#F59E0B"],
|
||||
"chartAnimation": "cascade",
|
||||
"backgroundColor": "#0F172A"
|
||||
}
|
||||
],
|
||||
"overlays": [
|
||||
{
|
||||
"type": "section_title",
|
||||
"in_seconds": 7.8,
|
||||
"out_seconds": 10.6,
|
||||
"text": "Systems",
|
||||
"subtitle": "Design once, apply everywhere",
|
||||
"accentColor": "#A78BFA"
|
||||
},
|
||||
{
|
||||
"type": "stat_reveal",
|
||||
"in_seconds": 13.4,
|
||||
"out_seconds": 16.8,
|
||||
"text": "82%",
|
||||
"subtitle": "Automated delivery path",
|
||||
"accentColor": "#34D399",
|
||||
"position": "bottom-right"
|
||||
}
|
||||
],
|
||||
"captions": [],
|
||||
"audio": {}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"theme": "flat-motion-graphics",
|
||||
"cuts": [
|
||||
{
|
||||
"id": "focusflow-hook",
|
||||
"source": "",
|
||||
"type": "hero_title",
|
||||
"in_seconds": 0,
|
||||
"out_seconds": 3.5,
|
||||
"text": "FocusFlow",
|
||||
"subtitle": "A calm planning app that protects deep work",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "focusflow-stat",
|
||||
"source": "",
|
||||
"type": "stat_card",
|
||||
"in_seconds": 3.5,
|
||||
"out_seconds": 7.5,
|
||||
"stat": "3h 12m",
|
||||
"subtitle": "Average weekly focus time recovered",
|
||||
"accentColor": "#F59E0B",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "focusflow-bars",
|
||||
"source": "",
|
||||
"type": "bar_chart",
|
||||
"in_seconds": 7.5,
|
||||
"out_seconds": 12.5,
|
||||
"title": "Average context switches per day",
|
||||
"chartData": [
|
||||
{ "label": "Before", "value": 42 },
|
||||
{ "label": "After", "value": 18 }
|
||||
],
|
||||
"chartColors": ["#FB7185", "#34D399"],
|
||||
"showGrid": true,
|
||||
"showValues": true,
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "focusflow-breakdown",
|
||||
"source": "",
|
||||
"type": "pie_chart",
|
||||
"in_seconds": 12.5,
|
||||
"out_seconds": 17.5,
|
||||
"title": "Where reclaimed time goes",
|
||||
"chartData": [
|
||||
{ "label": "Priority work", "value": 74 },
|
||||
{ "label": "Planning", "value": 16 },
|
||||
{ "label": "Meetings", "value": 10 }
|
||||
],
|
||||
"chartColors": ["#34D399", "#22D3EE", "#A78BFA"],
|
||||
"donut": true,
|
||||
"centerLabel": "Priority work",
|
||||
"centerValue": "74%",
|
||||
"showLegend": true,
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "focusflow-close",
|
||||
"source": "",
|
||||
"type": "text_card",
|
||||
"in_seconds": 17.5,
|
||||
"out_seconds": 21.5,
|
||||
"text": "Fewer pings. Better mornings.",
|
||||
"subtitle": "Small product, measurable calm.",
|
||||
"color": "#F8FAFC",
|
||||
"backgroundColor": "#0F172A"
|
||||
}
|
||||
],
|
||||
"overlays": [
|
||||
{
|
||||
"type": "section_title",
|
||||
"in_seconds": 7.7,
|
||||
"out_seconds": 10.4,
|
||||
"text": "Traction",
|
||||
"subtitle": "Illustrative pilot results",
|
||||
"accentColor": "#F59E0B"
|
||||
},
|
||||
{
|
||||
"type": "stat_reveal",
|
||||
"in_seconds": 13.2,
|
||||
"out_seconds": 16.8,
|
||||
"text": "74%",
|
||||
"subtitle": "Time redirected to priority work",
|
||||
"accentColor": "#34D399",
|
||||
"position": "bottom-right"
|
||||
}
|
||||
],
|
||||
"captions": [],
|
||||
"audio": {}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"theme": "flat-motion-graphics",
|
||||
"cuts": [
|
||||
{
|
||||
"id": "world-hook",
|
||||
"source": "",
|
||||
"type": "hero_title",
|
||||
"in_seconds": 0,
|
||||
"out_seconds": 3.5,
|
||||
"text": "The World in Numbers",
|
||||
"subtitle": "A zero-key showcase built from Remotion components only",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "world-population",
|
||||
"source": "",
|
||||
"type": "stat_card",
|
||||
"in_seconds": 3.5,
|
||||
"out_seconds": 7.5,
|
||||
"stat": "8.1B",
|
||||
"subtitle": "People sharing one connected planet",
|
||||
"accentColor": "#22D3EE",
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "world-cities",
|
||||
"source": "",
|
||||
"type": "bar_chart",
|
||||
"in_seconds": 7.5,
|
||||
"out_seconds": 13,
|
||||
"title": "Illustrative megacity population",
|
||||
"chartData": [
|
||||
{ "label": "Tokyo", "value": 37 },
|
||||
{ "label": "Delhi", "value": 33 },
|
||||
{ "label": "Shanghai", "value": 29 },
|
||||
{ "label": "Sao Paulo", "value": 22 }
|
||||
],
|
||||
"chartColors": ["#22D3EE", "#A78BFA", "#F59E0B", "#34D399"],
|
||||
"showGrid": true,
|
||||
"showValues": true,
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "world-energy",
|
||||
"source": "",
|
||||
"type": "pie_chart",
|
||||
"in_seconds": 13,
|
||||
"out_seconds": 18,
|
||||
"title": "Illustrative energy mix",
|
||||
"chartData": [
|
||||
{ "label": "Renewables", "value": 34 },
|
||||
{ "label": "Gas", "value": 27 },
|
||||
{ "label": "Coal", "value": 21 },
|
||||
{ "label": "Nuclear", "value": 18 }
|
||||
],
|
||||
"chartColors": ["#34D399", "#22D3EE", "#F97316", "#A78BFA"],
|
||||
"donut": true,
|
||||
"centerLabel": "Renewables",
|
||||
"centerValue": "34%",
|
||||
"showLegend": true,
|
||||
"backgroundColor": "#0F172A"
|
||||
},
|
||||
{
|
||||
"id": "world-close",
|
||||
"source": "",
|
||||
"type": "text_card",
|
||||
"in_seconds": 18,
|
||||
"out_seconds": 22,
|
||||
"text": "Big systems. Human-scale decisions.",
|
||||
"color": "#F8FAFC",
|
||||
"backgroundColor": "#0F172A"
|
||||
}
|
||||
],
|
||||
"overlays": [
|
||||
{
|
||||
"type": "section_title",
|
||||
"in_seconds": 7.8,
|
||||
"out_seconds": 10.8,
|
||||
"text": "Cities",
|
||||
"subtitle": "Dense hubs make scale visible",
|
||||
"accentColor": "#22D3EE"
|
||||
},
|
||||
{
|
||||
"type": "stat_reveal",
|
||||
"in_seconds": 14.2,
|
||||
"out_seconds": 17.6,
|
||||
"text": "34%",
|
||||
"subtitle": "Illustrative renewable share",
|
||||
"accentColor": "#34D399",
|
||||
"position": "bottom-right"
|
||||
}
|
||||
],
|
||||
"captions": [],
|
||||
"audio": {}
|
||||
}
|
||||
+3
-113
@@ -1,120 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-demo.sh — Render zero-key demo videos using only Remotion components
|
||||
# No API keys, no external services, no GPU required.
|
||||
#
|
||||
# Usage:
|
||||
# ./render-demo.sh # Render all demos
|
||||
# ./render-demo.sh world-in-numbers # Render one specific demo
|
||||
# ./render-demo.sh --list # List available demos
|
||||
#
|
||||
# Prerequisites: Node.js 18+, npm install in remotion-composer/
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
COMPOSER_DIR="$SCRIPT_DIR/remotion-composer"
|
||||
PROPS_DIR="$COMPOSER_DIR/public/demo-props"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/projects/demos/renders"
|
||||
|
||||
# Available demos
|
||||
declare -A DEMOS=(
|
||||
["world-in-numbers"]="The World in Numbers — 45s data visualization showcase"
|
||||
["code-to-screen"]="From Code to Screen — 50s developer education explainer"
|
||||
["focusflow-pitch"]="FocusFlow Pitch — 40s startup pitch deck video"
|
||||
)
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
list_demos() {
|
||||
echo ""
|
||||
echo -e "${CYAN}Available zero-key demos:${NC}"
|
||||
echo ""
|
||||
for name in "${!DEMOS[@]}"; do
|
||||
echo -e " ${GREEN}$name${NC} ${DEMOS[$name]}"
|
||||
done
|
||||
echo ""
|
||||
echo "Usage: ./render-demo.sh [demo-name]"
|
||||
echo " ./render-demo.sh (renders all)"
|
||||
echo ""
|
||||
}
|
||||
|
||||
render_demo() {
|
||||
local name="$1"
|
||||
local props_file="$PROPS_DIR/$name.json"
|
||||
local output_file="$OUTPUT_DIR/$name.mp4"
|
||||
|
||||
if [ ! -f "$props_file" ]; then
|
||||
echo "Error: Props file not found: $props_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}Rendering:${NC} ${DEMOS[$name]:-$name}"
|
||||
echo -e "${CYAN}Props:${NC} $props_file"
|
||||
echo -e "${CYAN}Output:${NC} $output_file"
|
||||
echo ""
|
||||
|
||||
cd "$COMPOSER_DIR"
|
||||
npx remotion render src/index.tsx Explainer "$output_file" \
|
||||
--props "$props_file" \
|
||||
--codec h264
|
||||
|
||||
if [ -f "$output_file" ]; then
|
||||
local size=$(du -h "$output_file" | cut -f1)
|
||||
echo ""
|
||||
echo -e "${GREEN}Done!${NC} $output_file ($size)"
|
||||
else
|
||||
echo "Render may have failed — check output above."
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
if [ "${1:-}" = "--list" ] || [ "${1:-}" = "-l" ]; then
|
||||
list_demos
|
||||
exit 0
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
exec python3 "$SCRIPT_DIR/render_demo.py" "$@"
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
||||
echo "render-demo.sh — Render zero-key demo videos"
|
||||
echo ""
|
||||
echo "These demos use ONLY Remotion components (animated charts, text cards,"
|
||||
echo "stat reveals, comparisons) — no API keys, no images, no external services."
|
||||
echo ""
|
||||
list_demos
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check prerequisites
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "Error: Node.js is required. Install from https://nodejs.org/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$COMPOSER_DIR/node_modules" ]; then
|
||||
echo -e "${YELLOW}Installing Remotion dependencies...${NC}"
|
||||
cd "$COMPOSER_DIR" && npm install
|
||||
fi
|
||||
|
||||
if [ -n "${1:-}" ]; then
|
||||
# Render specific demo
|
||||
if [ -z "${DEMOS[$1]+x}" ]; then
|
||||
echo "Unknown demo: $1"
|
||||
list_demos
|
||||
exit 1
|
||||
fi
|
||||
render_demo "$1"
|
||||
else
|
||||
# Render all demos
|
||||
echo -e "${CYAN}Rendering all zero-key demos...${NC}"
|
||||
for name in world-in-numbers code-to-screen focusflow-pitch; do
|
||||
render_demo "$name"
|
||||
done
|
||||
echo ""
|
||||
echo -e "${GREEN}All demos rendered!${NC} Check $OUTPUT_DIR/"
|
||||
fi
|
||||
exec python "$SCRIPT_DIR/render_demo.py" "$@"
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
COMPOSER_DIR = ROOT_DIR / "remotion-composer"
|
||||
PROPS_DIR = COMPOSER_DIR / "public" / "demo-props"
|
||||
OUTPUT_DIR = ROOT_DIR / "projects" / "demos" / "renders"
|
||||
|
||||
DEMO_DESCRIPTIONS = {
|
||||
"world-in-numbers": "Global scale story with titles, stats, and charts",
|
||||
"code-to-screen": "Developer workflow explainer with comparison and KPI cards",
|
||||
"focusflow-pitch": "Startup-style pitch built only from Remotion components",
|
||||
}
|
||||
|
||||
|
||||
def discover_demos() -> dict[str, Path]:
|
||||
if not PROPS_DIR.exists():
|
||||
return {}
|
||||
return {path.stem: path for path in sorted(PROPS_DIR.glob("*.json"))}
|
||||
|
||||
|
||||
def find_command(*names: str) -> str | None:
|
||||
for name in names:
|
||||
resolved = shutil.which(name)
|
||||
if resolved:
|
||||
return resolved
|
||||
return None
|
||||
|
||||
|
||||
def ensure_demo_environment() -> str:
|
||||
if not find_command("node", "node.exe"):
|
||||
raise SystemExit("Error: Node.js is required. Install it from https://nodejs.org/")
|
||||
|
||||
npm_cmd = find_command("npm.cmd", "npm", "npm.exe")
|
||||
if not npm_cmd:
|
||||
raise SystemExit("Error: npm is required but was not found on PATH.")
|
||||
|
||||
npx_cmd = find_command("npx.cmd", "npx", "npx.exe")
|
||||
if not npx_cmd:
|
||||
raise SystemExit("Error: npx is required but was not found on PATH.")
|
||||
|
||||
if not (COMPOSER_DIR / "node_modules").exists():
|
||||
print("Installing Remotion dependencies...")
|
||||
subprocess.run([npm_cmd, "install"], cwd=COMPOSER_DIR, check=True)
|
||||
|
||||
return npx_cmd
|
||||
|
||||
|
||||
def validate_props_file(path: Path) -> None:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
if not isinstance(payload.get("cuts"), list) or not payload["cuts"]:
|
||||
raise SystemExit(f"Error: {path} must define at least one cut.")
|
||||
|
||||
|
||||
def render_demo(name: str, props_path: Path, npx_cmd: str) -> None:
|
||||
validate_props_file(props_path)
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
output_path = OUTPUT_DIR / f"{name}.mp4"
|
||||
|
||||
print()
|
||||
print(f"Rendering: {name}")
|
||||
print(f"Props: {props_path}")
|
||||
print(f"Output: {output_path}")
|
||||
print()
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
npx_cmd,
|
||||
"remotion",
|
||||
"render",
|
||||
"src/index.tsx",
|
||||
"Explainer",
|
||||
str(output_path),
|
||||
"--props",
|
||||
str(props_path),
|
||||
"--codec",
|
||||
"h264",
|
||||
],
|
||||
cwd=COMPOSER_DIR,
|
||||
check=True,
|
||||
)
|
||||
|
||||
if output_path.exists():
|
||||
size_mb = output_path.stat().st_size / (1024 * 1024)
|
||||
print(f"Done: {output_path} ({size_mb:.1f} MB)")
|
||||
else:
|
||||
print("Render finished without creating the expected output file.")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render zero-key OpenMontage demo videos from checked-in Remotion props."
|
||||
)
|
||||
parser.add_argument("demo", nargs="?", help="Render one named demo instead of all demos.")
|
||||
parser.add_argument("--list", action="store_true", help="List available demo fixtures and exit.")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
demos = discover_demos()
|
||||
if not demos:
|
||||
raise SystemExit(f"Error: No demo prop files were found in {PROPS_DIR}.")
|
||||
|
||||
if args.list:
|
||||
print("Available zero-key demos:")
|
||||
for name in demos:
|
||||
description = DEMO_DESCRIPTIONS.get(name, "Checked-in Remotion demo")
|
||||
print(f" {name:20} {description}")
|
||||
return 0
|
||||
|
||||
if args.demo and args.demo not in demos:
|
||||
available = ", ".join(demos)
|
||||
raise SystemExit(f"Unknown demo '{args.demo}'. Available demos: {available}")
|
||||
|
||||
npx_cmd = ensure_demo_environment()
|
||||
selected = {args.demo: demos[args.demo]} if args.demo else demos
|
||||
|
||||
for name, props_path in selected.items():
|
||||
render_demo(name, props_path, npx_cmd)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -3,3 +3,5 @@ pyyaml>=6.0
|
||||
pydantic>=2.0
|
||||
jsonschema>=4.20
|
||||
python-dotenv>=1.0
|
||||
Pillow>=10.0
|
||||
requests>=2.31
|
||||
|
||||
Reference in New Issue
Block a user