Emotion fix - sample comparison

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.

1. Book-realistic emotions (as the LLM actually assigns them, e.g. "befehlend", "resigniert")

MARCIAN

EmotionFish-SpeechQwen3 Voice Design
befehlend
kalt und zynisch
fragend
entschlossen
ernst

LYSANDRA

EmotionFish-SpeechQwen3 Voice Design
resigniert
besorgt
abweisend
verächtlich
schroff

DARRAG

EmotionFish-SpeechQwen3 Voice Design
befehlend
entschlossen
dringend
drohend
überrascht

2. Basic emotions (happy / sad / angry / scared / excited)

MARCIAN

EmotionFish-SpeechQwen3 Voice Design
happy (glücklich)
sad (traurig)
angry (wütend)
scared (ängstlich)
excited (aufgeregt)

LYSANDRA

EmotionFish-SpeechQwen3 Voice Design
happy (glücklich)
sad (traurig)
angry (wütend)
scared (ängstlich)
excited (aufgeregt)

DARRAG

EmotionFish-SpeechQwen3 Voice Design
happy (glücklich)
sad (traurig)
angry (wütend)
scared (ängstlich)
excited (aufgeregt)

3. Controlled test — SAME exact line, only the tag/instruct changes

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".

Fish-Speech

VersionAudioPitch (Hz)Loudness (RMS)
Neutral (no tag)1600.047
[angry]2260.065
[happy]2030.064
[sad]920.021
[whisper]840.020

Qwen3 Voice Design

VersionAudioPitch (Hz)Loudness (RMS)
Neutral1580.057
Angry1580.065
Happy3100.072

4. English text/voice test (same character-role voice, English content)

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.

Fish-Speech (English)

EmotionAudioPitch (Hz)Loudness
Neutral760.080
Whispering750.066
Sad800.073
Angry730.088
Happy720.082
Shocked800.077

Qwen3 Voice Design (English)

EmotionAudioPitch (Hz)Loudness
Neutral1090.066
Whispering(breathy, pitch untrackable)0.040
Sad2740.049
Angry2970.090
Shocked3480.093
Happy3980.091

5. German text + English instruction (Qwen3 Voice Design, Marcian's cloned voice)

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.

EmotionAudioPitch (Hz)Loudness
Neutral2230.062
Sad2010.072
Angry3400.097
Happy2220.074
Shocked1750.068

6. ⚠ RE-TEST on the FIXED build (this is the one that matters)

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.

Qwen3 Voice Design — German text, English instruct

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.

EmotionAudioPitch (Hz)LoudnessDuration
Whisper1280.0484.48s
Sad1420.0485.76s
Neutral1790.0393.92s
Scared2030.0783.20s
Happy2250.0663.84s
Angry2690.0734.48s

Fish-Speech — inline [tag]

Still essentially two registers (quiet/low: whisper+sad — loud/high: everything else). Scared/happy/angry remain hard to tell apart.

EmotionAudioPitch (Hz)LoudnessDuration
Whisper1010.0213.95s
Sad910.0204.46s
Neutral1600.0473.90s
Scared2230.0644.23s
Happy2030.0643.90s
Angry2260.0654.50s

7. ★ THE ACTUAL FIX — voice_clone backend (read this one)

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).

A. Emotion instruct alone — recommended

EmotionAudioPitchDurationLoudness
Whisper2064.32s0.072
Sad1704.16s0.058
Neutral2253.84s0.071
Scared2394.16s0.072
Happy2343.84s0.070
Angry2333.92s0.071

B. Same, but with the accent clause appended — emotion collapses

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.

EmotionAudioPitchDurationLoudness
Whisper2423.92s0.062
Sad2324.08s0.069
Neutral2223.92s0.066
Scared2503.92s0.076
Happy2343.76s0.066
Angry2313.92s0.070

8. Different characters — you were right about the shouting

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.

DE_M_Darrag — calm 110 Hz reference

EmotionAudioPitchDuration
Whisper1124.56s
Sad1094.32s
Neutral1114.72s
Happy1104.40s
Angry1314.48s

Ernst Meinecke — professional narrator, 21s reference

EmotionAudioPitchDuration
Whisper1455.68s
Sad1354.88s
Neutral1545.12s
Happy1496.08s
Angry1515.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.

9. You were right — it's the model, not the settings

I checked what each container actually loads, and your hypothesis was exactly correct:

BackendModel loadedInstruction / emotion control?
voice_clone (8020)Qwen3-TTS-12Hz-1.7B-BaseNo
streaming (8023)same Base modelNo
VoiceDesign (8021)...-VoiceDesignYes
CustomVoice (8022)...-CustomVoiceYes

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.

CustomVoice, started and tested (speaker Ryan, German text)

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.

EmotionAudioPitchDurationLoudness
Whisper1215.76s0.037
Sad1875.92s0.043
Neutral1345.12s0.025
Happy1874.88s0.066
Scared1636.16s0.054
Angry2064.08s0.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.

10. VoiceDesign vs CustomVoice — the consistency problem

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.

VoiceDesign — "happy" ×3 (identical requests)

#AudioPitch
Run 1163 Hz
Run 2238 Hz
Run 3178 Hz
spread across identical calls75 Hz

VoiceDesign — "sad" ×3 (identical requests)

#AudioPitch
Run 1190 Hz
Run 2130 Hz
Run 3214 Hz
spread across identical calls84 Hz

CustomVoice — "sad" ×3 (identical requests)

#AudioPitch
Run 1125 Hz
Run 2116 Hz
Run 3119 Hz
spread across identical calls9 Hz

CustomVoice — "happy" ×3 (identical requests)

#AudioPitch
Run 1158 Hz
Run 2269 Hz
Run 3178 Hz
spread across identical calls111 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.

11. Fish-Speech — I was wrong, and it may be the best option

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:

Enginesad→angry separationRun-to-run noiseVerdict
Fish-Speech135 Hz0 Hzsignal, no noise
CustomVoice51 Hz26–51 Hzsignal ≈ noise
VoiceDesign101 Hzup to 189 Hznoise > 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:

Subdued — quiet, low

TagAudioPitchDurLoudness
sad91 Hz4.46s0.0205
whispering101 Hz3.95s0.0211
calm102 Hz3.81s0.0244
disappointed105 Hz3.99s0.0231
curious104 Hz3.99s0.0274
resigned115 Hz4.27s0.0353

Neutral / warm — mid

TagAudioPitchDurLoudness
neutral160 Hz3.9s0.047
tender168 Hz3.44s0.0413
sobbing177 Hz4.6s0.052

Energized — loud, high

TagAudioPitchDurLoudness
confident194 Hz4.32s0.0591
sarcastic200 Hz4.04s0.064
happy203 Hz3.9s0.0642
scared223 Hz4.23s0.0639
angry226 Hz4.5s0.0649
excited230 Hz4.18s0.0644
shouting234 Hz4.04s0.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.

12. English voice + English bracket tags (as requested) — and an important correction

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).

Simple single-word tags

EmotionTagAudioPitchDur
neutral(no tag)187 Hz4.23s
happy[happy]186 Hz3.95s
sad[sad]186 Hz4.18s
angry[angry]186 Hz4.32s
scared[scared]197 Hz3.95s

Rich descriptive tags (article's recommended style)

EmotionTagAudioPitchDur
neutral(no tag)187 Hz4.23s
happy[bright and delighted, barely containing a grin]185 Hz4.04s
sad[voice rough from crying, trying to sound normal]188 Hz4.27s
angry[furious, teeth clenched, spitting the words out]196 Hz5.25s
scared[breathless with fear, voice trembling]183 Hz3.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:

Voicesadangryrange
DE_M_Marcian91 Hz226 Hz135 Hz
DE_M_Darrag86948 Hz
DE_F_Lysandra18617610 Hz
Ernst Meinecke (narrator)1091112 Hz
EN_M_Alan_Rickman72808 Hz
EN_M_Albus_Dumbledore1891845 Hz

Marcian is the single outlier — and section 11's whole "three registers" map was built on him alone. His reference clip is an anomalous 3.1-second shout at 253 Hz, so a [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.

13. ★ How Fish-Speech S2 emotion ACTUALLY works

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.

Evidence A — the same tags, with and without a reference

TagNo referenceWith reference (cloning)
[sad]loudness 0.051 (−40%), 21% slower0.096 (−11%)
[angry]0.094, dynamics 0.324, darker timbre0.097, dynamics 0.245
[whispering]0.054 (−36%)0.091 (−16%)

Evidence B — emotion transfers through the reference with NO tag at all

Identical plain sentence, no bracket tags anywhere. Only the reference clip changed:

Reference usedPitchLoudnessBrightness
calm / neutral183 Hz0.0752699
scared reference106 Hz0.022 (5× quieter)2380
angry reference121 Hz0.108 (loud)1778 (dark)
happy reference210 Hz0.1062177

The 5 samples you asked for — tags with no competing reference

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).

EmotionTag usedAudioPitchDurLoudness
neutral(no tag)182 Hz3.58s0.084
happy[cheerful and bright, smiling as she speaks]222 Hz4.27s0.0997
sad[voice rough from crying, trying to sound normal]155 Hz3.2s0.2098
angry[furious, teeth clenched, spitting the words out]129 Hz3.72s0.1037
scared[breathless with fear, voice trembling]110 Hz4.13s0.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.

14. ★ THE PROOF — same text, same tags, cloning on vs off

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.

A. WITH a cloned voice (DE_F_Einfaches_Maedchen) — what you heard

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".

B. WITHOUT a reference voice — same text, same tags

LinePitchLoudness
[happy]293 Hz0.194 (loudest)
[uncertain]237 Hz0.148
[sad]184 Hz0.125
[hopeful]207 Hz0.084
[determined]170 Hz0.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.

15. Controlling the voice WITHOUT a reference clip

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 tagAudioPitch
(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.

Reproducible: same description + same seed = identical audio

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.

The catch: emotion tags also shift the voice

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.

TagAudioPitchLoudnessIdentity drift
(no emotion tag)72 Hz0.033—
[happy]174 Hz0.1130.033
[sad]—0.2090.212
[angry]123 Hz0.2010.044
[whisper]81 Hz0.1000.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.