Product video
Build a 60 to 120 second narrated video that presents a product using its real screens. The output is a HyperFrames project (one generated index.html plus assets) and a rendered MP4.
Write every deliverable (script, on-screen text, README) in the audience's language. This file is in English only because it is instructions.
Output layout
Create one folder per video, outside any code repository unless the user says otherwise:
<Product>-video/
ROTEIRO.md | SCRIPT.md script, claims table, points to confirm
LEIA-ME.md | README.md what is real, what is rebuilt, limits, how to re-run
01-.. 0N-.. captured assets, one folder per source
voz/ | voice/ narration segments (text and per-scene audio)
ferramentas/ | tools/ configs and gen.js (the composition generator)
hyperframes/
index.html GENERATED. Never edit by hand.
assets/{img,video,audio,fonts}
renders/
Workflow
Run the phases in order. Each one ends with a check; do not start the next phase until the check passes.
P1. Sources
- Locate every source the user named. A path from another machine will not exist: search for the same project under the current user's folders and say which one you used.
- Classify how the product can be shown:
- Live app, reachable: capture it (P3,
site-capture.cjs,record-interaction.cjs). - Public Figma file: capture frames through the prototype view (P3,
figma-frames.cjs). No token needed. - Behind a login with no access: ask for screenshots or a test account. Do not rebuild screens from memory.
- Live app, reachable: capture it (P3,
- Extract the product's own words: landing page copy, feature names, numbers, FAQ, terms. These are the only allowed source for claims.
Check: you can name the audience, the product's modules and the source of each fact you plan to use.
P2. Script
Read references/script.md and write the script file from its template. Rules that matter most:
- Budget 2.3 to 2.6 spoken words per second. 250 words is about 100 seconds.
- One idea per scene, 8 to 16 seconds per scene, 6 to 9 scenes.
- Every claim in the narration has a row in the claims table with its source. Leave out anything the sources cannot back.
- Write for the voice: brand names as they are pronounced, numbers spelled out, no abbreviations the voice would spell letter by letter.
Check: the claims table has no empty source cell, and the "points to confirm" list names every number that came from marketing copy.
P3. Assets
Read references/capture.md for config examples.
| Source | Script | Output |
|---|---|---|
| Live web app, static | scripts/site-capture.cjs | first fold, full page, step screenshots, scroll videos, cutouts |
| Live web app, flows | scripts/record-interaction.cjs | interaction clips with cursor or tap overlay |
| Public Figma file | scripts/figma-frames.cjs | one PNG per frame, cropped to the frame |
Also collect the brand: logo (vector if one exists), the brand font files with their license, and the color tokens from the product's CSS. Read the styles the product's own pages use with scripts/page-styles.cjs <url>: headline family, weight, line height and letter spacing, button styles and any frosted-glass surface. The video copies them instead of guessing.
Check: open a few captures and look at them. A capture that shows a login wall, a cookie banner or a dev overlay is a failed capture.
P4. Voice
Read references/voice.md. Ask which voice the user has before generating anything: a paid service reached through MCP or an API key comes first, a supplied narration second. A free local model is only for a user who asks for one, because it installs a model on their machine.
- Voice service through MCP: generate the whole narration as one continuous take (one paragraph per scene, one delivery tag at the start). Align it with
scripts/voice.py --existing, cut it at the pauses withscripts/split-take.py --level, then join the scene files withscripts/voice.py --parts "voz/cena-{id}.wav". Scene boundaries are exact and the tone stays the same from the first scene to the last. A narration requested scene by scene changes mood between requests. - Voice service through an API key:
scripts/voice.py --provider elevenlabs --el-voice <id>generates and joins.scripts/elevenlabs.pychecks the connection, ranks voices and composes music. - Narration supplied: pass
--existing. Scene boundaries come from aligning the transcript to the script.
With an expressive model, set the delivery with one [audio tag] at the start of the take. Each scene carries text (clean, for captions) and may carry tts (what the voice receives, when it differs).
In every mode the word timings are aligned to the script text, so captions never show a transcription error. Read the misheard list it prints: anything other than spelled-out numbers and homophones is a pronunciation problem. Fix the script text and regenerate.
Normalize the narration to about −18 LUFS before composing.
Check: voiceover.json exists, the total duration is within 10% of the target, misheard is explained, and scripts/voice-tone.py --scenes reports a spread of median pitch between scenes of about 3 semitones or less.
P5. Composition
Read references/hyperframes.md (the rules the renderer enforces) and start from assets/gen-template.js.
- Generate
index.htmlfrom a script. Timing changes every time the voice changes; a generator absorbs that, hand edits do not. - Anchor every animation to a phrase of the narration (
at(3, 'No painel')), never to a single short word. "no", "e" and "um" occur several times per scene and the first match is the wrong one. - Reserve the bottom 17% of the frame for captions. Nothing else enters that band.
- Show real screens inside a browser-window or device mockup. Put the explanation beside it, not on top of it.
- Prefer one clear highlight per sentence (a ring, a zoom, a cursor move) over many small ones.
- Headlines take the product's own headline style and tags take its surface language, both read in P3. Do not default to the heaviest weight.
- For stories, reels or shorts, read
references/vertical.mdand build a second, vertical composition from the same narration, starting fromassets/gen-vertical-template.js. Never crop the 16:9 one.
Check: npx hyperframes lint has 0 errors.
P6. Verify
npx hyperframes checkmust end with "Check passed" (lint, runtime, layout, motion, contrast).npx hyperframes snapshot --no-end --at <one time per scene, plus each highlight>and look at every frame. Build 2×2 review sheets with ffmpeg to review faster.- Compare each frame with what the narration says at that second. The usual defects: wrong screen for the sentence, a highlight on the wrong element, text wrapping, an element inside the caption band.
- Render a draft (
render -q draft), then verify the MP4 itself:ffprobefor duration and streams,loudnormfor loudness (target −16 to −17 LUFS with music), and frames extracted with ffmpeg.
Check: you have looked at a frame of every scene taken from the MP4, not only from snapshots.
P7. Deliver
Write the README with: what is real (captured), what was rebuilt in HTML with real data, what is conceptual, known limits, the commands to preview, check and render, and how to swap the voice. Then report to the user: what was made, what was verified, and what still needs a human decision (unconfirmed numbers, music, voice quality).
Pitfalls already paid for
| Code | Symptom | Cause and fix |
|---|---|---|
| X1 | Screens switch a second early | at() matched the first "no" or "e". Anchor on phrases. |
| X2 | An element stays hidden for whole scenes | Two fromTo tweens on the same element; the later one renders its "from" state immediately. Use one element per appearance, or to() from a CSS initial state. |
| X3 | Site answers 403 to the script but opens in a browser | Headless user agent. Use lib/browser.cjs, which sends a desktop UA. |
| X4 | "Executable doesn't exist" on launch | Installed browser build differs from the Playwright version. lib/browser.cjs falls back to the newest local Chromium, then Chrome. |
| X5 | Full-page screenshot is cut at one fold | The app scrolls an inner container. Find the largest scrollable element and grow the viewport to its height. |
| X6 | Scroll recording stutters | Screencast frame rate is variable. Render scrolls frame by frame instead. |
| X7 | You keep checking a file that never changes | The recorder writes to a different folder than the one you inspect. Print and verify the output path. |
| X8 | A counter renders tiny beside a huge "%" | A descendant selector (.num span) also matched the inner counter span. Use child selectors for labels. |
| X9 | Captions show a wrong word | Captions came from the raw transcript. Align timings to the script text (voice.py does this). |
| X10 | The voice mispronounces the brand or a term | Write it as spoken (a brand written as one word is narrated as two, a foreign term gets a native phrase), spell out numbers, expand abbreviations. |
| X11 | A second capture run erased the index of the first | Merge index files across runs instead of overwriting. |
| X12 | Video inside a timed wrapper fails the check | Timed media carries class="clip" itself; its container has no timing. |
| X13 | Prototype capture includes a toast or cookie bar | Close and hide overlays before the screenshot, then crop to the frame. |
| X14 | Recording shows prototype-only buttons | Hide test and dev controls by selector or text prefix before recording. |
| X15 | Claims the product cannot prove | "First", "total", "guaranteed" need a source. Leave them out or move them to "points to confirm". |
| X16 | A marketing number has no basis | Check where it came from before using it: search the docs and the git history of the page (git log -S'<number>'). A number raised by hand in a copy edit is not evidence. Drop it. |
| X17 | Vertical cut is unreadable | A desktop screen in 9:16 has 9 px text, and a crop of it still looks like a desktop. Show one adapted element at a time on a panel, the way the product's mobile landing page does (references/vertical.md). |
| X18 | Narration sounds dramatic or euphoric | Non-verbal tags ([sighs]), [excited] and [serious] overshoot in a product video. Use [calm] and [warm], regenerate only the scenes named, and compare takes with scripts/voice-tone.py. |
| X19 | Layout reads like a web page | Left-aligned column, section labels above headlines, a small logo in a corner. In vertical video: center everything, drop the labels, make the logo larger and centered. |
| X20 | Tagline sits far from the logo | The logo export has transparent padding. Trim the file and position against the real mark. |
| X21 | Narration is flat in some scenes and excited in others | Each scene was a separate request with its own tag, so each was a separate performance. Generate one continuous take with one tag, cut it at the pauses with scripts/split-take.py and measure with scripts/voice-tone.py --scenes. A spread of 5 semitones between scenes was heard as mood swings, 2 as even. |
| X22 | Headlines look heavy and unlike the product | The weight was assumed (800). Read the product's own headline style with scripts/page-styles.cjs and copy family, weight, line height and letter spacing. |
| X23 | Words run together in a headline | Tight letter spacing copied from a web page closes the gaps between words at video sizes. Add word-spacing: .07em. |
| X24 | A glass tag looks like solid white, or its blur pops in after the fade | The background is too opaque (keep the lower part near 0.6), or the fade is on a parent: opacity below 1 on an ancestor cuts the element off from what is behind it. Animate the element that has backdrop-filter. |
| X25 | The contrast check fails on text that looks fine | The check sampled a frame in the middle of a fade. Take a snapshot at the reported time. If the element is mid-entrance, move the entrance a few frames and run the check again. Very short compositions hit this most. |
Files in this skill
references/script.md: script template, pacing, claims table, pronunciation table.references/capture.md: configs for the three capture scripts, capture specs, brand assets.references/voice.md: TTS setup, voices, alignment, loudness, music, swapping the narration.references/hyperframes.md: composition rules, CLI commands, layout zones.references/vertical.md: 9:16 version for stories: safe area, camera on the screen, captions, splitting into parts.assets/gen-template.js: 16:9 composition generator to copy into the project's tools folder.assets/gen-vertical-template.js: vertical generator with a full and a short edition.scripts/lib/browser.cjs,scripts/site-capture.cjs,scripts/record-interaction.cjs,scripts/figma-frames.cjs,scripts/element-shots.cjs,scripts/page-styles.cjs,scripts/voice.py,scripts/split-take.py,scripts/voice-tone.py,scripts/elevenlabs.py,scripts/lib/eleven.py.
The ElevenLabs API code (--provider elevenlabs, elevenlabs.py) was written without a key to test against. Its first real run may need a fix; the --parts route and the local route are tested.