| --- |
| 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_v2.onnx` | cc-by-nc-sa-4.0 | fusion head retrained on conversational clips; contains the DailyDialog text branch, so it inherits its 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_v2 | fp32 | 38.19 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. |
| - **Fusion wins in-domain, not on the benchmark β measured, not guessed.** The |
| fused model reaches AP 0.959 vs 0.905 audio-only on held-out conversational |
| clips (Whisper transcripts), but on eot-bench the text branch scores AUC 0.485 β |
| chance β on real human-agent speech, so fusion there is the audio branch diluted |
| by noise (AUC 0.751 vs 0.770). Until a text branch trained on real spoken |
| dialogue exists, `audio_eot_v2` is the model to deploy. |
| - **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** |
|
|