Silencio-2 · weight-space edit

Fixing emotion tags by editing the weights, not retraining

Silencio-2 is a tiny English TTS (18M generation parameters) with inline emotion and sound tags. It was trained on too little tagged data, so most emotion tags were unreliable. Reading the weights showed why, and a rank-1 edit per layer fixed most of it, with zero gradient steps.

5/24 → 22/24
<whisper> takes that actually whisper
0.45 → 0.71
emotion separability, 7 classes (chance 0.14)
0.006 → 0.006
WER on untagged speech
0
gradient steps, about 5k free parameters

Listen

All clips come from the held-out test set: lines, seeds and two of the three voices were never used while building the edit. Same text, same voice, same seed, only the weights differ. Clips are loudness-normalized the same way generate() does by default. Every take, failures included, is in the explorer.

<whisper> the biggest change

Not every whisper works. The worst of the 24 test takes, voice 260 / "You kept the letter all these years." / seed 11, comes out as barely audible mumbling. Hear it in the explorer.

Other emotions

What was wrong

  1. The tag embeddings barely moved. Each tag row was initialized from a word (<sad> from "sad", <whisper> from "whisper") and still has 60–80% cosine similarity to it. Making the rows longer would not help: a LayerNorm sits before attention, so only the direction counts.
  2. The audio does not read the tags. An attention probe showed that audio frames attend to <happy> 100–3000× more than to an average word. For the other tags it is only 1–10×.
  3. The information is already there. Boosting attention to <whisper> directly in the activations (×30, all heads) produced a whisper in 62% of takes with clean words. ×100 broke speech. Boosting only the three heads that read tags the most did almost nothing.

The edit

Each of the 8 layers gets one rank-1 term added to its key projection:

WK ← WK + D · ( Σt ln gt · zt )ᵀ

That changes 524k weights (3.4% of the transformer, all inside in_proj), but the edit has only about 5k free parameters. Every variant was tried on a dev split and kept only if it improved. The final one was checked once on the test split.

Results

Held-out test, 168 emotion takesReleaseEdit
<whisper> actually whispers5/2422/24
…and the words are clean5/2410/24
Emotion separability, 7 classes0.4460.714
Consistency: sad / angry / fear / surprised / whisper.08 / .05 / .07 / .06 / .09.13 / .10 / .15 / .19 / .27
WER, untagged speech0.0060.006
WER, emotion takes (mostly whisper)0.0020.064
Sound tags: WER around them / sound length0.007 / 0.81 s0.014 / 0.84 s

How these were measured. WER comes from Whisper base. A take counts as a whisper when less than 40% of its loud frames are voiced (pyin). Separability is a nearest-centroid classifier on Whisper-base encoder features, after subtracting the mean of each (voice, line, seed) group, holding out whole groups. Consistency is how much the tag moves takes in the same direction (|mean change|² / mean |change|²). The dev split shows the same picture: whisper 0/24 → 24/24, separability 0.35 → 0.61.

What didn't work

Caveats

Every test take

3 voices × 4 lines × 2 seeds × 7 classes. "No tag" is the same line through the edited model, to check that plain speech did not change.

Try it

pip install -r requirements.txt   # from the Hugging Face repo

from silencio2 import Silencio2TTS
tts = Silencio2TTS.from_pretrained("BlueRoseLabs/Silencio-2", revision="weightedit")
tts.save("out.wav", "<whisper> Keep your voice down.", voice="260")