* feat(cli): add --lang and auto-infer phonemizer locale from voice prefix
`hyperframes tts` was calling Kokoro's `model.create(text, voice=, speed=)`
with no language argument, so Kokoro's default phonemizer (en-us) was
applied regardless of the voice selected. Picking `ef_dora` or `jf_alpha`
and feeding it Spanish or Japanese text produced English-phonemized
output.
Closes#349.
- `manager.ts`: add `SUPPORTED_LANGS`, `inferLangFromVoiceId`, and
`isSupportedLang`. Attach a `defaultLang` field to every bundled voice
and expand the bundled list with `ef_dora`, `ff_siwis`, `jf_alpha`,
`zf_xiaobei` so `--list` surfaces multilingual options.
- `synthesize.ts`: accept optional `lang: SupportedLang` in
`SynthesizeOptions`, forward it to the Python worker as `argv[7]`.
The worker introspects `Kokoro.create`'s signature and only passes
`lang=` when the installed kokoro-onnx version supports it. Returned
metadata now includes `lang` and `langApplied` so callers can detect
silent no-ops. Bump the cached script filename to `synth-v2.py` so
existing installs pick up the new script automatically.
- `commands/tts.ts`: add `--lang, -l` with validation against
`SUPPORTED_LANGS`. Resolution order is explicit `--lang` > inferred
from voice prefix > `en-us`. When explicit lang disagrees with the
voice-implied lang (legitimate for stylized accents), emit a
dim-level hint; suppress under `--json`. When kokoro-onnx silently
ignores the kwarg, log that too. Update `--list` with a new
"Lang code" column and add multilingual examples.
- Tests: new `manager.test.ts` covering every supported prefix, the
unknown-prefix fallback, case-insensitivity, `isSupportedLang`
validation, and a regression guard that every bundled voice has a
valid `defaultLang` matching its ID.
- Docs: `docs/packages/cli.mdx` and `skills/hyperframes/references/tts.md`
updated with the flag, examples, the espeak-ng dependency note for
non-English phonemization, and the voice-prefix → lang table.
Backward compatibility:
- English voices (a*/b* prefixes) continue to phonemize as en-us / en-gb
— no change.
- Non-English voices now phonemize correctly by default (bug fix, not a
regression).
- Older kokoro-onnx versions that don't know the `lang` kwarg keep
working via signature introspection; the CLI logs a dim note if
`--lang` was requested but ignored.
Verification:
- `bun --cwd packages/cli test` — 128 tests pass (incl. 17 new).
- `bunx oxlint` and `bunx oxfmt --check` clean on changed files.
- `bun run build` succeeds.
- `npx tsx packages/cli/src/cli.ts tts --help` / `--list` render cleanly;
invalid `--lang` produces a clean error with the valid-codes list.
* refactor(cli): simplify tts --lang implementation
Post-review cleanup on #351. Net -21 lines.
- Drop `defaultLang` field + `makeVoice()` helper from VoiceInfo —
compute via `inferLangFromVoiceId(v.id)` at read time in listVoices.
The only reader was the --list table; caching the derived value on
every voice added a self-consistency invariant we had to test.
- Drop redundant `lang` field from SynthesizeResult — caller already
knows the requested lang since it passed it in; only `langApplied`
carries information the caller can't derive.
- Use `errorBox` for --lang validation to match the house style in
render.ts (other validation errors already use errorBox).
- Reuse existing `langList` module constant in the validation error
instead of re-joining SUPPORTED_LANGS.
- Inline `DEFAULT_LANG` — used once in inferLangFromVoiceId.
- Trim WHAT-restating comments and the duplicate prefix-enumeration
JSDoc on inferLangFromVoiceId (VOICE_PREFIX_LANG already carries
per-row comments).
- Clean up orphaned `synth*.py` files in ~/.cache/hyperframes/tts
when writing the current versioned script, so repeated upgrades
don't leak files.
- Drop the `EN-US` case-sensitive-rejection test assertion — the CLI
lowercases input before validation, so accepting mixed case is a
feature, not a bug.
Tests: 16/16 in `manager.test.ts`, 127/127 full CLI suite pass.
Lint + format + typecheck clean.
Move per-command examples from the centralized `help.ts` record into
each command file as `export const examples: Example[]`. help.ts now
dynamically imports them at --help time.
This means adding a new command and its examples happens in one file
instead of two, reducing the chance of forgetting examples.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): add `tts` command for local text-to-speech via Kokoro-82M
Adds `hyperframes tts` — generate speech audio locally using Kokoro-82M
(ONNX), no API key needed. Mirrors the transcribe command architecture.
- New command: `hyperframes tts "text" --voice af_heart --output speech.wav`
- 54 voices across 8 languages, ~5x realtime on CPU
- Auto-downloads model (~311 MB) + voices (~27 MB) to ~/.cache/hyperframes/tts/
- Requires Python 3.8+ with kokoro-onnx installed
- Extracted shared `downloadFile` utility from whisper/manager.ts with
atomic .tmp→rename to prevent partial download corruption
- Added hyperframes-tts skill with voice selection guide
- Updated CLAUDE.md with TTS docs, voice table, and skill reference
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(tts): improve skill per skill-creator guidelines
- Move trigger info from body to frontmatter description
- Remove `trigger` field (not a valid frontmatter field)
- Remove CLI flag docs Claude can derive from --help
- Remove redundant voice tables (keep content-to-voice mapping)
- Fix composition audio example to use actual <audio> element pattern
- Keep non-obvious workflows: TTS+transcribe for captions, long scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(tts): add guidance for using external TTS sources
Help users understand when to use cloud TTS (voice cloning, broader
languages, higher quality) vs the built-in Kokoro model, and how
external audio integrates into the same composition workflow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(tts): prioritize HeyGen API as recommended cloud TTS
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(tts): remove external TTS section for now
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tts): set required: false on input arg so --list works standalone
Citty treats positional args as required by default unless explicitly
set to required: false. Without this, `hyperframes tts --list` fails
with "Missing required positional argument".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(tts): add --help examples and fix required:false for --list
Add examples section to `tts --help` matching the pattern from other
commands (transcribe, render, etc.). Fix citty positional arg requiring
explicit `required: false` for --list to work standalone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add CLI command checklist to CLAUDE.md
Ensure new commands always get --help examples in help.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>