product

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)

NeedAlready builtWhere
A "demo" data modelstudio_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 playbackGuidedDemo.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> autostartcomponents/studio-demo/GuidedDemo.tsx
House-voice narrationnarrateLine() — ElevenLabs house voice (STUDIO_HOUSE_VOICE_ID), content-hash cached in season-assets so a script/voice change re-renders only changed lines for freeservices/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 filterFeature×interface grid (Web / Mobile / JQ / Extension, shipped/partial/absent)docs/reference/FEATURES_BY_INTERFACE.md
A real browser that navigates/clicks/scrolls the apppersona-browser pool — one shared headless Chromium, session verbs navigate/scroll/click/goBack/extractText/extractForm, screenshots every 3sservices/persona-browser/{pool,session,index}.ts
ffmpeg mux + audio stitch + video uploadseason-video.ts (mux mp3→silent video), concatMp3Buffers + lovio-seal.ts (chunk+stitch long narration), render-kit/ (bundled ffmpeg/ffprobe), uploadVideo/uploadAudioseason-assets public URLsservices/season-video.ts, services/season-audio.ts, services/render-kit/
AI persona roles + persona cronsai_personas table with role flags (is_active, is_platform_writer); persona-driven cron loops with fairest-rotation, cooldowns, kill switches085_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/secondsservices/persona-actions/browse-action.ts
Automated external postingBluesky (AT Protocol, incl. video upload ≤50MB/60s) fully automated + env-gated; YouTube/TikTok/IG/FB via a human-in-loop posting consoleservices/cafe-bluesky.ts, services/social-posting.ts (/dashboard/admin/social-posting)
On-screen "a persona said something" beatPresenceOrb — dispatch presence:show {subject:'persona'}components/jq/PresenceOrb.tsx
Cron governanceheartbeat + CRON_REGISTRY toggle + isCronEnabled guard → one-tap kill at /dashboard/admin/cronsservices/system-heartbeats.ts, services/cron-toggles.ts
OG share cards~65 @vercel/og routes; dynamic [shareId] patternapp/api/og/**

The three real gaps to build

  1. LLM demo authoring — nothing generates steps[] today; demos are hand-authored JSON. (new)
  2. Live-page video capture — persona-browser only screenshots; Playwright recordVideo is used nowhere. (new — one context option + a WebM→MP4 step)
  3. A public /demos pagestudio_demos is super-admin-only; no public surface exists. (new — model on examples/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 false to ai_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:
    1. Authoring voice. She writes the narration script + the social caption + the /demos blurb in a consistent, on-brand voice (reuse write-persona-chapter-style voice anchoring).
    2. 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-cron already runs.
    3. A face for the demos page. "Tours curated by ⟨persona⟩" is honest branding, and she can drop a one-line "why this matters" via PresenceOrb the way browse-personas already surface scrapbook notes.
  • Her cron: demo_manager_tick — hourly (or daily), fairest-rotation, cooldowns, heartbeat, CRON_REGISTRY kill 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:

  1. New feature — in FEATURES but no feature_demos row (detected by array diff; this is the "as features evolve" signal — feature-index.ts is the intended source of truth, and the changelog doc explicitly says don't parse git).
  2. Stale demofeature_hash drifted from the current feature definition → regenerate + supersede.
  3. Uncovered — features with a Web surface (per FEATURES_BY_INTERFACE.md) and a stable route, never demoed.
  4. 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/extractForm to 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 emits steps[] conforming to sanitizeSteps (≤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:

ActionMeaningBacked by
nav:<route>go to a routesession.navigate
click:<text|selector>click an elementsession.click
scroll:<selector|delta>scroll to/bysession.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 onlydwell

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.screenshot mid-demo, or renderHtmlToPng) for the /demos card + OG.

Stage 4 — NARRATE + MUX (produce the MP4)

  1. Narrate each step's say via narrateLine() (house voice, cached) → per-step mp3 + its duration (probeDurationSeconds). Runs before RECORD so RECORD can pace to it.
  2. Stitch the per-step clips with inter-step silence padding sized to each step's on-screen dwell → one narration track (concatMp3Buffers, the lovio-seal pattern).
  3. Mux narration onto the recording, converting WebM→H.264/AAC MP4 (-c:a aac -shortest -movflags +faststart, the proven season-video.ts recipe). uploadVideoseason-assets public 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 /demos page (model on examples/page.tsx; reads is_published && status='approved' via a new public GET /api/studio-demos/public). Each demo shows the MP4 (fast, social-ready) with an "▶ Try it live" option that runs the interactive GuidedDemo on the real surface. OG card via new api/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_ENABLED flag); YouTube/TikTok/IG/FB feed the existing human-in-loop social-posting console 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_REGISTRY toggle (demo_manager_pick, demo_manager_record, demo_manager_narrate or one demo_manager_tick) → killable in ~60s at /dashboard/admin/crons if 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-demos precedent 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)

  1. 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 interactive GuidedDemo on 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.)
  2. Publishing autonomy: APPROVE-BEFORE-PUBLISH. The persona auto-generates + records + queues autonomously, but a human approves each demo before it reaches /demos or 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.
  3. 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), behind BLUESKY_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-posting console: 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).
  4. Action driver: YES — the generic persona-browser verb set (nav/click/scroll/type/highlight/wait) is the demo action vocabulary, keeping the existing mode:* host-onAction path 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 into studio_demos as status:'generated'. Reuse existing playback + narration. Ships value immediately: hand-review + publish interactive demos.
  • Phase 1 — the public /demos page (interactive embeds only) + feature_demos coverage table + the PICK loop. This is a shippable public advertising surface without video. ✅ Page shipped 2026-07-12 — public /demos (server component, SEO + OG via api/og/demo/[slug]) lists approved+published demos, each playable hands-free as a house-voice narrated caption walkthrough (PublicDemoPlayer), served by public GET /api/studio-demos/public + /public/:slug/prepared (approved-only, no internals leaked). The feature_demos coverage table + the auto-PICK diff are deferred to Phase 3 (the Demo Manager persona owns coverage); Phase-1 coverage is derived from studio_demos.feature_name.
  • Phase 2 — rendered MP4 + mux. ✅ v1 shipped 2026-07-12 (caption-card montage) — services/demo-video.ts renders a demo to a narrated MP4: one renderHtmlToPng caption frame per step held for that step's house-voice narration duration, concatenated + muxed with the bundled ffmpeg (arg builders in demo-video-args.ts, golden-tested; compose/mux verified locally producing a real A+V MP4 matching Σ step durations). Columns video_url/poster_url/video_status (migration 473); admin 🎬 Render video trigger (POST /:slug/render-video, fire-and-forget); /demos shows 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't recordVideo — true screen capture needs full Chromium + --headless=new + xvfb on Railway. ✅ 2b shipped 2026-07-12: per-step real-UI screenshots. services/demo-capture.ts drives 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 when DEMO_CAPTURE_BASE_URL (the deployed frontend origin) is set (+ optional DEMO_CAPTURE_USER_ID for 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.ts postDemoToBluesky posts an approved, video-ready demo's MP4 natively to Bluesky (cafe-bluesky bskyPost with embedVideoUrl — 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 (stores bluesky_uri, migration 474). Caption (buildDemoCaption, golden-tested) leads with the title, trims the description, ends on the graphene.fm/demos link, clamped to 300 chars.
  • Phase 3 — the Demo Manager persona + demo_manager_tick cron. ✅ v1 shipped 2026-07-12. is_demo_manager flag on ai_personas (migration 475, mirrors is_platform_writer); services/demo-manager-cron.ts runDemoManagerTick picks the next feature from the curated demoable-features.ts list that has no tour yet and LLM-authors one, landing it as status='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 the is_demo_manager persona → DEMO_MANAGER_USER_ID → any super-admin. Wired as an index.ts loop (6h) with recordHeartbeat('demo_manager_tick') + CRON_REGISTRY.demo_manager (OFF by default — enable at /dashboard/admin/crons). Remaining Phase-3 refinement: diff the full feature-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-posting candidates 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 (LLM steps[] 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) for feature_demos + studio_demos status/video columns + ai_personas.is_demo_manager.
  • Extend: services/persona-browser/session.ts (type, highlight verbs + a recordVideo capture context), services/studio-demos.ts (status lifecycle), routes/studio-demos.ts (/public, review actions), components/studio-demo/GuidedDemo.tsx (generic action verbs alongside mode:*), 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).

AUTO DEMO SYSTEM — Docs | HiveJournal