15 KiB
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.
- One deliberate exception (separate workstream): the AI-chat model + data-context update
in
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/<id>.mp4 + clips.manifest.json (per-clip metadata + caption cues).
COMPOSE → Remotion <OffthreadVideo> 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.jsexports a page-side init script (injected viapage.addInitScript) that appends a branded pointer element (pointer-events:none, top z-index) to<body>and exposeswindow.__cursor.moveTo(x, y, ms)(eased move) andwindow.__cursor.clickPulse()(ripple).- Each scene step: ease the visual cursor to a target → fire the real
page.mouse.clickat 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/<id>.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<Series>root sequencing all clips + bookends + act titles.src/components/ClipStage.tsx— wraps an<OffthreadVideo>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:{ "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— registerLiveFilmas the default composition; keep the oldFilmregistered 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 <id>(single) or all; group scenes by role; login per role; for each scene set up arecordVideocontext (1920×1080), run the scene script, close context to flush the.webm; convert each.webm→ constant 30fps.mp4viaffmpeg-static; probe duration; write/refreshclips.manifest.json.scripts/lib/cursor.js— injected synthetic-cursor page script + a Node-sidecursorhelper that drives moves/clicks.scripts/lib/record.js—recordVideocontext helpers; webm→mp4 + ffprobe-duration viaffmpeg-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:
- Model swap [DONE]:
model: 'qwen/qwen3-32b'→'openai/gpt-oss-120b'(now via anAI_MODELconst). gpt-oss-120b is a non-reasoning instruct model, so the existing<think>-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.
-
"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.
- Audit the role used for the demo (default: OWNER
-
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 →
waitForResponseon 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.mp4is gitignored (as before).
Testing
--scene <id>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 previewto scrub the assembled film;npx remotion renderfor 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.tsxmarketing/product-film/src/components/ClipStage.tsxmarketing/product-film/src/clips.manifest.jsonmarketing/product-film/scripts/capture-video.mjsmarketing/product-film/scripts/lib/cursor.jsmarketing/product-film/scripts/lib/record.jsmarketing/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(addffmpeg-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.