File size: 5,294 Bytes
8094ec7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | ---
license: apache-2.0
license_name: mixed-per-model
license_link: LICENSE
library_name: onnx
pipeline_tag: audio-classification
language:
- en
tags:
- end-of-turn-detection
- turn-taking
- voice-agents
- speech
- onnx
- from-scratch
datasets:
- pipecat-ai/smart-turn-data-v3.2-train
- Scicom-intl/semantic-vad-eot
- li2017dailydialog/daily_dialog
metrics:
- roc_auc
- average_precision
model-index:
- name: TurnWave
results:
- task:
type: audio-classification
name: End-of-turn detection
dataset:
type: livekit/eot-bench-data
name: eot-bench (English)
split: validation
metrics:
- type: roc_auc
value: 0.77
name: AUC
- type: average_precision
value: 0.602
name: Average precision
- type: false_cutoff_rate
value: 42.1
name: False cutoffs @300ms latency budget (%)
- type: false_cutoff_rate
value: 17.2
name: False cutoffs @600ms latency budget (%)
---
# TurnWave β end-of-turn detection for voice agents
Decides whether a caller has **finished speaking** or is only pausing, so a voice
agent neither interrupts them nor leaves an awkward silence. It replaces the fixed
300β700 ms silence timeout most pipelines still use.
**Trained from scratch β no pretrained weights anywhere.** A causal transformer
(RoPE, RMSNorm, SwiGLU) over the transcript tail, and a CNN over log-mel
spectrograms for prosody. Even the log-mel front end is hand-built on `torch.stft`,
so there is no torchaudio or librosa dependency.
## Benchmark
Scored by [LiveKit's eot-bench](https://github.com/livekit/eot-bench) harness on
real human-to-agent conversation, using their code and published baselines. Lower
is better; **bold marks the best per column.**
| model | false cutoffs @300 ms β | @600 ms β | latency @5% cutoff β |
|---|---|---|---|
| VAD baseline | 55.6% | 21.7% | 1600 ms |
| **TurnWave audio branch (this model)** | 42.1% | 17.2% | 1195 ms |
| SmartTurn v3.2 | 35.2% | 14.8% | 1051 ms |
| LiveKit Turn Detector v1 | **9.9%** | **4.5%** | **543 ms** |
TurnWave beats the VAD baseline on every metric the harness reports.
## Models in this repo
| file | licence | training data |
|---|---|---|
| `audio_eot_v2.onnx` | apache-2.0 | trained on smart-turn conversational clips |
| `audio_eot.onnx` | cc-by-4.0 | Phase 4; trained on semantic-vad-eot (CC BY 4.0) |
| `text_eot.int8.onnx` | cc-by-nc-sa-4.0 | trained on DailyDialog (CC BY-NC-SA 4.0) β non-commercial |
| `fusion_eot.onnx` | cc-by-nc-sa-4.0 | contains the text branch, so it inherits the same terms |
Each model's licence follows its training data, so they differ. `audio_eot_v2` is
the one the benchmark above measures and the one to use.
## Usage
```python
from huggingface_hub import hf_hub_download
from turnwave.infer import TurnDetector # pip install git+https://github.com/Nikhils-G/turnwave
detector = TurnDetector(hf_hub_download("Nikhil-09/turnwave", "audio_eot_v2.onnx"))
if detector.predict(audio=wav_16k) > 0.5:
respond()
```
16 kHz mono. The model reads the last 2 seconds ending at the decision point, which
sits 0.2 s into the pause β where a live agent decides, and where eot-bench scores.
| model | variant | CPU latency | size |
|---|---|---|---|
| audio_eot_v2 | fp32 | 4.81 ms | 14.0 MB |
| audio_eot | fp32 | 4.52 ms | 14.0 MB |
| text_eot | int8 | 5.02 ms | 7.2 MB |
| fusion_eot | fp32 | 9.35 ms | 42.4 MB |
INT8 is not applied blindly: dynamic quantization rewrites MatMul, so it speeds up
the transformer and *slows down* the conv-heavy branches. Each model ships whichever
variant measured faster.
## What this project found
The first version of this model scored **AP 0.945** on its own held-out test set and
**AUC 0.563** on eot-bench β barely above random. The policy sweep chose thresholds
of 0.0 and 1.0, meaning *ignore the model entirely*.
The cause was the training corpus, not the architecture. It derived from a dataset
whose own card declares `task_categories: [text-to-speech]` β read speech, whose
pauses are reading hesitations rather than conversational turn-yields. The model had
learned *"has this sentence finished being read aloud."*
Retraining on conversational data, changing nothing else, lifted AUC to **0.770**.
The in-domain score could never have revealed this; only a benchmark on data we did
not build could.
## Limitations
- **English only.** Other languages are in the training data but untested here.
- **Behind the production models**, and not a fair comparison: SmartTurn starts from
a pretrained Whisper encoder, LiveKit's is a fine-tuned 0.5B LLM distilled from a
7B teacher. This is 3.49M parameters from random initialisation.
- **The fusion model is stale.** It was trained on the read-speech corpus, which the
benchmark showed to be the wrong task. The conversational corpus has no
transcripts, so retraining fusion needs ASR first.
- **Non-commercial models included.** The text and fusion models derive from
DailyDialog (CC BY-NC-SA 4.0). Only the audio branches are permissively licensed.
Code, training scripts, and the full write-up: **https://github.com/Nikhils-G/turnwave**
|