diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index 09c5a9362c..0c8972bf4a 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -139,17 +139,20 @@ async function captureSnapshots( }) .catch(() => {}); - // Wait for sub-compositions to be mounted by the runtime - // (they're fetched and injected asynchronously via data-composition-src) + // Wait for ALL sub-compositions to be mounted by the runtime. + // The old check resolved when the first sub-timeline registered, causing + // "last beat black" bugs: beat-5's sub-comp hadn't loaded yet when the + // snapshot seeked into its time range. Now we count data-composition-src + // host elements and wait until we have a matching number of sub-timelines. await page .waitForFunction( () => { const tls = (window as any).__timelines; if (!tls) return false; - const keys = Object.keys(tls); - // Wait until at least one sub-composition timeline is registered - // (not counting "main" or empty registrations) - return keys.length >= 2 || keys.some((k) => k !== "main"); + const hosts = document.querySelectorAll("[data-composition-src]").length; + if (hosts === 0) return Object.keys(tls).length >= 1; + const subKeys = Object.keys(tls).filter((k) => k !== "main"); + return subKeys.length >= hosts; }, { timeout: timeoutMs }, ) @@ -293,8 +296,19 @@ async function captureSnapshots( if (tls) { for (const key in tls) { if (tls[key]?.seek) { + // Sub-composition timelines run in local time relative to + // their data-start. Seeking them to global time causes beats + // with exit animations to appear black (global t clamps past + // the exit). Compute local time: global_t - data_start. + const host = document.querySelector( + `[data-composition-id="${key}"]`, + ); + const dataStart = host + ? parseFloat(host.getAttribute("data-start") ?? "0") || 0 + : 0; + const localTime = Math.max(0, t - dataStart); tls[key].pause(); - tls[key].seek(t); + tls[key].seek(localTime); } } } diff --git a/skills/website-to-hyperframes/references/step-4-vo.md b/skills/website-to-hyperframes/references/step-4-vo.md index b0bd80eece..119edd8d3d 100644 --- a/skills/website-to-hyperframes/references/step-4-vo.md +++ b/skills/website-to-hyperframes/references/step-4-vo.md @@ -73,12 +73,18 @@ After the provider is selected, audition at least 2 voices with the first senten -H "x-api-key: $HEYGEN_API_KEY" | python3 -c \ "import json,sys; v=json.load(sys.stdin)['data']; [print(x['voice_id'], x['name'], x['language']) for x in v[:10]]" - # Generate audio — returns audio_url + word_timestamps in one call + # Generate audio — response: { "data": { "audio_url": ..., "word_timestamps": [...] } } curl -s -X POST "https://api.heygen.com/v3/voices/speech" \ -H "x-api-key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text":"Your script here","voice_id":"VOICE_ID","speed":1.0}' \ - | python3 -c "import json,sys; r=json.load(sys.stdin); print(r['audio_url']); open('transcript_raw.json','w').write(json.dumps(r.get('word_timestamps',[]),indent=2))" + | python3 -c " + import json,sys + r=json.load(sys.stdin) + d=r['data'] + print(d['audio_url']) + open('transcript_raw.json','w').write(json.dumps(d.get('word_timestamps',[]),indent=2)) + " # Then download the audio curl -sL "AUDIO_URL_FROM_ABOVE" --output narration.mp3 @@ -129,7 +135,16 @@ For ElevenLabs and HeyGen TTS, substitutions are usually unnecessary — they ha ## Transcribe for word-level timestamps -**If you used HeyGen v3 TTS:** word timestamps were already returned in the generate call (`word_timestamps: [{ word, start, end }]`). Save them as `transcript.json` — the format matches what the beat mapping step expects. No separate transcription step needed. +**If you used HeyGen v3 TTS:** word timestamps were returned in the generate call. Normalize the format before saving — HeyGen v3 uses `word` but the pipeline expects `text`: + +```python +import json +raw = json.load(open('transcript_raw.json')) +normalized = [{"text": w["word"], "start": w["start"], "end": w["end"]} for w in raw] +json.dump(normalized, open('transcript.json', 'w'), indent=2) +``` + +No separate transcription step needed. **If you used ElevenLabs or Kokoro:**