PTENES
MODULE 2.5

✅ Validate & render

The final pipeline step: lint with no errors, inspect with no overflow, render a draft to review frame by frame, validate the voiceover, render high quality, and generate both formats—all in the right order.

6
Topics
~35
Minutes
Practical
Level
Deliver
Type
Validation & Rendering Pipeline LINT → INSPECT → DRAFT → HIGH → MP4 × 2 lint 0 errors overlapping inspect 0 overflow --samples 16 draft 1 frame/scene ffmpeg -nostdin 👁 high --fps 30 ~3–4 min 16:9 MP4 1920 × 1080 YouTube 9:16 MP4 1080×1920 Shorts ① LINT ② INSPECT ③ DRAFT ④ HIGH
1

🧹 npx hyperframes lint

The first quality gate: the linter analyzes your index.html generated and reports structural issues before you spend time rendering. Goal: 0 errors.

Main Concept

The linter reads the index.html generated (not the build-index.mjs) and validates the composition structure. Errors stop the render — warnings are informational.

Always run after node build-index.mjs e before of any render. Lint is fast (<2 s) and does not start Chrome.

Command
# generate index.html before running lint
node build-index.mjs
npx hyperframes lint

# expected output (0 errors)
✔ 0 errors, 0 warnings
The 3 most common errors
✗
overlapping_clips

Two scene clips have overlapping time intervals. Fix this by adjusting the array AUDIO[] no build-index.mjs — durations must add up in strict sequence.

✗
multiple_root_compositions

More than one element with data-composition at the root of the document. HyperFrames only accepts one composition per file index.html.

✗
google_fonts_import

The lint detects a @import url('fonts.googleapis.com/...') in the CSS. External fonts are prohibited because headless Chrome has no internet access during rendering. Use node fetch-fonts.mjs to download the .woff2 and serve locally via assets/fonts/fonts.css.

💡
Lint in the development loop

During composition, run node build-index.mjs && npx hyperframes lint each time there’s a significant change to build-index.mjs. Cost: less than 2 seconds. Prevents surprises at render time.

Key concepts
🧹
0 errors
Required goal
⏱️
<2 s
No Chrome
🔤
Local fonts
fetch-fonts.mjs
📋
AUDIO[]
Sequential
2

🔍 npx hyperframes inspect --samples 16

The inspector opens headless Chrome, captures 16 frames distributed throughout the video, and audits each one: layout, text overflow, elements outside the canvas. Goal: 0 issues.

Command
# 16 samples work well for videos up to ~110s
npx hyperframes inspect --samples 16

# expected output
✔ Inspected 16 frames — 0 layout issues found
✓ Best practices for 0 issues
  • ✓ Add data-layout-ignore in off-canvas decorative elements (giant background words, glows, .bg-layer)
  • ✓ Prefer left/right instead of width for highlight markers
  • ✓ Test long code in overflow-x: auto with an explicitly defined width container
  • ✓ Use z-index:-1 in the glows to prevent them from leaking outside the bounding box
✗ Common causes of overflow
  • ✗ Code-line text overflowing the container — shorten it or break it into lines
  • ✗ SVG without viewBox defined — the auditor cannot calculate bounds
  • ✗ position: absolute without overflow: hidden in the parent
  • ✗ Decorative elements without data-layout-ignore — marked as false-positive overflow
⚠️
Inspect does not replace visual review

The inspector detects bounding-box overflow, not readability or contrast issues. After 0 problemas in inspect, you still need to visually review frames from the draft render — especially in scenes with text over gradients.

Key concepts
🔍
16 samples
Good coverage
🏷️
data-layout-ignore
Decorative
📐
Bounding box
Actual overflow
🧪
Headless Chrome
Real render
3

🎬 Draft render — iterate quickly

The mode --quality draft generates a low-resolution MP4 in seconds. Extract one frame per scene with ffmpeg -nostdin and check it visually before spending time on the final render.

Frame-by-frame review workflow
1
Generate the draft MP4
node build-index.mjs # generates index.html
npx hyperframes render --quality draft --output renders/draft.mp4
2
Extract 1 frame per scene with ffmpeg

Use the scene start time as <t>. For a scene that starts at 12.5 s, use -ss 12.5. The flag -nostdin is required on Windows/git-bash to prevent ffmpeg from consuming stdin and exiting without generating the file.

# frame from the scene that starts at t=12.5s
ffmpeg -nostdin -y -ss 12.5 -i renders/draft.mp4 \
-vframes 1 -update 1 frame-cena3.png
3
Open the PNG with the Read tool

Claude can view PNG images directly. Extract one frame per scene and check text alignment, overflow, animation placement, and readability against the background. Repeat for every scene.

4
Fix it and repeat

Edit the build-index.mjs, run node build-index.mjs && npx hyperframes render --quality draft again. The draft loop is cheap—iterate freely before the final render.

📊 Draft vs High — when to use each
Aspect Draft High
Objective Visual review Final deliverable
Speed Fast (<30 s) 3–4 min / 110s of video
Quality Reduced Maximum (30 fps)
Usage Iteration loop Once, at the end
💡
Recommended minimum coverage

1 frame per scene works well. For an 8-scene video, that's 8 ffmpeg calls. Focus on transitions and titles — those are the parts most likely to overflow.

Key concepts
🎬
--quality draft
Rapid iteration
📸
-vframes 1
Single frame
🛡️
-nostdin
Win/git-bash
🔁
-update 1
Overwrites PNG
4

👂 Validate the voiceover with the user

Claude doesn’t listen to audio. The user is responsible for validating the narration — and it should happen before from the high render to avoid wasting 3–4 minutes of processing time.

⚠️
Claude can’t listen to WAVs

The image analysis tool (Read) works for PNGs and frames, but WAV audio isn’t supported. All voiceover validation must be done by the user listening to the files assets/audio/sN.wav directly.

What to validate in the voice-over

Before rendering high quality, confirm with the user: correct pronunciation of technical terms, speed (pf_dora --speed 0.98 is the default), natural pauses between scenes, and a duration that fits each scene’s visuals.

✓ Audio validation checklist
  • ✓ Listen to each assets/audio/sN.wav before the final render
  • ✓ Confirm that acronyms were expanded correctly (e.g., "GSAP" → "jee-sap" sounds natural?)
  • ✓ Check that the WAV duration matches the visual scene
  • ✓ Check via ffprobe if the measured duration matches the AUDIO[]
✗ Don't skip audio validation
  • ✗ Don’t render in high quality without listening to the WAVs — costly rework
  • ✗ Don't rely only on measured duration — the speech may be truncated
  • ✗ Don’t use a speed above 1.05 — the voice sounds metallic and artificial
  • ✗ Don’t ignore incorrect pronunciations of English terms in the PT-BR text
Confirm duration with ffprobe
# measure the exact duration of each WAV
ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
assets/audio/s1.wav

# result: e.g. 14.2327
14.2327

# use this value in AUDIO[0] in build-index.mjs
const AUDIO = [14.23, /* s2 */ 11.80, ...]
💡
Brazilian Portuguese voices available in Kokoro

Beyond pf_dora (female, recommended default with --speed 0.98), there are pm_alex e pm_santa for male voice variations. If a term is pronounced incorrectly, rewrite it phonetically in assets/txt/sN.txt and regenerate the WAV.

Key concepts
👂
Human review
Claude doesn't listen
🎙️
pf_dora
speed 0.98
⏱️
ffprobe
Measures duration
📝
AUDIO[]
Synchronizes the scene
5

🚀 High render — delivery quality

After a clean lint, an inspect with no overflow, a reviewed draft, and approved narration, it’s time for the final render: --quality high --fps 30. A ~110s video is equivalent to ~3,500 frames and takes 3–4 minutes on a typical machine with 22 cores.

What happens in the high render

HyperFrames opens Chrome headless at full resolution (1920×1080 or 1080×1920), captures each frame as a PNG, passes them all to FFmpeg, and encodes H.264 with the WAV audio embedded. At 30 fps, 110 seconds = 3,300 frames.

Render time varies with the number of available colors. With 22 colors, expect ~3–4 minutes for 110s of video.

Command (render 16:9 high-quality)
# final 16:9 render (1920×1080)
node build-index.mjs &&
npx hyperframes render \
--quality high \
--fps 30 \
--output renders/meu-video-16x9.mp4

# output during rendering
⠸ Rendering frame 1547/3300 (46.9%)...
✔ Render complete → renders/meu-video-16x9.mp4
📊 Estimated render times (22 colors)
Short video (~60s)
~1,800 frames
≈ 1.5–2 min of rendering
Standard video (~110s)
~3,300 frames
≈ 3–4 min of rendering
Long video (~180s)
~5,400 frames
≈ 5–7 min of rendering
✓ Before starting the high render
  • ✓ npx hyperframes lint — 0 confirmed errors
  • ✓ npx hyperframes inspect --samples 16 — 0 layout issues
  • ✓ Draft visually checked (1 frame/scene)
  • ✓ Voice-over approved by the user
✗ Don't render in high quality if...
  • ✗ Lint still reports errors — it will fail midway through the render
  • ✗ Didn’t view any frames from the draft — risk of rework
  • ✗ The user hasn’t listened to the WAVs — they may need to re-render
  • ✗ O index.html was not regenerated after the last edit
Key concepts
🚀
--quality high
Full resolution
🎞️
30 fps
Default delivers
⏳
~3,500 frames
110s video
🏁
H.264 MP4
Ready to upload
6

📐 Generate both formats — 16:9 and 9:16

From the same project, generate the 16:9 (YouTube) and 9:16 (Shorts/Reels) versions in sequence. The critical rule: render right after generating each format — never leave two index.html simultaneous at the root.

Single index.html Rule

O build-index.mjs always overwrites the same one index.html. If you run both modes before rendering, the second overwrites the first and you lose the composition. The correct sequence is: generate → render → generate another version → render.

Complete sequence (correct order)
# ── STEP 1: 16:9 format ──────────────────────────
node build-index.mjs # no flag → 1920×1080
npx hyperframes render \
--quality high --fps 30 \
--output renders/meu-video-16x9.mp4

# ── STEP 2: 9:16 format ──────────────────────────
node build-index.mjs --vertical # → 1080×1920
npx hyperframes render \
--quality high --fps 30 \
--output renders/meu-video-9x16.mp4

# result: 2 files in renders/
renders/meu-video-16x9.mp4 # YouTube
renders/meu-video-9x16.mp4 # Shorts / Reels
📊 Formats and destinations
16:9 — Horizontal
Resolution: 1920 × 1080 px
Destination: YouTube, Vimeo, LinkedIn
Flag: node build-index.mjs (without a flag)
Output: renders/nome-16x9.mp4
9:16 — Vertical
Resolution: 1080 × 1920 px
Destination: YouTube Shorts, Instagram Reels, TikTok
Flag: node build-index.mjs --vertical
Output: renders/nome-9x16.mp4
💡
Complete pipeline in one script

To automate both formats at once, chain them with &&:

node build-index.mjs && npx hyperframes render --quality high --output renders/v-16x9.mp4 &&
node build-index.mjs --vertical && npx hyperframes render --quality high --output renders/v-9x16.mp4

O && ensures the 9:16 render starts only after the 16:9 render finishes successfully.

✓ Correct sequence
  • ✓ node build-index.mjs → render 16:9 → node build-index.mjs --vertical → render 9:16
  • ✓ Always confirm which one index.html is active before rendering
  • ✓ Name outputs with the suffix -16x9 e -9x16 from the start
✗ Errors that cost render time
  • ✗ Run build-index.mjs e build-index.mjs --vertical without rendering between the two
  • ✗ Render without --output explicit — can overwrite a previous render
  • ✗ Use the same output name for both formats
Key concepts
📺
1920×1080
YouTube
📱
1080×1920
Shorts/Reels
🚩
--vertical
Single flag
⚡
Generate → render
Required order

📋 Module 2.5 summary

What you learned
  • ✓ npx hyperframes lint: the 3 fatal errors and how to fix each one
  • ✓ npx hyperframes inspect --samples 16: overflow, data-layout-ignore and false positives
  • ✓ Draft render + frame extraction with ffmpeg -nostdin -y -ss <t> -vframes 1 -update 1
  • ✓ Claude doesn’t listen to audio — the user is responsible for voiceover validation
  • ✓ High render: --quality high --fps 30 · ~110s ≈ 3,500 frames ≈ 3–4 min
  • ✓ 16:9 → 9:16 sequence: generate and render each format before generating the next
Next track
T3
🔧 Inside HyperFrames
You’ve completed Learning Path 2 — Pipeline. The next path dives into the framework’s internals: how headless Chrome controls time, how FFmpeg encodes, and how GSAP integrates with frame-by-frame rendering.
Start Track 3: Under the Hood →