🔢 AUDIO[] as the single source of truth for timing
The array AUDIO[] contains the REAL durations (measured with ffprobe) for each WAV narration. It’s the only variable you need to fill in so that all video timing — HTML, GSAP, and audio — stays automatically synchronized.
The durations array governs everything. No time value appears anywhere else in the code. Change a WAV, update the corresponding entry in AUDIO[], run the generator — the entire video recalibrates automatically.
This is possible because the generator runs in Node.js and produces a static HTML file with all timings already calculated. The browser receives ready-made numbers — it doesn't need to calculate anything at runtime.
- ✓
data-startedata-durationof each.scene.clip - ✓ Absolute times for all GSAP tweens via
anim(i, s.start) - ✓
data-startof the elements<audio>on track 20 - ✓
TOTALof the composition and progress bar
- ✗ Kokoro produces different durations depending on the text and speed (
--speed 0.98) - ✗ Wrong estimate = permanently out-of-sync audio and visuals
- ✗ Headless rendering doesn't alert you — the video simply comes out wrong
- ✗ Never guess durations — always use
ffprobe
⏱️ The calculation of S[] — the timing series
The series S is generated by a single AUDIO.map(). Each object carries all the timing needed for scenes, captions, audio, and tweens — calculated once and propagated everywhere.
Three constants control all the pacing: LEAD=0.5 (visual comes in before the voice), TAIL=0.9 (visual holds after the voice) and FADE=0.45 (fade-in/out duration of the .scene-inner). An accumulator t grows with each scene.
Fundamental formulas: dur = LEAD + a + TAIL · audioStart = t + LEAD · end = t + dur.
After generating the HTML, the template displays in the console: OUT gerado · W×H · TOTAL = Xs · N cenas, followed by one line per scene with start, dur, audio@, and audioDur. Check these numbers before rendering.
🎬 sceneN() and anim(i,t) for each scene
Each scene has two functions: sceneN() returns the inner HTML of .scene-inner, e anim(i,t) generates GSAP code strings that will be embedded in the <script> from the final HTML. The fade of the .scene-inner is always generated by the shared part of anim().
The generator runs in Node.js, but GSAP executes in the browser (headless Chrome). anim() builds JavaScript strings with the already calculated absolute times — when the browser loads the HTML, these strings run with exact numbers, without recalculation. The fade of the .scene-inner is common to all scenes; the internal switch adds each scene’s specific tweens.
HyperFrames forces opacity:1 in the active clip. If you try to animate the .clip or the .scene, the fade animation doesn’t work. The animatable wrapper is always the child .scene-inner. The template already generates the fades for it—don't remove those lines.
🧱 Assembling the complete HTML
The four streams (scenesHTML, captionsHTML, audioHTML on track 20, animJS) are assembled into a final template string with the paused GSAP timeline recorded in window.__timelines["main"]. The environment (glow/grid) and the sentinel tl.set({},{},TOTAL) are also part of the assembly.
The final assembly is one giant template string that stitches together the four streams. Captions go on alternating tracks (2/4), and audio goes on track 20 (special). The GSAP timeline is created paused and registered in window.__timelines["main"] — the HyperFrames player controls it externally.
The sentinel tl.set({}, {}, TOTAL) extends the timeline to the end of the composition, ensuring that the progress bar reaches the end even when there are no tweens after the last scene.
yoyo:true covering the entire TOTAL. data-layout-ignore — does not interfere with the HyperFrames layout.backgroundPositionY+=128 throughout the entire video. Creates a sense of movement.SKILL.md decorative. Both with data-layout-ignore.📱 9:16 overrides via body.v and flag --vertical
The generator supports two formats: 16:9 (1920×1080) standard and 9:16 (1080×1920) for Shorts. The flag --vertical swap W and H, add the class body.v in the generated HTML and applies automatic CSS overrides. The output is always index.html.
The same build-index.mjs generates 16:9 and 9:16. The timing logic is identical — only the W/H dimensions change, along with the CSS for the body.v adjusts fonts, padding, and layout. You render twice with the same font, producing two synchronized formats.
Typical workflow: node build-index.mjs → renders 16:9 → node build-index.mjs --vertical → renders 9:16. Both overwrite index.html — never edit the HTML directly.
v on the body. Default CSS with large fonts for 1080p screens.class="v" to the body. CSS overrides adjust fonts and layout for mobile.🏁 The scene9() / case 9 CTA — unchanging signature
Scene 9 is the standard sign-off for all INEMA.CLUB videos. It shows "CONTINUES AT" + INEMA.CLUB with a glow and the URL 🌐 inema.club. It must never be removed, repositioned, or changed—it's part of the channel identity.
Scene 9 serves as a brand signature: anyone watching an INEMA.CLUB video always sees the same ending. This builds recognition and directs viewers to the site. The template already includes the CTA narration (s9.wav) and the HTML — you only provide the actual duration in AUDIO[].
Rule: AUDIO[8] (index 8, scene 9) is always the duration of assets/audio/s9.wav. Default narration: "This is INEMA dot CLUB content. Visit: inema dot club."
The CTA is at position 9 of BODIES[] and at index 8 of AUDIO[]. Removing or repositioning it breaks the timing of the entire video and removes the channel signature. When adapting the template for a new video, change only scenes 1–8 and update the durations in AUDIO[0..7]. AUDIO[8] is always the CTA WAV.
- ✓ Copy the entire template as
build-index.mjs - ✓ Fill in
AUDIO[0..7]with ffprobe — keepAUDIO[8] - ✓ Write
scene1()–scene8()— keepscene9() - ✓ Code
case 1–case 8inanim()— keepcase 9 - ✓ Run the generator and check the timing log before rendering
- ✗ Remove
scene9()ofBODIES[] - ✗ Remove
AUDIO[8]or leave the array with fewer than 9 entries - ✗ Replace scene 9 with content from another video
- ✗ Change the colors
.cta-inema/.cta-club
📋 Module 3.3 summary
- ✓
AUDIO[]controls 100% of the timing — HTML, GSAP, and audio - ✓
S[] = AUDIO.map()with LEAD/TAIL/FADE calculates start, dur, audioStart, end - ✓
sceneN()returns HTML;anim(i,t)generates GSAP strings with a fade on the.scene-inner - ✓ Four streams assembled in the HTML: scenesHTML, captionsHTML, audioHTML (track 20), animJS
- ✓ GSAP timeline paused at
window.__timelines["main"], sentineltl.set({},{},TOTAL) - ✓ Flag
--verticalswap W/H and addbody.v— output alwaysindex.html - ✓
scene9()/case 9is the INEMA.CLUB CTA — never remove it