Specs/2026-05-03-demo-factory-design.md
Demo Factory — Design Spec v1.0
1. Vision & Scope
What it is
Demo Factory is a Claude Code plugin that produces flashy, 30–60 second product demo videos from a running local dev server and project documentation. It is Phase 1 of a larger ArijitOS Demo Studio — a capability of Arijit's personal OS for building, demonstrating, and distributing products.
It is simultaneously: - A standalone tool — invoke it, get an MP4 demo video - A feeder — produce the script + screenshot bundle + audio formatted for Heygen (avatar) and 11Labs (voiceover) ingestion - A platform foundation — every artifact it produces is separable and re-usable by future extensions (LinkedIn carousels, YouTube Shorts, AI b-roll, UGC ads)
Phase 1 target products
Crawler Factory, Algolia Central, PRISM, NoCo II.
What it is NOT (Phase 1)
- Not a full video editor
- Not a UGC ad creator
- Not a LinkedIn carousel generator
- Not an AI b-roll generator
- All of the above are captured as Future Extensions (see Section 10)
2. Distribution Shape
A Claude Code plugin — not a single skill, not a standalone app.
demo-factory/
├── plugin.json # Plugin manifest
├── skills/
│ ├── demo-factory/ # Main conversational activator + pipeline conductor
│ ├── capture/ # Capture sub-skill (LLM-3: YAML generation only)
│ ├── synopsis/ # Doc synthesis sub-skill (LLM-1)
│ ├── script-writer/ # Multi-track script generation (LLM-2)
│ └── brand-extractor/ # Brand autodetect + proposal (LLM-4)
├── commands/
│ └── demo/ # /demo <product> <url> slash command
├── scripts/ # Deterministic TypeScript/Node — NO LLM
│ ├── workspace.ts # Run directory setup + state machine
│ ├── capture.ts # Playwright runner (replay + record modes)
│ ├── brand-detect.ts # CSS/favicon/meta scraper
│ ├── render.ts # Remotion CLI wrapper (per-format)
│ ├── upload.ts # Cloud archive uploader (StorageProvider)
│ ├── validate.ts # JSON Schema validation for all artifacts
│ └── preflight.ts # Dependency check + auto-install (runs on first invoke)
├── engine/ # Remotion compositions
│ ├── package.json
│ └── src/
│ ├── DemoComposition.tsx # Root, aspect-aware
│ ├── layers/
│ │ ├── ScreenLayer.tsx
│ │ ├── TextOverlayLayer.tsx
│ │ ├── VOPlayer.tsx
│ │ └── AvatarLayer.tsx
│ └── themes/
│ └── BrandContext.tsx
├── adapters/
│ ├── voice/elevenlabs.ts # VoiceProvider implementation
│ ├── avatar/heygen.ts # AvatarProvider implementation
│ └── storage/gdrive.ts # StorageProvider implementation (Phase 1)
├── cli/
│ └── demo-factory.ts # Standalone binary (no LLM, orchestrates scripts)
└── types/
├── script.ts # Multi-track script schema (TypeScript + JSON Schema)
├── brand.ts # Brand profile schema
├── capture.ts # Capture manifest schema
└── run.ts # Run state schema
Installation
# From Claude Code plugin marketplace
/plugin install arijitchowdhury/demo-factory
# Or from GitHub (local install)
/plugin install /path/to/demo-factory
# Installs the Remotion official skill as a sub-dependency
npx skills add remotion-dev/skills
Multi-user distribution
Teammates install the plugin in their own Claude Code installation — same slash command, same local path install. Each installation is fully standalone. No API bridge, no server, no shared runtime. This is the only supported "Cowork" surface: each user runs their own copy of the plugin.
See Section 15 for full prerequisites and preflight check.
Invocation — three surfaces, same engine
# Slash command (Claude Code)
/demo crawler-factory http://localhost:3000
# Conversational skill (any Claude Code session)
"Build a demo for Crawler Factory, dev server is on :3000"
# Standalone CLI (outside Claude Code)
demo-factory build --product crawler-factory --url http://localhost:3000
demo-factory build --product crawler-factory --url http://localhost:3000 --format=all
demo-factory build --product crawler-factory --url http://localhost:3000 --yolo
3. The LLM Discipline Boundary
Core rule: Deterministic scripts do the work. LLM is the brain for synthesis only. No LLM calling LLM.
The 4 LLM synthesis points (and nothing else)
| ID | Skill | Input | Output | Invoked when |
|---|---|---|---|---|
| LLM-1 | synopsis/ |
Raw docs text (vault + repo + pasted) | 2–3 para synopsis | G2 gate, once per run |
| LLM-2 | script-writer/ |
Synopsis + screenshot manifest + brand profile | script.json (validated) |
G3 gate, once per run |
| LLM-3 | capture/ |
Recorded Playwright interaction log | capture.yaml |
After first live-record session only |
| LLM-4 | brand-extractor/ |
Raw CSS/favicon/meta JSON from brand-detect.ts |
Proposed brand.yaml |
First run for a new product only |
What is always deterministic
- Playwright capture replay
- Playwright live-record execution
- CSS/favicon scraping (
brand-detect.ts) - Schema validation (
validate.ts) - Remotion render (
render.ts) - 11Labs API call and audio download
- Heygen export bundle generation
- Cloud upload
- Workspace creation and state transitions
- Cost estimates (calculated from script length and provider rate tables, no LLM)
- Brand drift detection (CSS diff comparison, no LLM)
4. Pipeline — The 4-Gate Flow
Every demo run is a state machine: init → captured → synopsized → scripted → rendered → done.
Each transition writes a state file (run.json) to the run workspace. The pipeline is resumable — if you rerun for a product with a run in state scripted, it skips to G3. Each gate is bypassable via --yolo once trusted.
Step -1 — Scope & Angle Selection
This step runs before any workspace is created. It is the answer to: "What story are we telling?"
A 30–60s demo cannot cover a whole product. DemoFactory must know the demo angle — the specific user story, feature flow, or value proposition this run is demonstrating — before it touches a single file, runs Playwright, or reads any documentation. The angle drives everything: which docs to read, which flow to record, which beats the script generates.
For a known product with previous runs
DemoFactory loads Products/<slug>/runs-index.json and presents angles already used:
Algolia Central has 3 previous demo angles:
1. Search-as-you-type UX (2026-04-10) — 42s, 9:16
2. Analytics dashboard tour (2026-04-18) — 58s, 9:16
3. Configuration onboarding (2026-04-25) — 35s, 9:16
Use an existing angle, describe a new one, or type "readiness" to check demo readiness.
If user picks an existing angle: load its capture.yaml and previous synopsis.md as starting context. The pipeline still runs all gates — but with a head start.
For a new product or new angle
DemoFactory asks two questions (only two — not an intake form):
1. What's the demo angle? Describe the specific flow or feature in 1–2 sentences.
(e.g., "The search-as-you-type UX — from empty search box to result click in 8 seconds.")
2. Who's watching? Pick one: [technical] [business] [mixed]
From the user's answer, DemoFactory derives:
- angle_slug — kebab-case identifier stored in run.json (e.g., search-as-you-type)
- audience — drives tone in LLM-2 script generation
- focus_keywords — 3–5 terms extracted from the angle description, used to filter docs in Step 2
If the user describes an angle, DemoFactory echoes back: "Got it — demoing [angle summary] for a [audience] audience. Starting capture." No further confirmation needed.
Why this matters for large products
Without an angle, the synopsis step drowns in irrelevant docs, the capture records the wrong flow, and the script tries to tell too many stories at once. The angle is the constraint that makes a 60s demo possible for a product like Algolia Central.
Step 0 — Invoke
User invokes via slash command, conversational skill, or CLI. Engine:
1. Creates run workspace: Runs/YYYY-MM-DD-<slug>-<seq>/ in vault, tagged with angle_slug
2. Creates binary workspace: ~/AI-Development/Video_Editing/binaries/<slug>/<run-id>/
3. Loads merged brand profile (base + product override)
4. Verifies dev server is reachable (deterministic health check)
5. Writes run.json with status init, including angle, audience, focus_keywords
Step 1 — Capture
scripts/capture.ts is invoked:
- If
Products/<slug>/capture.yamlexists: Replay mode — Playwright executes the script, screenshots taken per scene, output is acapture-manifest.json(scene ID, file path, timestamp, annotations). - If missing: Record mode — Playwright opens a browser window, user drives the app, every nav/click/wait is logged to
interaction-log.json. On exit, user confirms, then LLM-3 (capture/skill) converts the log intocapture.yamland persists it to the product workspace.
Output: binaries/<slug>/<run-id>/captures/ (PNG screenshots) + capture-manifest.json in vault run folder.
Gate 1 — Capture approval
Skill presents: "Captured N scenes. Here's the manifest with thumbnail paths." User can:
- Approve as-is
- Mark scenes as cut
- Trigger re-capture of specific scene IDs
- Request a full re-record
Step 2 — Synopsis
synopsis/ skill (LLM-1) must produce a tight, angle-scoped synopsis — not a product overview. It receives focus_keywords from run.json and uses them to filter which docs it reads.
Doc selection strategy (critical for large products)
Phase A — Orient (always, cheap):
Before reading any file content, build a file index:
1. List Projects/<slug>/ directory tree in vault (names + paths only, no content)
2. List repo root files if repo_path is configured
3. Score each file against focus_keywords — files whose name/path contains a keyword score higher
4. Sort by score descending, take top N that fit the token budget
Phase B — Read in priority order (bounded by token budgets):
| Source | Token budget | What to read |
|---|---|---|
| Vault angle-specific | 4,000 | Highest-scoring files from Phase A — typically the relevant Spec or PRD section |
| Vault overview | 2,000 | README.md or top-level overview doc in vault. Always read even if off-angle — provides product framing |
| Repo | 1,500 | README.md, CLAUDE.md only. Never read source files. git log --oneline -10 for recent changes |
| Pasted | 500 | Only if Phases A–B produced < 500 tokens of useful content |
Never read source code. .ts, .tsx, .py, .go etc. are explicitly excluded. Only documentation files (.md, .txt, config-as-docs .yaml with comments).
Phase C — Angle-filtered synthesis:
LLM-1 prompt explicitly states the angle and audience. It is instructed to synthesize only information relevant to the demo angle — not a general product description. Files that were read but contain no angle-relevant content should be explicitly noted as "read but not used" in the provenance report.
What the synopsis contains: - 1 paragraph: the specific problem this demo angle addresses - 1 paragraph: what the user sees in the demo flow (step by step) - 1 paragraph: the "wow moment" — the one thing that lands for this audience
Synthesizes from angle-filtered reading. Provenance is surfaced — skill reports exactly which files were read, which were used, and their last-modified dates.
Output: synopsis.md in vault run folder.
Gate 2 — Synopsis approval
Skill presents the synopsis with provenance. User can: - Approve as-is - Edit inline and approve - Paste an entirely different synopsis
Step 3 — Script
script-writer/ skill (LLM-2) generates a multi-track script object from the approved synopsis + capture manifest + merged brand profile.
The script is a structured JSON object. Every beat has:
interface Beat {
id: string;
role: "hook" | "problem" | "demo" | "cta"; // narrative position — enables per-role styling + selective re-generation
start_ms: number;
duration_ms: number;
screen_ref: string; // references a scene in capture-manifest
vo?: string; // voiceover line → 11Labs
text_overlay?: string; // on-screen kinetic text → Remotion TextOverlayLayer
avatar_line?: string; // avatar talking line → Heygen
}
interface Script {
product: string;
run_id: string;
angle_slug: string; // from run.json — ties script to the specific demo angle
total_duration_ms: number; // enforced ≤ 60000
beats: Beat[];
structure: "hook-problem-demo-cta"; // validated by LLM-2
}
validate.ts runs JSON Schema validation before presenting to user. If validation fails, LLM-2 is re-invoked with the validation errors (not the user's attention).
Output: script.json in vault run folder.
Gate 3 — Script approval
Skill presents the script as a beat table: beat × (screen thumbnail + VO + text overlay + avatar line + duration). Also shows:
- Total duration vs 60s budget
- Cost estimate: "11Labs ~$0.04 (auto). Heygen ~$0.30 (export mode — no API cost). Proceed?"
- Track selection: user chooses which tracks render for this run:
- --tracks=text — silent kinetic-text reel (default for first run)
- --tracks=vo,text — narrated explainer
- --tracks=vo,text,avatar — full stack
User can: - Approve as-is - Edit any beat inline - Change track selection - Reject and loop back to G2 with notes
Step 4 — Render
Executed in parallel per requested output format:
-
VoiceProvider (11Labs adapter,
automode by default): - Takesscript.vo_track(array of VO lines with timing) - Calls 11Labs API withvoice_idfrom brand profile - Downloads MP3 tobinaries/<slug>/<run-id>/audio/vo.mp3- Returns timing manifest (character → ms offset) -
AvatarProvider (Heygen adapter,
exportmode by default): - Takesscript.avatar_track+ avatar_id from brand profile + screenshot refs - Generates export bundle:binaries/<slug>/<run-id>/heygen-bundle/withmanifest.json+ assets - Ifautomode: calls Heygen API, polls for completion, downloads avatar MP4 -
Remotion render (
scripts/render.ts): - For each requested format (default: 9:16 only):- Assembles
<DemoComposition>with: active tracks, screenshot paths, VO MP3, brand profile, avatar MP4 (if available) - Fires
npx remotion render - Output:
binaries/<slug>/<run-id>/renders/<format>/demo.mp4
- Assembles
Gate 4 — Render approval
Skill prints: "Render complete. Path: binaries/.../renders/9:16/demo.mp4. Duration: 42s."
User can: - Approve → triggers Step 5 (archive) - Reject → loops back to G3 with notes (or G2 if synopsis was the problem) - Request re-render with different tracks or format
Step 5 — Archive
On approval:
1. Final MP4(s) uploaded to cloud archive via StorageProvider (Phase 1: Google Drive)
2. Cloud URL(s) returned and written to run record
3. Runs/INDEX.md in vault updated: run-id, product, status, formats, cloud URLs, duration, date
4. run.json state set to done
5. Console: "Demo archived. Cloud URL: [url]. Local: [path]."
5. Artifact Map
Every artifact is a first-class file, addressable, re-usable, re-renderable.
Vault (text, synced):
Projects/Video-Editing/
Products/<slug>/
brand.yaml # merged brand override (D9)
capture.yaml # Playwright replay script (D3) — also accepted from <app-repo>/demo/
config.yaml # product config (vault path, repo path, voice_id, avatar_id) — see DemoConfig schema below
Runs/
INDEX.md # master index of all runs
<run-id>/
run.json # pipeline state + metadata
capture-manifest.json # scene list + paths
synopsis.md # approved synopsis
script.json # approved multi-track script
render-manifest.json # output paths + cloud URLs
App-side (in each product's own repo — see DEMO-READY.md contract):
<app-repo>/demo/
config.yaml # DemoConfig — scenarios, api_mocks, doc paths (see schema below)
capture.yaml # Playwright replay script for this product
fixtures/ # API response fixtures for mock interception
*.json # JSON responses
*.txt # SSE streams (text/event-stream)
brand.yaml # optional brand override (auto-detected on first run)
Local disk (binaries, not synced):
~/AI-Development/Demo_Factory/binaries/<slug>/<run-id>/
captures/ # raw PNG screenshots
audio/
vo.mp3 # 11Labs VO audio
heygen-bundle/ # Heygen export assets (or avatar.mp4 if auto)
renders/
9-16/demo.mp4
16-9/demo.mp4 # only if --format includes it
1-1/demo.mp4
Cloud archive (finals):
Google Drive: /Demo-Factory-Archive/<slug>/<run-id>/
demo-9-16.mp4
demo-16-9.mp4 # if rendered
DemoConfig schema (TypeScript source: types/demo-config.ts)
interface ApiMock {
endpoint: string; // substring match against full request URL
method: string; // GET | POST | PUT | DELETE
fixture: string; // path relative to <app-repo>/demo/
content_type?: string; // explicit override; default: .txt→text/event-stream, .json→application/json
}
interface DemoScenario {
id: string; // kebab-case slug
title: string; // one-sentence description
angle: string; // narrative spine (1–2 sentences)
audience: string; // technical | business | mixed
capture: string; // filename of capture.yaml relative to demo/
api_mocks?: ApiMock[]; // Playwright route interceptions for this scenario
}
interface DemoConfig {
product: string; // slug, kebab-case
url: string; // dev server URL
docs?: {
vault_path?: string; // relative to vault root ArijitOS-Brain/Projects/
repo_path?: string; // path to app repo (. = current repo)
};
scenarios: DemoScenario[];
}
CLI flag: --repo <path> on build and readiness commands loads <path>/demo/config.yaml and uses <path>/demo/<scenario.capture> as the capture script. First scenario is used unless --scenario <id> is specified.
6. Output Formats
| Format | Resolution | Default | Flag |
|---|---|---|---|
| Vertical | 1080×1920 (9:16) | ✅ Yes | --format=9:16 |
| Landscape | 1920×1080 (16:9) | No | --format=16:9 |
| Square | 1080×1080 (1:1) | No | --format=1:1 |
| All | All three | No | --format=all |
Remotion compositions are aspect-aware from day one — same component tree renders all three. Screen captures are taken at 2× resolution for clean downscaling.
7. Brand System
Two-layer model
-
Base profile —
~/.demo-factory/brand/arijitos-base.yaml: - Default display + body fonts - Default avatar_id (Heygen) - Default voice_id (11Labs) - House color system - Global tone: "technical, builder, founder" -
Per-product override —
Products/<slug>/brand.yaml: - Only fields that differ from base - Typically: primary/accent colors, tagline, product-specific logo, product-specific tone
First-run brand setup for a new product
scripts/brand-detect.tsscrapes running app: CSS custom properties, favicon, meta theme-color, font-family declarations- Outputs raw
brand-data.json - LLM-4 (
brand-extractor/skill) interprets and proposesbrand.yaml - Skill shows the proposed profile (colors, fonts, logo, tone) side by side with the base
- User confirms/edits/approves
- Persisted to
Products/<slug>/brand.yaml
Subsequent runs: Load from saved override. Skill runs brand-detect.ts and compares to saved profile — if CSS has drifted, surfaces a warning (not a block): "Brand drift detected in primary color (#0066ff → #0055ee). Update profile?"
Brand schema
interface BrandProfile {
name: string;
colors: { primary: string; accent: string; bg: string; text: string; };
fonts: { display: string; body: string; mono?: string; };
logo: { path: string; dark_variant?: string; };
tone: string[];
tagline?: string;
voice_id: string; // 11Labs voice ID
avatar_id: string; // Heygen avatar ID
}
8. Provider Interfaces
VoiceProvider
interface VoiceProvider {
estimate(voTrack: VOLine[]): CostEstimate;
generate(voTrack: VOLine[], voiceId: string, mode: "auto" | "export"): Promise<VoiceOutput>;
}
interface VoiceOutput {
mode: "auto" | "export";
audioPath?: string; // auto mode: path to MP3
exportBundle?: string; // export mode: path to bundle folder
timingManifest: TimingEntry[];
}
AvatarProvider
interface AvatarProvider {
estimate(avatarTrack: AvatarLine[]): CostEstimate;
generate(avatarTrack: AvatarLine[], avatarId: string, mode: "auto" | "export"): Promise<AvatarOutput>;
}
interface AvatarOutput {
mode: "auto" | "export";
videoPath?: string; // auto mode: path to avatar MP4
exportBundle?: string; // export mode: path to Heygen bundle
}
StorageProvider
interface StorageProvider {
upload(localPath: string, destination: string): Promise<UploadResult>;
}
interface UploadResult {
url: string; // public or shareable URL
provider: string;
}
Phase 1 adapters:
- voice/elevenlabs.ts — auto mode default
- avatar/heygen.ts — export mode default
- storage/gdrive.ts — Google Drive (existing CloudStorage mount)
9. Reuse Map — Research → Implementation
| Researched Skill | What we take |
|---|---|
Remotion official skill (remotion-dev/skills) |
Install as dependency. 28 modular rules (animations, compositions, captions, lottie) feed directly into our engine/ compositions. |
| GSAP official skills (SplitText, ScrollTrigger) | Kinetic typography patterns for TextOverlayLayer — staggered reveals, scramble effects, spring physics on text. |
| wshuyi/remotion-video-skill | TTS-to-timing sync pattern for VOPlayer — MiniMax/Edge TTS approach inspires our 11Labs adapter timing manifest. |
| digitalsamba/claude-code-video-toolkit | Primary reference architecture — their Playwright + Remotion + FFmpeg combined pipeline is our design target. Brand profiles, transitions, reusable components. |
| Yusuke710/manim-skill | Gate loop architecture — Plan→Code→Render→Iterate maps exactly to our G1→G2→G3→G4. Autonomous feedback-driven iteration within each gate is the pattern. |
| browser-use/video-use | Subtitle burn-in styling patterns for TextOverlayLayer. Filler word removal approach inspires VO track cleanup in script-writer. |
| talknerdytome-labs/wiggle-claude-skill | Logo animation patterns (fade, bounce, pulse, wiggle) for the brand intro scene in DemoComposition. |
| YouTube Clipper Skill | Semantic scene annotation approach — chapter/segment semantic labeling inspires how we annotate captured scenes in capture-manifest.json. |
Playwright MCP --save-video |
Informs our record-mode capture — we use the same Playwright browser but capture screenshots per scene rather than raw screen recording. |
| Algorithmic-art (p5.js) skill | Background animation / particle effects patterns for brand intro/outro scene transitions if needed. |
10. Future Extensions (from requirements)
| ID | Extension | Notes |
|---|---|---|
| F1 | UGC Ad Creation | Different tone/pacing/actor framing. Same capture + script engine, different composition. |
| F2 | Nano Banana Image Gen | From script beats → Gemini 3 presentation-grade images. Feeds slides and LinkedIn. |
| F3 | Visual Storyboard | Script beats → storyboard image sequence. Input to AI video generators. |
| F4 | AI B-roll (Veo/Sora/Kling) | Hosted video gen for transitions/atmospheric shots composited into Remotion timeline. |
| F5 | LinkedIn Carousel | Script + screens → multi-slide PNG/PDF carousel. |
| F6 | Multi-Channel Distribution Pack | --format=all produces 9:16, 16:9, 1:1 + caption copy + hashtags. |
| F7 | Full Video Editing Suite | Clip cutting, multi-source compositing, color grading. Demo Factory is one capability inside. |
| F8 | Demo Library | Runs/INDEX.md foundation already in place. Searchable in Obsidian. |
| F9 | Avatar Library | Multiple avatars per product / persona, managed profiles. |
| F10 | Voice Library | Multiple 11Labs voice profiles. A/B test voices across renders. |
| F11 | Re-cut / Update Pipeline | Re-run against evolved product. Diff old vs new run. |
| F12 | Analytics Loop | Engagement metrics → optimize future scripts. |
| F13 | Cloud Archive Expansion | S3/R2/YouTube Unlisted as StorageProvider alternatives. |
11. Open Sub-Decisions
| ID | Decision | Candidates | Status |
|---|---|---|---|
| SD-1 | Cloud archive target | Google Drive (Phase 1 default), S3/R2, YouTube Unlisted | Deferred; Phase 1 = Google Drive |
| SD-2 | CLI packaging | ts-node runner vs compiled binary via esbuild or pkg |
Deferred to implementation plan |
| SD-3 | Remotion render environment | Local Node.js vs Docker vs remote rendering (Remotion Cloud) | Phase 1 = local; deferred if render times too slow |
12. Non-Goals (explicitly out of scope for Phase 1)
- Real-time video generation (AI models generating frames from scratch)
- Multi-user / team workflow
- Video hosting / publishing (we archive to cloud; distribution is manual)
- Audio mixing / background music (future extension)
- Animated b-roll (Future Extension F4)
13. Constraints
- No LLM calling LLM. LLM invoked only at the 4 synthesis points defined in Section 3.
- No secrets in repo. API keys via env or
~/.demo-factory/credentials. Never committed. - Type-safe artifacts. All artifact schemas have TypeScript types + JSON Schema definitions. Validated by
validate.tsbefore every gate. - Bypassable gates.
--yoloflag skips all gates for trusted pipelines. Individual gate skip:--skip-g1, etc. - Resumable runs. Every step writes state. Re-invoking for a run-id resumes from last completed gate.
- Render is idempotent. Same
script.json+ assets → same MP4. Re-render does not require re-capturing or re-scripting. - Brand profile is injectable. No hardcoded colors, fonts, or logos anywhere in engine or scripts.
- Preflight before every first run.
scripts/preflight.tsmust pass (or have a fresh marker) before any pipeline gate executes. Hard fails on missing Node ≥ 18 or ffmpeg. See Section 15.
14. Visual Design — Light Modern Theme
The default rendered video frame style is Light Modern: clean, developer-friendly, and neutral enough that the product UI (shown in the browser window) is always the hero — not the frame around it.
Design Language
| Property | Value |
|---|---|
| Background | White (#FFFFFF); outer card #F8F9FA |
| Browser chrome | Simulated browser bar — traffic lights, URL bar, single tab |
| Text primary | Near-black #1A1A2E (headings) |
| Text secondary | Medium gray #4A4A6A (body, captions) |
| Accent | Brand primary color — CTA badges, progress bar, kinetic text highlights |
| Typography | Display + body: Inter (fallback); mono: JetBrains Mono |
| Shadow | 0 4px 24px rgba(0,0,0,0.08) |
| Radius | Cards: 12px; UI elements: 8px |
Layout per format
| Format | Layout |
|---|---|
| 9:16 vertical | Browser chrome panel top 70%; bottom 30% = kinetic text + brand bar |
| 16:9 landscape | Browser chrome left 70%; right panel = kinetic text + VO captions |
| 1:1 square | Browser chrome full-bleed; top text overlay band; bottom brand bar |
Implementation (engine/src/themes/BrandContext.tsx)
BrandContext.tsx exports a useTheme() hook. Light Modern is the base theme — injected into context when no product override specifies a theme. Brand primary and accent from BrandProfile are overlaid on top at render time.
const LIGHT_MODERN_THEME = {
bg: '#FFFFFF',
cardBg: '#F8F9FA',
textPrimary: '#1A1A2E',
textSecondary: '#4A4A6A',
border: 'rgba(0,0,0,0.08)',
radius: { card: '12px', element: '8px' },
shadow: '0 4px 24px rgba(0,0,0,0.08)',
font: { display: 'Inter', body: 'Inter', mono: 'JetBrains Mono' },
} as const;
Alternative themes (Phase 2)
dark-cinematic and bold-gradient token sets exist as stubs in engine/src/themes/ and are selectable via --theme flag. Phase 1 ships Light Modern only.
15. Prerequisites & Preflight Check
System dependencies (user must install — not auto-installable)
| Dependency | Required for | Min version | Install |
|---|---|---|---|
| Node.js | Everything | 18 LTS | brew install node / nvm use 18 |
| ffmpeg | Remotion video render | Any recent | brew install ffmpeg / apt install ffmpeg / choco install ffmpeg |
| Git | Repo doc source (Section 4 Step 2) | Any | Usually pre-installed |
ffmpeg is the critical one — Remotion shells out to it for every render. The plugin will not render without it.
npm packages (auto-installed by plugin on first run)
scripts/preflight.ts runs npm install inside the plugin directory on first invocation. No manual action needed.
Key packages installed:
| Package | Purpose |
|---|---|
@remotion/cli, remotion |
Video composition and render |
@playwright/test |
Capture (record + replay mode) |
playwright (chromium binary) |
Browser for capture — npx playwright install chromium runs automatically |
typescript, ts-node |
Script execution |
ajv |
JSON Schema validation (validate.ts) |
zod |
Runtime type validation in scripts |
axios |
11Labs + Heygen API calls |
External service credentials
Stored in ~/.demo-factory/credentials (never in repo). Plugin reads from env first, falls back to credentials file.
| Credential | Required | When needed | Default Phase 1 behaviour |
|---|---|---|---|
ELEVEN_LABS_API_KEY |
Yes | Step 4, VO generation | Preflight warns if missing; VO track silently skipped if not set and --tracks includes vo |
HEYGEN_API_KEY |
No (Phase 1) | Step 4, avatar auto mode only | Phase 1 default is export mode — no API key needed |
| Google Drive | No (Phase 1) | Step 5, archive | Uses existing macOS CloudStorage mount at ~/Library/CloudStorage/ — no API key needed |
Preflight check — scripts/preflight.ts
Runs automatically on every first invocation for a new install. Also runnable manually: demo-factory preflight.
Checks in order:
- Node version —
node --version≥ 18. Hard fail if below. - ffmpeg —
which ffmpeg. Hard fail with platform-specific install instructions if missing. - Git —
which git. Soft warn if missing (repo doc source unavailable; vault + pasted still work). - npm packages — checks
node_modulesexists and key packages present. Runsnpm installif not. - Playwright browser — checks Chromium binary exists. Runs
npx playwright install chromiumif missing. - 11Labs key — checks
ELEVEN_LABS_API_KEYin env or credentials file. Soft warn if missing (VO track disabled until set). - Dev server — at run time (not preflight), checks
http://<url>is reachable. Hard fail if not.
On success, writes ~/.demo-factory/.preflight-passed marker. Subsequent runs skip steps 1–5 unless the marker is older than 7 days.
Failure behaviour
Hard fails (preflight exits, run blocked): - Node < 18 - ffmpeg not found
Soft warns (preflight continues, feature degraded): - Git not found → repo doc source disabled - 11Labs key missing → VO track disabled; plugin shows "Set ELEVEN_LABS_API_KEY to enable voiceover" - Heygen key missing → ignored in Phase 1 (export mode needs no key)
Platform-aware install instructions are printed for hard fails:
✗ ffmpeg not found.
macOS: brew install ffmpeg
Ubuntu: sudo apt install ffmpeg
Windows: choco install ffmpeg
— or download from https://ffmpeg.org/download.html
Re-run after installing.
16. Demo Readiness Check
A distinct mode — not a pipeline run. Triggered when the user says:
- "Am I demo ready?"
- "Is [product] demo ready?"
- "Check [product] demo readiness"
- /demo <slug> --check
It answers: can we ship a demo video for this product right now, and if not, what's missing?
What it checks
All checks are deterministic (no LLM). Each check is independent.
| Check | Pass condition | Fail consequence |
|---|---|---|
| Dev server | GET http://<url> returns 200 within 5s |
Cannot capture — need running server |
| Capture script | Products/<slug>/capture.yaml exists |
Must record first; no replay available |
| Brand profile | Products/<slug>/brand.yaml exists |
Will use base profile; first render may need brand review |
| Vault docs | ≥ 1 .md file exists in Projects/<slug>/ |
Synopsis step will need user to paste docs |
| Demo config | <repo>/demo/config.yaml valid + all fixtures present |
api_mocks will be skipped; dynamic scenes may not render |
| Previous runs | Any entry in Runs/INDEX.md for this slug |
No resume available; cold start only |
| Binary workspace | binaries/<slug>/ exists with ≥ 1 run |
No cached captures; first run will be slower |
| 11Labs key | ELEVEN_LABS_API_KEY present in env or credentials |
VO track unavailable |
Note: Demo config check is only run when --repo <path> is provided to the readiness command.
Output format
Demo readiness check — algolia-central
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Dev server http://localhost:3000 responds (200 OK)
✅ Capture script Products/algolia-central/capture.yaml (3 flows recorded)
✅ Brand profile Products/algolia-central/brand.yaml (primary: #003DFF)
✅ Vault docs 7 docs found in Projects/algolia-central/
Specs/2026-04-10-search-ux.md (high relevance)
PRD/2026-03-15-algolia-central-prd.md (high relevance)
+ 5 more
⚠️ Previous runs 2 runs found (last: 2026-04-25, status: done)
✅ 11Labs key Present
Overall: READY — run /demo algolia-central or describe an angle to start.
Recommended angle (based on your most recent run): search-as-you-type UX
Previous runs:
2026-04-25 search-as-you-type 42s 9:16 ✅ archived
2026-04-18 analytics-dashboard 58s 9:16 ✅ archived
Or if not ready:
Demo readiness check — algolia-central
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ Dev server Cannot reach http://localhost:3000 (connection refused)
→ Start the dev server first
✅ Capture script Products/algolia-central/capture.yaml (3 flows recorded)
⚠️ Brand profile Not found — will auto-detect on first run
✅ Vault docs 7 docs found
❌ 11Labs key Not set
→ Set ELEVEN_LABS_API_KEY to enable voiceover track
Overall: NOT READY — 2 blockers.
Fix: start the dev server, then run /demo algolia-central.
What it does NOT do
- Does not start the pipeline
- Does not read docs or run LLM synthesis
- Does not write any state files
- Does not create a run workspace
It is a pure read-only audit. Safe to run at any time.
17. Token Discipline & Anthropic API Best Practices
Core rule: each LLM call is atomic and scoped
No synthesis call receives conversation history. Each of the 4 LLM points is a fresh, single-turn API request with exactly the context it needs. The pipeline state machine (run.json) maintains continuity between steps — the LLM does not.
Model selection
Match model capability to task complexity. Never default everything to the most powerful model.
| Point | Skill | Model | Rationale |
|---|---|---|---|
| LLM-1 | synopsis/ |
claude-sonnet-4-6 |
Balanced synthesis; Opus overkill for 2–3 para summary |
| LLM-2 | script-writer/ |
claude-opus-4-7 |
Most complex: multi-track, timing constraints, narrative arc |
| LLM-3 | capture/ |
claude-haiku-4-5 |
Structured extraction only: interaction log → YAML |
| LLM-4 | brand-extractor/ |
claude-haiku-4-5 |
Structured extraction only: CSS JSON → brand.yaml |
Prompt caching
Use cache_control: { type: "ephemeral" } on stable blocks at each synthesis point. Cache hits reduce cost ~90% and latency ~85% on repeat runs.
| Synthesis point | What gets cached |
|---|---|
| LLM-1 | System prompt; vault doc text (if unchanged since last run — compare mtime) |
| LLM-2 | System prompt; approved synopsis; brand profile |
| LLM-3 | System prompt |
| LLM-4 | System prompt; base brand profile |
Cache hit target: ≥ 80% of input tokens on repeat runs of the same product.
Input token budgets
Hard limits enforced by validate.ts before the API call is made. Never trust the LLM to stay within limits — truncate upstream.
| Point | Max input tokens | Truncation strategy |
|---|---|---|
| LLM-1 | 12,000 | Vault gets 8,000; repo gets 3,000; pasted gets 1,000. Each source truncates to its budget before assembly. Section headers always preserved. |
| LLM-2 | 8,000 | Synopsis ≤ 1,000; capture manifest ≤ 4,000; brand profile ≤ 500; JSON Schema ≤ 2,500. |
| LLM-3 | 4,000 | Interaction log truncated. Preflight warns at record time if session exceeds ~90s (≈ 3,500 tokens). |
| LLM-4 | 2,000 | brand-detect.ts outputs only what fits; truncates at scraper level, not LLM level. |
Output constraints
All 4 synthesis points request structured JSON. The JSON Schema for the expected output is passed in the prompt. validate.ts runs schema validation on every response.
| Point | Max output tokens | Output format |
|---|---|---|
| LLM-1 | 600 | Plain markdown (2–3 paragraphs) |
| LLM-2 | 3,000 | script.json — validated against types/script.ts JSON Schema |
| LLM-3 | 1,500 | capture.yaml — validated against types/capture.ts JSON Schema |
| LLM-4 | 400 | brand.yaml — validated against types/brand.ts JSON Schema |
Retry logic: if validation fails, re-invoke with the validation errors appended. Maximum 2 retries. On third failure, hard fail with the raw LLM output and error surfaced to user — no silent loops.
No conversation accumulation
Skills must never append LLM synthesis outputs to the ongoing conversation context and then pass that context to the next synthesis call. The only data passed between pipeline steps is structured artifacts on disk (run.json, synopsis.md, script.json). This is enforced by the architecture: each skill reads from disk, not from conversation state.
Streaming
Synthesis calls (messages.create) are not streamed — they resolve fully before validate.ts runs. Gate presentations (the user-facing output at each gate checkpoint) are streamed from the skill's own narration, separate from the synthesis call.
Extended thinking (LLM-2 only, optional)
LLM-2 (script-writer) may use extended thinking for complex timing-constrained scripts. Budget: 2,000 thinking tokens. Gated behind --thinking flag. Not on by default — adds ~3s latency and cost.
Skill construction
All skills in this plugin (demo-factory/, capture/, synopsis/, script-writer/, brand-extractor/) are created and maintained using the skill-creator:skill-creator superpowers skill. No skill file is hand-written. This ensures consistent structure, description quality, and trigger accuracy across all skills.
18. Testing Strategy
Governed by TestingSOPs. All three layers required before any module is considered done. TDD (test-first) is mandatory.
Test layers per module
| Module | Unit | Integration | Contract | Layer 4 (E2E) |
|---|---|---|---|---|
validate.ts |
✅ schema pass/fail, error messages | — | — | — |
workspace.ts |
✅ slugify, state machine transitions, run ID format | — | ✅ run.json schema via zod | — |
preflight.ts |
✅ version checks, env key detection | — | — | — |
readiness.ts |
✅ file presence checks, server mock | — | — | — |
brand-detect.ts |
✅ CSS var extraction logic | — | ✅ brand-data.json schema | — |
capture.ts |
✅ parseCaptureYaml, buildSceneId | ⚠️ skip-real-browser (Playwright) | ✅ CaptureManifest schema | — |
engine/BrandContext |
✅ ThemeProvider merge, useTheme defaults, token values | — | ✅ ThemeTokens shape | — |
engine/ScreenLayer |
✅ opacity/scale interpolation math | — | ✅ props contract | — |
engine/TextOverlayLayer |
✅ roleStyles map, translateY math | — | ✅ BeatRole → style mapping | — |
engine/DemoComposition |
✅ msToFrames, beat → frame mapping | — | ✅ DemoCompositionProps schema | — |
engine/VOPlayer |
✅ renders Audio element | — | — | — |
engine/AvatarLayer |
✅ opacity fade, position styles | — | — | — |
adapters/elevenlabs |
✅ estimateVoCost, buildTtsPayload | ⚠️ skip-real-api (11Labs) | ✅ VOLine, VoiceOutput schemas | — |
adapters/heygen |
✅ estimateAvatarCost, buildExportBundle | ⚠️ skip-real-api (Heygen) | ✅ AvatarLine, manifest shape | — |
adapters/gdrive |
✅ buildDestinationPath, resolveCloudStoragePath | ⚠️ skip-real-mount (GDrive) | — | — |
scripts/render.ts |
✅ ASPECT_DIMENSIONS, error paths (mocked Remotion) | ⚠️ skip-real-render (ffmpeg) | ✅ RenderManifest schema | ⚠️ skip-real-render |
scripts/upload.ts |
✅ archiveRun flow (mocked gdrive), run.json mutation | — | ✅ UploadResult schema | — |
cli/demo-factory.ts |
✅ parseArgs, flag parsing | — | — | — |
Legend:
- ✅ Required and implemented (or must be implemented)
- ⚠️ skip-real-X — marked with vitest skip or custom marker; run only with real credentials/infrastructure
Validation Risk Surface
| Test layer | What it proves | What it does NOT prove |
|---|---|---|
tsc --noEmit |
Type safety, correct interfaces, no hallucinated properties | Live API behavior, runtime data shape |
vitest unit |
Pure function behavior, state machine transitions, interpolation math | Network calls, browser rendering, real render output |
vitest integration (adapter layer) |
API response shape in real network conditions | Full pipeline timing, MP4 output quality |
| Browser / E2E (Layer 4) | Full pipeline end-to-end, MP4 produced, correct duration | — |
Remaining risk after all non-skip tests pass: The actual MP4 output quality (frame timing, audio sync, browser screenshot fidelity) is only provable via Layer 4 with real ffmpeg + Playwright. This is the single remaining unverified risk surface after all unit/integration tests pass.
Remotion engine test approach
Remotion hooks (useCurrentFrame, useVideoConfig, spring, interpolate) are mocked via engine/src/__mocks__/remotion.ts. The mock:
- useCurrentFrame() returns a configurable frame number (default: 0)
- useVideoConfig() returns { fps: 30, width: 1080, height: 1920, durationInFrames: 1800 }
- spring() returns a linear 0→1 value based on frame
- interpolate() delegates to the actual Remotion interpolate (it's pure math, no runtime dependency)
- AbsoluteFill, Img, Audio, Video, Composition render as plain <div> or <span> elements
This allows testing component logic (theme consumption, conditional rendering, prop contracts) without a real Remotion render environment.
Pre-commit gate (all four required)
npx tsc --noEmit # 0 type errors
npx vitest run # all non-skip tests pass
npx prettier --check . # 0 formatting issues
npx eslint . # 0 lint errors
Layer 4 — deferred to CI
Full pipeline E2E test (capture → render → MP4 file exists and has correct duration) is gated behind DEMO_FACTORY_E2E=true and skipped in local unit runs. Requires: ffmpeg installed, Playwright chromium installed, dev server running. Run only in CI against a controlled test product.
Testing SOPs cross-reference
See TestingSOPs for:
- Three-layer test structure (unit / integration / contract)
- Test naming convention: test_{what}_{condition}_{expected_result}
- Mock strategy (adapter injection, not monkey-patching)
- Definition of Done (all six gates before a module is done)