# LynkedUp Pro — Live Product Film (design) **Date:** 2026-05-30 **Status:** Approved (pending user review of this spec) **Supersedes:** the still-image marketing film in `marketing/product-film/` (kept only as history) --- ## Goal Build a new flagship product film (~15–16 min, hard cap 20 min) for a B2B audience of roofing-company **owners** and **operators**, in which **real, live product interactions** are the backbone: cursor moves, clicks a control, the system loads, and the result is revealed on camera. It replaces the previous Ken-Burns-over-stills film as the primary marketing/sales asset. ## Audience & constraints - **Dual B2B audience:** owners (oversight, control, ROI) + operators (daily workflow). - **Format:** 1080p (1920×1080), 16:9, 30fps, **dark mode**. - **Narration:** on-screen **captions + music bed + a written VO script** delivered in the repo for voicing later. No spoken VO generated now. - **Bookends:** open on the real landing-page hero; **close on the swinging-crane landing CTA** (signature closer). - **Zero CRM source changes from the film pipeline.** The capture stage only *drives* the running app via Playwright; the synthetic cursor is injected from the Playwright side, never added to app source. - **One deliberate exception (separate workstream):** the AI-chat model + data-context update in `src/components/Chatbot.jsx` (see §9). This is an intentional product change the AI Assistant segment depends on, not a film artifact. --- ## Architecture — two-stage pipeline We reuse the existing `marketing/product-film/` Remotion project (theme, captions, audio, build scripts) and add a **video capture stage** beside the current still capture. ``` CAPTURE → Playwright drives the running CRM (local dev server, real APIs), records each scripted interaction to video (one .webm per beat) → ffmpeg → constant-30fps public/clips/.mp4 + clips.manifest.json (per-clip metadata + caption cues). COMPOSE → Remotion stitches clips + caption/pillar overlays + music + crane bookends → out/lynkedup-pro-live-film.mp4. ``` We drive the **local dev server with the real APIs** (the existing vite dev runner that loads `.env` and invokes real `/api` handlers), so async loads (e.g. `/api/storm-history`) genuinely populate on camera — the "system is thinking" reveal is real. ### Why this shape - True video frames preserve smooth load/transition animations (modal fades, map tiles streaming, panels populating) that a screenshot burst would stutter through. - One clip per beat keeps each segment independently re-filmable and the build incremental. --- ## Synthetic cursor (the key technique) Playwright's recorded video has **no OS cursor** (headless Chromium has no visible pointer). We inject one from the Playwright side: - `scripts/lib/cursor.js` exports a page-side init script (injected via `page.addInitScript`) that appends a branded pointer element (`pointer-events:none`, top z-index) to `` and exposes `window.__cursor.moveTo(x, y, ms)` (eased move) and `window.__cursor.clickPulse()` (ripple). - Each scene step: ease the visual cursor to a target → fire the **real** `page.mouse.click` at the same coordinates (so the app actually responds) → trigger the click pulse. - The visual cursor and the real pointer stay in lockstep, baked into the footage. No post-hoc cursor compositing, no drift. This injection happens entirely from Playwright; **no app source is modified.** --- ## Scene scripting model Each beat is an isolated scene file: `scripts/scenes/.mjs` exporting `async (page, cursor, ctx) => {...}` describing the choreography. Scenes are grouped by login role; the orchestrator logs in once per role and films that role's scenes in one SPA session (auth is in-memory, so we navigate via `history.pushState` + `popstate`, never `page.goto`, exactly as the existing still capture does). **Async reveals are driven by the real event, not a fixed sleep** — e.g. Storm Intel uses `page.waitForResponse(/\/api\/storm-history/)` so the clip captures the actual panel fill. **Form typing** uses `page.type()` so text appears field-by-field on camera (lead creation). **Multi-role:** owner (`justin`), field-agent (`LUP-1040`), sub-con (`carlos`), password `password`. The orchestrator already groups scenes by role. ### Beats vs. scene files The film has **21 beats** (`0a`, `0b`, and `1`–`19` in the structure table below). Two of them — **`0b` Promise** and **`18` Breadth flash** — are caption-over-**still** b-roll that reuses the existing `capture.mjs` stills, so they need **no** video scene file. That leaves **19 live video scene files** in `scripts/scenes/`: `landing-hero`, `storm-intel`, `territory-map`, `lead-create`, `lead-verify`, `dispatch`, `kanban`, `sub-tasks`, `pro-canvas`, `estimates`, `owner-snapshot`, `operator-dashboard`, `people-skills`, `project-detail`, `documents`, `org-access`, `role-perspective`, `ai-assistant`, `crane-close`. --- ## Film structure (~15–16 min, 8 acts, 19 segments) | # | Act / Segment | Live interaction | ~sec | | --- | ---------------------------- | -------------------------------------------------------------------------------- | ---- | | 0a | Landing hero | Real landing, ambient glow, headline | 12 | | 0b | Promise | Pain→promise captions over subtle b-roll | 18 | | **ACT 1 — Win the work** | | 1 | Storm Intelligence | Pin-drop → NOAA/IEM storm history fetches & fills (hail/wind/flood) | 55 | | 2 | Territory Command | Pin + draw a zone → assign canvasser (ZoneAssignmentModal) | 50 | | 3 | Lead Creation | Storm zone → CreateLeadPage, Quick/Full toggle, type fields, save → toast | 60 | | 4 | Lead Verification | Verify/enrich an inbound lead | 30 | | **ACT 2 — Run the operation** | | 5 | LynkDispatch | Click lead card → AI recommendation drawer → accept → log + storm-mode toggle | 55 | | 6 | Pipeline (Kanban) | Drag a lead card across stages → campaign toast countdown → edit message | 50 | | 7 | Subcontractor Tasks | Owner assigns a sub task → status flow | 35 | | **ACT 3 — Estimate & sell** | | 8 | Pro-Canvas / Estimate Builder| Measurements, template pick, XP/level-up gamification | 60 | | 9 | Estimates library | Open estimate → material/cost breakdown | 35 | | **ACT 4 — See the business** | | 10 | Owner Snapshot | Click KPI card → FinancialDetailsModal; budget/pie/funnel charts; commission; storm attribution | 55 | | 11 | Operator Dashboard | Animated revenue counter, live weather widget, leaderboard | 30 | | **ACT 5 — Manage the team** | | 12 | People & Skill Scores | Search person → detail → edit a skill score → save; masked PII | 50 | | 13 | Project Details (tabbed) | Open a project → cycle tabs (exact tab set confirmed from OwnerProjectDetail.jsx) | 55 | | 14 | Documents | Document library browse/preview | 25 | | **ACT 6 — Control & trust** | | 15 | Org Settings / Access Control| Toggle a permission in the matrix → change-log records it; commission rules edit | 55 | | 16 | Role Perspective | Same workspace logged in as owner → employee → sub (access-control proof) | 35 | | **ACT 7 — Intelligence** | | 17 | AI Assistant | Ask the assistant a scripted question → streamed end-to-end answer (see §9) | 35 | | **ACT 8 — Close** | | 18 | Breadth flash | Fast cuts: vendors, documents, remaining screens (still b-roll) | 18 | | 19 | Crane close | Swinging-crane landing CTA + end card / logo | 22 | **≈ 14 min of segments + titles/transitions → ~15–16 min final.** Within the 20-min cap. --- ## Remotion composition **New components** - `src/LiveFilm.tsx` — new `` root sequencing all clips + bookends + act titles. - `src/components/ClipStage.tsx` — wraps an `` clip with optional browser chrome (ScreenFrame), a subtle zoom-punch on key-moment timestamps, and caption overlays read from the manifest. - `src/clips.manifest.json` — per-clip metadata: ```jsonc { "fps": 30, "clips": [ { "id": "storm-intel", "file": "clips/storm-intel.mp4", "role": "owner", "durationFrames": 1650, // from ffprobe "act": "Win the work", "pillarTitle": "Storm Intelligence", "keyMoments": [ { "frame": 420, "zoom": 1.08 } ], "captions": [ { "fromFrame": 60, "toFrame": 240, "text": "Drop a pin anywhere." } ] } ] } ``` - `src/Root.tsx` — register `LiveFilm` as the **default** composition; keep the old `Film` registered as a secondary composition. **Reused components:** `theme.ts`, `Caption`, `PillarTitle`, `AudioTrack`, `BlueprintBg`, `Crane` / `CraneCard` / `craneSwing` (close), `TypeReveal`. **VO script:** `marketing/product-film/VO-SCRIPT.md` — per-segment narration lines aligned to caption timing, for voicing later. --- ## Capture scripts (Node, Playwright) **New** - `scripts/capture-video.mjs` — orchestrator: parse `--scene ` (single) or all; group scenes by role; login per role; for each scene set up a `recordVideo` context (1920×1080), run the scene script, close context to flush the `.webm`; convert each `.webm` → constant 30fps `.mp4` via `ffmpeg-static`; probe duration; write/refresh `clips.manifest.json`. - `scripts/lib/cursor.js` — injected synthetic-cursor page script + a Node-side `cursor` helper that drives moves/clicks. - `scripts/lib/record.js` — `recordVideo` context helpers; webm→mp4 + ffprobe-duration via `ffmpeg-static`. - `scripts/scenes/*.mjs` — one file per beat (see scene list). **Reused:** `scripts/capture.mjs` for breadth-flash stills. **New dependency:** `ffmpeg-static` (avoids a system ffmpeg install on Windows). --- ## §9 — AI Assistant (separate CRM workstream) > **Status (2026-05-30): model swap + data budgeting DONE.** Implemented and verified in > `src/components/Chatbot.jsx` (helper unit tests 8/8; esbuild parse clean). Remaining AI work > before filming segment 17: lock the deterministic demo question(s) and verify a complete > end-to-end answer **live** against the running app. The AI Assistant segment depends on deliberate changes to `src/components/Chatbot.jsx`: 1. **Model swap [DONE]:** `model: 'qwen/qwen3-32b'` → **`'openai/gpt-oss-120b'`** (now via an `AI_MODEL` const). gpt-oss-120b is a non-reasoning instruct model, so the existing ``-stripping regex becomes a no-op — cleaner streaming for on-camera capture. 1a. **Context budgeting [DONE]:** added `capList()` caps on every previously-unbounded role list (OWNER/CONTRACTOR projects 10, CONTRACTOR milestones 10, ADMIN non-compliant vendors 8, SUBCONTRACTOR tasks 10, FIELD_AGENT pending-sigs 8), a hard `clampToTokenBudget()` on the assembled system prompt (4,000 tok ≈ 16K chars), and a 12-message history bound. Makes a context-window overflow structurally impossible regardless of mock-data volume. 2. **"Update the data properly":** ensure `generateRoleContext(user, storeData)` provides an **accurate and sufficient** (right-sized, not dumped) data context so a scripted demo question gets a complete, end-to-end answer. Concretely: - Audit the role used for the demo (default: OWNER `justin`) and confirm the summarized slices (KPIs, projects, milestones, vendor compliance, documents, personnel) are internally consistent with the mock store the rest of the film shows. - Trim the heaviest lists to the highest-signal entries so the answer is focused, while keeping enough breadth that the demo "feels end to end." - Define **one deterministic demo question per filmed role** that the trimmed context fully and correctly answers. 3. **Capture reliability:** the scene films a real streamed answer; if the Groq backend is slow/flaky during capture, the scene falls back to a scripted prompt + pre-seeded reply so the segment is reliable. (The demo question is deterministic regardless.) This workstream is sequenced **before** the `ai-assistant` capture scene and is the only part of the project that edits CRM source. --- ## Risks & mitigations - **Leaflet tiles load async** → wait for tile-settle before interacting (else blank-map clips). Pre-warm + settle delay. - **Nondeterministic API timing** → `waitForResponse` on the real endpoint drives the reveal. - **recordVideo records CSS pixels** (no deviceScaleFactor) → record at 1920×1080 = exactly 1080p; convert to constant 30fps with ffmpeg. - **Pro-Canvas / Estimate Builder** is the highest-scripting-risk scene (drawing canvas, measurements, templates, XP) → script carefully; the level-up modal, if triggerable, is a delight moment. - **AI Assistant backend** → deterministic prompt + `waitForResponse`, scripted fallback reply. - **Render scale** → ~15–16 min 1080p30 is a large render (likely 30–60+ min, multi-hundred-MB mp4). Output `out/lynkedup-pro-live-film.mp4` is gitignored (as before). --- ## Testing - `--scene ` films one beat at a time for fast iteration. - After capture, ffprobe asserts each clip's duration; a pre-render smoke check fails fast if any manifest-referenced clip file is missing. - `npx remotion preview` to scrub the assembled film; `npx remotion render` for final. - AI workstream: verify the demo question returns a complete answer from the trimmed context before filming `ai-assistant`. --- ## File structure summary **Create** - `marketing/product-film/src/LiveFilm.tsx` - `marketing/product-film/src/components/ClipStage.tsx` - `marketing/product-film/src/clips.manifest.json` - `marketing/product-film/scripts/capture-video.mjs` - `marketing/product-film/scripts/lib/cursor.js` - `marketing/product-film/scripts/lib/record.js` - `marketing/product-film/scripts/scenes/*.mjs` (19 live video scene files; see "Beats vs. scene files") - `marketing/product-film/VO-SCRIPT.md` **Modify** - `marketing/product-film/src/Root.tsx` (register LiveFilm as default) - `marketing/product-film/package.json` (add `ffmpeg-static`; capture-video script entry) - `src/components/Chatbot.jsx` (model swap + data-context update — §9, separate workstream) **Reuse unchanged** - `marketing/product-film/scripts/capture.mjs`, theme/caption/audio/crane components. --- ## Out of scope - Spoken voiceover (script only, voiced later). - The old still-based `Film.tsx` (retained as secondary composition, not deleted). - Any CRM source change other than the AI-chat model + data context in §9.