Trade With Viet
Session Explainer
Internal use only — enter access password
pipeline

BEUP Clip: CNBC Gap Fixes + Product Demo Recording

2026-06-15 · explainer.tradewithviet.com

BEUP Clip pipeline upgraded to CNBC-quality standard with real product demo B-roll

A 5-gap analysis against CNBC short-form production was resolved: real MP4 video backgrounds (not JPEGs), generic bar charts, a hook-scene mandate, and a new demo_recording field that swaps generic stock footage for actual screen recordings of Canvas dashboards, Google Sheets, or Notion pages.

StatusDoneScopebeup-clip pipelineDate2026-06-15Files changed5

6-layer thinking chainChuỗi tư duy 6 tầng

L1
Raw requestYêu cầu gốc
"kiểm tra xem đây có phải là gap ko, nếu có thì bổ sung; còn gap gì khác ko" — 5-gap CNBC analysis
L2
Reframe — what was really neededDiễn giải lại — nhu cầu thật
The pipeline produced animated-text knowledge clips but used static JPEGs for all backgrounds and had no path to show the actual BEUP product being used. At CNBC level, two things matter: cinematic video B-roll behind text scenes, and at least one scene showing the real product in motion. The gaps were structural — not content gaps but pipeline gaps.
L3
ConstraintsRàng buộc
['Playwright Chromium sandbox cannot load file:// video URLs — requires a local HTTP server', 'Existing runners (canvas-recording-runner.js, beup-notion-recorder.js) are already URL-based but have no bridge to beup-clip', 'google-sheets-recording-runner.js is profile-based with no generic URL mode', 'Two of the five gaps (color grading, lower thirds) were already implemented — reading code before building is non-negotiable']
L4
Options weighedCác lựa chọn cân nhắc
['Option A: Implement all 5 gaps fresh — wasted effort if some already exist', 'Option B: Verify each gap by reading actual code, then build only what is missing — chosen', 'Option C: Treat recording as a separate workflow (beup-video-record), never bridge — leaves product demo scenes using generic stock forever', 'Option D: Add a video_query prefix convention (e.g. canvas:slug) — conflates query semantics with recording dispatch, harder to read']
L5
Principle invokedNguyên tắc áp dụng
Verify before building: read the code first. Two of five gaps were false alarms (color grading via beup-warm.cube was already wired; lower-third source citations were already in the renderer). Implementing them again would have duplicated logic silently.
L6
Pick + recognition signalLựa chọn + dấu hiệu nhận biết
Built only Gaps 1, 4, 5 (video B-roll + generic bar chart + hook mandate). Then added a new demo_recording field bridging to the existing recording runners. Rejected: prefix convention (opaque), separate workflow only (no bridge). Recognition signal: next time a pipeline has multiple sources with priority, add a named field per source type rather than overloading a single field with prefixes.

How the pieces connect

beup-clip-broll.py is now a dual-mode asset fetcher: it fetches generic stock video (Envato cache → Pexels) for video_query scenes AND dispatches to platform-specific Playwright runners for demo_recording scenes. Both write to broll-map.json — the renderer reads from that single file and picks the highest-priority source per scene.

Beatsheetbeatsheet.vi.jsonB-roll Fetcherbeup-clip-broll.pybroll-map.jsonvideos + demos dictsStock VideoEnvato → PexelsCanvas Runnercanvas-recording-runner.jsNotion Runnerbeup-notion-recorder.jsRenderermediaForScene() priority chain
Technical termThuật ngữ kỹ thuậtPlain nameTên dễ hiểuRoleVai trò
video_queryStock B-roll hintString field on a beatsheet scene; broll.py searches Envato cache then Pexels and saves the clip under broll-map.json["videos"]
demo_recordingProduct demo hintObject field with type/url/duration; broll.py dispatches to the matching Playwright runner and saves clip under broll-map.json["demos"]
mediaForScene()Source pickerIn renderer: checks demos[scene.id] first → videos[video_query] → photos[photo_query]. First match wins.
etsy-generic profileGeneric Canvas scroll tourPre-built interaction in canvas-recording-runner.js: opens any Canvas URL, does a scroll-down tour at 1080×1920. Works on any BEUP Canvas dashboard without a custom profile.
startFileServer()Local video serverNode.js http.createServer on port 0 (auto-assign), started in render.js main() before Playwright launches. Solves Chromium's file:// video block inside the recording sandbox.

Read the diagram top-down: beatsheet feeds broll.py which fans out to either the stock-video APIs (for generic scenes) or the Playwright runners (for product demo scenes). Both outputs merge into broll-map.json. The renderer reads that single map and applies a three-tier priority chain at render time — so a scene can have all three fields set and the best available source wins automatically.

Lesson to carry forward: When you have multiple media sources with priority, encode each as a named field in the data (demo_recording / video_query / photo_query) and resolve at read-time. Overloading a single field with prefix conventions (canvas:slug) trades brevity for confusion.

What was verified and built

1
Gap audit
Read render.js, beup-clip-mux.py, beup-warm.cube. Gaps 2 (color grading) and 3 (lower thirds) confirmed already implemented. No duplicate build.
2
Gap 1 — Video B-roll
Added startFileServer() to render.js (Node http on port 0), replaced photoDataUri() with mediaForScene() supporting video type, added .photo-layer video CSS with object-fit:cover + duotone filter. Fixed portrait-file bug in beup-clip-broll.py (was filtering width>height, corrected to height>width).
3
Gap 4 — Generic bar chart
Added barChartSvg() to render.js (SVG, scaleY animation, up to 6 bars, teal palette, highlight_first). Added bar_chart case to chartBlock(). Documented kind:bar_chart in beatsheet-schema.md (was missing from schema despite being in code).
4
Gap 5 — Hook mandate
Documented hook scene requirements in distillation-method.md: ≤14-word stat/question, video_query mandatory (aerial/action footage), source if citing data. Added stat_card kind documentation.
5
demo_recording field
Added extract_demos(), record_canvas_demo(), record_notion_demo(), record_sheets_demo(), record_demo() dispatcher to beup-clip-broll.py. Demos written to broll-map.json["demos"]. mediaForScene() in render.js checks demos[scene.id] before stock video. Full field documented in beatsheet-schema.md and distillation-method.md.

Layered decision cardsCác quyết định theo tầng

Verify before building any gap fix
L1Gap analysis said 5 things were missing from the pipeline
L2The claim was based on external observation, not code inspection. Two gaps may already exist.
L3Code inspection takes 5 minutes. Duplicate implementation creates silent conflicts (two LUT paths, two source citation renderers).
L4A: Fix all 5 immediately — fast but risky. B: Read the code first, then build only what is missing — chosen. C: Ask user which are real — puts research burden on user.
L5Read the code before writing code. External gap analysis is a hypothesis, not a fact.
L6Gaps 2 and 3 were already wired (beup-warm.cube + --lut flag; renderer lines 188-193 for source citation). Saved building two features that were already there. Recognition signal: if a gap analysis was written by observing behavior, not reading source, verify in code first.
Local HTTP server to serve video files in Playwright
L1Playwright Chromium needed to play MP4 video backgrounds but file:// URLs are blocked inside the sandbox
L2The renderer runs a Chromium instance via Playwright's recordVideo. That Chromium instance cannot load local file:// URLs for
L3Playwright sandbox blocks file:// video. The video files exist on disk. The renderer already has control of the Node.js process that spawns Chromium.
L4A: Convert video to data URI (base64) — mp4 files are 10-100MB, would bloat the HTML and likely OOM the page. B: Serve via a local HTTP server started by the renderer — zero HTML size impact, fast. C: Pre-process all clips to data URIs — same OOM problem. D: Use a CDN URL — requires upload, adds latency.
L5The render process owns the Node.js environment. Spin up a local HTTP server (port 0 = auto-assign) in the same process, serve files by path, close it after recording. Zero external dependency.
L6Added startFileServer() using Node's built-in http module (no npm dependency). Server closes in the finally block after Playwright recording. Recognition signal: when Playwright's Chromium sandbox blocks local file access, a localhost HTTP server in the parent Node process is the minimal solution.
demo_recording as a named field, not a video_query prefix
L1Product demo scenes need actual screen recordings, not generic stock B-roll — how to express this in the beatsheet?
L2The beatsheet is the single source of truth read by both the broll script and the renderer. The field needs to be expressive enough to dispatch to three different runners (canvas/notion/sheets) without being opaque.
L3video_query is already used for stock B-roll search. Existing runners have different CLIs and different output conventions. The renderer must know which dict to look in (videos vs demos) to find the clip.
L4A: Prefix convention on video_query (canvas:url) — overloads the field, hard to read, parsing fragile. B: Separate named field demo_recording with type/url/duration — explicit, self-documenting, extends cleanly. C: Pre-bake demo clips manually and reference them as video_query — works but breaks the pipeline automation. D: Call beup-video-record before beup-clip — separate workflow, no automation bridge.
L5Named fields over conventions: when a field means fundamentally different things (stock search vs screen recording), use two fields — not one field with a parsing rule.
L6Added demo_recording: {type, url, duration, click?, profile?, segment?}. broll-map.json gets a demos dict alongside videos. mediaForScene() checks demos[scene.id] first. Recognition signal: if a field is being overloaded to mean two different things via a prefix/convention, split it into two named fields.
bar_chart in schema vs only in code
L1bar_chart kind was added to beup-clip-render.js but never documented in beatsheet-schema.md
L2The schema is the contract beatsheet authors read. If a feature exists in code but not in the schema, it's effectively invisible — authors won't use it and future maintainers won't know it exists.
L3beatsheet-schema.md had cycle/trap/pareto documented but not bar_chart. The feature was implemented in render.js but undiscoverable.
L4A: Leave schema as-is, document in distillation-method.md only — split docs, one place to check. B: Add to schema with full jsonc example — single authoritative reference. C: Add a TODO comment in schema — deferred, likely never done.
L5The schema is the canonical contract. If code and schema drift, schema wins for discoverability — update it immediately after adding a feature.
L6Added full bar_chart section to beatsheet-schema.md with jsonc example including all fields. Recognition signal: after adding a new scene kind to the renderer, always add it to beatsheet-schema.md in the same commit.

Artifact mapBản đồ tệp tạo ra

PathĐường dẫnWhatLà gìWho reads itAi dùng
~/beup-workspace/scripts/beup-clip-render.jsAdded startFileServer(), mediaForScene() with video+demo priority chain, barChartSvg(), .photo-layer video CSS, demo lookup in mediaForScene()Playwright renderer
~/beup-workspace/scripts/beup-clip-broll.pyAdded subprocess import, extract_demos(), record_canvas_demo(), record_notion_demo(), record_sheets_demo(), record_demo() dispatcher, demo loop in main()B-roll + demo fetcher
~/.claude/skills/beup-clip/references/beatsheet-schema.mdAdded demo_recording field docs (canvas/notion/sheets examples), bar_chart section with jsonc exampleBeatsheet authors / LLM prompt
~/.claude/skills/beup-clip/references/distillation-method.mdRewrote background-media section: demo_recording > video_query > photo_query priority; added hook mandateBeatsheet authors / LLM prompt
~/.claude/skills/beup-clip/SKILL.mdUpdated Step 3 B-roll description to mention demo_recording dispatch and recording runnersSkill orchestration reference
Pending manual actions — required before fully liveViệc cần làm thủ công — trước khi hoàn tất
  • Integration test: create a beatsheet with demo_recording:{type:canvas, url:canvas.beup.space/restaurant-delivery?demo=1, duration:8} and run beup-clip-broll.py --dry-run to verify it's detected
  • For sheets type: add a 'generic' profile to google-sheets-recording-runner.js if you want URL-only Sheets recording without a named profile
  • Beat sync (music dip at key stat moments) — not implemented; would need per-scene timing metadata + FFmpeg volume filter with timestamps

Check your understanding

Why can't Playwright's Chromium load video files via file:// URLs during screen recording?
Why was demo_recording added as a separate field instead of using a prefix on video_query (e.g. 'canvas:url')?
In your own words: what is the priority order in mediaForScene() and why does demo_recording rank highest?
Mastery checklist — tick what you can explain unpromptedBảng tự đánh giá — tích những gì bạn tự giải thích được