docs(frank-tts): the harness sends a voice only in voice_id

The section this PR adds claimed speech.voice was the forwarded voice key. It is not: the openai-speech transport builds {model, input, response_format, voice?} and sets voice only when the CALL passed voice_id, tts.localVoice is the local Kokoro backend key, and speech.voice is the TUI speak-aloud voice whose enum is Kokoro-only. A restarted session proved it the honest way - the tts device arrived at 7772 with no voice key and got "unknown voice None; available voices: narrator1, narrator2, narrator3" - so the fresh-process success earlier that day was the run that was told to pass voice_id, not evidence of a setting.

The law now reads: voice comes from voice_id alone, so a voice-less call is the harness default shape (the endpoint grew DEFAULT_VOICE in its own PR to answer it), and speech.voice must stay a Kokoro name - a pack name there breaks the vocalizer and does nothing for synthesis. config.yml reverted to am_fenrir on the operator box, and the worklog entry that recorded narrator1 as wiring carries the correction.
This commit is contained in:
Frank committed 2026-10-18 19:21:22 -07:00
1 parent 7e391d4325
commit 0d70f115d1
2 files changed
+32 -13

No files matched your search

+17 -10
View File
@@ -154,17 +154,24 @@ The operator harness reaches this endpoint through
`baseUrl: http://192.168.0.143:7772/v1`, `auth: none`, and the runner api `baseUrl: http://192.168.0.143:7772/v1`, `auth: none`, and the runner api
`openai-speech` on the **model** row (naming a runner api at *provider* level `openai-speech` on the **model** row (naming a runner api at *provider* level
fails the whole file's validation and disables every custom provider, which is fails the whole file's validation and disables every custom provider, which is
how the fleet's chat endpoints disappear) — plus `config.yml`: how the fleet's chat endpoints disappear) — plus `config.yml`
`modelRoles.speech: frank-tts (dankclaw)/frank-tts` and `speech.voice: `modelRoles.speech: frank-tts (dankclaw)/frank-tts`. Four laws, read out of the
narrator1`. Three laws, read out of the harness bundle and measured the day of harness bundle and measured the day of the flip:
the flip:
- **The forwarded voice key is `speech.voice`, not `tts.localVoice`.** The - **The only voice the harness can send is the `voice_id` argument of the call.**
latter belongs to the local Kokoro backend and still carries `bm_george`; it The transport builds `{model, input, response_format, voice?}` and adds `voice`
is what answered when the role pointed at `local/kokoro`. `speech.voice` **only** when the caller passed `voice_id`; no setting feeds it.
becomes the request's `voice`, and this endpoint treats an unknown name as an `tts.localVoice` is the *local Kokoro backend's* voice (it is what answered
error rather than a default — so a Kokoro-style revert has to move **both** `voice=kokoro/bm_george, backend=local` in the control test), and
keys together. `speech.voice` is the TUI's **speak-aloud** voice — its enum is Kokoro's list,
and it never reaches an HTTP request. So a harness call arrives voice-less by
default, which is exactly why the endpoint grew `DEFAULT_VOICE`: the declared
pack answers a voice-less request and the journal marks that line `(default)`,
while an **unknown** name still 400s with the list that exists.
- **`speech.voice` must stay a Kokoro name.** Setting it to a pack name does not
select a pack; it hands the vocalizer a value outside its enum. (The first
draft of this section said the opposite and was reverted the same day, after a
fresh session's call came back `unknown voice None`.)
- **There is no silent fallback.** An explicitly configured `modelRoles.speech` - **There is no silent fallback.** An explicitly configured `modelRoles.speech`
yields exactly one candidate, so a dead 7772 fails the speech device with yields exactly one candidate, so a dead 7772 fails the speech device with
*"No available speech model matches the speech role"* instead of quietly *"No available speech model matches the speech role"* instead of quietly
+15 -3
View File
@@ -2706,9 +2706,10 @@ every restart since the cutover.
actually selects a voice. Engine-side ledger for the harness request: actually selects a voice. Engine-side ledger for the harness request:
`TTFA 67.5 ms`, `Total 2158.8 ms`, `audio 3.76 s, RTF 0.574`. `TTFA 67.5 ms`, `Total 2158.8 ms`, `audio 3.76 s, RTF 0.574`.
- **The harness `speech` role moved off Kokoro**: `config.yml` - **The harness `speech` role moved off Kokoro**: `config.yml`
`modelRoles.speech: frank-tts (dankclaw)/frank-tts` and `speech.voice: `modelRoles.speech: frank-tts (dankclaw)/frank-tts`. No voice key moves with it.
narrator1`. `tts.localVoice` stays `bm_george` — that key belongs to the local The bundle is unambiguous: the speech device forwards the call's `voice_id` and
backend, which is what answered in the control test. nothing else, `tts.localVoice` belongs to the local backend (it is what answered
in the control test), and `speech.voice` is the TUI's speak-aloud enum.
- Two harness laws learned by breaking it first (both now on [[Frank TTS]]): an - Two harness laws learned by breaking it first (both now on [[Frank TTS]]): an
explicit speech role has **no fallback candidate**, so a dead endpoint errors explicit speech role has **no fallback candidate**, so a dead endpoint errors
instead of degrading; and `models.yml` loads at process start while instead of degrading; and `models.yml` loads at process start while
@@ -2722,3 +2723,14 @@ every restart since the cutover.
voice leg works; open-notebook itself is what is missing), the engine page's voice leg works; open-notebook itself is what is missing), the engine page's
"Deploy order — nothing below has been run" is now a **Deploy record**, and the "Deploy order — nothing below has been run" is now a **Deploy record**, and the
homepage card lost `wip: down` so the admin panel probes 7772 for real. homepage card lost `wip: down` so the admin panel probes 7772 for real.
- Corrected the same evening, by breaking it in a restarted session: a harness
call that names no voice reaches 7772 with **no `voice` key at all** — `400
unknown voice None; available voices: narrator1, narrator2, narrator3` —
because no setting feeds one. The earlier `Saved 180524 bytes
(voice=narrator1)` succeeded only because that run was *told* to pass
`voice_id`. The endpoint therefore grew `DEFAULT_VOICE` (its own PR): a
voice-less request speaks the declared pack and the journal line carries
`(default)`, while an unknown name still 400s with the list. Reverting the role
to Kokoro is one key, not two, and `speech.voice: narrator1` — briefly recorded
above as wiring — went back to `am_fenrir`: that key is the vocalizer's Kokoro
enum, so a pack name there breaks speak-aloud and does nothing for synthesis.