mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 13:27:13 +00:00
fix(snapshot): two sub-composition seek bugs causing black beats
Bug 1: snapshot fired before all sub-compositions loaded - Old: resolved when keys.length >= 2 (first sub-comp registered) - Fix: wait until all data-composition-src hosts have timelines - Verified: beats 2-4 now visible in huly-promo v4 snapshots Bug 2: sub-comp timelines seeked to global time, not local time - Old: tls[key].seek(globalTime) — with exit animations, global t=29 clamps to GSAP end (t=7), elements at opacity:0 from exit fade - Fix: seek to global_t - host.data_start (correct local time) - Verified: beat-5 (starts at t=26.7) seeked to local t=2.3 at global t=29 — 'Join the Movement' now visible in snapshots Both bugs confirmed via render vs snapshot comparison before fixing. Also fix HeyGen v3 API docs: response is data.audio_url not audio_url directly; word_timestamps uses 'word' key not 'text' — normalize before saving as transcript.json
This commit is contained in:
@@ -139,17 +139,20 @@ async function captureSnapshots(
|
|||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
// Wait for sub-compositions to be mounted by the runtime
|
// Wait for ALL sub-compositions to be mounted by the runtime.
|
||||||
// (they're fetched and injected asynchronously via data-composition-src)
|
// 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
|
await page
|
||||||
.waitForFunction(
|
.waitForFunction(
|
||||||
() => {
|
() => {
|
||||||
const tls = (window as any).__timelines;
|
const tls = (window as any).__timelines;
|
||||||
if (!tls) return false;
|
if (!tls) return false;
|
||||||
const keys = Object.keys(tls);
|
const hosts = document.querySelectorAll("[data-composition-src]").length;
|
||||||
// Wait until at least one sub-composition timeline is registered
|
if (hosts === 0) return Object.keys(tls).length >= 1;
|
||||||
// (not counting "main" or empty registrations)
|
const subKeys = Object.keys(tls).filter((k) => k !== "main");
|
||||||
return keys.length >= 2 || keys.some((k) => k !== "main");
|
return subKeys.length >= hosts;
|
||||||
},
|
},
|
||||||
{ timeout: timeoutMs },
|
{ timeout: timeoutMs },
|
||||||
)
|
)
|
||||||
@@ -293,8 +296,19 @@ async function captureSnapshots(
|
|||||||
if (tls) {
|
if (tls) {
|
||||||
for (const key in tls) {
|
for (const key in tls) {
|
||||||
if (tls[key]?.seek) {
|
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<HTMLElement>(
|
||||||
|
`[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].pause();
|
||||||
tls[key].seek(t);
|
tls[key].seek(localTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 \
|
-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]]"
|
"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" \
|
curl -s -X POST "https://api.heygen.com/v3/voices/speech" \
|
||||||
-H "x-api-key: $HEYGEN_API_KEY" \
|
-H "x-api-key: $HEYGEN_API_KEY" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"text":"Your script here","voice_id":"VOICE_ID","speed":1.0}' \
|
-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
|
# Then download the audio
|
||||||
curl -sL "AUDIO_URL_FROM_ABOVE" --output narration.mp3
|
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
|
## 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:**
|
**If you used ElevenLabs or Kokoro:**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user