Auto-Demo System — self-generating feature demos + a public /demos page
Status: design / not built (2026-07-12). Key decisions locked (§5). A system that auto-picks a feature, auto-generates a hands-free guided demo of it (script + house-voice narration + a screen-recorded video), and auto-publishes it — behind a human approval gate — to a public /demos page that doubles as advertising, plus Bluesky (auto-post) and the YouTube/TikTok/IG/FB posting console. Managed over time by a "Demo Manager" AI persona as features evolve.
Locked: demos render as both an MP4 (default, social-ready) and a live "Try it live" interactive tour; nothing publishes or posts without human approval (autonomy earned per-channel later); launch channels = /demos + Bluesky (auto) + the human-in-loop console; the demo action vocabulary = the generic persona-browser verb set (nav/click/scroll/type/highlight).
The strategic finding from a full-codebase inventory: ~80% of this already exists. This is a composition job — wiring Demo Studio + the feature registry + the persona-browser + the render kit into one pipeline — plus three genuinely-new pieces (LLM demo authoring, live-page video capture, a public demos surface). Art-first / honesty-first per ETHOS: demos are labeled as automated product tours, never faked results.
1. What already exists (reuse map)
| Need | Already built | Where |
|---|---|---|
| A "demo" data model | studio_demos table — {slug, title, description, surface, steps jsonb, is_published}; step = {caption, say, action?, dwell_ms?} (≤40 steps, sanitizeSteps) | migration 467_studio_demos.sql, services/studio-demos.ts |
| Hands-free playback | GuidedDemo.tsx — fetches /prepared, runs each step's action on the host, shows caption, plays narration, auto-advances; clean mode hides chrome for recording; ?demo=<slug> autostart | components/studio-demo/GuidedDemo.tsx |
| House-voice narration | narrateLine() — ElevenLabs house voice (STUDIO_HOUSE_VOICE_ID), content-hash cached in season-assets so a script/voice change re-renders only changed lines for free | services/studio-narrate.ts |
| Machine-readable feature list (the PICK source) | FEATURES: Feature[] — 45 entries, each {name, description, category, routes?, files, migrations?} | apps/frontend/src/lib/feature-index.ts |
| Web-drivability filter | Feature×interface grid (Web / Mobile / JQ / Extension, shipped/partial/absent) | docs/reference/FEATURES_BY_INTERFACE.md |
| A real browser that navigates/clicks/scrolls the app | persona-browser pool — one shared headless Chromium, session verbs navigate/scroll/click/goBack/extractText/extractForm, screenshots every 3s | services/persona-browser/{pool,session,index}.ts |
| ffmpeg mux + audio stitch + video upload | season-video.ts (mux mp3→silent video), concatMp3Buffers + lovio-seal.ts (chunk+stitch long narration), render-kit/ (bundled ffmpeg/ffprobe), uploadVideo/uploadAudio → season-assets public URLs | services/season-video.ts, services/season-audio.ts, services/render-kit/ |
| AI persona roles + persona crons | ai_personas table with role flags (is_active, is_platform_writer); persona-driven cron loops with fairest-rotation, cooldowns, kill switches | 085_ai_personas.sql, 230_ai_persona_platform_writer.sql, writer-fleet-cron.ts, persona-browse-cron.ts |
| Autonomous open-web browsing (for competitive/marketing research) | persona_browser.autonomous_browse — LLM tool-loop that browses + writes back a journal + scrapbook pick, throttled by $/steps/seconds | services/persona-actions/browse-action.ts |
| Automated external posting | Bluesky (AT Protocol, incl. video upload ≤50MB/60s) fully automated + env-gated; YouTube/TikTok/IG/FB via a human-in-loop posting console | services/cafe-bluesky.ts, services/social-posting.ts (/dashboard/admin/social-posting) |
| On-screen "a persona said something" beat | PresenceOrb — dispatch presence:show {subject:'persona'} | components/jq/PresenceOrb.tsx |
| Cron governance | heartbeat + CRON_REGISTRY toggle + isCronEnabled guard → one-tap kill at /dashboard/admin/crons | services/system-heartbeats.ts, services/cron-toggles.ts |
| OG share cards | ~65 @vercel/og routes; dynamic [shareId] pattern | app/api/og/** |
The three real gaps to build
- LLM demo authoring — nothing generates
steps[]today; demos are hand-authored JSON. (new) - Live-page video capture — persona-browser only screenshots; Playwright
recordVideois used nowhere. (new — one context option + a WebM→MP4 step) - A public
/demospage —studio_demosis super-admin-only; no public surface exists. (new — model onexamples/page.tsx)
Plus one cross-cutting design decision: today a demo action is a raw string decoded only by the Studio cockpit's onAction (mode:*). To demo arbitrary features we need a generic action vocabulary — see §3.
2. The "Demo Manager" persona — the owner of the loop
(Directly answering "maybe we need a demo-manager AI persona role.") Yes — model it exactly like is_platform_writer.
- Schema: add
is_demo_manager boolean default falsetoai_personas(new migration). Exactly one persona holds it. She gets an identity (display name, avatar, a short "I make the product tours" bio) and a writing voice for captions/social posts — distinct from the narration voice, which is the studio house voice (STUDIO_HOUSE_VOICE_ID). This keeps a consistent house sound across all demos while letting the persona have a personality in text. - Why a persona and not just a cron: three reasons that map to real capabilities she already-almost-has:
- Authoring voice. She writes the narration script + the social caption + the
/demosblurb in a consistent, on-brand voice (reusewrite-persona-chapter-style voice anchoring). - Judgment over the catalog. She owns the coverage question — which features lack a demo, which demo is stale, what to make next — the same "fairest-rotation over a work-list" logic
writer-fleet-cronalready runs. - A face for the demos page. "Tours curated by ⟨persona⟩" is honest branding, and she can drop a one-line "why this matters" via
PresenceOrbthe way browse-personas already surface scrapbook notes.
- Authoring voice. She writes the narration script + the social caption + the
- Her cron:
demo_manager_tick— hourly (or daily), fairest-rotation, cooldowns, heartbeat,CRON_REGISTRYkill switch. Each tick does at most one unit of work (pick+author one demo, or record one, or re-narrate one) to bound spend, exactly like the writer fleet stops at scaffold. - Trust tiers (mirrors writer-fleet "stops at scaffold"): she may author + record + queue autonomously, but publishing to public / posting externally is gated (§5) until a per-stage trust flag is raised — so a runaway persona can't ship a broken tour to advertising.
3. The pipeline (five stages)
Each stage is an independent, heartbeat-reporting, kill-switchable step. The Demo Manager persona sequences them; each is resumable.
PICK ─────► AUTHOR ─────► RECORD ─────► NARRATE+MUX ─────► REVIEW ─► PUBLISH/POST
(feature (steps[] via (drive route, (house-voice per (admin (/demos page +
registry LLM + a recordVideo step, stitch, approve) Bluesky auto,
diff) live look) → WebM) mux → MP4) console for rest)
Stage 1 — PICK (which feature to demo)
Enumerate FEATURES from feature-index.ts. Join against a new feature_demos table {feature_name, demo_slug, feature_hash, generated_at, status} that records which features have a current demo and the feature's definition-hash at generation time. Then the Demo Manager prioritizes:
- New feature — in
FEATURESbut nofeature_demosrow (detected by array diff; this is the "as features evolve" signal —feature-index.tsis the intended source of truth, and the changelog doc explicitly says don't parse git). - Stale demo —
feature_hashdrifted from the current feature definition → regenerate + supersede. - Uncovered — features with a Web surface (per
FEATURES_BY_INTERFACE.md) and a stableroute, never demoed. - Freshness rotation — oldest published demo, re-narrate if the house voice changed (free via content-hash).
Filter: only features with (a) a Web interface and (b) at least one concrete route are drivable. JQ-chat-only / mobile-only / extension-only features are skipped (or flagged for a human-made demo).
Stage 2 — AUTHOR (generate the script) — new
An LLM writes the steps[]. Two-pass, grounded in the real UI (not hallucinated):
- Look pass (grounding): open a persona-browser session on the feature's
route,navigate+extractText/extractFormto capture what's actually on screen (headings, buttons, form fields, key selectors). This is the same capability the autonomous-browse loop already uses — pointed at our own app. - Write pass: feed
{feature.name, feature.description, route, on-screen elements}→ LLM emitssteps[]conforming tosanitizeSteps(≤40 steps;say≤1200 chars; each step = a caption + a narration line + an action + a dwell). Voice-anchored to the Demo Manager persona.
The action vocabulary (the key new capability). Instead of the cockpit-specific mode:* string, standardize a generic verb set that maps 1:1 to persona-browser session verbs, so the recorder drives the demo directly in Playwright rather than relying on each page implementing onAction:
| Action | Meaning | Backed by |
|---|---|---|
nav:<route> | go to a route | session.navigate |
click:<text|selector> | click an element | session.click |
scroll:<selector|delta> | scroll to/by | session.scroll |
type:<selector>::<text> | type into a field | (new session verb — trivial page.fill) |
highlight:<selector> | pulse/spotlight an element | (new — inject an overlay before dwell) |
wait:<ms> | dwell only | dwell |
This unifies "a persona browses the app" with "a demo drives the app" — one verb set, one Chromium. It also means the same steps[] plays two ways: (a) live in-browser via GuidedDemo for the interactive on-site embed (keep the existing onAction path for mode:*-style host actions), and (b) headless via the recorder for the video. Backward compatible — existing mode:* demos still work.
Stage 3 — RECORD (screen-record the demo) — new capture leg
Reuse the persona-browser Chromium pool, but create a capture context with Playwright's recordVideo: { dir, size: {width:1280,height:800} } (the one option used nowhere today). Drive the authored action sequence; dwell each step for max(dwell_ms, narrationDuration) so the video paces to the narration (see §4 — narration is rendered before recording). Close the context → Playwright writes a silent WebM.
- Throttle: video capture is RAM-heavier than screenshotting (pool already caps at 5 sessions on Railway). Give capture its own single-slot queue, off-peak, cron-gated — one recording at a time.
- Also grab a poster frame (
page.screenshotmid-demo, orrenderHtmlToPng) for the/demoscard + OG.
Stage 4 — NARRATE + MUX (produce the MP4)
- Narrate each step's
sayvianarrateLine()(house voice, cached) → per-step mp3 + its duration (probeDurationSeconds). Runs before RECORD so RECORD can pace to it. - Stitch the per-step clips with inter-step silence padding sized to each step's on-screen dwell → one narration track (
concatMp3Buffers, thelovio-sealpattern). - Mux narration onto the recording, converting WebM→H.264/AAC MP4 (
-c:a aac -shortest -movflags +faststart, the provenseason-video.tsrecipe).uploadVideo→season-assetspublic URL.
Because narration and dwell are pre-aligned in stage 3, the mux is a clean overlay — no drift.
Stage 5 — REVIEW → PUBLISH → POST — governance gate
Generated demo lands status: 'generated' in a review queue (extend 467's table with status generated|approved|rejected|archived + video_url, poster_url, feature_name). Extend the existing /dashboard/admin/demo-studio page (closest template — it already lists demos, edits steps JSON, toggles is_published, and "Regenerate") into a review board: preview video, edit script, Approve / Reject / Regenerate.
On Approve:
- On-site → publish to the new public
/demospage (model onexamples/page.tsx; readsis_published && status='approved'via a new publicGET /api/studio-demos/public). Each demo shows the MP4 (fast, social-ready) with an "▶ Try it live" option that runs the interactiveGuidedDemoon the real surface. OG card via newapi/og/demo/[slug]. - External (advertising) → the Demo Manager drafts a caption in her voice; Bluesky auto-posts the MP4 (already automated + video-capable, behind its existing
BLUESKY_AUTOPOST_ENABLEDflag); YouTube/TikTok/IG/FB feed the existing human-in-loopsocial-postingconsole as candidates (operator uploads, marks posted).
4. Governance, spend & honesty (non-negotiable, matches repo culture)
- Every stage reports a heartbeat and sits behind a
CRON_REGISTRYtoggle (demo_manager_pick,demo_manager_record,demo_manager_narrateor onedemo_manager_tick) → killable in ~60s at/dashboard/admin/cronsif it burns spend. - Budgets like
persona_browse_throttle: caps on LLM authoring calls/day, capture minutes/day, and a hard $/day ceiling. TTS is mostly free (cached). - Nothing goes public or external without approval (recommended default). Autonomous publish/post is a later trust-tier flip, per-channel, once the queue has proven itself — never the initial state.
- Honesty: demos are labeled automated product tours (the
odessa-demosprecedent already labels synthetic content). No demo ever shows a fabricated result, testimonial, or metric. Ad copy claims only what the tour actually shows.
5. Decisions (locked 2026-07-12)
- Demo artifact form on
/demos: BOTH. Every published demo shows a rendered MP4 as the default card (social-ready, fast to skim) with a "▶ Try it live" upgrade that runs the interactiveGuidedDemoon the real surface. → the video capture leg (§3 stage 3) is in-scope, and the public page renders both forms. (Interactive can ship first in Phase 1; MP4 lands in Phase 2 — the page is designed for both from day one.) - Publishing autonomy: APPROVE-BEFORE-PUBLISH. The persona auto-generates + records + queues autonomously, but a human approves each demo before it reaches
/demosor any external channel. Per-channel autonomous posting is an earned, later trust-tier flip, never the initial state. No demo — on-site or external — publishes unreviewed. - Channels at launch: on-site
/demos+ Bluesky (auto) + the YouTube/TikTok/IG/FB console. All three are in scope:/demos— the public advertising surface itself (core deliverable).- Bluesky — fully automated incl. video upload (
cafe-bluesky.ts), behindBLUESKY_AUTOPOST_ENABLED; the persona drafts the caption, a human still approves the demo before it posts (per decision 2). - YouTube / TikTok / IG / FB — via the existing human-in-loop
social-postingconsole: approved demo MP4s appear as candidates, the operator uploads + marks posted. (No auto-upload API for these — assisted, not automated.) - X / LinkedIn: deferred (no integration exists; revisit if they become priority channels).
- Action driver: YES — the generic persona-browser verb set (
nav/click/scroll/type/highlight/wait) is the demo action vocabulary, keeping the existingmode:*host-onActionpath for the interactive embed. This is the load-bearing decision that makes arbitrary-feature demos possible and unifies "persona browses" with "demo drives."
Still open (not blocking; decide before the relevant phase)
- Demo Manager competitive-research scope: should she also autonomously browse rival demo pages (extending
autonomous_browse) to inform framing, or stay scoped to our own catalog? Recommend catalog-only for v1; revisit at Phase 3. - Cadence of
demo_manager_tick: hourly vs daily, and the per-day spend ceilings. Decide at Phase 3 with real cost data from Phase 0–2.
6. Phasing
- Phase 0 — authoring (highest leverage, no new infra): LLM
steps[]generator + the two-pass "look then write" grounding, writing intostudio_demosasstatus:'generated'. Reuse existing playback + narration. Ships value immediately: hand-review + publish interactive demos. - Phase 1 — the public
/demospage (interactive embeds only) +feature_demoscoverage table + the PICK loop. This is a shippable public advertising surface without video. ✅ Page shipped 2026-07-12 — public/demos(server component, SEO + OG viaapi/og/demo/[slug]) lists approved+published demos, each playable hands-free as a house-voice narrated caption walkthrough (PublicDemoPlayer), served by publicGET /api/studio-demos/public+/public/:slug/prepared(approved-only, no internals leaked). Thefeature_demoscoverage table + the auto-PICK diff are deferred to Phase 3 (the Demo Manager persona owns coverage); Phase-1 coverage is derived fromstudio_demos.feature_name. - Phase 2 — rendered MP4 + mux. ✅ v1 shipped 2026-07-12 (caption-card montage) —
services/demo-video.tsrenders a demo to a narrated MP4: onerenderHtmlToPngcaption frame per step held for that step's house-voice narration duration, concatenated + muxed with the bundled ffmpeg (arg builders indemo-video-args.ts, golden-tested; compose/mux verified locally producing a real A+V MP4 matching Σ step durations). Columnsvideo_url/poster_url/video_status(migration 473); admin 🎬 Render video trigger (POST /:slug/render-video, fire-and-forget);/demosshows the MP4 (poster +<video>) with the interactive player as "Try it live." ⚠️ Real screen-recording deferred: the pool launches Chromium as headless-shell, which can'trecordVideo— true screen capture needs full Chromium +--headless=new+xvfbon Railway. ✅ 2b shipped 2026-07-12: per-step real-UI screenshots.services/demo-capture.tsdrives the shared headless-shell Chromium (screenshots work there — no xvfb) through a demo's action sequence — navigating/clicking/scrolling/highlighting per step, optionally signed in as a capture user via an admin magic-link (auth.admin.generateLink, the auth-sso pattern) — and screenshots the real page after each step; the frame is that screenshot with a caption bar overlaid (captionedScreenshotHtml). Fully fallback-safe + config-gated: capture only runs whenDEMO_CAPTURE_BASE_URL(the deployed frontend origin) is set (+ optionalDEMO_CAPTURE_USER_IDfor authed routes); any per-step failure falls back to a caption card, so it can never fail a render and is dormant until configured. Built on prod-proven primitives (getSharedBrowser+page.screenshot+generateLink) but not locally runnable (no local Chromium) — validate on prod. ✅ 2c shipped 2026-07-12: Bluesky posting.services/demo-social.tspostDemoToBlueskyposts an approved, video-ready demo's MP4 natively to Bluesky (cafe-blueskybskyPostwithembedVideoUrl— the one channel with automated video upload). Human-triggered per approve-before-publish (an admin 🦋 Post to Bluesky button on approved+ready demos; no autonomous fire — that's a later trust tier). Refuses unless approved + published +video_status='ready'; won't double-post (storesbluesky_uri, migration 474). Caption (buildDemoCaption, golden-tested) leads with the title, trims the description, ends on thegraphene.fm/demoslink, clamped to 300 chars. - Phase 3 — the Demo Manager persona +
demo_manager_tickcron. ✅ v1 shipped 2026-07-12.is_demo_managerflag onai_personas(migration 475, mirrorsis_platform_writer);services/demo-manager-cron.tsrunDemoManagerTickpicks the next feature from the curateddemoable-features.tslist that has no tour yet and LLM-authors one, landing it asstatus='generated'(pending review) — it never approves/renders/posts (approve-before-publish stays human). Bounded: one demo/tick, stops when the review queue is full (8) or all covered; creator resolves to theis_demo_managerpersona →DEMO_MANAGER_USER_ID→ any super-admin. Wired as anindex.tsloop (6h) withrecordHeartbeat('demo_manager_tick')+CRON_REGISTRY.demo_manager(OFF by default — enable at/dashboard/admin/crons). Remaining Phase-3 refinement: diff the fullfeature-index.ts(not the curated subset) + stale-demo regeneration on feature-hash drift + auto-render/auto-post as an earned per-channel trust tier. - Phase 4 — console distribution — approved demo MP4s become
social-postingcandidates for YouTube/TikTok/IG/FB (operator-uploaded). Per-channel autonomous posting is a later trust-tier flip, not part of this phase.
Each phase is independently valuable and independently killable.
7. Files this will touch (grounding for a build session)
- New:
services/demo-author.ts(LLMsteps[]gen),services/demo-recorder.ts(recordVideo + drive + mux),services/demo-manager-cron.ts,app/demos/page.tsx,app/api/og/demo/[slug]/route.tsx, migration(s) forfeature_demos+studio_demosstatus/video columns +ai_personas.is_demo_manager. - Extend:
services/persona-browser/session.ts(type,highlightverbs + arecordVideocapture context),services/studio-demos.ts(status lifecycle),routes/studio-demos.ts(/public, review actions),components/studio-demo/GuidedDemo.tsx(generic action verbs alongsidemode:*),dashboard/admin/demo-studio/page.tsx(review board),cron-toggles.ts+system-health.ts(register the crons). - Read-only source of truth:
apps/frontend/src/lib/feature-index.ts,docs/reference/FEATURES_BY_INTERFACE.md.
See also: NOVEL_STUDIO_PLAN.md (Demo Studio's origin), EMBERKILN_PIPELINE_ARCHITECTURE.md (render kit), ETHOS.md (honesty bright line).