Fix demo rendering and baseline requirements

This commit is contained in:
calesthio
2026-04-12 07:18:00 -07:00
parent cf3527fbbe
commit 1f2ed2f05d
8 changed files with 423 additions and 116 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+2
View File
@@ -3,3 +3,5 @@ pyyaml>=6.0
pydantic>=2.0
jsonschema>=4.20
python-dotenv>=1.0
Pillow>=10.0
requests>=2.31
+2
View File
@@ -11,5 +11,7 @@ setup(
"pydantic>=2.0",
"jsonschema>=4.20",
"python-dotenv>=1.0",
"Pillow>=10.0",
"requests>=2.31",
],
)