fix(frank-tts): a voice-less request now speaks the declared default instead of 400ing #18

Merged
dank merged 1 commits from fix/frank-tts-default-voice into main 2026-10-18 19:26:08 -07:00
Owner

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.

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.
dank added 1 commit 2026-10-18 19:21:37 -07:00
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 main 2026-10-18 19:26:08 -07:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#18