emotion2vec-web-distill (student v4)

A 2.23M-parameter, 9.7 MB ONNX distillation of emotion2vec+ large (~300M params, ~650 MB as fp32 ONNX) that runs in the browser in real time on plain WASM — no server, no WebGPU required. Raw 16 kHz waveform in, 18 emotion-anchor cosines out, with the mel frontend baked into the graph so the JavaScript side does zero DSP.

It was built to drive a game avatar's facial expression from live microphone audio. It is not a general speech-emotion-recognition system: it is a small, fast approximation of one teacher's opinion, published because a speech-emotion model this size that runs client-side did not seem to exist.

What it predicts

The teacher's classifier projects its embedding onto 9 emotion classes (angry, disgusted, fearful, happy, neutral, other, sad, surprised, unknown). We defined 18 expression "pose anchors" as unit-normalized weighted mixes of those classifier projection rows (weights in distill-anchor-mix.json — e.g. tense = 0.5·angry + 0.5·fearful, content = 0.5·happy + 0.5·neutral).

The training target for a 3-second window is the cosine similarity between the teacher's window embedding and each of the 18 anchors:

cos18 = normalize(teacher_embedding) @ anchors.T

The student learns to predict those 18 cosines directly from audio. Poses, in output order: calm, content, warm, happy, amused, excited, surprised, smug, confused, skeptical, annoyed, disgusted, afraid, angry, tense, sad, weary, bored.

This is a distillation of a model's judgment, not of human ground truth. The teacher's biases (and ours, via the anchor mixes) are inherited.

I/O contract

Input waveform — float32 (batch, 48000), raw 16 kHz mono in [-1, 1] (3 s window)
Output cosines — float32 (batch, 18), teacher-space anchor cosines
Suggested cadence every 750 ms over a sliding ring buffer
Softmax temperature 0.0375 (calibrated, see below; read it from the meta JSON, don't hard-code)
SHA-256 136b83ee1cbf16b102b341398534604a7f8ddadff1f07b50bc76739fa04a3795

To turn cosines into blend weights, apply softmax(cosines / T) with the temperature from distill-student-v4.fused-meta.json.

Why 0.0375 and not the teacher's 0.05: distillation compresses the spread of the output cosines. Measured on 96,577 never-trained windows, the teacher/student deviation ratio is ~1.33× and uniform across all 18 anchors, so a single temperature correction (0.05 / 1.33) restores the teacher's weight distribution exactly, and — because argmax is temperature-invariant — without changing which emotion wins. The calibration is a property of this model generation; a future v5 will carry its own number in its own meta.

Usage (browser, onnxruntime-web)

import * as ort from 'onnxruntime-web';

const meta = await (await fetch('distill-student-v4.fused-meta.json')).json();
const session = await ort.InferenceSession.create('distill-student-v4.fused.onnx');

// Last 3 s of a 16 kHz mono mic ring buffer, float32 in [-1, 1].
async function emotionWeights(wave /* Float32Array(48000) */) {
  const { cosines } = await session.run({
    waveform: new ort.Tensor('float32', wave, [1, 48000]),
  });
  const T = meta.output.softmax_temperature;
  const z = Array.from(cosines.data, (c) => c / T);
  const m = Math.max(...z);
  const exps = z.map((v) => Math.exp(v - m));
  const sum = exps.reduce((a, b) => a + b, 0);
  return exps.map((v) => v / sum); // index-aligned with meta.output.poses
}

Tip: if you run this alongside another onnxruntime-web session on the WebGPU backend, serialize all session.run() calls through one queue — ort-web's WebGPU backend throws Session mismatch when two sessions' runs interleave.

Python sanity check:

import numpy as np, onnxruntime as ort
sess = ort.InferenceSession("distill-student-v4.fused.onnx")
wave = np.zeros((1, 48000), np.float32)  # substitute real 16 kHz audio
cos = sess.run(None, {"waveform": wave})[0]  # (1, 18)

Architecture and training

  • Student: 64-mel log spectrogram → 4 conv stages (32→64→128→256) → 2-layer pre-norm transformer encoder (d=256, 4 heads, GELU) → mean-pool → linear head to 18 cosines. 2,231,986 parameters.
  • Objective: MSE on standardized (z-space) targets; SpecAugment; early stopping on held-out z-MSE (best: epoch 18, z-MSE 0.5646).
  • Splits: grouped so no speaker, actor, or meeting series straddles the train/held-out line. 351,281 train / 95,468 held-out windows.
  • Fused export: the exact torchaudio mel frontend the student trained behind (n_fft 400, hop 160, 64 mels, 50–7600 Hz, HTK scale, log(·+1e-6)) is reproduced inside the ONNX graph as a conv1d STFT + mel matmul, and the input/output standardization constants are baked in. Export is parity-checked against the PyTorch model on real audio.

Evaluation

Ground truth is the teacher's window-level output. const predicts the train-mean cosines; ridge is a ridge regression from 128-d mel summary statistics. Top-anchor agreement = the student's argmax matches the teacher's.

split windows student const ridge
held-out corpus (all) 95,468 63.8% 49.8% 53.9%
— AMI (meetings) 6,912 80.0% 69.7% 69.0%
— CREMA-D (acted) 22,194 64.7% 11.1% 37.4%
— LibriVox drama 47,482 58.9% 60.4% 58.1%
— LibriSpeech (read) 10,876 72.4% 74.6% 71.7%
— TTS synthetic 8,004 64.8% 43.2% 37.0%
human free speech (held-out) 412 59.2% 60.0% 57.8%
human free speech (dev) 598 51.7% 46.0% 45.5%
human acted takes 99 43.4% 37.4% 36.4%

Mean absolute error per face channel lands well inside the blend deadband of the target application; the numbers above are the stricter argmax view.

Read the baselines honestly. On calm-dominated splits (drama, read speech, spontaneous human speech) always answering "calm" is a strong baseline, and the student only ties it. The diagnostic that removes that flattery — windows where the teacher says something other than calm:

split teacher not-calm share student agrees there answers calm anyway
held-out corpus 50% 46.6% 29.4%
CREMA-D 89% 66.7% 3.4%
TTS synthetic 57% 55.5% 16.4%
human free speech ~50% ~14% ~72%

Known weaknesses: on real spontaneous speech the student is far more conservative than its teacher — when the teacher hears a non-calm emotion in free speech, the student agrees only ~14% of the time and rarely produces angry at all. Distinct anger-placement work is planned for v5. For its built purpose (subtle continuous expression blending, where the temperature calibration restores full dynamic range) this conservatism is acceptable; for anything that depends on catching emotional events in natural conversation, it is not.

Training data

source license role
CREMA-D ODbL 1.0 7.4k acted emotional clips, 91 actors
LibriSpeech train-clean-100 CC BY 4.0 100 h read speech, 251 speakers
AMI corpus (Mix-Headset) CC BY 4.0 spontaneous meeting speech
LibriVox dramatic readings (archive.org) public domain emotional read drama
OpenAI TTS synthetic (gpt-4o-mini-tts, gpt-audio-1.5) generated for this project emotion-prompted synthetic speech

Targets are the teacher's outputs over these corpora; no human emotion labels were used for training (CREMA-D's nominal labels served as diagnostics only).

Privacy: no private voice data was used in training. The maintainer's own recordings were used exclusively for evaluation ("human" rows above) and are not distributed.

Intended use and limitations

Intended: driving continuous, low-stakes expressive output — avatar faces, ambient visualization, creative tools — client-side, from a user's own microphone, with the audio never leaving their machine.

Not intended, and not validated for: emotion analytics on other people, hiring or admissions screening, surveillance, clinical or mental-health assessment, lie detection, or any consequential decision about a person. Beyond the ethics, the numbers above say it plainly: this model misses most non-calm emotional events in natural speech. It predicts what one model would say about a 3-second window of mostly-English audio; it does not read minds.

License and attribution

The teacher, emotion2vec/emotion2vec_plus_large, is released under the FunASR Model Open Source License, which covers "model weights and their derivatives, including finetuned models" and requires sharing under the same terms with attribution. This distilled student is such a derivative (trained on the teacher's outputs; its output space is built from the teacher's classifier rows), so it is published under the same FunASR Model License (see LICENSE). Note the license does not explicitly address commercial use and states the models are provided "for reference and learning purposes"; evaluate it for your own use case.

Credit for everything the student knows goes to the emotion2vec authors and the FunASR team at Alibaba:

@article{ma2023emotion2vec,
  title={emotion2vec: Self-Supervised Pre-Training for Speech Emotion Representation},
  author={Ma, Ziyang and Zheng, Zhisheng and Ye, Jiaxin and Li, Jinchao and Gao, Zhifu and Zhang, Shiliang and Chen, Xie},
  journal={arXiv preprint arXiv:2312.15185},
  year={2023}
}
@inproceedings{gao2023funasr,
  title={FunASR: A Fundamental End-to-End Speech Recognition Toolkit},
  author={Gao, Zhifu and Li, Zerui and Wang, Jiaming and Luo, Haoneng and Shi, Xian and Chen, Mengzhe and Li, Yabin and Zuo, Lingyun and Du, Zhihao and Xiao, Zhangyu and Zhang, Shiliang},
  booktitle={INTERSPEECH},
  year={2023}
}
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for thomashallock/emotion2vec-web-distill

Quantized
(1)
this model

Paper for thomashallock/emotion2vec-web-distill