docs(film): live product film design spec + implementation plan

This commit is contained in:
Satyam Rastogi
2026-05-30 20:33:02 +05:30
parent 2ce3395006
commit 48f6d8d094
2 changed files with 1620 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,295 @@
# 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 (~1516 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/<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.js` exports a page-side init script (injected via `page.addInitScript`)
that appends a branded pointer element (`pointer-events:none`, top z-index) to `<body>` 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/<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 (~1516 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 → ~1516 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:
```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 <id>` (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
`<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.
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** → ~1516 min 1080p30 is a large render (likely 3060+ min, multi-hundred-MB
mp4). Output `out/lynkedup-pro-live-film.mp4` is 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 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.