4
stars
18
commits
1
repos using this model
1
linked in READMEs
Aug 2, 2026
updated
A Whisper-based model that detects and localizes vocal bursts (laughs, coughs, sneezes, sighs, gasps, cries, screams, etc.) in audio, returning precise start/end timestamps for each event.
model_v2.ptThe recommended default checkpoint is model_v2.pt (972 MB), fine-tuned on real in-the-wild audio. The original model.pt (v1) is trained on synthetic soundscapes only and is superseded — it is kept for reproducibility, documented under Previous version — v1.
⚠️ Two things are easy to get wrong, so they are stated up front:
inference.py still auto-downloads model.pt when you do not pass a checkpoint. Pass model_v2.pt explicitly.inference.py's built-in post-processing defaults are still the v1-era values (threshold=0.65, merge_gap=0.3, min_dur=0.5). Pass the v2 values explicitly — they dominate the measured F1 (see below).threshold = 0.50 # was 0.65 in v1
merge_gap = 0.10 # was 0.30 in v1
min_duration = 0.10 # was 0.50 in v1 <-- the one that matters
Ground-truth bursts have a median duration of ~180 ms. A min_duration of 0.5 s therefore
discards ~96 % of real bursts before matching. On one identical checkpoint, only changing
post-processing moved event F1 from 0.243 to 0.598 — a larger effect than any training change
made for v2. If you read older instructions in this card recommending 0.65 / 0.3 / 0.5, those are
the v1 numbers and are not recommended any more.
from huggingface_hub import hf_hub_download
from inference import load_model, detect_vocal_bursts # inference.py from this repo
# 1. Download the recommended checkpoint
ckpt = hf_hub_download("laion/vocalburst-locator", "model_v2.pt")
# 2. Load it (v1 would be loaded if you omit `checkpoint`)
model, fe, device = load_model("cuda", checkpoint=ckpt) # or "cpu"
# 3. Detect, with the v2 post-processing values
events = detect_vocal_bursts(
"audio.mp3",
model=model, fe=fe, device=device,
threshold=0.50,
merge_gap=0.10,
min_dur=0.10,
)
for ev in events:
print(f"{ev['start']:.2f}s - {ev['end']:.2f}s (confidence: {ev['confidence']:.2f})")
Command line equivalent:
python inference.py audio.mp3 \
--checkpoint "$(python -c 'from huggingface_hub import hf_hub_download; print(hf_hub_download("laion/vocalburst-locator","model_v2.pt"))')" \
--threshold 0.50 --merge-gap 0.10 --min-dur 0.10 --device cuda
Raw state dict (if you build the model yourself — same WhisperSegmenter state dict as v1, 485 tensors, LoRA already merged):
import torch
sd = torch.load("model_v2.pt", map_location="cpu")
model.load_state_dict(sd) # same keys as model.pt
Re-measured on a held-out set of 992 real, in-the-wild expressive-speech clips, each checkpoint given a post-processing sweep to find its best possible operating point:
| event F1 @ IoU 0.5 | precision | recall | best threshold | |
|---|---|---|---|---|
model.pt (v1, synthetic) | 0.152 | 0.469 | 0.207 | 0.80 |
model_v2.pt | 0.607 | 0.678 | 0.669 | 0.50 |
4.0x higher F1 on real audio. Note also how v1 fails: it only reaches usable precision at threshold 0.80, where recall collapses to 0.21 — on real recordings it is very unsure, and buying precision costs it four fifths of the events. v2 operates at 0.50 with recall 0.67.
| Your audio | Checkpoint |
|---|---|
| Expressive speech, in-the-wild (default choice) | model_v2.pt |
| In-the-wild audio that also contains music, SFX or non-speech backgrounds | model_v2_mixed.pt |
| Synthetic soundscapes / reproducing the original results | model.pt (v1, superseded) |
On real audio the two v2 weights are statistically indistinguishable; they differ only on the
synthetic-soundscape domain. Both use the same post-processing values above. Details in the
model_v2_mixed.pt section.
🔗 Ensemble: pair this detector with the captioner
laion/vocalburst-captioning-whisper— locate bursts here, then caption each detected segment with that model. See the threshold study below.
pip install torch transformers soundfile librosa huggingface_hub
Detected 3 vocal burst(s) in audio.mp3:
1. 2.14s - 3.82s (duration: 1.68s, confidence: 0.89)
2. 8.50s - 9.12s (duration: 0.62s, confidence: 0.74)
3. 15.30s - 16.94s (duration: 1.64s, confidence: 0.92)
JSON output (--json):
{
"file": "audio.mp3",
"events": [
{"start": 2.14, "end": 3.82, "confidence": 0.89, "duration": 1.68},
{"start": 8.5, "end": 9.12, "confidence": 0.74, "duration": 0.62},
{"start": 15.3, "end": 16.94, "confidence": 0.92, "duration": 1.64}
]
}
This model performs binary frame-level segmentation on audio: for each 20ms frame in a 30-second audio clip, it predicts whether a vocal burst is occurring. Post-processing then groups these frame-level predictions into discrete events with timestamps and confidence scores.
Audio (16kHz, 30s) → Whisper-small Encoder (LoRA rank-8 merged) → 1500 frame embeddings
→ Linear(768→384) + GELU + Dropout
→ Conv1d(384, kernel=7) + GELU + Dropout (temporal smoothing)
→ Linear(384→1) → sigmoid → 1500 probabilities
→ Post-processing → [(start, end, confidence), ...]
The model uses OpenAI's Whisper-small encoder as the audio feature backbone. During training, the encoder was adapted using LoRA (rank 8, alpha 16) on the q_proj and v_proj attention matrices. The LoRA weights have been merged into the base weights, so no adapter library is needed at inference time. All three checkpoints (model.pt, model_v2.pt, model_v2_mixed.pt) share this architecture and load with identical code.
| File | Size | Description |
|---|---|---|
model_v2.pt | 972 MB | Recommended. Fine-tuned on real in-the-wild expressive speech |
model_v2_mixed.pt | 972 MB | v2 trained on a mix of real speech + synthetic soundscapes (keeps the synthetic domain) |
model.pt | 972 MB | v1, synthetic-only training. Superseded — see Previous version |
head_only.pt | 5.3 MB | v1 segmentation head weights only (use with your own Whisper-small encoder) |
inference.py | - | Standalone inference script with CLI and Python API |
train.py | - | Full training script (supports frozen/LoRA/fine-tuning modes) |
generate_dataset.py | - | Synthetic training data generator |
download_sources.py | - | Downloads source audio from HuggingFace datasets |
config.json | - | Model configuration and training hyperparameters |
vocalburst_threshold_report.html | - | Interactive ensemble threshold study report |
| Parameter | Recommended (v2) | inference.py built-in default | Description |
|---|---|---|---|
threshold | 0.50 | 0.65 | Detection confidence threshold (0-1). Higher = fewer false positives, lower = fewer missed events. |
merge_gap | 0.10 | 0.3 | Merge predicted segments closer than this (seconds). Prevents a single event from being split into fragments. |
min_dur | 0.10 | 0.5 | Discard predicted events shorter than this (seconds). The v1 default of 0.5 discards ~96 % of real bursts. |
checkpoint | model_v2.pt | model.pt (auto-downloaded) | Which weights to load. |
device | auto | auto | "cpu", "cuda", or "cuda:0" etc. Auto-detects GPU if available. |
The "built-in default" column is what the script uses if you pass nothing; it has been left at the v1 values for backwards compatibility. Pass the recommended column explicitly.
Imagine the model is a security guard watching for vocal bursts. It has to make a decision for every moment of audio: "Is this a vocal burst, or not?"
There are four possible outcomes:
REALITY
Vocal Burst Not a VB
┌─────────────┬─────────────┐
MODEL Yes │ True Pos ✓ │ False Pos ✗ │ ← "False alarm"
SAYS: │ (correct!) │ (oops) │
├─────────────┼─────────────┤
No │ False Neg ✗ │ True Neg ✓ │ ← "Missed it"
│ (missed!) │ (correct!) │
└─────────────┴─────────────┘
Precision = Of everything the model flagged, how many were real? TP / (TP + FP)
Recall = Of all real vocal bursts, how many did the model catch? TP / (TP + FN)
F1 Score = The harmonic mean of precision and recall — balances both into one number.
threshold — The confidence cutoffThe model outputs a confidence score (0 to 1) for every 20ms frame. The threshold decides: "How confident must the model be before we call it a vocal burst?"
low threshold → Model flags almost everything
✓ High recall (catches most VBs)
✗ Low precision (many false alarms)
Think: paranoid security guard
high threshold → Model only flags when very sure
✓ High precision (almost no false alarms)
✗ Low recall (misses quieter/ambiguous VBs)
Think: lazy security guard
For model_v2.pt the swept best operating point on real audio is 0.50. (For v1 on synthetic
data it was 0.65; for v1 on real audio it was 0.80, where recall collapses — see
Previous version.)
min_dur — Minimum event durationAfter grouping confident frames into events, discard any event shorter than min_dur.
min_dur = 0.1s → Recommended for v2 on real audio
✓ Keeps short coughs/gasps and the ~180 ms median real burst
✗ Slightly more short false positives
min_dur = 0.5s → The old v1 default
✓ Filters noise spikes in synthetic soundscapes
✗ Discards ~96 % of real bursts
min_dur = 1.0s → Only keeps long events
✗ Misses almost everything on real audio
This is the single most impactful knob. On synthetic soundscapes, mixed-in bursts are long
(0.5–3 s) and a large min_dur cheaply removes false positives — which is why v1 shipped 0.5.
On real recordings the ground-truth median burst is ~180 ms, so the same setting throws away
the majority of true events.
merge_gap — Gap tolerance for mergingIf two detected segments are separated by less than merge_gap, merge them into one event.
merge_gap = 0.0s → No merging. A laugh with a brief pause becomes 2 events.
Result: Over-counting (more events than expected)
merge_gap = 0.1s → Recommended for v2. Bridges frame-level dropouts without
swallowing neighbouring bursts.
merge_gap = 1.0s → Even 1-second gaps get bridged.
Result: Separate nearby events might merge into one big event
Because real bursts are short and can occur close together, a large merge_gap fuses distinct
events; 0.10 s is the swept-best value for v2.
Making the model more cautious (↑ precision) always means it will miss more real events (↓ recall), and vice versa. You can't eliminate false positives without also losing some true positives.
← More conservative More aggressive →
Precision: ████████████████░░░░ (goes DOWN as you lower threshold)
Recall: ░░░░████████████████ (goes UP as you lower threshold)
↑
Sweet spot (F1 max)
Choose your trade-off based on your application:
Same architecture, initialised from model.pt, then fine-tuned end-to-end (encoder
unfrozen, encoder LR 1e-5, head LR 5e-4, linear schedule, BCE with pos_weight 2) on
98,296 real 30 s clips with CrisperWhisper-derived burst timestamps. An intermediate
stage over ~1M additional windows was run and discarded — see below.
The defaults published with v1 (threshold 0.65, merge_gap 0.3, min_duration 0.5) are
badly mismatched to real data: ground-truth bursts have a median duration of 180 ms,
so min_duration = 0.5 discards ~96 % of them before matching. On the identical
checkpoint, sweeping post-processing moved event F1 from 0.243 to 0.598 — a larger
effect than any training change we made. Recommended for v2:
threshold = 0.50 # was 0.65
merge_gap = 0.10 # was 0.30
min_duration = 0.10 # was 0.50 <-- the one that matters
An intermediate fine-tuning stage over 1,044,713 windows cut from the same corpus hurt: F1 fell from 0.598 to 0.482. Cause: the window extractor kept only windows that contained at least one burst, so 100 % of that training set was positive. Without burst-free examples the detector learns that bursts are everywhere — precision fell from 0.649 to 0.578 and binary detection accuracy from 0.913 to 0.853. A subsequent stage on the balanced set recovered it to 0.607. If you train on your own data, keep negatives in.
model_v2_mixed.pt — broader domain coverageA third weight, for the case where the audio is not only expressive speech. Same architecture and same loading code as the others.
model_v2.pt is fine-tuned on real expressive speech only and, in the process,
forgot the synthetic-soundscape domain v1 was trained on — music beds, sound
effects, non-speech backgrounds. model_v2_mixed.pt is trained on a mix: the
regenerated v1 soundscape corpus (33,012 clips, 50 % burst-free by construction)
plus 40,000 classifier-confirmed DramaBox clips.
| real speech (992) | real, relabelled | held-out real (500) | synthetic soundscapes | |
|---|---|---|---|---|
model.pt (v1) | 0.162 | 0.170 | 0.186 | 0.740 |
model_v2.pt | 0.607 | 0.607 | 0.625 | 0.513 |
model_v2_mixed.pt | 0.597 | 0.609 | 0.617 | 0.726 |
Event F1 @ IoU 0.5.
Which to use. On real audio the two v2 weights are statistically indistinguishable — every difference sits inside the bootstrap confidence interval and the sign flips between validation sets. Do not read 0.607 vs 0.597 as a ranking. The one difference that is robust is the synthetic column: +0.21, CI [+0.16, +0.27].
model_v2_mixed.ptmodel_v2.ptSame post-processing recommendation for both: threshold 0.50, merge_gap 0.10, min_duration 0.10.
Three attempts to beat 0.607 on real audio failed. Training on 1,044,713 edge-case windows that were 100 % positive dropped F1 to 0.482; precision fell first, as a detector with no negatives learns that bursts are everywhere. A 100k positive / 100k negative "mirror" set — negatives made by excising the burst from the same clip — reached only 0.458, so simply restoring the positive/negative balance was not the fix either. The mix above is the first variant that does not lose ground, and it still does not gain any on real speech.
A hypothesis we tested and discarded: that the training labels were heavily contaminated, because a classifier pass rejected 50.41 % of the source burst detections. Controls showed that figure is mostly an artefact of the 300 ms cut length — feeding the same classifier 3,000 certainly real bursts truncated to 300 ms yields 51.3 % "no burst", against 13.7 % at full length. On the actual labels the rejection rate is 7.76 %. A paired control (identical clips and schedule, only the labels cleaned) moved F1 by −0.007 / +0.004 / +0.003 across three validation sets, every interval straddling zero. Label cleaning changed nothing measurable.
The real-audio validation sets are 992 and 500 clips, which cannot resolve differences below roughly ±0.03. Their labels come from an ASR model, not from human annotation, so the achievable ceiling is unknown — a model cannot score above the labels' own agreement rate. Whether 0.61 is near that ceiling or far below it has not been measured.
model.pt)Superseded by
model_v2.pt. Kept for reproducibility and for the synthetic-soundscape domain; on real in-the-wild audio it scores event F1 0.152 versus 0.607 for v2.
Evaluated on 300 held-out synthetic soundscapes with the v1 inference settings (threshold=0.65, merge_gap=0.3s, min_dur=0.5s):
| Metric | Value |
|---|---|
| Event F1 | 0.752 |
| Event Precision | 0.897 |
| Event Recall | 0.781 |
| Binary Detection Accuracy | 0.810 |
| Frame Accuracy (all) | 0.928 |
On that synthetic test set the model catches ~78% of vocal burst events with ~90% precision. That number does not transfer to real recordings — see the real-audio comparison.
from inference import load_model, detect_vocal_bursts
# omitting `checkpoint` auto-downloads model.pt (v1)
model, fe, device = load_model("cuda")
events = detect_vocal_bursts("audio.mp3", model=model, fe=fe, device=device)
python inference.py audio.mp3 # v1 weights + v1 defaults
python inference.py audio.mp3 --checkpoint ./model.pt --threshold 0.7 --min-dur 0.3
python inference.py audio.mp3 --json
| Threshold | Precision | Recall | F1 | False Positives | Missed Events |
|---|---|---|---|---|---|
| 0.40 | 0.62 | 0.89 | 0.73 | Many | Few |
| 0.65 | 0.90 | 0.78 | 0.75 | Few | Some |
| 0.85 | 0.95 | 0.55 | 0.70 | Very few | Many |
| Use Case | threshold | min_dur | merge_gap | What changes |
|---|---|---|---|---|
| Balanced (v1 default) | 0.65 | 0.5 | 0.3 | Good all-around on synthetic data |
| High precision (no false alarms) | 0.80 | 0.7 | 0.3 | ↑ precision, ↓ recall |
| High recall (catch everything) | 0.45 | 0.2 | 0.5 | ↑ recall, ↓ precision |
| Noisy audio (music, crowds) | 0.75 | 0.6 | 0.3 | Reduces noise-triggered FPs |
| Short events (coughs, gasps) | 0.60 | 0.2 | 0.2 | Catches brief events |
| Long events only (extended laughs) | 0.65 | 1.0 | 0.5 | Ignores anything <1s |
These recipes were tuned on synthetic soundscapes. For real audio with model_v2.pt, start from
0.50 / 0.10 / 0.10.
head_only.pt (v1 head)If you already have Whisper-small loaded or want to use a different Whisper variant:
import torch
from transformers import WhisperModel
# Load your own whisper encoder
whisper = WhisperModel.from_pretrained("openai/whisper-small")
encoder_out = whisper.encoder(input_features=mel_features).last_hidden_state # [B, 1500, 768]
# Load just the segmentation head
head_sd = torch.load("head_only.pt", map_location="cpu")
# head_sd contains: proj.0.weight, proj.0.bias, temporal.0.weight, temporal.0.bias, out.weight, out.bias
# Apply: proj → permute → temporal → permute → out → squeeze → sigmoid
We compared frozen encoder, LoRA rank 2/4/8 with the v1 post-processing (threshold=0.65, merge_gap=0.3s, min_dur=0.5s, pos_weight=2):
| Model | Trainable Params | Event F1 | Precision | Recall | Binary Det |
|---|---|---|---|---|---|
| Frozen encoder | 295K (0.12%) | 0.589 | 0.786 | 0.645 | 0.733 |
| LoRA rank-2 | 1.55M (0.64%) | 0.734 | 0.886 | 0.768 | 0.803 |
| LoRA rank-4 | 1.77M (0.73%) | 0.744 | 0.878 | 0.794 | 0.807 |
| LoRA rank-8 | 2.21M (0.91%) | 0.752 | 0.897 | 0.781 | 0.810 |
Key findings:
This detector is designed to be used as an ensemble with the fine-tuned captioner
laion/vocalburst-captioning-whisper: the locator finds where vocal bursts
occur (start/end timestamps); each detected segment is then cut and described by the captioner
(Whisper-small fine-tuned on vocal-burst captions). Together they turn raw audio into timestamped, captioned vocal-burst events that feed
the LAION Universal Audio Annotation Pipeline.
⚠️ This study was run with
merge_gap = 0.3 s, min_dur = 0.5 s— the v1 post-processing. Its threshold recommendation (0.85–0.89) is tied to those settings and does not carry over tomodel_v2.pt, where the recommended operating point isthreshold 0.50, merge_gap 0.10, min_duration 0.10.
We swept the detector's confidence threshold from 0.85 to 0.92 (1% steps) on 150 audio samples
(clean-speech false-positive checks + clips with inserted bursts + isolated bursts), with
merge_gap = 0.3 s, min_dur = 0.5 s. For every (sample × threshold) the detector's segments were
captioned by laion/vocalburst-captioning-whisper and the audio + (start, end, caption) list was sent to Gemini 3.1 Pro, which
rated three axes 0–5 (5 = perfect): caption quality, timestamp accuracy, and completeness
(do the detections cover ALL real vocal bursts, penalizing both misses and false positives). That is
1,200 independent LLM judgments; overall = mean of the three axes.
| rank | threshold | overall | completeness | caption quality | timestamp accuracy |
|---|---|---|---|---|---|
| 🥇 | 0.88 | 3.475 | 3.11 | 3.24 | 4.07 |
| 🥈 | 0.89 | 3.469 | 3.15 | 3.18 | 4.08 |
| 🥉 | 0.85 | 3.466 | 3.11 | 3.22 | 4.07 |
| 4 | 0.90 | 3.445 | 3.10 | 3.24 | 4.00 |
| 5 | 0.86 | 3.411 | 3.05 | 3.14 | 4.04 |
| 6 | 0.87 | 3.390 | 3.07 | 3.10 | 4.00 |
| 7 | 0.91 | 3.364 | 3.05 | 3.14 | 3.91 |
| 8 | 0.92 | 3.363 | 3.02 | 3.15 | 3.92 |
Findings: scores are tightly clustered across 0.85–0.92 (the detections change little in that band); threshold ≈ 0.88 is the sweet spot (best overall). Timestamp accuracy is consistently strong (~4.0), caption quality is moderate (~3.2), and completeness is the weakest axis (~3.0–3.15) — it degrades at the highest thresholds (0.91–0.92) as real bursts start being missed.
📊 Full interactive report (stats table + audio players + predictions + per-clip Gemini scores for the
top-3 thresholds): vocalburst_threshold_report.html.
# 1. Download source audio (~15K vocal bursts, ~13K backgrounds)
python download_sources.py
# 2. Generate synthetic soundscapes (~33K samples)
python generate_dataset.py
# 3. Train with LoRA (best v1 configuration)
CUDA_VISIBLE_DEVICES=0 \
FREEZE_ENCODER=1 LORA_RANK=8 LORA_ALPHA=16 \
POS_WEIGHT=2 DET_THRESHOLD=0.65 POST_MERGE_GAP=0.3 POST_MIN_DUR=0.5 \
EPOCHS=15 LR=5e-4 ENCODER_LR=2e-4 \
python train.py
For a v2-style run on real audio, initialise from a checkpoint with INIT_WEIGHTS, unfreeze the
encoder, and set the eval/post-processing variables to the v2 values
(DET_THRESHOLD=0.5 POST_MERGE_GAP=0.1 POST_MIN_DUR=0.1) — otherwise the reported eval metrics
will be dominated by the mismatched POST_MIN_DUR.
The training script is controlled entirely via environment variables:
| Variable | Default | Description |
|---|---|---|
FREEZE_ENCODER | 0 | Set to 1 to freeze Whisper encoder (required for LoRA) |
LORA_RANK | 0 | LoRA rank (0=disabled, 8=recommended) |
LORA_ALPHA | 0 | LoRA alpha (0=auto: rank×2) |
POS_WEIGHT | 4.0 | BCE positive class weight (2.0 recommended for precision) |
DET_THRESHOLD | 0.5 | Detection threshold for eval metrics |
POST_MERGE_GAP | 0.5 | Post-processing merge gap (seconds) |
POST_MIN_DUR | 0.3 | Post-processing min duration (seconds) |
LR | 2e-4 | Head learning rate |
ENCODER_LR | 0 | Encoder/LoRA learning rate (0=same as LR) |
EPOCHS | 6 | Training epochs |
MAX_BSZ | 0 | Max batch size cap (0=unlimited, auto-probed) |
INIT_WEIGHTS | - | Path to checkpoint for weight initialization |
RESUME_MODE | none | Resume training: none, latest, or best |
DATA_DIR | vb_dataset | Path to training data |
OUT_DIR | vb_output | Output directory for checkpoints and logs |
The synthetic dataset generator creates audio soundscapes by mixing:
Each sample produces an .mp3 audio file and a .json metadata file:
{
"events": [
{"start_time": 3.21, "end_time": 4.85},
{"start_time": 12.50, "end_time": 13.10}
],
"duration_sec": 24.5,
"bg_type": "music",
"n_vocal_bursts": 2
}
model_v2.pt is fine-tuned on real expressive speech and has lost some of v1's synthetic-soundscape performance (0.513 vs 0.740 event F1 on synthetic); use model_v2_mixed.pt if music/SFX backgrounds matter.Slap Face false positivesWhen pairing this locator with
laion/vocalburst-classifier-single
in a detect-then-classify pipeline, note that the classifier over-predicts Slap Face as
top-1 on in-the-wild speech. The recommended mitigation is to skip that label and take the
runner-up class. See that model's README for details and a code snippet.
@misc{vocalburst-locator-2025,
title={Vocal Burst Locator: Whisper-based Vocal Burst Segmentation},
author={LAION},
year={2025},
publisher={HuggingFace},
url={https://huggingface.co/laion/vocalburst-locator}
}
Apache 2.0
18 commits
4
stars
18
commits
1
repos using this model
1
linked in READMEs
Aug 2, 2026
updated
A Whisper-based model that detects and localizes vocal bursts (laughs, coughs, sneezes, sighs, gasps, cries, screams, etc.) in audio, returning precise start/end timestamps for each event.
model_v2.ptThe recommended default checkpoint is model_v2.pt (972 MB), fine-tuned on real in-the-wild audio. The original model.pt (v1) is trained on synthetic soundscapes only and is superseded — it is kept for reproducibility, documented under Previous version — v1.
⚠️ Two things are easy to get wrong, so they are stated up front:
inference.py still auto-downloads model.pt when you do not pass a checkpoint. Pass model_v2.pt explicitly.inference.py's built-in post-processing defaults are still the v1-era values (threshold=0.65, merge_gap=0.3, min_dur=0.5). Pass the v2 values explicitly — they dominate the measured F1 (see below).threshold = 0.50 # was 0.65 in v1
merge_gap = 0.10 # was 0.30 in v1
min_duration = 0.10 # was 0.50 in v1 <-- the one that matters
Ground-truth bursts have a median duration of ~180 ms. A min_duration of 0.5 s therefore
discards ~96 % of real bursts before matching. On one identical checkpoint, only changing
post-processing moved event F1 from 0.243 to 0.598 — a larger effect than any training change
made for v2. If you read older instructions in this card recommending 0.65 / 0.3 / 0.5, those are
the v1 numbers and are not recommended any more.
from huggingface_hub import hf_hub_download
from inference import load_model, detect_vocal_bursts # inference.py from this repo
# 1. Download the recommended checkpoint
ckpt = hf_hub_download("laion/vocalburst-locator", "model_v2.pt")
# 2. Load it (v1 would be loaded if you omit `checkpoint`)
model, fe, device = load_model("cuda", checkpoint=ckpt) # or "cpu"
# 3. Detect, with the v2 post-processing values
events = detect_vocal_bursts(
"audio.mp3",
model=model, fe=fe, device=device,
threshold=0.50,
merge_gap=0.10,
min_dur=0.10,
)
for ev in events:
print(f"{ev['start']:.2f}s - {ev['end']:.2f}s (confidence: {ev['confidence']:.2f})")
Command line equivalent:
python inference.py audio.mp3 \
--checkpoint "$(python -c 'from huggingface_hub import hf_hub_download; print(hf_hub_download("laion/vocalburst-locator","model_v2.pt"))')" \
--threshold 0.50 --merge-gap 0.10 --min-dur 0.10 --device cuda
Raw state dict (if you build the model yourself — same WhisperSegmenter state dict as v1, 485 tensors, LoRA already merged):
import torch
sd = torch.load("model_v2.pt", map_location="cpu")
model.load_state_dict(sd) # same keys as model.pt
Re-measured on a held-out set of 992 real, in-the-wild expressive-speech clips, each checkpoint given a post-processing sweep to find its best possible operating point:
| event F1 @ IoU 0.5 | precision | recall | best threshold | |
|---|---|---|---|---|
model.pt (v1, synthetic) | 0.152 | 0.469 | 0.207 | 0.80 |
model_v2.pt | 0.607 | 0.678 | 0.669 | 0.50 |
4.0x higher F1 on real audio. Note also how v1 fails: it only reaches usable precision at threshold 0.80, where recall collapses to 0.21 — on real recordings it is very unsure, and buying precision costs it four fifths of the events. v2 operates at 0.50 with recall 0.67.
| Your audio | Checkpoint |
|---|---|
| Expressive speech, in-the-wild (default choice) | model_v2.pt |
| In-the-wild audio that also contains music, SFX or non-speech backgrounds | model_v2_mixed.pt |
| Synthetic soundscapes / reproducing the original results | model.pt (v1, superseded) |
On real audio the two v2 weights are statistically indistinguishable; they differ only on the
synthetic-soundscape domain. Both use the same post-processing values above. Details in the
model_v2_mixed.pt section.
🔗 Ensemble: pair this detector with the captioner
laion/vocalburst-captioning-whisper— locate bursts here, then caption each detected segment with that model. See the threshold study below.
pip install torch transformers soundfile librosa huggingface_hub
Detected 3 vocal burst(s) in audio.mp3:
1. 2.14s - 3.82s (duration: 1.68s, confidence: 0.89)
2. 8.50s - 9.12s (duration: 0.62s, confidence: 0.74)
3. 15.30s - 16.94s (duration: 1.64s, confidence: 0.92)
JSON output (--json):
{
"file": "audio.mp3",
"events": [
{"start": 2.14, "end": 3.82, "confidence": 0.89, "duration": 1.68},
{"start": 8.5, "end": 9.12, "confidence": 0.74, "duration": 0.62},
{"start": 15.3, "end": 16.94, "confidence": 0.92, "duration": 1.64}
]
}
This model performs binary frame-level segmentation on audio: for each 20ms frame in a 30-second audio clip, it predicts whether a vocal burst is occurring. Post-processing then groups these frame-level predictions into discrete events with timestamps and confidence scores.
Audio (16kHz, 30s) → Whisper-small Encoder (LoRA rank-8 merged) → 1500 frame embeddings
→ Linear(768→384) + GELU + Dropout
→ Conv1d(384, kernel=7) + GELU + Dropout (temporal smoothing)
→ Linear(384→1) → sigmoid → 1500 probabilities
→ Post-processing → [(start, end, confidence), ...]
The model uses OpenAI's Whisper-small encoder as the audio feature backbone. During training, the encoder was adapted using LoRA (rank 8, alpha 16) on the q_proj and v_proj attention matrices. The LoRA weights have been merged into the base weights, so no adapter library is needed at inference time. All three checkpoints (model.pt, model_v2.pt, model_v2_mixed.pt) share this architecture and load with identical code.
| File | Size | Description |
|---|---|---|
model_v2.pt | 972 MB | Recommended. Fine-tuned on real in-the-wild expressive speech |
model_v2_mixed.pt | 972 MB | v2 trained on a mix of real speech + synthetic soundscapes (keeps the synthetic domain) |
model.pt | 972 MB | v1, synthetic-only training. Superseded — see Previous version |
head_only.pt | 5.3 MB | v1 segmentation head weights only (use with your own Whisper-small encoder) |
inference.py | - | Standalone inference script with CLI and Python API |
train.py | - | Full training script (supports frozen/LoRA/fine-tuning modes) |
generate_dataset.py | - | Synthetic training data generator |
download_sources.py | - | Downloads source audio from HuggingFace datasets |
config.json | - | Model configuration and training hyperparameters |
vocalburst_threshold_report.html | - | Interactive ensemble threshold study report |
| Parameter | Recommended (v2) | inference.py built-in default | Description |
|---|---|---|---|
threshold | 0.50 | 0.65 | Detection confidence threshold (0-1). Higher = fewer false positives, lower = fewer missed events. |
merge_gap | 0.10 | 0.3 | Merge predicted segments closer than this (seconds). Prevents a single event from being split into fragments. |
min_dur | 0.10 | 0.5 | Discard predicted events shorter than this (seconds). The v1 default of 0.5 discards ~96 % of real bursts. |
checkpoint | model_v2.pt | model.pt (auto-downloaded) | Which weights to load. |
device | auto | auto | "cpu", "cuda", or "cuda:0" etc. Auto-detects GPU if available. |
The "built-in default" column is what the script uses if you pass nothing; it has been left at the v1 values for backwards compatibility. Pass the recommended column explicitly.
Imagine the model is a security guard watching for vocal bursts. It has to make a decision for every moment of audio: "Is this a vocal burst, or not?"
There are four possible outcomes:
REALITY
Vocal Burst Not a VB
┌─────────────┬─────────────┐
MODEL Yes │ True Pos ✓ │ False Pos ✗ │ ← "False alarm"
SAYS: │ (correct!) │ (oops) │
├─────────────┼─────────────┤
No │ False Neg ✗ │ True Neg ✓ │ ← "Missed it"
│ (missed!) │ (correct!) │
└─────────────┴─────────────┘
Precision = Of everything the model flagged, how many were real? TP / (TP + FP)
Recall = Of all real vocal bursts, how many did the model catch? TP / (TP + FN)
F1 Score = The harmonic mean of precision and recall — balances both into one number.
threshold — The confidence cutoffThe model outputs a confidence score (0 to 1) for every 20ms frame. The threshold decides: "How confident must the model be before we call it a vocal burst?"
low threshold → Model flags almost everything
✓ High recall (catches most VBs)
✗ Low precision (many false alarms)
Think: paranoid security guard
high threshold → Model only flags when very sure
✓ High precision (almost no false alarms)
✗ Low recall (misses quieter/ambiguous VBs)
Think: lazy security guard
For model_v2.pt the swept best operating point on real audio is 0.50. (For v1 on synthetic
data it was 0.65; for v1 on real audio it was 0.80, where recall collapses — see
Previous version.)
min_dur — Minimum event durationAfter grouping confident frames into events, discard any event shorter than min_dur.
min_dur = 0.1s → Recommended for v2 on real audio
✓ Keeps short coughs/gasps and the ~180 ms median real burst
✗ Slightly more short false positives
min_dur = 0.5s → The old v1 default
✓ Filters noise spikes in synthetic soundscapes
✗ Discards ~96 % of real bursts
min_dur = 1.0s → Only keeps long events
✗ Misses almost everything on real audio
This is the single most impactful knob. On synthetic soundscapes, mixed-in bursts are long
(0.5–3 s) and a large min_dur cheaply removes false positives — which is why v1 shipped 0.5.
On real recordings the ground-truth median burst is ~180 ms, so the same setting throws away
the majority of true events.
merge_gap — Gap tolerance for mergingIf two detected segments are separated by less than merge_gap, merge them into one event.
merge_gap = 0.0s → No merging. A laugh with a brief pause becomes 2 events.
Result: Over-counting (more events than expected)
merge_gap = 0.1s → Recommended for v2. Bridges frame-level dropouts without
swallowing neighbouring bursts.
merge_gap = 1.0s → Even 1-second gaps get bridged.
Result: Separate nearby events might merge into one big event
Because real bursts are short and can occur close together, a large merge_gap fuses distinct
events; 0.10 s is the swept-best value for v2.
Making the model more cautious (↑ precision) always means it will miss more real events (↓ recall), and vice versa. You can't eliminate false positives without also losing some true positives.
← More conservative More aggressive →
Precision: ████████████████░░░░ (goes DOWN as you lower threshold)
Recall: ░░░░████████████████ (goes UP as you lower threshold)
↑
Sweet spot (F1 max)
Choose your trade-off based on your application:
Same architecture, initialised from model.pt, then fine-tuned end-to-end (encoder
unfrozen, encoder LR 1e-5, head LR 5e-4, linear schedule, BCE with pos_weight 2) on
98,296 real 30 s clips with CrisperWhisper-derived burst timestamps. An intermediate
stage over ~1M additional windows was run and discarded — see below.
The defaults published with v1 (threshold 0.65, merge_gap 0.3, min_duration 0.5) are
badly mismatched to real data: ground-truth bursts have a median duration of 180 ms,
so min_duration = 0.5 discards ~96 % of them before matching. On the identical
checkpoint, sweeping post-processing moved event F1 from 0.243 to 0.598 — a larger
effect than any training change we made. Recommended for v2:
threshold = 0.50 # was 0.65
merge_gap = 0.10 # was 0.30
min_duration = 0.10 # was 0.50 <-- the one that matters
An intermediate fine-tuning stage over 1,044,713 windows cut from the same corpus hurt: F1 fell from 0.598 to 0.482. Cause: the window extractor kept only windows that contained at least one burst, so 100 % of that training set was positive. Without burst-free examples the detector learns that bursts are everywhere — precision fell from 0.649 to 0.578 and binary detection accuracy from 0.913 to 0.853. A subsequent stage on the balanced set recovered it to 0.607. If you train on your own data, keep negatives in.
model_v2_mixed.pt — broader domain coverageA third weight, for the case where the audio is not only expressive speech. Same architecture and same loading code as the others.
model_v2.pt is fine-tuned on real expressive speech only and, in the process,
forgot the synthetic-soundscape domain v1 was trained on — music beds, sound
effects, non-speech backgrounds. model_v2_mixed.pt is trained on a mix: the
regenerated v1 soundscape corpus (33,012 clips, 50 % burst-free by construction)
plus 40,000 classifier-confirmed DramaBox clips.
| real speech (992) | real, relabelled | held-out real (500) | synthetic soundscapes | |
|---|---|---|---|---|
model.pt (v1) | 0.162 | 0.170 | 0.186 | 0.740 |
model_v2.pt | 0.607 | 0.607 | 0.625 | 0.513 |
model_v2_mixed.pt | 0.597 | 0.609 | 0.617 | 0.726 |
Event F1 @ IoU 0.5.
Which to use. On real audio the two v2 weights are statistically indistinguishable — every difference sits inside the bootstrap confidence interval and the sign flips between validation sets. Do not read 0.607 vs 0.597 as a ranking. The one difference that is robust is the synthetic column: +0.21, CI [+0.16, +0.27].
model_v2_mixed.ptmodel_v2.ptSame post-processing recommendation for both: threshold 0.50, merge_gap 0.10, min_duration 0.10.
Three attempts to beat 0.607 on real audio failed. Training on 1,044,713 edge-case windows that were 100 % positive dropped F1 to 0.482; precision fell first, as a detector with no negatives learns that bursts are everywhere. A 100k positive / 100k negative "mirror" set — negatives made by excising the burst from the same clip — reached only 0.458, so simply restoring the positive/negative balance was not the fix either. The mix above is the first variant that does not lose ground, and it still does not gain any on real speech.
A hypothesis we tested and discarded: that the training labels were heavily contaminated, because a classifier pass rejected 50.41 % of the source burst detections. Controls showed that figure is mostly an artefact of the 300 ms cut length — feeding the same classifier 3,000 certainly real bursts truncated to 300 ms yields 51.3 % "no burst", against 13.7 % at full length. On the actual labels the rejection rate is 7.76 %. A paired control (identical clips and schedule, only the labels cleaned) moved F1 by −0.007 / +0.004 / +0.003 across three validation sets, every interval straddling zero. Label cleaning changed nothing measurable.
The real-audio validation sets are 992 and 500 clips, which cannot resolve differences below roughly ±0.03. Their labels come from an ASR model, not from human annotation, so the achievable ceiling is unknown — a model cannot score above the labels' own agreement rate. Whether 0.61 is near that ceiling or far below it has not been measured.
model.pt)Superseded by
model_v2.pt. Kept for reproducibility and for the synthetic-soundscape domain; on real in-the-wild audio it scores event F1 0.152 versus 0.607 for v2.
Evaluated on 300 held-out synthetic soundscapes with the v1 inference settings (threshold=0.65, merge_gap=0.3s, min_dur=0.5s):
| Metric | Value |
|---|---|
| Event F1 | 0.752 |
| Event Precision | 0.897 |
| Event Recall | 0.781 |
| Binary Detection Accuracy | 0.810 |
| Frame Accuracy (all) | 0.928 |
On that synthetic test set the model catches ~78% of vocal burst events with ~90% precision. That number does not transfer to real recordings — see the real-audio comparison.
from inference import load_model, detect_vocal_bursts
# omitting `checkpoint` auto-downloads model.pt (v1)
model, fe, device = load_model("cuda")
events = detect_vocal_bursts("audio.mp3", model=model, fe=fe, device=device)
python inference.py audio.mp3 # v1 weights + v1 defaults
python inference.py audio.mp3 --checkpoint ./model.pt --threshold 0.7 --min-dur 0.3
python inference.py audio.mp3 --json
| Threshold | Precision | Recall | F1 | False Positives | Missed Events |
|---|---|---|---|---|---|
| 0.40 | 0.62 | 0.89 | 0.73 | Many | Few |
| 0.65 | 0.90 | 0.78 | 0.75 | Few | Some |
| 0.85 | 0.95 | 0.55 | 0.70 | Very few | Many |
| Use Case | threshold | min_dur | merge_gap | What changes |
|---|---|---|---|---|
| Balanced (v1 default) | 0.65 | 0.5 | 0.3 | Good all-around on synthetic data |
| High precision (no false alarms) | 0.80 | 0.7 | 0.3 | ↑ precision, ↓ recall |
| High recall (catch everything) | 0.45 | 0.2 | 0.5 | ↑ recall, ↓ precision |
| Noisy audio (music, crowds) | 0.75 | 0.6 | 0.3 | Reduces noise-triggered FPs |
| Short events (coughs, gasps) | 0.60 | 0.2 | 0.2 | Catches brief events |
| Long events only (extended laughs) | 0.65 | 1.0 | 0.5 | Ignores anything <1s |
These recipes were tuned on synthetic soundscapes. For real audio with model_v2.pt, start from
0.50 / 0.10 / 0.10.
head_only.pt (v1 head)If you already have Whisper-small loaded or want to use a different Whisper variant:
import torch
from transformers import WhisperModel
# Load your own whisper encoder
whisper = WhisperModel.from_pretrained("openai/whisper-small")
encoder_out = whisper.encoder(input_features=mel_features).last_hidden_state # [B, 1500, 768]
# Load just the segmentation head
head_sd = torch.load("head_only.pt", map_location="cpu")
# head_sd contains: proj.0.weight, proj.0.bias, temporal.0.weight, temporal.0.bias, out.weight, out.bias
# Apply: proj → permute → temporal → permute → out → squeeze → sigmoid
We compared frozen encoder, LoRA rank 2/4/8 with the v1 post-processing (threshold=0.65, merge_gap=0.3s, min_dur=0.5s, pos_weight=2):
| Model | Trainable Params | Event F1 | Precision | Recall | Binary Det |
|---|---|---|---|---|---|
| Frozen encoder | 295K (0.12%) | 0.589 | 0.786 | 0.645 | 0.733 |
| LoRA rank-2 | 1.55M (0.64%) | 0.734 | 0.886 | 0.768 | 0.803 |
| LoRA rank-4 | 1.77M (0.73%) | 0.744 | 0.878 | 0.794 | 0.807 |
| LoRA rank-8 | 2.21M (0.91%) | 0.752 | 0.897 | 0.781 | 0.810 |
Key findings:
This detector is designed to be used as an ensemble with the fine-tuned captioner
laion/vocalburst-captioning-whisper: the locator finds where vocal bursts
occur (start/end timestamps); each detected segment is then cut and described by the captioner
(Whisper-small fine-tuned on vocal-burst captions). Together they turn raw audio into timestamped, captioned vocal-burst events that feed
the LAION Universal Audio Annotation Pipeline.
⚠️ This study was run with
merge_gap = 0.3 s, min_dur = 0.5 s— the v1 post-processing. Its threshold recommendation (0.85–0.89) is tied to those settings and does not carry over tomodel_v2.pt, where the recommended operating point isthreshold 0.50, merge_gap 0.10, min_duration 0.10.
We swept the detector's confidence threshold from 0.85 to 0.92 (1% steps) on 150 audio samples
(clean-speech false-positive checks + clips with inserted bursts + isolated bursts), with
merge_gap = 0.3 s, min_dur = 0.5 s. For every (sample × threshold) the detector's segments were
captioned by laion/vocalburst-captioning-whisper and the audio + (start, end, caption) list was sent to Gemini 3.1 Pro, which
rated three axes 0–5 (5 = perfect): caption quality, timestamp accuracy, and completeness
(do the detections cover ALL real vocal bursts, penalizing both misses and false positives). That is
1,200 independent LLM judgments; overall = mean of the three axes.
| rank | threshold | overall | completeness | caption quality | timestamp accuracy |
|---|---|---|---|---|---|
| 🥇 | 0.88 | 3.475 | 3.11 | 3.24 | 4.07 |
| 🥈 | 0.89 | 3.469 | 3.15 | 3.18 | 4.08 |
| 🥉 | 0.85 | 3.466 | 3.11 | 3.22 | 4.07 |
| 4 | 0.90 | 3.445 | 3.10 | 3.24 | 4.00 |
| 5 | 0.86 | 3.411 | 3.05 | 3.14 | 4.04 |
| 6 | 0.87 | 3.390 | 3.07 | 3.10 | 4.00 |
| 7 | 0.91 | 3.364 | 3.05 | 3.14 | 3.91 |
| 8 | 0.92 | 3.363 | 3.02 | 3.15 | 3.92 |
Findings: scores are tightly clustered across 0.85–0.92 (the detections change little in that band); threshold ≈ 0.88 is the sweet spot (best overall). Timestamp accuracy is consistently strong (~4.0), caption quality is moderate (~3.2), and completeness is the weakest axis (~3.0–3.15) — it degrades at the highest thresholds (0.91–0.92) as real bursts start being missed.
📊 Full interactive report (stats table + audio players + predictions + per-clip Gemini scores for the
top-3 thresholds): vocalburst_threshold_report.html.
# 1. Download source audio (~15K vocal bursts, ~13K backgrounds)
python download_sources.py
# 2. Generate synthetic soundscapes (~33K samples)
python generate_dataset.py
# 3. Train with LoRA (best v1 configuration)
CUDA_VISIBLE_DEVICES=0 \
FREEZE_ENCODER=1 LORA_RANK=8 LORA_ALPHA=16 \
POS_WEIGHT=2 DET_THRESHOLD=0.65 POST_MERGE_GAP=0.3 POST_MIN_DUR=0.5 \
EPOCHS=15 LR=5e-4 ENCODER_LR=2e-4 \
python train.py
For a v2-style run on real audio, initialise from a checkpoint with INIT_WEIGHTS, unfreeze the
encoder, and set the eval/post-processing variables to the v2 values
(DET_THRESHOLD=0.5 POST_MERGE_GAP=0.1 POST_MIN_DUR=0.1) — otherwise the reported eval metrics
will be dominated by the mismatched POST_MIN_DUR.
The training script is controlled entirely via environment variables:
| Variable | Default | Description |
|---|---|---|
FREEZE_ENCODER | 0 | Set to 1 to freeze Whisper encoder (required for LoRA) |
LORA_RANK | 0 | LoRA rank (0=disabled, 8=recommended) |
LORA_ALPHA | 0 | LoRA alpha (0=auto: rank×2) |
POS_WEIGHT | 4.0 | BCE positive class weight (2.0 recommended for precision) |
DET_THRESHOLD | 0.5 | Detection threshold for eval metrics |
POST_MERGE_GAP | 0.5 | Post-processing merge gap (seconds) |
POST_MIN_DUR | 0.3 | Post-processing min duration (seconds) |
LR | 2e-4 | Head learning rate |
ENCODER_LR | 0 | Encoder/LoRA learning rate (0=same as LR) |
EPOCHS | 6 | Training epochs |
MAX_BSZ | 0 | Max batch size cap (0=unlimited, auto-probed) |
INIT_WEIGHTS | - | Path to checkpoint for weight initialization |
RESUME_MODE | none | Resume training: none, latest, or best |
DATA_DIR | vb_dataset | Path to training data |
OUT_DIR | vb_output | Output directory for checkpoints and logs |
The synthetic dataset generator creates audio soundscapes by mixing:
Each sample produces an .mp3 audio file and a .json metadata file:
{
"events": [
{"start_time": 3.21, "end_time": 4.85},
{"start_time": 12.50, "end_time": 13.10}
],
"duration_sec": 24.5,
"bg_type": "music",
"n_vocal_bursts": 2
}
model_v2.pt is fine-tuned on real expressive speech and has lost some of v1's synthetic-soundscape performance (0.513 vs 0.740 event F1 on synthetic); use model_v2_mixed.pt if music/SFX backgrounds matter.Slap Face false positivesWhen pairing this locator with
laion/vocalburst-classifier-single
in a detect-then-classify pipeline, note that the classifier over-predicts Slap Face as
top-1 on in-the-wild speech. The recommended mitigation is to skip that label and take the
runner-up class. See that model's README for details and a code snippet.
@misc{vocalburst-locator-2025,
title={Vocal Burst Locator: Whisper-based Vocal Burst Segmentation},
author={LAION},
year={2025},
publisher={HuggingFace},
url={https://huggingface.co/laion/vocalburst-locator}
}
Apache 2.0
18 commits