Teaching XTTS v2 to Feel: English First, Then Nepali
XTTS v2 लाई भावना सिकाउँदा
We already had a Nepali XTTS v2 that read text cleanly. What it could not do was sound like it meant anything. This is the account of adding seven emotion tags to it — first in English, where a usable emotional-speech corpus exists, then in Nepali, where we had to build one out of films.
Two results are worth stating up front, because neither is what we expected. On English, LoRA and a full fine-tune came out nearly indistinguishable — LoRA marginally ahead on emotion, the full fine-tune marginally ahead on intelligibility — so a 173 MB adapter matched a 5.3 GB checkpoint, and we picked between them by ear. On Nepali, starting from a model that already knew English emotion bought us nothing measurable: there was no free transfer across languages, and the model learned Nepali emotion only from Nepali audio.
Base: Oshara Nepali XTTS v2 (epoch 10) · Method:
<emotion>tag tokens · Data: EmoVoice-DB (en) + 1.83 h mined from Nepali film (ne) · Langs: en · ne
00 · How to read the samples in this post
One property of the training data shapes everything that follows, so it belongs before the results rather than buried in a caveat.
EmoVoice-DB pairs emotional wording with emotional delivery: its angry rows say angry things, its sad rows say sad things, and that is true for each of the seven emotions. So the tag and the text always agreed during training, and the model learned them together. It follows that the tags work best exactly where the training data lived — when the sentence itself is the angry one. Give it a flat sentence and ask for anger with the tag alone and you are asking for something it never saw.
Here is that difference, same model, same voice, same seed — only the wording changes:
Spoken text
You promised me you would handle it, and you did nothing at all!
Spoken text
So this is what you decided to do about it.
We measured the gap over five seeds per tag. With emotion-matched wording the classifier agrees with the tag 29/35 of the time in English; with deliberately flat wording, 5/35. That is what the tags are for and where they hold, and every sample below uses emotion-appropriate text for exactly that reason.
01 · How you make a TTS model take direction
XTTS v2 has no emotion input. It takes text and six seconds of reference audio, and its prosody comes from whatever that reference happens to sound like. If you want angry, the conventional move is to hand it an angry reference clip — which means emotion control is really reference-clip curation, and you cannot ask for an emotion you do not already have a recording of.
We wanted emotion to be part of the text, so the API is a tag:
<angry> You promised me you would handle it, and you did nothing at all!
Mechanically this is small. Seven tokens are appended to the tokenizer’s vocabulary — one per emotion, taking ids 6681–6687 in a 6,689-token vocab — and the GPT learns what each one does to the audio tokens that follow. Nothing else about the architecture changes: the DVAE, the vocoder and the Perceiver resampler never learn that emotions exist.
The seven categories come from EmoVoice-DB: angry, disgusted, fearful, happy, neutral, sad, surprised. The taxonomy is inherited from the dataset, not chosen by us — which matters later, when three of the seven turn out to be nearly unminable from real film audio.
The detail that decides whether any of this works
XTTS is conditioned on a reference clip, and by default the trainer cuts that
reference out of the target utterance itself
(gpt_use_masking_gt_prompt_approach=True). For emotion training that setting is
fatal, and quietly so. If the prompt audio is a slice of the same angry sentence you
are asking the model to produce, then the anger already reached the model through the
audio, and the <angry> token has nothing left to explain. Training converges
nicely and the tags control nothing.
So every row needs a neutral reference clip from the same speaker. The tag is then the only place the emotion can come from. Getting that right is trivial in EmoVoice-DB, which has neutral recordings of every voice. In Nepali film audio it turned out to be most of the work — section 07.
02 · A warning about the scorer, before any numbers
Every emotion number in this post comes from emotion2vec+ large (300M params, 42.5k hours) — the best open multilingual speech-emotion classifier we found, and the one our Nepali mining pipeline uses to label clips. It is also demonstrably a weak labeller, and we can put a number on that rather than hand-wave it.
While mining Nepali film audio (section 06) every clip got the classifier’s proposed emotion and a human reviewer’s verdict. Across 2,632 reviewed clips, the classifier’s label survived review unchanged 40.9% of the time:
Table view
| proposed label | reviewed | kept | relabelled | rejected | kept % |
|---|---|---|---|---|---|
| sadness | 215 | 135 | 17 | 63 | 62.8% |
| neutral | 383 | 220 | 34 | 129 | 57.4% |
| anger | 766 | 402 | 79 | 285 | 52.5% |
| happiness | 1,105 | 305 | 192 | 608 | 27.6% |
| disgusted | 56 | 8 | 20 | 28 | 14.3% |
| fearful | 42 | 4 | 25 | 13 | 9.5% |
| surprised | 65 | 2 | 27 | 36 | 3.1% |
The failure is not uniform, and that is the part that matters here. On the four
emotions that carry the dataset it holds up 43.0% of the time; on the three rare ones
it manages 8.6% — surprised survived 2 times out of 65. It also over-produces
neutral and has a pair of other/unknown heads it retreats to when unsure.
So the evaluation and the labelling share a bias. We score our models with the
same classifier that mislabels rare emotions and defaults to neutral, which means our
per-emotion numbers inherit its skew. Where the model looks worst — fearful,
surprised — is exactly where the classifier is least trustworthy, and we genuinely
cannot separate “the model failed to produce fear” from “the classifier cannot hear
fear”. Read the numbers as a consistent relative yardstick across our own models,
not as absolute measurements of emotional quality. The classifier is also ~0.92
confident when it agrees with the tag and ~0.88 when it does not, so its own
confidence will not tell you which case you are in.
That is the honest frame for everything below. It is also the argument for the listening test we have not yet run.
03 · English: the same run twice, LoRA and full fine-tune
Starting from our Nepali XTTS v2 checkpoint (epoch 10 — the one from the earlier post), we trained the tags on EmoVoice-DB twice, changing only the adaptation method.
The data
EmoVoice-DB, 21,050 training rows and a 350-row held-out split — 50 clips per emotion, five voices (ash, ballad, coral, sage, verse). Every row is conditioned on a separate neutral clip from the same voice.
Full fine-tune
All 518M GPT parameters at learning rate 5e-6, plus a separate optimizer group at
1e-4 for text_embedding and text_head. That second group exists because the
seven new tag rows start at exactly zero while pretrained rows have a much larger
norm; at 5e-6 they reach L2 ≈ 0.0095 after 500 steps, which after the GPT’s LayerNorm
is indistinguishable from a zero vector. All seven tags would stay interchangeable
and control nothing.
LoRA
Base frozen. Rank 64, alpha 128, dropout 0.05 on c_attn / c_proj / c_fc, at
1e-4 — with text_embedding and text_head in modules_to_save, for the same
reason. 122.6M trainable parameters against 518.5M, and a 173 MB adapter instead of
a 5.6 GB checkpoint.
Everything else held identical
5 epochs, 3,290 steps, batch 32 × grad-accum 2, bf16, AdamW (0.9/0.96, wd 1e-2), MultiStepLR ×0.5, same base checkpoint, same splits.
full fine-tuneLoRA
Table view
| step | full FT | LoRA | LoRA ahead by |
|---|---|---|---|
| 0 | 3.6914 | 3.6700 | 0.0214 |
| 500 | 3.0233 | 2.9555 | 0.0678 |
| 658 | 3.0109 | 2.9487 | 0.0622 |
| 1,000 | 2.9700 | 2.9155 | 0.0545 |
| 1,316 | 2.9574 | 2.9077 | 0.0497 |
| 1,500 | 2.9411 | 2.8922 | 0.0489 |
| 1,974 | 2.9299 | 2.8835 | 0.0464 |
| 2,000 | 2.9233 | 2.8726 | 0.0507 |
| 2,500 | 2.9146 | 2.8678 | 0.0468 |
| 2,632 | 2.9108 | 2.8661 | 0.0447 |
| 3,000 | 2.8998 | 2.8515 | 0.0483 |
| 3,290 | 2.8963 | 2.8532 | 0.0431 |
LoRA’s eval loss is lower at every checkpoint, and its artifact is 1/31st the size. On the metrics that matter downstream, though, the two runs split — and the margins are small enough that “split” is the honest word:
| Metric | Full fine-tune | LoRA | Ahead |
|---|---|---|---|
| Emotion accuracy | 0.412 | 0.427 | LoRA, +0.015 (≈4 clips) |
| Macro-F1 | 0.356 | 0.378 | LoRA, +0.022 |
| Best eval loss | 2.8963 | 2.8515 | LoRA |
| WER (round-trip) | 0.028 | 0.030 | Full FT |
| CER (round-trip) | 0.0059 | 0.0076 | Full FT |
| ScoreQ MOS | 3.799 | 3.709 | Full FT |
| SQUIM MOS | 4.182 | 4.215 | LoRA |
| Clips scored / classifier declined | 272 / 78 | 274 / 76 | — |
| Trainable params | 518.5M | 122.6M | LoRA |
| Deployable artifact | 5.3 GB | 173 MB | LoRA, 31× smaller |
Per emotion, LoRA’s whole advantage is concentrated in one place: surprised F1
0.280 against 0.130. Elsewhere they trade single clips — the full fine-tune is better
on sad (0.421 vs 0.384), LoRA on happy (0.652 vs 0.619), and the rest is noise.
Do not over-read this table. +0.015 accuracy is about four clips out of 274, from a single training run at a single seed, judged by the classifier section 02 is about. The two MOS estimators disagree in direction. The defensible summary is that LoRA matched a full fine-tune at 1/31st the size, which is a real and useful result, and that nothing here establishes either as better at emotion.
One number is worth pulling out: loss_text_ce finished at 0.0191 for both runs,
identical to four decimals. Both methods learned the tag tokens equally well as
symbols. The entire gap is loss_mel_ce — acoustic realisation. The tags were never
the hard part; turning them into sound was.
So we picked by ear. Listening to matched pairs, the full fine-tune’s emotional speech was consistently more natural — LoRA’s angry and happy read as louder and faster rather than genuinely different in delivery. That agrees with the intelligibility and ScoreQ columns and disagrees with the emotion-classifier columns, which is roughly what you would expect if the classifier rewards arousal over authenticity. The full fine-tune is what runs in production.
Worth being blunt about the shape of that decision: we let a listening impression override a 0.015 accuracy difference. We think that is right — see section 02 for why we distrust the classifier at this resolution — but we have not run a formal listening test, so it remains a judgement call rather than a result. If the adapter’s 31× size advantage matters to you more than a small naturalness difference, the table above is an argument for LoRA and we would not call it wrong.
04 · Before and after, in English
Seven emotions, seven sentences whose words carry them — the condition the tags were trained for. Each is rendered twice, with the reference clip, the seed and the temperature held fixed:
- Before — the base Nepali checkpoint. Its
text_embeddinghas 6,681 rows: the seven<emotion>tokens do not exist in it, so there is no tag to give it and the untagged reading is the only “before” available. It is a competent narrator with no emotional control at all. - After — the shipped full fine-tune, with the emotion’s tag on the front.
Spoken text
I got the job, and I start on Monday!
Spoken text
I got the job, and I start on Monday!
Spoken text
She left without saying goodbye, and the house has been quiet ever since.
Spoken text
She left without saying goodbye, and the house has been quiet ever since.
Spoken text
You promised me you would handle it, and you did nothing at all!
Spoken text
You promised me you would handle it, and you did nothing at all!
Spoken text
You actually came all this way just to see me? I can't believe it!
Spoken text
You actually came all this way just to see me? I can't believe it!
Spoken text
How could anyone leave this filthy mess behind for me to clean up?
Spoken text
How could anyone leave this filthy mess behind for me to clean up?
Spoken text
Something is moving out there, and I do not know what it is.
Spoken text
Something is moving out there, and I do not know what it is.
Spoken text
The meeting is scheduled for four o'clock on Tuesday afternoon.
Spoken text
The meeting is scheduled for four o'clock on Tuesday afternoon.
Scored against the tag that asked for them, the “before” column lands on the right
emotion 2 of 7 times (neutral, and surprised — which the question mark in the
text gets for free), and the “after” column 6 of 7. The one miss is fearful,
which the classifier files as unknown rather than as a competing emotion — the same
emotion, and the same classifier weakness, that section 02 warned about and section 05
measures.
05 · Scoring the English model
The shipped full fine-tune, on all 350 held-out EmoVoice-DB clips, scored with
emotion2vec+ large. The classifier retreated to its other/unknown heads on 78 of
them — those are refusals to name an emotion rather than competing guesses, so they
are excluded and the numbers below cover the 272 clips it was willing to decide.
Emotion accuracy 0.412 · macro-F1 0.356, against 0.143 for a random pick among the seven labels.
| Emotion | Precision | Recall | F1 | n |
|---|---|---|---|---|
| happy | 0.605 | 0.634 | 0.619 | 41 |
| neutral | 0.328 | 0.915 | 0.483 | 47 |
| sad | 0.471 | 0.381 | 0.421 | 42 |
| disgusted | 0.500 | 0.343 | 0.407 | 35 |
| angry | 0.478 | 0.324 | 0.386 | 34 |
| surprised | 0.250 | 0.088 | 0.130 | 34 |
| fearful | 0.200 | 0.026 | 0.045 | 39 |

Read the columns rather than the diagonal and the pattern is clear. 131 of 272
clips were classified neutral — under-acting is the default failure, and every
emotion leaks into that column. happy is the strongest tag by a distance (F1 0.619);
fearful and surprised are the two the model and the classifier fail on together.
Precision beats recall almost everywhere — disgusted 0.500 precision against 0.343
recall. When the model commits to an emotion it is usually right; the problem is that
it often declines to commit.
Speech quality was never in doubt: WER 0.028, CER 0.006, ScoreQ 3.80, SQUIM 4.18
(round-trip through whisper-base.en). Whatever the tags did, they did not damage the
voice.
06 · Nepali: there is no dataset, so we built one
For Nepali there is no EmoVoice-DB. There is no emotional-speech corpus at all. What there is, in quantity, is Nepali cinema — where people shout, weep and plead for a living.
So we built a mining pipeline. Point it at YouTube links or search phrases and it returns labelled, transcribed, single-speaker emotional clips:
Fetch and separate
Download each video’s audio, then run Demucs over the full 44.1 kHz stereo track and keep only the vocals stem. Film audio is wall-to-wall score and ambience; separating before slicing means the emotion classifier never sees the soundtrack. A useful side effect: music-only stretches come out near-silent and get dropped by an RMS gate before any model runs.
Slice on the speaker’s own pauses
Silero VAD, not fixed windows. One clip is one utterance. Fixed windows split sentences mid-word, which truncates the emotion and hands the classifier a clip straddling a boundary the speaker never made.
Reject what has no usable label at all
Three content gates run before the emotion model. Singing (prosody dictated by melody, not feeling) and wordless vocalisation (sobbing and laughing are plainly emotional but carry no text) are caught by AudioSet tagging; more than one speaker is caught by clustering ECAPA speaker embeddings over overlapping windows.
Classify, then gate hard
emotion2vec+ large scores every survivor. A clip is kept only if the winning probability clears a floor and the runner-up stays under a ceiling — rejecting both “all low” (the model recognised nothing in particular) and “two high at once” (the clip is genuinely ambiguous). Thresholds are per emotion, in a version-controlled YAML, because the classifier is not equally trustworthy per class.
Transcribe, then have a human check it
Survivors are transcribed with a Nepali IndicConformer, then queued into a Gradio review Space where a person plays the clip and accepts it, corrects its emotion, or throws it out. Reviewers claim one film at a time so several can work in parallel.
Table view
| stage | what happens here | clips | share of sliced |
|---|---|---|---|
| Utterances sliced | Silero VAD, 17 films, Demucs-separated vocals | 13,928 | 100.0% |
| Survived the gates | content filters + per-emotion confidence gate | 3,767 | 27.0% |
| Reviewed by a human | played back and judged in the review Space | 2,632 | 18.9% |
| Accepted | label confirmed or corrected, kept as training data | 1,470 | 10.6% |
The yield is the point. 13,928 utterances in, 1,470 out at that snapshot — 10.6%. And the automatic gates were the aggressive part:
| Rejection reason | Clips | Share |
|---|---|---|
all_low — classifier not confident enough |
6,991 | 68.8% |
multi_speaker — more than one voice |
1,629 | 16.0% |
no_speech |
986 | 9.7% |
singing |
449 | 4.4% |
non_emotion / ambiguous / near_silence / wordless |
106 | 1.0% |
Review continued past that snapshot; the pool that actually trained the model was 2,070 accepted clips, 2,067 after dropping three longer than the trainer’s 11.5 s ceiling — 1.83 hours of Nepali emotional speech in total.
And it came out badly skewed
This is the single most important fact about the Nepali dataset, and it is a direct consequence of what Nepali films are full of. People shout and people joke; they are quietly heartbroken far less often, on camera, in a way a classifier will commit to.
This is the Nepali data that trained the model, per emotion:
| Emotion | Train clips | Train audio | Share of train | Eval clips |
|---|---|---|---|---|
| angry | 640 | 35.8 min | 34.4% | 33 |
| happy | 489 | 28.1 min | 27.0% | 30 |
| neutral | 451 | 22.1 min | 21.2% | 24 |
| sad | 355 | 16.8 min | 16.1% | 22 |
| disgusted | 13 | 0.9 min | 0.8% | 0 |
| fearful | 7 | 0.3 min | 0.3% | 0 |
| surprised | 3 | 0.1 min | 0.1% | 0 |
| total | 1,958 | 1.73 h | 109 |
Angry and happy alone are 61% of it — 64 minutes between them. Sad gets under
17 minutes. And disgusted, fearful and surprised together come to 1.3
minutes across 23 clips, none of which even survived into the eval split: they are
present in the tag vocabulary and absent from the data.
Remember this table when you read the Nepali results in section 09. It predicts them almost exactly.
The reviewer-agreement numbers from section 02 are the other half of this story. The
three rare emotions collapse entirely — surprised survived review 3.1% of the time,
fearful 9.5%, disgusted 14.3% — which is why the Nepali dataset ends up
effectively four-class. It is not that Nepali films contain no fear; it is that we
cannot yet find it at any useful precision.
This is the argument for keeping the human in the loop at this data scale. The gates buy a 10× reduction in listening time; they do not buy correctness. Anything downstream that treats a gated-but-unreviewed clip as ground truth is training on a label that is wrong more often than right.
07 · Who is speaking? Inventing the speaker labels
Now the problem from section 01 comes due. Every training row needs a neutral clip of the same voice as its reference, or the tag stops being load-bearing. But the mining pipeline records which film a clip came from, not who is talking — and a film is a whole cast. Hand-identifying a reference voice for every speaker across 17 films is not practical, and there is no cast metadata to lean on.
So we deduced the speakers from the audio, by clustering.
One video at a time
A film is one cast, one microphone chain, one room. Comparing across films buys nothing — no actor recurs — and costs a lot, because the recording differences between two films are large enough to look like speaker differences, which is exactly how two unrelated actors get merged into one cluster.
One vector per clip
ECAPA-TDNN (speechbrain/spkrec-ecapa-voxceleb) over 2.0 s windows at a 1.0 s
hop, sub-silence-floor windows dropped, then the mean of the window embeddings,
length-normalised. The window length is not arbitrary: the pipeline’s own speaker
check measured that at 2.0 s a cosine distance of 0.50 separates one voice from two
at 92.5% recall with no false rejects, while at 1.0 s the two distributions overlap
almost entirely. Same encoder, same window, so that calibration carries over.
Project the emotion out
Speaker embeddings encode emotional style, and it shows up as sub-clusters within one speaker — so an actor who shouts in half their scenes reads as two people. Emotion, though, is a nuisance variable we happen to have labels for: the pipeline already stored the classifier’s full posterior over seven emotions for every clip. We regress the embeddings on those posteriors and keep the residual — classical nuisance compensation (NAP/WCCN) with emotion in the role of the channel. Per video, the posteriors explained 22.6%–38.9% of the embedding variance. That is how much of “who this sounds like” was actually “how they felt”.
Choose k per video, and lean toward too many
NME-SC — spectral clustering that picks its own number of speakers from the normalised maximum eigengap — rather than a fixed k. The two errors are not symmetric: splitting one actor into two clusters still yields a same-voice reference, while merging two actors yields the wrong voice. So wherever the trade-off is exposed, the settings lean toward more clusters. Where the affinity graph came out disconnected, p is raised until it reconnects and k re-read.
Then measure whether it worked
The residual risk is reported, not assumed: the adjusted mutual information between cluster assignment and emotion label. Near 0 means the clusters are about voices; large means they are partly about emotions, and any reference built from them is worth less.
The run deduced 113 speakers across 17 films (6.6 per film), with emotion/cluster AMI at median 0.039, worst case 0.180 — so the clusters are about voices, with one film worth watching. No clip ended up alone in its cluster, which matters: a singleton has no same-voice partner to reference.
Turning clusters into references is a separate, deliberately conservative step. For each row, prefer a clip that is: from the same cluster, labelled neutral, not the row itself, never from the eval split (eval audio conditioning a training row is leakage of the kind that shows up as a good number), at least 6 s long (the trainer draws a random 3–6 s prompt slice, and a 2 s reference contributes one padded second), reviewer-confirmed over merely gate-passed, and least-used first — so one short clip does not become the prompt for two hundred rows and teach the model that its quirks are what the cluster sounds like.
What that yielded, on the 1,958 Nepali training rows that actually trained the model:
| Reference for the row | Rows | Share |
|---|---|---|
| Neutral clip, same deduced voice ✅ | 1,637 | 83.6% |
| Neutral clip, different cluster in the same film | 241 | 12.3% |
| The utterance itself | 80 | 4.1% |
196 distinct clips serve as references; the busiest serves 90 rows, the median 5. Per film, coverage runs from 100% down to 0% — three films had no usable neutral clip for any of their speakers at all.
Neither fallback is good, and we would rather name the cost than bury it. A different-cluster reference keeps the tag load-bearing but teaches the model that prompt timbre does not determine output timbre — corrosive to the voice cloning that is XTTS’s whole point. Self-reference keeps the right voice but lets the target emotion leak in through the prompt, so for those 80 rows the tag is not the only emotion cue. 16% of the Nepali data trains on one compromise or the other. Mining more neutral clips is the actual fix.
08 · Does English emotion transfer to Nepali?
This was the experiment we most wanted to run. We had a model that had learned emotion in English. Emotion is often argued to be substantially language-independent — shouting is shouting. So: start from the English-emotion checkpoint and train on Nepali, and see whether the English knowledge gives a head start.
The setup:
- Base checkpoint: the shipped English-emotion full fine-tune — not the plain Nepali model. The tag tokens are already trained and already mean something.
- Training data: 1,958 Nepali rows (1.73 h) mixed with 839 EmoVoice-DB English rows — exactly 30.0%. The English is there as a rehearsal set: without it the English emotions drift away while the model learns Nepali ones.
- 11 epochs of 88 steps, batch 32 × grad-accum 2, lr 5e-6 with the same 1e-4 tag group, bf16.
- Nepali emotion distribution: angry 640, happy 489, neutral 451, sad 355, and then the cliff — disgusted 13, fearful 7, surprised 3.
Before and after, in Nepali
Here “before” is a real three-way comparison, not a single reading: the
English-emotion model does have all seven tags trained — just never on Nepali. So
we can ask it for <happy>, <sad> and <angry> on a Nepali sentence and hear
exactly what English-only training transfers.
Both models get the identical Nepali sentence, the identical in-domain neutral film reference, and the same seed. The only difference is 528 steps of Nepali training.
Spoken text
अब मलाई थाहा भयो यहाँ साँच्चै के भइरहेको छ।
Spoken text
अब मलाई थाहा भयो यहाँ साँच्चै के भइरहेको छ।
Spoken text
अब मलाई थाहा भयो यहाँ साँच्चै के भइरहेको छ।
Spoken text
अब मलाई थाहा भयो यहाँ साँच्चै के भइरहेको छ।
Spoken text
अब मलाई थाहा भयो यहाँ साँच्चै के भइरहेको छ।
Spoken text
अब मलाई थाहा भयो यहाँ साँच्चै के भइरहेको छ।
The “before” column is the transfer result: the tags were fully trained, in English, and applying them to Nepali text does not deliver the emotion asked for. The English emotions did not carry across. The training curve says the same thing from the other direction — eval loss at step 0 was 4.0222, well above where either English run started (≈3.69), so the English-emotion model came to this data no better prepared than a fresh start would have been.
The “after” column is what 1.73 hours of mined Nepali film audio bought: delivery that reads as the emotion the tag asked for. That is the encouraging half of this experiment — the transfer did not happen, but the new data worked, and it worked on a language with no emotional-speech corpus at all.
How far did the Nepali data take it?
train loss (epoch end)eval lossgap
Table view
| epoch | step | train | eval | train − eval |
|---|---|---|---|---|
| 1 | 88 | 3.6337 | 3.5880 | 0.0457 |
| 2 | 176 | 3.4707 | 3.5463 | -0.0756 |
| 3 | 264 | 3.4453 | 3.5246 | -0.0793 |
| 4 | 352 | 3.4382 | 3.5115 | -0.0733 |
| 5 | 440 | 3.3690 | 3.5008 | -0.1318 |
| 6 | 528 | 3.3752 | 3.4738 | -0.0986 |
| 7 | 616 | 3.3571 | 3.4932 | -0.1361 |
| 8 | 704 | 3.3148 | 3.4888 | -0.1740 |
| 9 | 792 | 3.2974 | 3.4895 | -0.1921 |
| 10 | 880 | 3.2508 | 3.4864 | -0.2356 |
| 11 | 968 | 3.2666 | 3.4854 | -0.2188 |
Eval loss falls to 3.4738 at epoch 6 (step 528) and never beats it again, drifting to 3.4854 by epoch 11. Training loss keeps descending the whole way, 3.6337 → 3.2666. The gap between them goes from −0.046 at epoch 1 to +0.219 at epoch 11.
That shape has one honest reading: the model has extracted what 1.73 hours of Nepali audio has to give, and everything after epoch 6 is memorisation. Step 528 is the checkpoint we kept and the one scored below.
Checkpoint selection is not measuring emotion. Re-synthesising one fixed Nepali
<angry> probe at all 13 saved checkpoints, the classifier calls it angry at steps
352, 440, 616 and 968 — but not at step 528, the checkpoint with the best eval loss.
Loss is picking the checkpoint; nothing in that choice is optimising tag control, and
single probes are far too noisy to pick on instead. A proper emotion-aware
checkpoint criterion is on the list.
09 · Scoring the Nepali model
Same classifier, on the 109-row held-out Nepali split (108 scored; one generation came
out degenerate). The split contains only four of the seven emotions — the pipeline
could not mine disgusted, fearful or surprised in usable quantity — so those are
not scored rather than scored as zeros. The classifier declined on none of the 108.
Emotion accuracy 0.454 · macro-F1 0.464.
| Emotion | Precision | Recall | F1 | n |
|---|---|---|---|---|
| neutral | 0.531 | 0.708 | 0.607 | 24 |
| angry | 0.514 | 0.562 | 0.537 | 32 |
| sad | 0.583 | 0.318 | 0.412 | 22 |
| happy | 0.412 | 0.233 | 0.298 | 30 |

angryandneutralgenuinely work. 18/32 and 17/24 on the diagonal, both with precision above 0.51 — and they are the first and third best-resourced emotions in the pool. High-arousal and no-arousal are the two ends the model found first, and the two it had the most audio for.sadis precise but timid — precision 0.583, recall 0.318. When it produces sadness it is convincing; it often produces something flatter. This is the 17-minute problem. Sadness is the thinnest of the four emotions the model has any real data for, and it is audibly the weakest of the four: to our ears the angry and happy takes carry their emotion, and the sad ones frequently do not. We do not think this is a modelling failure so much as a missing 40 minutes of Nepali weeping.happyscores lowest (F1 0.298), confused toward angry nine times — which is the one place where the classifier and our ears disagree. Happy is the second best-resourced emotion and it sounds like it; what the numbers are catching is partly the classifier’s own happy/angry confusion, the same one that made a proposedhappinesssurvive human review only 27.6% of the time in section 02, and that led reviewers to relabel 58 mined “happy” clips as anger. Loud-happy and loud-angry overlap in Nepali film comedy for the classifier as much as for the model.disgusted,fearfulandsurprisedare not evaluated at all, because 1.3 minutes of training audio produced nothing to evaluate. Those three tags exist on the Nepali model and do not work.
0.454 is not “better than English’s 0.412”. The Nepali model is asked for four emotions and the English model for seven, and the Nepali split is 108 clips where one clip moves accuracy by 0.9 points. Both numbers also come from the classifier section 02 is about. They say something was learned in both languages; they do not rank the two.
Speech quality and the round-trip
| Metric | English (full FT) | Nepali (step 528) |
|---|---|---|
| ScoreQ MOS (no-reference) | 3.799 | 3.219 |
| SQUIM MOS | 4.182 | 4.054 |
| Round-trip WER | 0.028 | 0.344 |
| Round-trip CER | 0.006 | 0.137 |
The MOS drop is real but expected: the Nepali model was trained on Demucs-separated film audio, and separation artefacts are part of what it learned to reproduce. The English model learned from clean studio recordings.
The Nepali round-trip uses the fine-tuned Nepali IndicConformer from our own
mining pipeline — the same model that transcribed the training clips — rather than
Whisper. That choice is the difference between a number and noise: multilingual
whisper-large-v3 returns WER 1.011 on this audio, i.e. more errors than reference
words, which describes Whisper’s Nepali rather than our synthesis.
IndicConformer has its own limits, so read 0.344 / 0.137 as an upper bound on error rather than a clean intelligibility score. It is a CTC model fine-tuned on read OpenSLR Nepali with punctuation stripped, being asked to transcribe film-domain synthesis — out of domain in both directions. It is also the recogniser that produced the reference transcripts our model trained on, so its own mistakes are partly baked into the ground truth we are scoring against. And the two languages’ numbers are not comparable to each other: different recognisers, different scripts, character error rates on Devanagari and on Latin are not the same measurement.
10 · What we actually learned
Emotion did not transfer across languages for free. Starting from a model that had thoroughly learned seven English emotion tags gave no measurable head start on Nepali: the zero-shot Nepali takes did not deliver the emotion asked for, and the step-0 eval loss was worse than either English run’s. Nepali emotion came from Nepali audio and nothing else.
But 1.73 hours of mined film audio was enough to get real emotion in Nepali. For a
language with no emotional-speech corpus, built entirely from YouTube, angry and
neutral are solid and sad is convincing when it commits. That is the result we
would repeat.
And we can see the ceiling. The eval curve flattening at epoch 6 while training loss keeps falling is the cleanest signal in the project. It is not a hyperparameter problem to tune around; it is a data volume problem.
Per emotion, the quality tracks the minutes almost exactly. Angry got 35.8
minutes and works; happy got 28.1 and works; sad got 16.8 and is the one of the four
that audibly does not carry its emotion; disgusted, fearful and surprised got
1.3 minutes between them and do not work at all. Nepali films supply anger and
comedy generously and quiet grief sparingly, so the corpus we could mine is skewed
the same way the model now is. The single highest-value thing we can do next is
find more sad Nepali audio — not tune anything.
The data ceiling is a labelling ceiling, not an audio ceiling. There is no shortage
of Nepali film. There is a shortage of clips we can confidently label — surprised
survived human review 3.1% of the time. Three of seven emotions are currently
unreachable in Nepali, which is why the Nepali model is a four-class model wearing a
seven-class tag set.
The tags work where the data lived. Because every training row paired emotional words with emotional delivery, the tags are strongest on emotionally-worded text and weak on flat text (29/35 against 5/35 in English). If you want a model that can put anger into a neutral sentence, that has to be trained for deliberately.
The reference-clip problem is as important as the emotion data. 84% coverage of same-voice neutral references took ECAPA embeddings, emotion-nuisance projection and per-video spectral clustering. The remaining 16% is a known tax on the result.
Our evaluator is part of the experiment. A classifier whose labels survive human review 40.9% of the time is scoring every emotion number here, and on English it ranked the two adaptation methods the opposite way round from both our ears and the intelligibility metrics. Treat the numbers as a relative yardstick and the ears as the arbiter.
Where this goes next
The open question is the one we started with, asked better. Not “does English emotion transfer to Nepali for free” — it does not — but can a model be built so that it does?
- Separate the tag from the language.
<angry>is currently one token doing two jobs: naming an emotion, and carrying whatever acoustic realisation the training languages showed it. A conditioning path that factorises emotion from language-specific prosody is the obvious structural fix. - Train multilingually from the start, rather than sequentially. Sequential training is what produced the negative transfer result; the tag may only become language-independent if it is forced to be during training.
- Mine for sadness specifically, and for the three missing emotions. The fix for
Nepali
sadis more Nepalisad, and general-purpose mining will not find it — the gate rejects quiet, low-arousal speech at exactly the rate that produced this skew. That means per-emotion thresholds tuned for recall on sadness, source material chosen for it, and a Nepali-tuned emotion classifier rather than a multilingual one. - Get a better evaluator. Everything in section 02 argues for a human listening test to calibrate against, and for not trusting a single classifier’s per-class numbers.
- Add emotion-neutral text to the training mix, so the tag has to carry the emotion on its own at least some of the time.
- Pick checkpoints on something emotional, not on mel cross-entropy alone.
The mining pipeline, the review Space and the dataset are the reusable part of this work — none of it is Nepali-specific except the transcriber. If it works for Nepali film, it should work for any language with a film industry and no emotional-speech corpus, which is most of them.
Appendix · Reproducing the numbers
Both evaluations are two commands: synthesise the held-out split, then score it.
--classifier emotion2vec-large is the seven-class multilingual scorer;
--only-decided drops the clips it refuses to call an emotion instead of giving them
a confusion-matrix column, and --only-present-tags keeps emotions absent from the
split out of the report rather than showing them as all-zero rows.
# English — the shipped full fine-tune, on EmoVoice-DB's 350-row held-out split
python scripts/evaluation/evaluation.py \
--eval-csv MyTTSDataset/metadata_eval.pre_bhavvani.csv \
--model-config ./runs_xtts_nep_base/fullfinetune_emovoicedb/config.json \
--out-dir eval-outputs-en-fullft
python scripts/evaluation/evaluate_metrics.py \
--eval-csv MyTTSDataset/metadata_eval.pre_bhavvani.csv \
--audio-dir eval-outputs-en-fullft \
--classifier emotion2vec-large \
--only-decided --only-present-tags --create-confusion-metric-image
# The LoRA run, for the section 03 comparison. Its adapter stacks on the frozen
# Nepali base, which config.py no longer points at, so name it explicitly.
python scripts/evaluation/evaluation.py \
--eval-csv MyTTSDataset/metadata_eval.pre_bhavvani.csv \
--model-config ./runs_xtts_nep_base/lora_emovoicedb/config.json \
--base-checkpoint pretrained_model_xtts_nep/model.pth \
--out-dir eval-outputs-en-lora
# Nepali — the step-528 checkpoint, on the 109-row YouTube-mined split.
# --language ne is required: that csv carries no language column, so it would
# otherwise fall back to config.LANGUAGE and synthesise Nepali text as English.
# --asr-backend indicconformer runs the round-trip through the pipeline's own
# Nepali recogniser instead of Whisper.
python scripts/evaluation/evaluation.py \
--eval-csv MyTTSDataset/metadata_youtube_emotion_eval.csv \
--model-config ./runs_xtts_nep_base/xtts_nepali_youtube_emotion-*/config.json \
--language ne \
--out-dir eval-outputs-ne-youtube
python scripts/evaluation/evaluate_metrics.py \
--eval-csv MyTTSDataset/metadata_youtube_emotion_eval.csv \
--audio-dir eval-outputs-ne-youtube \
--classifier emotion2vec-large \
--asr-backend indicconformer \
--only-decided --only-present-tags --create-confusion-metric-image
Loss curves come from each run’s losses.json; the dataset funnel and reviewer
agreement from yt_emotion_dataset/{rejected,dataset}.csv at the 2026-09-02
snapshot; the speaker figures from
speaker_deduction/{clustering_report.json,speakers.csv} and the two run logs. The
before/after grids and the wording comparison were rendered at temperature 0.75,
repetition penalty 5.0, fixed seeds, one reference clip per language.
Sample sizes, in one place, because several of these are small. English emotion scoring 272 of 350 clips (78 undecided). Nepali 108 clips across 4 emotions (32/30/24/22) — one clip is 0.9 accuracy points. Wording comparison 35 takes per condition in English, 15 in Nepali. Human review 2,632 clips. Speaker clustering 2,605 clips across 17 films. Single training run per configuration, single seed, no error bars: treat small differences as noise.