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