diff --git a/Makefile b/Makefile index 42362ef8..4960270d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/remotion-composer/public/demo-props/code-to-screen.json b/remotion-composer/public/demo-props/code-to-screen.json new file mode 100644 index 00000000..bf8474ac --- /dev/null +++ b/remotion-composer/public/demo-props/code-to-screen.json @@ -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": {} +} diff --git a/remotion-composer/public/demo-props/focusflow-pitch.json b/remotion-composer/public/demo-props/focusflow-pitch.json new file mode 100644 index 00000000..a65d687c --- /dev/null +++ b/remotion-composer/public/demo-props/focusflow-pitch.json @@ -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": {} +} diff --git a/remotion-composer/public/demo-props/world-in-numbers.json b/remotion-composer/public/demo-props/world-in-numbers.json new file mode 100644 index 00000000..c2335141 --- /dev/null +++ b/remotion-composer/public/demo-props/world-in-numbers.json @@ -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": {} +} diff --git a/render-demo.sh b/render-demo.sh index 58056ba5..b9dfffd8 100644 --- a/render-demo.sh +++ b/render-demo.sh @@ -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" "$@" diff --git a/render_demo.py b/render_demo.py new file mode 100644 index 00000000..7c277784 --- /dev/null +++ b/render_demo.py @@ -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()) diff --git a/requirements.txt b/requirements.txt index d78bea90..026ad933 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ pyyaml>=6.0 pydantic>=2.0 jsonschema>=4.20 python-dotenv>=1.0 +Pillow>=10.0 +requests>=2.31 diff --git a/setup.py b/setup.py index 883af726..bbbfca67 100644 --- a/setup.py +++ b/setup.py @@ -11,5 +11,7 @@ setup( "pydantic>=2.0", "jsonschema>=4.20", "python-dotenv>=1.0", + "Pillow>=10.0", + "requests>=2.31", ], )