Skill Tracer
Skills › HyperFrames › media-use › tts.md

tts.md

258 lines · skills/media-use/audio/references/tts.md at a595755

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Text To Speech
2
3`npx hyperframes tts` synthesizes locally with Kokoro. It does not accept a
4`--provider` or `--words` flag. For HeyGen audio plus word timestamps, use the
5bundled `heygen-tts.mjs` script below.
6
7> **Run the Preflight first — no credential is not a green light to silently use the local voice.** Before generating a voiceover, complete the sign-in **Preflight** (see `../../SKILL.md` → Preflight): run `npx hyperframes auth status`, recommend signing in, and **STOP for the user's choice** (sign in for HeyGen voices, or continue offline with local Kokoro). This applies to a one-off "generate a voiceover" request just as much as inside a full workflow.
8

Text To Speech

npx hyperframes tts synthesizes locally with Kokoro. It does not accept a --provider or --words flag. For HeyGen audio plus word timestamps, use the bundled heygen-tts.mjs script below.

Run the Preflight first — no credential is not a green light to silently use the local voice. Before generating a voiceover, complete the sign-in Preflight (see ../../SKILL.md → Preflight): run npx hyperframes auth status, recommend signing in, and STOP for the user's choice (sign in for HeyGen voices, or continue offline with local Kokoro). This applies to a one-off "generate a voiceover" request just as much as inside a full workflow.
Raw lines 1 to 8
9## Narrating a HyperFrames docs video
11Anything that will sit on hyperframes.heygen.com uses one narrator, so the site
12does not sound like two products.
14| | |
15| -------------- | ---------------------------------------------- |
16| Voice | **River** — "Relaxed, Neutral, Informative" |
17| Provider | ElevenLabs |
18| `voice_id` | `SAz9YHcvj6GT2YYXdXww` |
19| Model | `eleven_multilingual_v2` |
20| Pace | 145–155 words per minute, with room to breathe |
21| Music under it | about −31 LUFS, never masking the voice |
23```bash
24curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/SAz9YHcvj6GT2YYXdXww" \
25 -H "xi-api-key: $ELEVENLABS_API_KEY" -H "Content-Type: application/json" \
26 -d '{"text":"...","model_id":"eleven_multilingual_v2"}' -o take.mp3
27```
29This is the voice every user-journey film on the docs site already uses. Falling
30back to local Kokoro because a key was not to hand produces a film that sounds
31wrong beside the others — three docs videos were built that way and had to be
32re-voiced. If you cannot reach ElevenLabs, say so and stop rather than
33substituting a different voice.
35Use another voice only for a documented reason, and write the reason down.

Narrating a HyperFrames docs video

Anything that will sit on hyperframes.heygen.com uses one narrator, so the site does not sound like two products.

VoiceRiver — "Relaxed, Neutral, Informative"
ProviderElevenLabs
voice_idSAz9YHcvj6GT2YYXdXww
Modeleleven_multilingual_v2
Pace145–155 words per minute, with room to breathe
Music under itabout −31 LUFS, never masking the voice
curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/SAz9YHcvj6GT2YYXdXww" \
  -H "xi-api-key: $ELEVENLABS_API_KEY" -H "Content-Type: application/json" \
  -d '{"text":"...","model_id":"eleven_multilingual_v2"}' -o take.mp3

This is the voice every user-journey film on the docs site already uses. Falling back to local Kokoro because a key was not to hand produces a film that sounds wrong beside the others — three docs videos were built that way and had to be re-voiced. If you cannot reach ElevenLabs, say so and stop rather than substituting a different voice.

Use another voice only for a documented reason, and write the reason down.

Raw lines 9 to 36
37## Available routes
39Gemini is an explicit alternative to the automatic provider order below. A
40request to use Gemini already chooses the provider; do not redirect that user
41to HeyGen sign-in. Read the Gemini section for its credential requirement.
43| Order | Provider | Env trigger | Voice IDs | Word timestamps | Audio format |
44| ----- | ----------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------- | -------------------- |
45| 1 | HeyGen (Starfish) | `$HEYGEN_ACCESS_TOKEN` / `$HEYGEN_API_KEY` / `~/.hyperframes/credentials` | UUIDs from `GET /v3/voices?engine=starfish`, or your cloned voice ids (`heygen-voice.mjs`) | **Yes** (`word_timestamps[]` in response) | mp3 → wav via ffmpeg |
46| 2 | ElevenLabs | `$ELEVENLABS_API_KEY` | UUIDs from elevenlabs.io dashboard | No | mp3 → wav via ffmpeg |
47| 3 | Kokoro-82M | always (local fallback) | `am_michael`, `af_heart`, … (54 voices) | No | wav direct |
49```bash
50# Local Kokoro CLI
51npx hyperframes tts "Welcome to HyperFrames" -o narration.wav
52```

Available routes

Gemini is an explicit alternative to the automatic provider order below. A request to use Gemini already chooses the provider; do not redirect that user to HeyGen sign-in. Read the Gemini section for its credential requirement.

OrderProviderEnv triggerVoice IDsWord timestampsAudio format
1HeyGen (Starfish)$HEYGEN_ACCESS_TOKEN / $HEYGEN_API_KEY / ~/.hyperframes/credentialsUUIDs from GET /v3/voices?engine=starfish, or your cloned voice ids (heygen-voice.mjs)Yes (word_timestamps[] in response)mp3 → wav via ffmpeg
2ElevenLabs$ELEVENLABS_API_KEYUUIDs from elevenlabs.io dashboardNomp3 → wav via ffmpeg
3Kokoro-82Malways (local fallback)am_michael, af_heart, … (54 voices)Nowav direct
# Local Kokoro CLI
npx hyperframes tts "Welcome to HyperFrames" -o narration.wav
Raw lines 37 to 53
54## Self-contained HeyGen (no CLI) — `scripts/heygen-tts.mjs`
56The published `hyperframes tts` CLI synthesizes locally with Kokoro only. When you
57want HeyGen specifically — best quality **plus** word timestamps in one call — use
58the skill's bundled script, which calls the HeyGen v3 REST API directly and needs
59no CLI provider plumbing:
61The script resolves a HeyGen credential the same way the CLI does — first source
62wins: a host-injected OAuth `$HEYGEN_ACCESS_TOKEN` → an unexpired login in
63`~/.hyperframes/credentials` → `$HEYGEN_API_KEY` → `$HYPERFRAMES_API_KEY` → a
64project `.env` (auto-loaded, walks up ≤5 dirs) → an API key saved in
65`~/.hyperframes/credentials` (isolated from heygen-cli;
66`$HYPERFRAMES_CONFIG_DIR` overrides the root; `HYPERFRAMES_ENV=dev` uses `dev/`). An OAuth login is sent as
67`Authorization: Bearer`; an API key as `X-Api-Key`; only OAuth includes
68`X-HeyGen-Source: cli`. The server decides HyperFrames eligibility and charges;
69explicit API keys use API billing. An expired or foreign OAuth session gives way
70to an env key; without one it stops rather than selecting a saved API key, with
71a hint to run `npx hyperframes auth refresh`.
73```bash
74# Only needed if you haven't run `npx hyperframes auth login`:
75export HEYGEN_API_KEY=... # or put it in a project .env
77# Synthesize + capture word timestamps in one call (skips a Whisper pass)
78node skills/media-use/audio/scripts/heygen-tts.mjs \
79 "Welcome to HyperFrames." -o narration.wav --words narration.words.json
81node skills/media-use/audio/scripts/heygen-tts.mjs ./script.txt -o narration.wav
82node skills/media-use/audio/scripts/heygen-tts.mjs --list # public starfish voices
83```
85- **Voice:** `--voice <id>` must be a **starfish** voice_id (`--list`, or `GET /v3/voices?engine=starfish`) or one of your cloned voice ids (below). v2-catalog ids are rejected with HTTP 400. Omit `--voice` (English) and it defaults to **Marcia** (`05f19352e8f74b0392a8f411eba40de1`, a fixed default so the choice is deterministic). Non-English with no `--voice` falls back to the first matching catalog voice.
86- **Output:** `.wav` → transcoded to 44.1k mono via ffmpeg; `.mp3` → raw bytes (no ffmpeg needed).
87- **Words:** `--words <path>` writes the flat `[{id,text,start,end}]` shape below, drop-in for the captions pipeline. HeyGen's `<start>`/`<end>` boundary sentinels are filtered out and ids are re-contiguous.
88- **Non-English:** `--lang <code>` (anything but `en`) is sent as the request `language`.

Self-contained HeyGen (no CLI) — scripts/heygen-tts.mjs

The published hyperframes tts CLI synthesizes locally with Kokoro only. When you want HeyGen specifically — best quality plus word timestamps in one call — use the skill's bundled script, which calls the HeyGen v3 REST API directly and needs no CLI provider plumbing:

The script resolves a HeyGen credential the same way the CLI does — first source wins: a host-injected OAuth $HEYGEN_ACCESS_TOKEN → an unexpired login in ~/.hyperframes/credentials → $HEYGEN_API_KEY → $HYPERFRAMES_API_KEY → a project .env (auto-loaded, walks up ≤5 dirs) → an API key saved in ~/.hyperframes/credentials (isolated from heygen-cli; $HYPERFRAMES_CONFIG_DIR overrides the root; HYPERFRAMES_ENV=dev uses dev/). An OAuth login is sent as Authorization: Bearer; an API key as X-Api-Key; only OAuth includes X-HeyGen-Source: cli. The server decides HyperFrames eligibility and charges; explicit API keys use API billing. An expired or foreign OAuth session gives way to an env key; without one it stops rather than selecting a saved API key, with a hint to run npx hyperframes auth refresh.

# Only needed if you haven't run `npx hyperframes auth login`:
export HEYGEN_API_KEY=...   # or put it in a project .env

# Synthesize + capture word timestamps in one call (skips a Whisper pass)
node skills/media-use/audio/scripts/heygen-tts.mjs \
  "Welcome to HyperFrames." -o narration.wav --words narration.words.json

node skills/media-use/audio/scripts/heygen-tts.mjs ./script.txt -o narration.wav
node skills/media-use/audio/scripts/heygen-tts.mjs --list   # public starfish voices
  • Voice: --voice <id> must be a starfish voice_id (--list, or GET /v3/voices?engine=starfish) or one of your cloned voice ids (below). v2-catalog ids are rejected with HTTP 400. Omit --voice (English) and it defaults to Marcia (05f19352e8f74b0392a8f411eba40de1, a fixed default so the choice is deterministic). Non-English with no --voice falls back to the first matching catalog voice.
  • Output: .wav → transcoded to 44.1k mono via ffmpeg; .mp3 → raw bytes (no ffmpeg needed).
  • Words: --words <path> writes the flat [{id,text,start,end}] shape below, drop-in for the captions pipeline. HeyGen's <start>/<end> boundary sentinels are filtered out and ids are re-contiguous.
  • Non-English: --lang <code> (anything but en) is sent as the request language.
Raw lines 54 to 89
Runs code scripts/heygen-tts.mjs :54
90## Cloned voices — `scripts/heygen-voice.mjs`
92Clone a voice from an mp3 or wav recording, then pass the printed id to
93`heygen-tts.mjs --voice`. Same credential resolution as above. Any API refusal
94(clone limit reached, plan upgrade required, voice not found) prints HeyGen's own
95message to stderr and exits 1.
97```bash
98node skills/media-use/audio/scripts/heygen-voice.mjs clone take.mp3 --name "My voice" # → {"voice_id":"..."}
99node skills/media-use/audio/scripts/heygen-voice.mjs list --prefix "My" # → [{"voice_id","name","created_at"}]
100node skills/media-use/audio/scripts/heygen-voice.mjs delete <voice_id>
101```
103`clone` waits up to 120 s for the clone to finish. `list` reads
104`GET /v3/voices?type=private`.

Cloned voices — scripts/heygen-voice.mjs

Clone a voice from an mp3 or wav recording, then pass the printed id to heygen-tts.mjs --voice. Same credential resolution as above. Any API refusal (clone limit reached, plan upgrade required, voice not found) prints HeyGen's own message to stderr and exits 1.

node skills/media-use/audio/scripts/heygen-voice.mjs clone take.mp3 --name "My voice"  # → {"voice_id":"..."}
node skills/media-use/audio/scripts/heygen-voice.mjs list --prefix "My"   # → [{"voice_id","name","created_at"}]
node skills/media-use/audio/scripts/heygen-voice.mjs delete <voice_id>

clone waits up to 120 s for the clone to finish. list reads GET /v3/voices?type=private.

Raw lines 90 to 105
Runs code scripts/heygen-voice.mjs :90
106## When to use which provider
108| Goal | Use |
109| --------------------------------------------------------- | --------------------------------------------------------- |
110| Best voice quality + word timestamps in one call | **HeyGen** |
111| Drop-in cloud TTS, big voice catalog | **ElevenLabs** |
112| Offline, no API key, fast iteration | **Kokoro** |
113| Directed delivery with Gemini prebuilt or custom voices | **Gemini** (explicit selection; transcription for timing) |
114| Non-English multilingual with deterministic phonemization | **Kokoro** (`ef_dora`, `jf_alpha`, `zf_xiaobei`, …) |

When to use which provider

GoalUse
Best voice quality + word timestamps in one callHeyGen
Drop-in cloud TTS, big voice catalogElevenLabs
Offline, no API key, fast iterationKokoro
Directed delivery with Gemini prebuilt or custom voicesGemini (explicit selection; transcription for timing)
Non-English multilingual with deterministic phonemizationKokoro (ef_dora, jf_alpha, zf_xiaobei, …)
Raw lines 106 to 115
116## Gemini narration
118Use the shared audio engine, not `hyperframes tts`. Authenticate with either:
120- `GEMINI_API_KEY` or `GOOGLE_API_KEY` (first set key wins).
121- A service-account JSON file at `GOOGLE_APPLICATION_CREDENTIALS`, or injected
122 JSON in `GCS_CREDS`. Install `google-auth requests` in the Python 3 environment
123 used by the helper. A configured file takes precedence over injected JSON.
125API keys take precedence over service accounts: unset both key variables to use
126OAuth. Never put credentials in a request file or composition. The helper
127obtains a fresh OAuth token for each generation with the
128`generative-language.retriever` scope. The quota project resolves from
129`GOOGLE_CLOUD_PROJECT`, then `GCLOUD_PROJECT_ID`, then the service-account JSON.
130User ADC files and metadata-server authentication are not supported.
132Both routes call the Gemini Developer Interactions API, not Cloud TTS or Vertex
133AI. Those APIs have separate model catalogs and access requirements.
135Save this as `audio_request.json` in the project:
137```json
139 "provider": "gemini",
140 "tts_model": "gemini-3.8-flash-tts",
141 "voice": "Kore",
142 "lang": "en",
143 "style": "Warm, clear, conversational. Leave a short pause between sentences.",
144 "lines": [
145 { "id": "intro", "text": "Every word has a moment. Let the picture follow the voice." }
146 ],
147 "bgm": { "mode": "none" }
149```
151```bash
152node <SKILL_DIR>/audio/scripts/audio.mjs \
153 --request ./audio_request.json --hyperframes . --out ./audio_meta.json --only tts
154```
156The engine saves `assets/voice/intro.wav` (or `intro-2.wav` when a file of yours already has that name; `voices[].path` says which), measures its duration, and transcribes
157it into `voices[].words` in `audio_meta.json`. Check that every requested line
158has audio and nonempty word timings before building a captioned video. Review
159the timings against the actual audio; transcription is estimated alignment,
160not native TTS timestamps. Do not distribute words evenly across a clip.
162- **Models:** `gemini-3.8-flash-tts` (default), `gemini-3.8-flash-lite-tts`,
163 `gemini-3.1-flash-tts-preview`, `gemini-2.5-pro-preview-tts`, and
164 `gemini-2.5-flash-preview-tts`. Use these exact Developer API IDs; Cloud TTS
165 aliases such as `gemini-2.5-flash-tts` are not accepted.
166- **Voice:** `Kore` by default; pass another prebuilt voice or an existing custom
167 voice ID for 3.8. Older models require prebuilt voices. Creating or replicating
168 voices is outside this helper.
169- **Delivery:** Put directions in `style`, not in spoken `text`. Each line can
170 override `style`. 3.8 uses structured annotations; older models receive a
171 delivery prompt before the transcript. Check that directions were not spoken.
172 Use style for pacing; numeric `speed` must be omitted or 1.
173- **Audio:** 3.8 returns a complete WAV. Older models return mono 16-bit PCM,
174 which the helper wraps as WAV at the returned sample rate without resampling.
175 No generation calls run during playback or rendering.
176- **Timing:** This adapter requests no native word timestamps. It uses the
177 existing transcription pass. `lang` selects transcription language; Gemini
178 infers speech language from the text.
180An API error is reported as a failed line; an explicitly chosen Gemini voice
181never silently falls back to another provider. Check the engine's anomalies
182and output metadata, not only its exit code.
184API contract: [Google's speech generation guide](https://ai.google.dev/gemini-api/docs/speech-generation).

Gemini narration

Use the shared audio engine, not hyperframes tts. Authenticate with either:

  • GEMINI_API_KEY or GOOGLE_API_KEY (first set key wins).
  • A service-account JSON file at GOOGLE_APPLICATION_CREDENTIALS, or injected JSON in GCS_CREDS. Install google-auth requests in the Python 3 environment used by the helper. A configured file takes precedence over injected JSON.

API keys take precedence over service accounts: unset both key variables to use OAuth. Never put credentials in a request file or composition. The helper obtains a fresh OAuth token for each generation with the generative-language.retriever scope. The quota project resolves from GOOGLE_CLOUD_PROJECT, then GCLOUD_PROJECT_ID, then the service-account JSON. User ADC files and metadata-server authentication are not supported.

Both routes call the Gemini Developer Interactions API, not Cloud TTS or Vertex AI. Those APIs have separate model catalogs and access requirements.

Save this as audio_request.json in the project:

{
  "provider": "gemini",
  "tts_model": "gemini-3.8-flash-tts",
  "voice": "Kore",
  "lang": "en",
  "style": "Warm, clear, conversational. Leave a short pause between sentences.",
  "lines": [
    { "id": "intro", "text": "Every word has a moment. Let the picture follow the voice." }
  ],
  "bgm": { "mode": "none" }
}
node <SKILL_DIR>/audio/scripts/audio.mjs \
  --request ./audio_request.json --hyperframes . --out ./audio_meta.json --only tts

The engine saves assets/voice/intro.wav (or intro-2.wav when a file of yours already has that name; voices[].path says which), measures its duration, and transcribes it into voices[].words in audio_meta.json. Check that every requested line has audio and nonempty word timings before building a captioned video. Review the timings against the actual audio; transcription is estimated alignment, not native TTS timestamps. Do not distribute words evenly across a clip.

  • Models: gemini-3.8-flash-tts (default), gemini-3.8-flash-lite-tts, gemini-3.1-flash-tts-preview, gemini-2.5-pro-preview-tts, and gemini-2.5-flash-preview-tts. Use these exact Developer API IDs; Cloud TTS aliases such as gemini-2.5-flash-tts are not accepted.
  • Voice: Kore by default; pass another prebuilt voice or an existing custom voice ID for 3.8. Older models require prebuilt voices. Creating or replicating voices is outside this helper.
  • Delivery: Put directions in style, not in spoken text. Each line can override style. 3.8 uses structured annotations; older models receive a delivery prompt before the transcript. Check that directions were not spoken. Use style for pacing; numeric speed must be omitted or 1.
  • Audio: 3.8 returns a complete WAV. Older models return mono 16-bit PCM, which the helper wraps as WAV at the returned sample rate without resampling. No generation calls run during playback or rendering.
  • Timing: This adapter requests no native word timestamps. It uses the existing transcription pass. lang selects transcription language; Gemini infers speech language from the text.

An API error is reported as a failed line; an explicitly chosen Gemini voice never silently falls back to another provider. Check the engine's anomalies and output metadata, not only its exit code.

API contract: Google's speech generation guide.

Raw lines 116 to 185
Runs code scripts/audio.mjs :152
186## ffmpeg requirement
188HeyGen + ElevenLabs return mp3. The bundled HeyGen helper transcodes to wav
189when `--output` ends in `.wav` (the default and what downstream `ffprobe` +
190Whisper expect). If you'd rather skip the transcode, pass `-o file.mp3`.
191Without `ffmpeg` on PATH, wav output from cloud providers fails; the local
192Kokoro CLI writes wav directly.

ffmpeg requirement

HeyGen + ElevenLabs return mp3. The bundled HeyGen helper transcodes to wav when --output ends in .wav (the default and what downstream ffprobe + Whisper expect). If you'd rather skip the transcode, pass -o file.mp3. Without ffmpeg on PATH, wav output from cloud providers fails; the local Kokoro CLI writes wav directly.

Raw lines 186 to 193
194## Voice selection (Kokoro)
196Default `af_heart`. Curated picks:
198| Content type | Voice |
199| ----------------- | ---------------------- |
200| Product demo | `af_heart`, `af_nova` |
201| Tutorial / how-to | `am_adam`, `bf_emma` |
202| Marketing / promo | `af_sky`, `am_michael` |
203| Documentation | `bf_emma`, `bm_george` |
204| Casual / social | `af_heart`, `af_sky` |
206Run `npx hyperframes tts --list` for the bundled set.

Voice selection (Kokoro)

Default af_heart. Curated picks:

Content typeVoice
Product demoaf_heart, af_nova
Tutorial / how-toam_adam, bf_emma
Marketing / promoaf_sky, am_michael
Documentationbf_emma, bm_george
Casual / socialaf_heart, af_sky

Run npx hyperframes tts --list for the bundled set.

Raw lines 194 to 207
208## Multilingual (Kokoro voice prefix → language)
210The first letter of a Kokoro voice ID picks the phonemizer language; `--lang` overrides auto-detection.
212| Prefix | Language |
213| ------ | -------------------- |
214| `a` | American English |
215| `b` | British English |
216| `e` | Spanish |
217| `f` | French |
218| `h` | Hindi |
219| `i` | Italian |
220| `j` | Japanese |
221| `p` | Brazilian Portuguese |
222| `z` | Mandarin |
224```bash
225npx hyperframes tts "La reunión empieza a las nueve" --voice ef_dora
226npx hyperframes tts "Today is a nice day" --voice af_heart
227```
229Valid `--lang` codes (only needed to override the voice's auto-detected language): `en-us`, `en-gb`, `es`, `fr-fr`, `hi`, `it`, `pt-br`, `ja`, `zh`.
231Non-English phonemization requires `espeak-ng` system-wide (`brew install espeak-ng` / `apt-get install espeak-ng`).

Multilingual (Kokoro voice prefix → language)

The first letter of a Kokoro voice ID picks the phonemizer language; --lang overrides auto-detection.

PrefixLanguage
aAmerican English
bBritish English
eSpanish
fFrench
hHindi
iItalian
jJapanese
pBrazilian Portuguese
zMandarin
npx hyperframes tts "La reunión empieza a las nueve" --voice ef_dora
npx hyperframes tts "Today is a nice day" --voice af_heart

Valid --lang codes (only needed to override the voice's auto-detected language): en-us, en-gb, es, fr-fr, hi, it, pt-br, ja, zh.

Non-English phonemization requires espeak-ng system-wide (brew install espeak-ng / apt-get install espeak-ng).

Raw lines 208 to 232
233## Speed
235- `0.7-0.8` — tutorial, complex content, accessibility
236- `1.0` — natural pace (default)
237- `1.1-1.2` — intros, transitions, upbeat content
238- `1.5+` — rarely appropriate, test carefully
240The `hyperframes tts` command honors `--speed` for Kokoro. Provider-specific
241helpers document their own pacing controls.

Speed

  • 0.7-0.8 — tutorial, complex content, accessibility
  • 1.0 — natural pace (default)
  • 1.1-1.2 — intros, transitions, upbeat content
  • 1.5+ — rarely appropriate, test carefully

The hyperframes tts command honors --speed for Kokoro. Provider-specific helpers document their own pacing controls.

Raw lines 233 to 242
243## Long scripts
245Past a few paragraphs, write the text to a `.txt` file and pass the path. Inputs over ~5 minutes of speech may benefit from splitting into segments.

Long scripts

Past a few paragraphs, write the text to a .txt file and pass the path. Inputs over ~5 minutes of speech may benefit from splitting into segments.

Raw lines 243 to 246
247## HeyGen word-timestamp shape
249When `--words <path>` is passed to a HeyGen call, the file is written in the same flat shape `transcribe` produces — drop-in compatible with the captions pipeline:
251```json
253 { "id": "w0", "text": "Hi", "start": 0.0, "end": 0.21 },
254 { "id": "w1", "text": "there", "start": 0.22, "end": 0.55 }
256```
258For ElevenLabs / Kokoro, run `npx hyperframes transcribe narration.wav --model small.en` to get the same shape.

HeyGen word-timestamp shape

When --words <path> is passed to a HeyGen call, the file is written in the same flat shape transcribe produces — drop-in compatible with the captions pipeline:

[
  { "id": "w0", "text": "Hi", "start": 0.0, "end": 0.21 },
  { "id": "w1", "text": "there", "start": 0.22, "end": 0.55 }
]

For ElevenLabs / Kokoro, run npx hyperframes transcribe narration.wav --model small.en to get the same shape.

Raw lines 247 to 258