Same lines, same voices, run through both backends after the fix. Fish-Speech gets a translated English [tag] baked into the text; Qwen3 Voice Design gets the native German instruct sentence, unchanged.
| Emotion | Fish-Speech | Qwen3 Voice Design |
|---|---|---|
| befehlend | ||
| kalt und zynisch | ||
| fragend | ||
| entschlossen | ||
| ernst |
| Emotion | Fish-Speech | Qwen3 Voice Design |
|---|---|---|
| resigniert | ||
| besorgt | ||
| abweisend | ||
| verächtlich | ||
| schroff |
| Emotion | Fish-Speech | Qwen3 Voice Design |
|---|---|---|
| befehlend | ||
| entschlossen | ||
| dringend | ||
| drohend | ||
| überrascht |
| Emotion | Fish-Speech | Qwen3 Voice Design |
|---|---|---|
| happy (glücklich) | ||
| sad (traurig) | ||
| angry (wütend) | ||
| scared (ängstlich) | ||
| excited (aufgeregt) |
| Emotion | Fish-Speech | Qwen3 Voice Design |
|---|---|---|
| happy (glücklich) | ||
| sad (traurig) | ||
| angry (wütend) | ||
| scared (ängstlich) | ||
| excited (aufgeregt) |
| Emotion | Fish-Speech | Qwen3 Voice Design |
|---|---|---|
| happy (glücklich) | ||
| sad (traurig) | ||
| angry (wütend) | ||
| scared (ängstlich) | ||
| excited (aufgeregt) |
This isolates the emotion variable cleanly (sections 1 & 2 above used different lines per emotion). Measured acoustics (pitch/loudness) are shown next to each — Fish-Speech shows a large, consistent shift (sad/whisper much lower & quieter, angry/happy higher & louder); Qwen3 shows a strong shift for "happy" but almost none for "angry".
| Version | Audio | Pitch (Hz) | Loudness (RMS) |
|---|---|---|---|
| Neutral (no tag) | 160 | 0.047 | |
| [angry] | 226 | 0.065 | |
| [happy] | 203 | 0.064 | |
| [sad] | 92 | 0.021 | |
| [whisper] | 84 | 0.020 |
| Version | Audio | Pitch (Hz) | Loudness (RMS) |
|---|---|---|---|
| Neutral | 158 | 0.057 | |
| Angry | 158 | 0.065 | |
| Happy | 310 | 0.072 |
Testing whether emotion differentiation is stronger in English than German. Pitch numbers below are measured (not felt) — Fish-Speech barely moves at all in English (72-80Hz across every emotion); Qwen3 Voice Design shows a genuinely clean, coherent gradient: sad < angry < shocked < happy. This is the clearest sign yet that Qwen3 Voice Design's emotional differentiation is much stronger in English than German — Fish-Speech stays weak regardless of language.
| Emotion | Audio | Pitch (Hz) | Loudness |
|---|---|---|---|
| Neutral | 76 | 0.080 | |
| Whispering | 75 | 0.066 | |
| Sad | 80 | 0.073 | |
| Angry | 73 | 0.088 | |
| Happy | 72 | 0.082 | |
| Shocked | 80 | 0.077 |
| Emotion | Audio | Pitch (Hz) | Loudness |
|---|---|---|---|
| Neutral | 109 | 0.066 | |
| Whispering | (breathy, pitch untrackable) | 0.040 | |
| Sad | 274 | 0.049 | |
| Angry | 297 | 0.090 | |
| Shocked | 348 | 0.093 | |
| Happy | 398 | 0.091 |
Same German dialogue line, but the instruct sentence is written in English instead of German, to see if that captures some of section 4's improvement while keeping the actual book text in German. Angry stood out clearly (340Hz vs ~200Hz baseline); sad/happy/shocked stayed muddled together.
| Emotion | Audio | Pitch (Hz) | Loudness |
|---|---|---|---|
| Neutral | 223 | 0.062 | |
| Sad | 201 | 0.072 | |
| Angry | 340 | 0.097 | |
| Happy | 222 | 0.074 | |
| Shocked | 175 | 0.068 |
Everything in sections 1–5 above was generated while a JavaScript bug was silently breaking the app.
A typeof check on a const in its temporal dead zone was throwing during startup, which stopped
the whole bundle from initializing past that point. Those earlier samples were measuring a half-working app, so please
disregard them. Below is the same controlled test (identical German line, identical voice, only the emotion varying)
re-run after the fix — plus emotion instructions are now written in English, which Qwen3 follows far more reliably.
Now a clean, correctly-ordered gradient, and note the durations vary sensibly too: sad drags (5.76s), scared rushes (3.20s). That's real prosody, not just volume.
| Emotion | Audio | Pitch (Hz) | Loudness | Duration |
|---|---|---|---|---|
| Whisper | 128 | 0.048 | 4.48s | |
| Sad | 142 | 0.048 | 5.76s | |
| Neutral | 179 | 0.039 | 3.92s | |
| Scared | 203 | 0.078 | 3.20s | |
| Happy | 225 | 0.066 | 3.84s | |
| Angry | 269 | 0.073 | 4.48s |
Still essentially two registers (quiet/low: whisper+sad — loud/high: everything else). Scared/happy/angry remain hard to tell apart.
| Emotion | Audio | Pitch (Hz) | Loudness | Duration |
|---|---|---|---|---|
| Whisper | 101 | 0.021 | 3.95s | |
| Sad | 91 | 0.020 | 4.46s | |
| Neutral | 160 | 0.047 | 3.90s | |
| Scared | 223 | 0.064 | 4.23s | |
| Happy | 203 | 0.064 | 3.90s | |
| Angry | 226 | 0.065 | 4.50s |
Why everything above was misleading: Voice Design re-rolls the voice on every single call. I generated the identical prompt 4 times and got 174, 232, 174 and 264 Hz — an 89 Hz spread with nothing changed. That is the same size as the "emotion differences" I was reporting, so those numbers were mostly random noise. It also explains what you heard: the male→female jumps, and "sad sounds happy". Voice Design designs a new voice from the prompt; it is not a stable speaker, so it is the wrong tool for an audiobook.
voice_clone is deterministic — the same input produced bit-identical audio twice over. So on this backend, any difference you hear is genuinely the emotion, not a re-roll. Character identity holds (measured timbre drift ≤ 0.011).
| Emotion | Audio | Pitch | Duration | Loudness |
|---|---|---|---|---|
| Whisper | 206 | 4.32s | 0.072 | |
| Sad | 170 | 4.16s | 0.058 | |
| Neutral | 225 | 3.84s | 0.071 | |
| Scared | 239 | 4.16s | 0.072 | |
| Happy | 234 | 3.84s | 0.070 | |
| Angry | 233 | 3.92s | 0.071 |
Two style directives in one instruct compete and the trailing one wins: the range squashes to 222–250 Hz and sad all but disappears. I've now dropped the accent clause whenever a line carries an emotion — a cloned voice already gets its accent from the reference WAV.
| Emotion | Audio | Pitch | Duration | Loudness |
|---|---|---|---|---|
| Whisper | 242 | 3.92s | 0.062 | |
| Sad | 232 | 4.08s | 0.069 | |
| Neutral | 222 | 3.92s | 0.066 | |
| Scared | 250 | 3.92s | 0.076 | |
| Happy | 234 | 3.76s | 0.066 | |
| Angry | 231 | 3.92s | 0.070 |
Marcian's reference clip is the culprit for the shouting: it is only 3.1 seconds long and sits at 253 Hz, which is shouting range for a male voice, with the narrowest dynamic range of any voice checked. Cloning reproduces that faithfully, so every Marcian line shouts no matter what emotion is requested.
But swapping to calm voices exposes the deeper problem. On these two the emotional pitch range is only 19–22 Hz — barely audible. Marcian's wider 69 Hz spread was mostly just his shouty baseline having room to fall, not better emotional control. So voice_clone keeps identity rock-steady but delivers only weak emotion.
| Emotion | Audio | Pitch | Duration |
|---|---|---|---|
| Whisper | 112 | 4.56s | |
| Sad | 109 | 4.32s | |
| Neutral | 111 | 4.72s | |
| Happy | 110 | 4.40s | |
| Angry | 131 | 4.48s |
| Emotion | Audio | Pitch | Duration |
|---|---|---|---|
| Whisper | 145 | 5.68s | |
| Sad | 135 | 4.88s | |
| Neutral | 154 | 5.12s | |
| Happy | 149 | 6.08s | |
| Angry | 151 | 5.28s |
Where this leaves us: Fish-Speech → two registers only, everything shouts. Voice Design → strong changes but re-rolls the voice every call, so identity drifts and results are random. voice_clone → perfectly stable identity but only ~20 Hz of emotion. None of the three running backends does both. CustomVoice (port 8022) is down — per the Qwen3-TTS docs that is the variant built specifically for timbre control plus emotional style, and it is the one option not yet tested.
I checked what each container actually loads, and your hypothesis was exactly correct:
| Backend | Model loaded | Instruction / emotion control? |
|---|---|---|
| voice_clone (8020) | Qwen3-TTS-12Hz-1.7B-Base | No |
| streaming (8023) | same Base model | No |
| VoiceDesign (8021) | ...-VoiceDesign | Yes |
| CustomVoice (8022) | ...-CustomVoice | Yes |
So my earlier "voice_clone responds to emotion" claim was wrong — that was a Base model being mildly perturbed by instruction text it does not understand, which is why the range was only 19–22 Hz. I have also confirmed on HuggingFace that no "Instruct" variant of Qwen3-TTS exists — the family is exactly these three.
This is the strongest real emotion control measured so far: 85 Hz range, with speaker identity holding (timbre drift ≤0.008) and sensible pacing (angry fast 4.08s, scared slow 6.16s). Repeat runs confirm the effect is genuine, not noise — sad lands at 111–136 Hz and angry at 159–210 Hz, ranges that never overlap.
| Emotion | Audio | Pitch | Duration | Loudness |
|---|---|---|---|---|
| Whisper | 121 | 5.76s | 0.037 | |
| Sad | 187 | 5.92s | 0.043 | |
| Neutral | 134 | 5.12s | 0.025 | |
| Happy | 187 | 4.88s | 0.066 | |
| Scared | 163 | 6.16s | 0.054 | |
| Angry | 206 | 4.08s | 0.053 |
The catch: CustomVoice exposes only 9 fixed speakers (Ryan, Aiden, Vivian, Serena, Uncle_Fu, Dylan, Eric…) and its API has no reference-audio field at all — it cannot clone. So it cannot voice the book's 57 cloned characters. Qwen3-TTS splits cloning and emotion across two models that cannot be combined: Base clones but ignores instructions, CustomVoice obeys instructions but cannot clone, VoiceDesign obeys instructions but invents a new voice on every call.
Your plan (narrator on voice_clone, characters on VoiceDesign) is sound in structure — the narrator on voice_clone is exactly right: identity is rock-steady and he needs no emotion. But before committing 50 hours to VoiceDesign for the characters, listen to this.
Each block below is the same character, same line, same emotion, called three times. Nothing changes between runs. VoiceDesign has no seed parameter, so it cannot be pinned.
| # | Audio | Pitch |
|---|---|---|
| Run 1 | 163 Hz | |
| Run 2 | 238 Hz | |
| Run 3 | 178 Hz | |
| spread across identical calls | 75 Hz | |
| # | Audio | Pitch |
|---|---|---|
| Run 1 | 190 Hz | |
| Run 2 | 130 Hz | |
| Run 3 | 214 Hz | |
| spread across identical calls | 84 Hz | |
| # | Audio | Pitch |
|---|---|---|
| Run 1 | 125 Hz | |
| Run 2 | 116 Hz | |
| Run 3 | 119 Hz | |
| spread across identical calls | 9 Hz | |
| # | Audio | Pitch |
|---|---|---|
| Run 1 | 158 Hz | |
| Run 2 | 269 Hz | |
| Run 3 | 178 Hz | |
| spread across identical calls | 111 Hz | |
Averaged over 3 runs each, VoiceDesign's emotions do not separate reliably: neutral 136 Hz,
sad 158, happy 231, angry 237 — but "happy" alone ranged 122–311 Hz across identical calls. The variation
within one emotion is larger than the difference between emotions, and sad landed above neutral
(backwards). Over ~2000 lines that means a character whose pitch wanders line to line.
CustomVoice's "sad" by contrast is tight (116–125 Hz, 9 Hz spread) — genuinely reliable. Its weakness is
coverage: 9 fixed speakers, no cloning.
Correction: Fish-Speech is perfectly deterministic. The same request twice produced byte-identical audio (same MD5, 0 Hz spread) because the app pins a per-voice seed. That changes the picture completely:
| Engine | sad→angry separation | Run-to-run noise | Verdict |
|---|---|---|---|
| Fish-Speech | 135 Hz | 0 Hz | signal, no noise |
| CustomVoice | 51 Hz | 26–51 Hz | signal ≈ noise |
| VoiceDesign | 101 Hz | up to 189 Hz | noise > signal |
And it is three registers, not two — I under-sold it earlier. Because output is deterministic, this map is exact rather than a sample. Emotion tags reliably land in one of three intensity levels:
| Tag | Audio | Pitch | Dur | Loudness |
|---|---|---|---|---|
| sad | 91 Hz | 4.46s | 0.0205 | |
| whispering | 101 Hz | 3.95s | 0.0211 | |
| calm | 102 Hz | 3.81s | 0.0244 | |
| disappointed | 105 Hz | 3.99s | 0.0231 | |
| curious | 104 Hz | 3.99s | 0.0274 | |
| resigned | 115 Hz | 4.27s | 0.0353 |
| Tag | Audio | Pitch | Dur | Loudness |
|---|---|---|---|---|
| neutral | 160 Hz | 3.9s | 0.047 | |
| tender | 168 Hz | 3.44s | 0.0413 | |
| sobbing | 177 Hz | 4.6s | 0.052 |
| Tag | Audio | Pitch | Dur | Loudness |
|---|---|---|---|---|
| confident | 194 Hz | 4.32s | 0.0591 | |
| sarcastic | 200 Hz | 4.04s | 0.064 | |
| happy | 203 Hz | 3.9s | 0.0642 | |
| scared | 223 Hz | 4.23s | 0.0639 | |
| angry | 226 Hz | 4.5s | 0.0649 | |
| excited | 230 Hz | 4.18s | 0.0644 | |
| shouting | 234 Hz | 4.04s | 0.0649 |
What this means for the book: Fish clones all 57 characters, holds every identity perfectly stable, and
gives three dependable intensity levels — subdued for sad/whispered/resigned lines, neutral for ordinary narration,
energized for angry/excited/frightened ones. What it will not do is distinguish angry from happy: both sit in the
energized band (226 vs 203 Hz). The emotional contour of a scene comes through; the specific colour does not.
Determinism has a practical bonus too: a re-run reproduces identical audio, so an interrupted export resumes cleanly
instead of drifting.
Voice: EN_F_Melina (best dynamic range of the English references, 10.4s clip).
English text, English tags in square brackets before the line, per the S2 word-level-control article.
I tested both plain single-word tags and the article's richer free-form style (e.g. its own example
[voice rough from crying, trying to sound normal], and pairing a physical reaction with an emotion).
| Emotion | Tag | Audio | Pitch | Dur |
|---|---|---|---|---|
| neutral | (no tag) | 187 Hz | 4.23s | |
| happy | [happy] | 186 Hz | 3.95s | |
| sad | [sad] | 186 Hz | 4.18s | |
| angry | [angry] | 186 Hz | 4.32s | |
| scared | [scared] | 197 Hz | 3.95s |
| Emotion | Tag | Audio | Pitch | Dur |
|---|---|---|---|---|
| neutral | (no tag) | 187 Hz | 4.23s | |
| happy | [bright and delighted, barely containing a grin] | 185 Hz | 4.04s | |
| sad | [voice rough from crying, trying to sound normal] | 188 Hz | 4.27s | |
| angry | [furious, teeth clenched, spitting the words out] | 196 Hz | 5.25s | |
| scared | [breathless with fear, voice trembling] | 183 Hz | 3.95s |
Correction to section 11 — I over-claimed. Both tag styles above land within
183–197 Hz, i.e. essentially no emotional differentiation, and the richer tags were no better
than the simple ones. Checking further, every other voice behaves the same way:
| Voice | sad | angry | range |
|---|---|---|---|
| DE_M_Marcian | 91 Hz | 226 Hz | 135 Hz |
| DE_M_Darrag | 86 | 94 | 8 Hz |
| DE_F_Lysandra | 186 | 176 | 10 Hz |
| Ernst Meinecke (narrator) | 109 | 111 | 2 Hz |
| EN_M_Alan_Rickman | 72 | 80 | 8 Hz |
| EN_M_Albus_Dumbledore | 189 | 184 | 5 Hz |
[sad] tag has enormous room to pull him
down. Normal, well-recorded references barely move at all. I also tested whether reference length was the cause by
truncating two voices to 3 seconds — it made no difference, so it is the unusual character of that particular clip,
not its duration.
I investigated the mechanism instead of guessing. The finding explains every confusing result above:
Fish-Speech emotion is driven by the REFERENCE CLIP, not by the text tags.
When you supply reference audio, S2 does in-context prosody imitation — it copies the reference's delivery, and that
overrides bracket tags almost entirely. Tags only work when there is no reference competing with them.
| Tag | No reference | With reference (cloning) |
|---|---|---|
| [sad] | loudness 0.051 (−40%), 21% slower | 0.096 (−11%) |
| [angry] | 0.094, dynamics 0.324, darker timbre | 0.097, dynamics 0.245 |
| [whispering] | 0.054 (−36%) | 0.091 (−16%) |
Identical plain sentence, no bracket tags anywhere. Only the reference clip changed:
| Reference used | Pitch | Loudness | Brightness |
|---|---|---|---|
| calm / neutral | 183 Hz | 0.075 | 2699 |
| scared reference | 106 Hz | 0.022 (5× quieter) | 2380 |
| angry reference | 121 Hz | 0.108 (loud) | 1778 (dark) |
| happy reference | 210 Hz | 0.106 | 2177 |
These finally differentiate: 10× loudness spread, pitch 110–222 Hz, and genuine intonation differences (happy pstd 45 vs scared pstd 5). Caveat: with no reference the model also picks its own voice, so identity is not controlled here (speaker distance 0.027 vs 0.011 for a true clone).
| Emotion | Tag used | Audio | Pitch | Dur | Loudness |
|---|---|---|---|---|---|
| neutral | (no tag) | 182 Hz | 3.58s | 0.084 | |
| happy | [cheerful and bright, smiling as she speaks] | 222 Hz | 4.27s | 0.0997 | |
| sad | [voice rough from crying, trying to sound normal] | 155 Hz | 3.2s | 0.2098 | |
| angry | [furious, teeth clenched, spitting the words out] | 129 Hz | 3.72s | 0.1037 | |
| scared | [breathless with fear, voice trembling] | 110 Hz | 4.13s | 0.0208 |
What this means — and the way forward. The trade-off is inherent: a reference gives you the character's identity but suppresses tag-based emotion; no reference gives you emotion but an uncontrolled voice. But Evidence B points at a real solution: give each character several reference clips, one per emotional register (neutral, subdued, intense) and pick the matching one per line. That uses the mechanism instead of fighting it — delivering cloned identity and emotion, both fully deterministic.
Your five-line example, generated twice through Fish Audio S2 Pro. Identical text, identical tags. The only difference is whether a cloned reference voice was supplied.
Pitch across all five emotions: 241–268 Hz — a 27 Hz spread. Every line lands in the same place, which is exactly what you reported: "no difference between the lines".
| Line | Pitch | Loudness |
|---|---|---|
| [happy] | 293 Hz | 0.194 (loudest) |
| [uncertain] | 237 Hz | 0.148 |
| [sad] | 184 Hz | 0.125 |
| [hopeful] | 207 Hz | 0.084 |
| [determined] | 170 Hz | 0.061 (quietest) |
123 Hz spread and 3.2× loudness range — four to five times the differentiation, in a sensible order (happy highest and loudest, determined lowest and quietest). Because this is one single generation, the voice stays consistent throughout; the earlier "5 different voices" problem only happened when each emotion was generated as its own separate call.
This is the trade-off in one page: Fish gives you the cloned character voice, or the emotion tags — never both. The reference clip is prepended as an in-context example and the model imitates its delivery, which overrides the tags.
If no reference clip is used, the voice can still be steered — by describing it in a tag. S2-Pro accepts free-form descriptions, so gender, age and character are controllable directly:
| Description tag | Audio | Pitch |
|---|---|---|
(no tag) | 182 Hz | |
[deep male voice] | 91 Hz | |
[young woman's voice] | 191 Hz | |
[elderly man, frail and raspy] | 93 Hz | |
[small child, high voice] | 202 Hz | |
[gruff old warrior, deep and gravelly] | 72 Hz |
That is a 72–202 Hz range — clearly distinguishable men, women and children, all without any reference clip.
Three runs of the same description and seed produced byte-identical files (matching MD5). So a character can be pinned as "description + seed" and will sound the same every time — this fixes the "5 different voices" problem from section 13.
Same character description throughout, only the emotion tag changes. Emotion clearly works (174 Hz range, 6.4× loudness) — but the character drifts as well. "Identity drift" below compares timbre against the neutral take; a true cloned voice stays at 0.001–0.011, and roughly 0.03+ starts to read as a different person.
| Tag | Audio | Pitch | Loudness | Identity drift |
|---|---|---|---|---|
(no emotion tag) | 72 Hz | 0.033 | — | |
[happy] | 174 Hz | 0.113 | 0.033 | |
[sad] | — | 0.209 | 0.212 | |
[angry] | 123 Hz | 0.201 | 0.044 | |
[whisper] | 81 Hz | 0.100 | 0.069 |
Please judge this by ear — do these five still sound like the same person, or like different speakers? If they hold together well enough, this is a workable path for the audiobook: define each character as a description plus a fixed seed, and emotion tags will actually function. If they do not, then cloned voices with neutral delivery remains the honest option.