Files
LynkedUpPro_CRM/docs/superpowers/specs/2026-05-30-live-product-film-design.md
T

296 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.