Context: tools/tts is this repo's text-to-speech layer. It dispatches between a
local Kokoro ONNX backend and OpenAI's tts-1, and validates the caller's requested
voice name against per-backend catalogs before synthesizing. /do-debrief uses it to
deliver spoken briefs to Telegram.
Problem
bf_alice is documented in two places as an available Kokoro voice, but it does not exist in the code's voice catalog, and requesting it is a hard failure rather than a fallback.
Documented as available:
Absent from the actual catalogs:
KOKORO_VOICES (tools/tts/__init__.py:27-38) lists ten voices; bf_alice is not among them. The bf_* entries are bf_emma and bf_isabella.
tools/tts/README.md:77, the catalog table the skill-context file points readers to, lists the same ten.
_VOICE_FALLBACK_MAP (:47-66) has no bf_alice key.
Current behavior. _resolve_voice (:161-197) checks the target backend set, then the other backend's set, then errors. Since bf_alice is in neither, the function hits :191 and returns:
{"error": "unknown voice: 'bf_alice'. Available kokoro: [...]. Available openai: [...]"}
synthesize returns that dict alone (:409-411) with no path, duration, or backend key. Any caller that reads result["path"] without checking for error first raises KeyError. So a debrief run that follows its own context doc's advice does not get a different voice — it gets a failed synthesis.
Desired outcome. One source of truth. Either bf_alice is a real voice in the Kokoro voice pack and belongs in KOKORO_VOICES, _VOICE_FALLBACK_MAP, and the README table, or it is not and both documentation references should be corrected to name a voice that exists (bf_emma or bf_isabella).
Definitions
| Term |
Meaning |
| Kokoro |
Local ONNX TTS model (kokoro-v1.0.onnx) with a bundled voice pack (voices-v1.0.bin), used when available to avoid cloud cost and latency. |
_VOICE_FALLBACK_MAP |
Remap table applied when the caller asks for a voice belonging to the backend that dispatch did not pick. |
| skill-context file |
Repo-specific addendum layered onto a generic global skill. See docs/features/skill-context-convention.md. |
Solution sketch
First determine which way the mismatch resolves — that is a question, not an assumption:
- Check whether
bf_alice is actually present in the shipped voices-v1.0.bin voice pack. The Kokoro voice pack is the ground truth; KOKORO_VOICES is a hand-maintained mirror of it, so the mirror could be the thing that is wrong.
- If present in the pack: add it to
KOKORO_VOICES, give it a _VOICE_FALLBACK_MAP entry (the other bf_* voices map to fable and shimmer), and add it to the README table. Synthesize one clip to confirm it renders.
- If absent from the pack: correct
.claude/skill-context/do-debrief.md:21 and the :68 comment to name an existing voice.
Whichever way it goes, consider whether KOKORO_VOICES should be derived from the voice pack rather than hand-maintained, since that is the class of drift that produced this. That is a judgment call for the planner, not a requirement here.
Acceptance criteria
Downstream context
Surfaced while planning #3330 (pre-recorded question-tree voice interview), which builds a persistent clip library on top of tools/tts and therefore has to know which voice names are real. Deliberately not absorbed into that plan — it is a pre-existing catalog inconsistency with its own blast radius.
Triage 2026-09-15
Status: confirmed — but the doc claims (.claude/skill-context/do-debrief.md:21, tools/tts/__init__.py:68 comment) are actually correct, and the catalog is the bug. Verified against the installed pack: Kokoro('~/.cache/kokoro-onnx/kokoro-v1.0.onnx', '~/.cache/kokoro-onnx/voices-v1.0.bin').get_voices() returns 54 voices including bf_alice (and also bf_lily, likewise undocumented/uncataloged). KOKORO_VOICES at tools/tts/__init__.py:27-38 only lists 10 of the 54, and _resolve_voice() (tools/tts/__init__.py:161-194) treats that set as a hard allow-list, so a genuinely valid, installed voice is rejected outright.
Single-line hotfix: Yes. Add "bf_alice", to the KOKORO_VOICES set at tools/tts/__init__.py (within lines 27-38), and add bf_alice to the catalog table row in tools/tts/README.md:77. (Also worth adding bf_lily, present in the pack but never mentioned anywhere, while touching this.)
Reduce-complexity option: Yes — the two-place (arguably three, counting the tools/tts/__init__.py:68 comment) hand-maintained curated subset is exactly the kind of duplicated data that drifts from the real pack. Recommend deriving KOKORO_VOICES at runtime from Kokoro.get_voices() (or generating the constant from it) instead of hand-listing a subset in code, docs, and a comment independently — that would have made this class of bug structurally impossible.
Problem
bf_aliceis documented in two places as an available Kokoro voice, but it does not exist in the code's voice catalog, and requesting it is a hard failure rather than a fallback.Documented as available:
.claude/skill-context/do-debrief.md:21— "--voicedefaults toam_michael(Kokoro);bf_aliceis the female alternative."tools/tts/__init__.py:68— trailing comment onKOKORO_DEFAULT_VOICE: "bf_alice is a nice change for a female voice"Absent from the actual catalogs:
KOKORO_VOICES(tools/tts/__init__.py:27-38) lists ten voices;bf_aliceis not among them. Thebf_*entries arebf_emmaandbf_isabella.tools/tts/README.md:77, the catalog table the skill-context file points readers to, lists the same ten._VOICE_FALLBACK_MAP(:47-66) has nobf_alicekey.Current behavior.
_resolve_voice(:161-197) checks the target backend set, then the other backend's set, then errors. Sincebf_aliceis in neither, the function hits:191and returns:synthesizereturns that dict alone (:409-411) with nopath,duration, orbackendkey. Any caller that readsresult["path"]without checking forerrorfirst raisesKeyError. So a debrief run that follows its own context doc's advice does not get a different voice — it gets a failed synthesis.Desired outcome. One source of truth. Either
bf_aliceis a real voice in the Kokoro voice pack and belongs inKOKORO_VOICES,_VOICE_FALLBACK_MAP, and the README table, or it is not and both documentation references should be corrected to name a voice that exists (bf_emmaorbf_isabella).Definitions
kokoro-v1.0.onnx) with a bundled voice pack (voices-v1.0.bin), used when available to avoid cloud cost and latency._VOICE_FALLBACK_MAPdocs/features/skill-context-convention.md.Solution sketch
First determine which way the mismatch resolves — that is a question, not an assumption:
bf_aliceis actually present in the shippedvoices-v1.0.binvoice pack. The Kokoro voice pack is the ground truth;KOKORO_VOICESis a hand-maintained mirror of it, so the mirror could be the thing that is wrong.KOKORO_VOICES, give it a_VOICE_FALLBACK_MAPentry (the otherbf_*voices map tofableandshimmer), and add it to the README table. Synthesize one clip to confirm it renders..claude/skill-context/do-debrief.md:21and the:68comment to name an existing voice.Whichever way it goes, consider whether
KOKORO_VOICESshould be derived from the voice pack rather than hand-maintained, since that is the class of drift that produced this. That is a judgment call for the planner, not a requirement here.Acceptance criteria
bf_aliceeither is or is not in the shipped voice pack, with the check recorded.KOKORO_VOICES,_VOICE_FALLBACK_MAP,tools/tts/README.md:77,.claude/skill-context/do-debrief.md:21, and thetools/tts/__init__.py:68comment all agree.Downstream context
Surfaced while planning #3330 (pre-recorded question-tree voice interview), which builds a persistent clip library on top of
tools/ttsand therefore has to know which voice names are real. Deliberately not absorbed into that plan — it is a pre-existing catalog inconsistency with its own blast radius.Triage 2026-09-15
Status: confirmed — but the doc claims (
.claude/skill-context/do-debrief.md:21,tools/tts/__init__.py:68comment) are actually correct, and the catalog is the bug. Verified against the installed pack:Kokoro('~/.cache/kokoro-onnx/kokoro-v1.0.onnx', '~/.cache/kokoro-onnx/voices-v1.0.bin').get_voices()returns 54 voices includingbf_alice(and alsobf_lily, likewise undocumented/uncataloged).KOKORO_VOICESattools/tts/__init__.py:27-38only lists 10 of the 54, and_resolve_voice()(tools/tts/__init__.py:161-194) treats that set as a hard allow-list, so a genuinely valid, installed voice is rejected outright.Single-line hotfix: Yes. Add
"bf_alice",to theKOKORO_VOICESset attools/tts/__init__.py(within lines 27-38), and addbf_aliceto the catalog table row intools/tts/README.md:77. (Also worth addingbf_lily, present in the pack but never mentioned anywhere, while touching this.)Reduce-complexity option: Yes — the two-place (arguably three, counting the
tools/tts/__init__.py:68comment) hand-maintained curated subset is exactly the kind of duplicated data that drifts from the real pack. Recommend derivingKOKORO_VOICESat runtime fromKokoro.get_voices()(or generating the constant from it) instead of hand-listing a subset in code, docs, and a comment independently — that would have made this class of bug structurally impossible.