ChristophSchuhmann's picture
Taxonomy patch: relabel indices 79/80/81 (Hand Scratching Head, Hand Slaps, Slap Face) -> 'No burst' due to production false positives; weights unchanged
14c1b81 verified
|
Raw
History Blame Contribute Delete
7.47 kB
---
license: cc-by-4.0
tags: [audio-classification, vocal-bursts, non-verbal, voiceclap]
pretty_name: VocalBurst Classifier (single-burst)
---
# VocalBurst Classifier — single-burst
A sharper classifier specialised for clips that contain **exactly one isolated vocal burst** (no speech).
Same taxonomy (82 classes + `no_burst`). **Softmax** (single most-likely type). Use it as the "name the
burst" stage after a detector has isolated a segment.
> ⚠️ **Taxonomy patch (label mapping only, weights unchanged):** the three classes originally trained at
> indices **79 `Hand Scratching Head`**, **80 `Hand Slaps`** and **81 `Slap Face`** are now labelled
> **`No burst`** because of excessive false positives in production. All 83 logits and indices are
> intact. See [Taxonomy patch](#taxonomy-patch-indices-79--80--81-are-now-labelled-no-burst) before
> using raw logits.
## Architecture
`laion/voiceclap-commercial` (768-d, frozen) → **MLP 1024-wide × 3 deep** → 83-way softmax. ~3M params.
## Training
Trained ONLY on **clean single-burst clips**: Gemini-confirmed DACVAE positives (82 classes, exactly one
label, no speech) + pure Emilia `no_burst` clips (~15.9k). Cleaner labels, no speech confusion → sharper.
50 epochs, AdamW, cross-entropy, best-mAP checkpoint.
> **v2 (current):** retrained with **9,000 multilingual burst-free negatives** (FLEURS: Chinese, Hindi, Bengali, Arabic, Persian, Urdu, Tamil, Telugu, Vietnamese, Thai, Indonesian, Japanese, Korean, Swahili, Yoruba, Zulu, Turkish, Russian) — fixes hallucinated bursts on non-European speech.
## Results (held-out pure single-burst clips)
| metric | fine (82) | coarse (16 families) |
|---|---|---|
| macro mAP | **0.68** | **0.87** |
| top-1 exact | 60% | – |
| a true label in top-3 | 88% | – |
On isolated bursts it names the coarse family very reliably (~0.87) and the fine subtype well.
## How it works
Two stages: a **frozen** `laion/voiceclap-commercial` audio encoder turns a clip into a **768-d embedding**
(`encode_waveform`, auto-downloaded — the repo needs no extra setup), then this small trained **MLP head**
maps it to **83 outputs = 82 VocalBurst classes + `no_burst`** (taxonomy: [LAION-AI/voice-taxonomies · vocalburst](https://github.com/LAION-AI/voice-taxonomies/tree/main/vocalburst)).
A **no-burst gate**: if `P(no_burst) ≥ 0.5` the clip is declared burst-free (no false alarm); otherwise the top classes are returned. Clips are truncated to the first 30 s (the encoder's window).
## Usage
```python
from inference import VocalBurstClassifier
clf = VocalBurstClassifier("laion/vocalburst-classifier-single") # HF repo id, or a local checkout dir
print(clf.predict("clip.wav")) # -> {no_burst, p_no_burst, top1, predictions:[(class,prob)], group}
```
## Taxonomy patch: indices 79 / 80 / 81 are now labelled `No burst`
> **This section supersedes the earlier "Known caveat: `Slap Face` false positives" note.**
> That note recommended skipping `Slap Face` as top-1 and falling through to the runner-up.
> The problem turned out to be broader than one class and is now fixed in the label mapping
> itself, so the manual skip rule is **no longer needed** — do not apply it on top of this.
### What changed
The model was **originally trained** with three fine classes in the
`hand_and_body_sounds` family:
| index | original class name (as trained) | original group |
|---|---|---|
| **79** | `Hand Scratching Head` | `hand_and_body_sounds` |
| **80** | `Hand Slaps` | `hand_and_body_sounds` |
| **81** | `Slap Face` | `hand_and_body_sounds` |
In large-scale production use — re-classifying ~1.5 M CrisperWhisper-detected burst
timestamps from in-the-wild expressive speech — all three fired as top-1 far more often
than is plausible, and manual spot-checks showed the great majority were **false
positives**: the true event was usually a laugh, gasp, sigh, grunt, or simply no burst at
all. `Slap Face` was the worst offender, but `Hand Slaps` and `Hand Scratching Head`
behaved the same way. These three are percussive, broadband, very short events, and the
frozen embedder appears to map a lot of unrelated transient energy (mouth clicks, mic
bumps, plosives, clipping) into that corner of the space.
Accordingly, in `classes.json` **all three labels have been replaced with `"No burst"`**,
and `class_to_group.json` maps `"No burst"` to the group `no_burst`.
### What did *not* change
**The weights were not retrained and not altered.** `model.pt` is bit-identical to the
previous revision. The head still emits **83 logits** and every index keeps its original
position, so **existing checkpoints, cached embeddings, cached logits and stored
`predicted_index` values all stay valid**. This is purely a relabelling of the output
mapping.
### What this means if you use the raw logits
* `argmax` still returns an integer in `[0, 82]`. Indices 79, 80 and 81 now *decode* to
the string `"No burst"` instead of the three hand-sound names.
* `"No burst"` therefore appears **three times** in `classes.json`. Do not use
`classes.index("No burst")` to recover an index — it will always give 79. Always keep
the integer index as the identity and use the list only for display.
* Index **82** is still the model's own dedicated `no_burst` class, spelled
`no_burst` (lowercase, underscore) to keep the pre-existing gate code working:
`P(no_burst) ≥ 0.5` still refers to index 82 alone.
* The full set of "this is not a burst" outcomes is therefore
`{79, 80, 81, 82}`. If you want a single boolean, test membership in that set
(or test `class_to_group[label] == "no_burst"`), not equality against one index.
* If you need the original three-way hand-sound distinction back (e.g. your domain really
does contain slaps and you have evaluated it), the information is still in the logits —
just restore the names at indices 79/80/81 in your own copy of `classes.json`.
* The coarse family `hand_and_body_sounds` now contains only `Finger Snaps` (index 78).
```python
probs = model(emb).softmax(-1)
idx = int(probs.argmax(-1)) # keep the INDEX as the identity
label = classes[idx] # 79/80/81 -> "No burst"
is_burst = idx not in (79, 80, 81, 82)
```
This is an observation from downstream use on in-the-wild speech, not a formal error
analysis on the model's own held-out split — the reported metrics above were computed
**before** this relabelling and still refer to the original 83-way taxonomy.
## Files
`model.pt` (MLP weights, unchanged) · `config.json` (arch) · `classes.json` (83 labels, index order;
79/80/81 relabelled `No burst`) · `class_to_group.json` (fine→coarse families) · `inference.py` ·
`example.py` · `requirements.txt`.
## Interactive demos (audio + predictions)
- Multi-label predictions vs ground truth: https://projects.laion.ai/procedural-voice-captions/vocalburst-predictions/
- Single-burst classifier: https://projects.laion.ai/procedural-voice-captions/vocalburst-single/
- Two-stage detect→classify combo: https://projects.laion.ai/procedural-voice-captions/vocalburst-combo/
- On MOSS character voices: https://projects.laion.ai/procedural-voice-captions/vocalburst-character/
Training data + embeddings: [laion/vocalburst-classification](https://huggingface.co/datasets/laion/vocalburst-classification).
Embedder: [laion/voiceclap-commercial](https://huggingface.co/laion/voiceclap-commercial). License: CC-BY-4.0.