Discover lists speech models with the voices each accepts and a + Add that
puts the model on tts.roster. The Roster card lists every model with a voice
picker, Test, Make default and Remove. Test on any row (or a discovered model
not yet added) fills the test panel's voice list with that model's voices, so
Orpheus and Kokoro can be heard one voice at a time before either is chosen.
The default is a pair — PUT /config/tts/default sets tts.model and tts.voice
together and refuses a voice the model does not accept, naming the ones it
does. The generic setter no longer takes tts.model/tts.voice one at a time,
which is how a Kokoro voice got paired with Orpheus. A default that leaves
the roster stops being the default.
Users pick from the voices of every roster model, grouped by model in
Settings; the stored value is "model|voice" so read-aloud sends the voice to
the model that accepts it. A bare voice saved before there was a roster is
read as a voice of the default model. chooseTTS is the one place the pair is
decided, shared by read-aloud, the admin test and the settings options.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
"These settings don't work" — picking a model and testing with a voice
returned 500 every time. The gateway said why, once asked directly:
voice must be one of the following voices: [autumn diana hannah austin daniel troy]
The screen was listing twelve Orpheus voices and six Kokoro ones in a
single flat list with no indication of which model would accept which,
because LITELLM_TTS_VOICES — written for one model — was treated as a
list that applied to all of them, and the Orpheus lists were pushed in
beside it. Choosing Orpheus and testing it with a Kokoro voice is not a
configuration; it is an error, and it was the default outcome.
LiteLLM cannot supply this. /model/info reports mode audio_speech for
all four models and carries no voice field for any of them. So the
mapping lives here, keyed by family so the gateway alias and the
upstream id resolve to one list, and every list was taken from the
provider rather than from documentation:
Groq Orpheus English autumn diana hannah austin daniel troy (stated by Groq)
Groq Orpheus Arabic abdullah fahad sultan lulwa noura aisha (stated by Groq)
Fish s2.1-pro alloy (alloy returns audio; the rest 400)
Kokoro sherpa/kokoro:* from LITELLM_TTS_VOICES (the gateway's own list)
The environment still wins for the model it was written for, so the
local gateway's voices can change without a code change — but it
answers for that model only. A model with no list at all is offered
nothing rather than another model's voices, and a voice known to belong
to a different family is refused.
There were two copies of this knowledge before: getLiteLLMTTSVoicesForModel
branched by family and fell through to the env list for any model it did
not recognise — which is how Fish came to be offered six Kokoro voices.
One table now.
Also in this commit: citation renumbering skips fenced code, inline code
and math, so arr[2][1] in a code block is never rewritten. Renumbering at
render time was tried and reverted — it also has to skip HTML attributes,
and every such region is another regex branch. It stays at the answer
boundary, and the saved-chat boundary is next.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Both were removed from the gateway in favour of Kokoro, so the app's curated
voice lists for them went too. Kokoro's own check no longer has to exclude
them — it names its own voices and the list is open, so it accepts anything
that is not another family's voice.
Voices stay curated per family rather than discovered. Models are discovered
from /model/info; voices are not, because no TTS provider exposes its voice list
consistently, and a list guessed from a model id is how a picker ends up
offering a voice the provider rejects.
Tests that pinned the retired voices now pin the families that remain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
Voice lists were a single flat set from LITELLM_TTS_VOICES, so picking a
model could leave an incompatible voice selected and the request would
fail at the gateway. Voices are now resolved per model family (Kokoro,
Kitten, Supertonic, Groq Orpheus EN/AR), with a compatibility check that
falls back through user → admin → env → first valid voice. Groq Orpheus
requests also pin response_format to wav.
Also refreshes the cardiac/respiratory auscultation samples, extends the
well-visit component, and fixes the Android launch theme background
(@null → colorPrimary) so the splash does not flash through.
NOTE: this is in-progress work that was already sitting uncommitted in
the working tree; it is committed here as-is so the tree was clean for
the release bump.