Why the harness cannot pre-name a voice — read out of the bundle: the openai-speech transport builds {model, input, response_format, voice?} and adds voiceonly when the call passed voice_id. tts.localVoice is the local Kokoro backend's key; speech.voice is the TUI's speak-aloud voice, an enum of Kokoro names that never reaches an HTTP request. A voice-less synthesis call is therefore the harness's normal shape, not a client bug.
The change.DEFAULT_VOICE (env, empty by default) resolves an absent voice; the journal marks such a line (default) so an unrequested voice is never mistaken for one that was asked for:
Request
Result
voice=narrator2
unchanged — that pack, journal names it
no voice, DEFAULT_VOICE=narrator1
200 on narrator1, journal speech voice=narrator1 (default) …
no voice, DEFAULT_VOICE empty
400, as before — strictness is still the code default
voice=nosuch
400 with the list, whatever the default is — the no-silent-fallback law is untouched
Two seam tests, one per row that changed; scripts/frank-tts.env ships DEFAULT_VOICE=narrator1 with the reasoning in its header.
./tests/run.sh → 446/446. After merge: ./scripts/deploy.sh dankclaw --service frank-tts (the shim image rebuilds — app.py is COPYed). Vault lore for the knob rides in a follow-up commit once #17 lands, so the two PRs do not fight over the same page.
Found by asking the restarted harness for a voiceover. With `modelRoles.speech` pointing at frank-tts and the model omitting `voice_id`, 7772 answered:
```
400 {"error": {"message": "unknown voice None; available voices: narrator1, narrator2, narrator3"}}
```
**Why the harness cannot pre-name a voice** — read out of the bundle: the `openai-speech` transport builds `{model, input, response_format, voice?}` and adds `voice` **only** when the call passed `voice_id`. `tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is the TUI's **speak-aloud** voice, an enum of Kokoro names that never reaches an HTTP request. A voice-less synthesis call is therefore the harness's normal shape, not a client bug.
**The change.** `DEFAULT_VOICE` (env, empty by default) resolves an *absent* voice; the journal marks such a line `(default)` so an unrequested voice is never mistaken for one that was asked for:
| Request | Result |
|---|---|
| `voice=narrator2` | unchanged — that pack, journal names it |
| no `voice`, `DEFAULT_VOICE=narrator1` | 200 on narrator1, journal `speech voice=narrator1 (default) …` |
| no `voice`, `DEFAULT_VOICE` empty | 400, as before — strictness is still the code default |
| `voice=nosuch` | **400 with the list**, whatever the default is — the no-silent-fallback law is untouched |
Two seam tests, one per row that changed; `scripts/frank-tts.env` ships `DEFAULT_VOICE=narrator1` with the reasoning in its header.
`./tests/run.sh` → **446/446**. After merge: `./scripts/deploy.sh dankclaw --service frank-tts` (the shim image rebuilds — `app.py` is COPYed). Vault lore for the knob rides in a follow-up commit once #17 lands, so the two PRs do not fight over the same page.
The omp speech device forwards a voice only when the call passes voice_id - there is no setting that feeds one (speech.voice is the TUI speak-aloud Kokoro enum, tts.localVoice belongs to the local backend) - so every harness voiceover arrives with no voice key and the shim answered 400 "unknown voice None". Measured on the deployed endpoint 2026-10-18, from a restarted session with the speech role pointed here.
DEFAULT_VOICE makes the absent case a spoken operator choice instead of a code surprise: the pack named there answers a voice-less request and the journal line is marked (default), so an unrequested voice never reads later like a requested one. An UNKNOWN name still 400s with the list that exists - the no-silent-fallback law that this endpoint was built on is untouched, and the strict shape stays the code default (empty DEFAULT_VOICE), which the second new seam test pins. Ships narrator1 in scripts/frank-tts.env.
dank
merged commit 7889a68497 into main2026-10-18 19:26:08 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by asking the restarted harness for a voiceover. With
modelRoles.speechpointing at frank-tts and the model omittingvoice_id, 7772 answered:Why the harness cannot pre-name a voice — read out of the bundle: the
openai-speechtransport builds{model, input, response_format, voice?}and addsvoiceonly when the call passedvoice_id.tts.localVoiceis the local Kokoro backend's key;speech.voiceis the TUI's speak-aloud voice, an enum of Kokoro names that never reaches an HTTP request. A voice-less synthesis call is therefore the harness's normal shape, not a client bug.The change.
DEFAULT_VOICE(env, empty by default) resolves an absent voice; the journal marks such a line(default)so an unrequested voice is never mistaken for one that was asked for:voice=narrator2voice,DEFAULT_VOICE=narrator1speech voice=narrator1 (default) …voice,DEFAULT_VOICEemptyvoice=nosuchTwo seam tests, one per row that changed;
scripts/frank-tts.envshipsDEFAULT_VOICE=narrator1with the reasoning in its header../tests/run.sh→ 446/446. After merge:./scripts/deploy.sh dankclaw --service frank-tts(the shim image rebuilds —app.pyis COPYed). Vault lore for the knob rides in a follow-up commit once #17 lands, so the two PRs do not fight over the same page.