Communitygithub.com

product-video

Produce a narrated product, demo or investor video with HyperFrames from a live web app, a prototype or a public Figma file. Covers the script with a claims table, asset capture (screens, scroll and interaction recordings, Figma frames), local TTS or a supplied narration, word timings aligned to the script, karaoke captions, a composition generator, validation and render. Use when the user asks for a product video, demo video, investor video, 'vídeo de apresentação', 'roteiro de vídeo', 'extrair assets para vídeo', a HyperFrames composition, or to re-voice or rebuild an existing one.

What is product-video?

product-video is a Claude Code agent skill that produce a narrated product, demo or investor video with HyperFrames from a live web app, a prototype or a public Figma file. Covers the script with a claims table, asset capture (screens, scroll and interaction recordings, Figma frames), local TTS or a supplied narration, word timings aligned to the script, karaoke captions, a composition generator, validation and render. Use when the user asks for a product video, demo video, investor video, 'vídeo de apresentação', 'roteiro de vídeo', 'extrair assets para vídeo', a HyperFrames composition, or to re-voice or rebuild an existing one.

Works with✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/arven-org/product-video-skill/tree/main

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

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

  1. 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.
  2. 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.
  3. 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.

SourceScriptOutput
Live web app, staticscripts/site-capture.cjsfirst fold, full page, step screenshots, scroll videos, cutouts
Live web app, flowsscripts/record-interaction.cjsinteraction clips with cursor or tap overlay
Public Figma filescripts/figma-frames.cjsone 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 with scripts/split-take.py --level, then join the scene files with scripts/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.py checks 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.html from 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.md and build a second, vertical composition from the same narration, starting from assets/gen-vertical-template.js. Never crop the 16:9 one.

Check: npx hyperframes lint has 0 errors.

P6. Verify

  1. npx hyperframes check must end with "Check passed" (lint, runtime, layout, motion, contrast).
  2. 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.
  3. 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.
  4. Render a draft (render -q draft), then verify the MP4 itself: ffprobe for duration and streams, loudnorm for 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

CodeSymptomCause and fix
X1Screens switch a second earlyat() matched the first "no" or "e". Anchor on phrases.
X2An element stays hidden for whole scenesTwo 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.
X3Site answers 403 to the script but opens in a browserHeadless user agent. Use lib/browser.cjs, which sends a desktop UA.
X4"Executable doesn't exist" on launchInstalled browser build differs from the Playwright version. lib/browser.cjs falls back to the newest local Chromium, then Chrome.
X5Full-page screenshot is cut at one foldThe app scrolls an inner container. Find the largest scrollable element and grow the viewport to its height.
X6Scroll recording stuttersScreencast frame rate is variable. Render scrolls frame by frame instead.
X7You keep checking a file that never changesThe recorder writes to a different folder than the one you inspect. Print and verify the output path.
X8A counter renders tiny beside a huge "%"A descendant selector (.num span) also matched the inner counter span. Use child selectors for labels.
X9Captions show a wrong wordCaptions came from the raw transcript. Align timings to the script text (voice.py does this).
X10The voice mispronounces the brand or a termWrite 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.
X11A second capture run erased the index of the firstMerge index files across runs instead of overwriting.
X12Video inside a timed wrapper fails the checkTimed media carries class="clip" itself; its container has no timing.
X13Prototype capture includes a toast or cookie barClose and hide overlays before the screenshot, then crop to the frame.
X14Recording shows prototype-only buttonsHide test and dev controls by selector or text prefix before recording.
X15Claims the product cannot prove"First", "total", "guaranteed" need a source. Leave them out or move them to "points to confirm".
X16A marketing number has no basisCheck 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.
X17Vertical cut is unreadableA 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).
X18Narration sounds dramatic or euphoricNon-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.
X19Layout reads like a web pageLeft-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.
X20Tagline sits far from the logoThe logo export has transparent padding. Trim the file and position against the real mark.
X21Narration is flat in some scenes and excited in othersEach 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.
X22Headlines look heavy and unlike the productThe 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.
X23Words run together in a headlineTight letter spacing copied from a web page closes the gaps between words at video sizes. Add word-spacing: .07em.
X24A glass tag looks like solid white, or its blur pops in after the fadeThe 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.
X25The contrast check fails on text that looks fineThe 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.

Related Skills