Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- app.py +5 -1
- docs/Turn Detection - Aman.md +259 -0
- frontend/layout.py +94 -57
- misc/figures/chart1_branch_latency.png +0 -0
- misc/figures/chart2_pizza_problem.png +0 -0
- misc/figures/chart3_latency_breakdown.png +0 -0
- misc/figures/chart4_in_distribution.png +0 -0
- misc/figures/chart5_ood.png +0 -0
- misc/figures/chart6_dataset_effect.png +0 -0
- misc/figures/chart7_preset_comparison.png +0 -0
app.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
import traceback
|
| 2 |
import numpy as np
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from backend.orchestrator import run_pipeline
|
| 5 |
from backend.presets import list_presets
|
| 6 |
from frontend.layout import build_app
|
|
|
|
| 1 |
import traceback
|
| 2 |
import numpy as np
|
| 3 |
+
try:
|
| 4 |
+
import spaces
|
| 5 |
+
except ModuleNotFoundError:
|
| 6 |
+
import types
|
| 7 |
+
spaces = types.SimpleNamespace(GPU=lambda fn: fn)
|
| 8 |
from backend.orchestrator import run_pipeline
|
| 9 |
from backend.presets import list_presets
|
| 10 |
from frontend.layout import build_app
|
docs/Turn Detection - Aman.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **Turn Detection for Hinglish Voice AI**
|
| 2 |
+
|
| 3 |
+
A Gradio based dashboard to play around and understand different composable components to come up with best Turn Detection Stack.
|
| 4 |
+
|
| 5 |
+
## 1. Executive Summary
|
| 6 |
+
|
| 7 |
+
This project covers building an end to end turn detection system optimized for Hindi-English code-switch language. As we understand that silence doesn’t mean the speaker is done talking, for example: “Main aaj office jaun… ya na jaun ?” That pause after first ‘jaun’ shouldn’t be considered as speech done. To distinguish actual complete and incomplete sentences, these turn detection model comes handy. Though it’s very simplified answer but the things get interesting as we have to do this on a streaming audio.
|
| 8 |
+
Instead of building one stack, I made a dashboard where users can try multiple compostable stacks to understand which ones are good enough to take on further. The good part is one can live see the probabilities from different models (currently for acoustic only) live on the dashboard.
|
| 9 |
+
To easily understand I divide it in two branches as:
|
| 10 |
+
|
| 11 |
+
1. **Acoustic Branch:** Tiny, fast model, around 39M params that takes on acoustic signals only. No transcriptions or LLMs, just how a person sounds.
|
| 12 |
+
2. **Semantic Branch:** This one understands the meaning of the speech and then answers if the speaker is done or not yet.
|
| 13 |
+
|
| 14 |
+
Spoiler: for the semantic side I built a streaming path that reads meaning from a growing transcript instead of waiting for full text generation — what's still ahead is unifying that with the acoustic branch into one trainable model, not just voting between them.
|
| 15 |
+
Aim is to build fast, accurate, local models to perform turn detection which helps the voice agent respond accordingly without False Alarms on Pauses and Hinglish supported.
|
| 16 |
+
|
| 17 |
+
What I'd highlight: it's tiny and fully local (no cloud APIs), the acoustic side has a 0.0 false-alarm rate so it never cuts a thinking user off, and it's built Hinglish-first rather than English-default.
|
| 18 |
+
|
| 19 |
+
What I'd highlight: it's tiny and fully local (no cloud APIs), the acoustic side has a 0.0 false-alarm rate so it never cuts a thinking user off, and it's built Hinglish-first rather than English-default.
|
| 20 |
+
|
| 21 |
+
## 2. Problem
|
| 22 |
+
|
| 23 |
+
In voice AI, the agent needs to know the **exact moment** the speaker is done speaking. But real life conversations are messy.
|
| 24 |
+
|
| 25 |
+
| Scenario | What the User Says | What Actually Happens |
|
| 26 |
+
| :---- | :---- | :---- |
|
| 27 |
+
| **Done** | "I'll have a pizza." | 2 seconds of silence. Agent should reply. |
|
| 28 |
+
| **Thinking** | "I'll have a pizza... \[2s pause\] ...and a Coke." | 2 seconds of silence. Agent must wait. |
|
| 29 |
+
|
| 30 |
+
We know simple VAD (Voice Activity Detection) filter won’t work here, it treats both ‘pauses’ and ‘end’ as equal silences and agent will either interrupt or wait for a time bound like 2 sec before interrupting.
|
| 31 |
+
In both cases it’s not good, it’s annoying.
|
| 32 |
+
|
| 33 |
+
### Why Hinglish Makes It Harder
|
| 34 |
+
|
| 35 |
+
Hindi-English code-switching adds a unique challenge of new fillers like "matlab", "toh", "haan", "um" confuse English-trained models.
|
| 36 |
+
Acoustic models may catchup cross language (not that great tho) but Semantic ones need to understand the meaning hence can’t apply eng-only models.
|
| 37 |
+
|
| 38 |
+
## 3. Research
|
| 39 |
+
|
| 40 |
+
Before jumping to code the solution, I personally like to spend a lot of my time going through research papers and articles to understand the existing ones and the best approaches to build future ones.
|
| 41 |
+
Here is the gist of the research:
|
| 42 |
+
|
| 43 |
+
* **VAD only (silence based):** fails as we saw earlier. By research among webrtcvad and silero\_vad, the later one is chosen as webrtcvad is sensitive to flag random noises as speech.
|
| 44 |
+
* **ASR \+ text rules:** can't understand sound patterns, hence fails.
|
| 45 |
+
* **Encoder x Pooler x Header:** Best stack for acoustic branch, basically works by encoding the audio using Whisper or wav2vec. Though whisper is good in resourceful languages, a research on low resource language shows wav2vec as better encoder for acoustic properties.
|
| 46 |
+
* **Smart Turn:** It's a direct model available already trained on the given dataset of the challenge post, so kind of used as off the shelf acoustic model. Under the hood it also has v2 as Silero VAD (gate) → Wav2Vec2 encoder → mean pooling → linear classifier → binary complete/incomplete. For v3 the same but encoder changed to Whisper tiny and pooler to cross-attention pooling.
|
| 47 |
+
* **FastTurn (ASLP-lab):** This paper suggested using unified model which fuses a streaming acoustic path with incremental semantic understanding. Paper evaluates Chinese only, released no code/weights. Worth trying on Hinglish.
|
| 48 |
+
* **TEN Turn Detection:** This approach basically suggests passing the transcript to the LLM, it's slow but it's the one where extra labels like 'wait' could be added for cases where speaker says fillers like: "....ek sec ruko…" then comes back to complete the sentence.
|
| 49 |
+
* **LiveKit End-of-Turn:** a real BERT-style checkpoint distilled from a Qwen2.5-7B teacher, fine-tuned for exactly this task. This one is special coz it utilises existing power of most llm that is next token prediction. It smartly checks for probability of next token to be \<end\_term\> and hence identifies if speaker is finished or not.
|
| 50 |
+
* **Moshi / GPT-Live:** Not a turn detector but worth noting as it's a full duplex meaning it can speak and listen at the same time and hence can feel like someone talking on call. It has a component as Turn Detection.
|
| 51 |
+
|
| 52 |
+
Extras:
|
| 53 |
+
|
| 54 |
+
* There are lot more other research done along the way that saved time by ruling out lot of components earlier like many worse encoders, poolers and headers and hence for acoustic settled for (Whisper, wav2vec) x (Mean, Cross Attention) x (Linear, MLP) combinations.
|
| 55 |
+
* Similarly for semantic, I researched LLMs and settled on Qwen2.5-0.5B as the current model, with Qwen3-0.6B as the future fine-tune base.
|
| 56 |
+
|
| 57 |
+
Currently due to time and resource constraint, few of the acoustic and semantic stacks are still not tried.
|
| 58 |
+
|
| 59 |
+
## 4. Data Understanding & Preparation
|
| 60 |
+
|
| 61 |
+
Coming to Dataset, the challenge gave the Smart Turn dataset but while training models I realised it’s a TTS dataset means the there is no real human speaking. Why this matters coz some of the acoustic models, when evaluated on real recordings later after training on this TTS dataset was not performing good as they overfitted on TTS acoustic signals hence failed on real ones.
|
| 62 |
+
This emerges a new task of obtaining real dataset, which comes out to be Easyturn dataset comprised of real speeches and curated for Turn Detection model training and validation. But it was only chinese or related languages hence not good for understanding niche Hindi-Eng acoustic signals.
|
| 63 |
+
*Note: Here I refer Train, Test, Val, Eval terms, the prior 3 terms are self understood, the Eval is basically the final evaluation of models on real speeches with constructed labels.*
|
| 64 |
+
Here comes the need of real Hin-Eng dataset which was not available (or I couldn’t find) for turn detection. Hence I decided to build it. Fetched the real speech dataset HiACC \+ FLEURS which is basically complete sentences.
|
| 65 |
+
But for our usecase we need incomplete and complete both labelled. To generate realistic incomplete sentences, I used the longest pause of the sentence as breaking point and truncated the audio. This way I managed to generate a set of complete and incomplete real speech dataset.
|
| 66 |
+
|
| 67 |
+
| Dataset | Real? | Labels | Size | Notes |
|
| 68 |
+
| :---- | :---- | :---- | :---- | :---- |
|
| 69 |
+
| Smart Turn Dataset | No (TTS) | endpoint\_bool | 1,168 train | Hindi/Marathi, TTS |
|
| 70 |
+
| Easy-Turn | Yes | Native C/I | \~9.3GB | Real-only shards |
|
| 71 |
+
| HiACC \+ FLEURS | Yes | Constructed | 2,322 \+ FLEURS | Truncated clips |
|
| 72 |
+
| Truncation Pilot | Yes | Constructed | 10 | Pilot run data |
|
| 73 |
+
| OOD Test | Yes | Constructed | 5 clips | Real human readers |
|
| 74 |
+
|
| 75 |
+
Truncation Pilot methodology: As told, real complete clips cut at their most prominent internal VAD pause, with ~250ms of −55dBFS synthetic noise. Directly tests False-Alarm rate on mid-sentence pauses. N=10 (3 English, 4 Hindi, 3 Hinglish).
|
| 76 |
+
|
| 77 |
+
Stratification: challenge data split 70/15/15 train/val/test by Labels to stay class-balanced.
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 5. Approach / Architecture
|
| 82 |
+
|
| 83 |
+
The approach is basically trying and testing all reasonably possible combinations both in Acoustic as well as Semantic branches.
|
| 84 |
+
*By reasonable I mean based on the research we saw earlier.*
|
| 85 |
+
Let’s understand the architecture now:
|
| 86 |
+
As we already discussed about two branches Acoustic and Semantic, let’s deep dive in each.
|
| 87 |
+
|
| 88 |
+
### The Acoustic Branch (Fast & Robust)
|
| 89 |
+
|
| 90 |
+
* **What it does:** Listens to the raw audio waveform and detects turn-completion patterns in the *prosody* — the rhythm and pitch of speech: a drop in pitch, a breath, or trailing silence.
|
| 91 |
+
* **How it works:** Uses frozen audio embeddings from whisper tiny through a Cross-Attention pooling layer and a Linear head.
|
| 92 |
+
* **Speed:** Negligible latency (0.05ms for head) running in real-time.
|
| 93 |
+
* **Superpower:** 0.0 False Alarm Rate on mid-sentence pauses in pilot tests.
|
| 94 |
+
|
| 95 |
+
### The Semantic Branch (Smart but Slow) (Chart 1)
|
| 96 |
+
|
| 97 |
+
* **What it does:** Reads transcripts to judge grammatical and semantic completeness.
|
| 98 |
+
* **How it works:** Audio → Whisper ASR → Qwen2.5-0.5B LLM → Prediction.
|
| 99 |
+
* **Speed:** \~2.4s total (1.5s ASR \+ 1.0s LLM).
|
| 100 |
+
* **Superpower:** Catches explicit cues like "wait a second" that acoustic models miss.
|
| 101 |
+
* Note: this semantic branch also has the streaming variant described in the section right below.
|
| 102 |
+
|
| 103 |
+

|
| 104 |
+
|
| 105 |
+
### A Faster Variant: The Streaming Semantic Branch
|
| 106 |
+
|
| 107 |
+
The 2.4s Whisper+Qwen path is too slow to be the only option, so I also built a **streaming version**. Here's the trick: Whisper generates text *one token at a time* and needs the whole sentence before it can output anything useful. You can't easily ask it "what's the partial transcript so far?" mid-sentence.
|
| 108 |
+
But a **CTC model** (Connectionist Temporal Classification) works differently, one forward pass gives you a transcript for the entire audio you've fed it, so feeding it "a bit more audio" just means running it again on the longer clip. No generation loop, no waiting for the end. I plan to use theainerd/Wav2Vec2-large-xlsr-hindi (a Hindi code-switching CTC model) to produce a growing transcript, and re-run the Qwen classifier on each growing version.
|
| 109 |
+
|
| 110 |
+
### The Fusion Layer
|
| 111 |
+
|
| 112 |
+
The two branches vote. The acoustic branch runs continuously and instantly. The semantic branch updates asynchronously. When they disagree, the acoustic branch wins by default (safety-first), but the semantic branch can override if it detects strong explicit signals.
|
| 113 |
+
Weighted vote: P\_final \= α·P\_acoustic \+ (1−α)·P\_semantic (α=0.6). Acoustic wins by default (safety-first); semantic can override on strong explicit signals (wait → forced incomplete).
|
| 114 |
+
|
| 115 |
+
## 6. Experimentation & Modeling
|
| 116 |
+
|
| 117 |
+
We tested the value of pooling capacity, encoder size, and training data by training four variants on whisper\_tiny embeddings, plus a 3-way dataset comparison.
|
| 118 |
+
**Training details:** frozen encoders, only pooling/head trained (\~1M params); binary cross-entropy; AdamW lr=1e-4, batch=32, 10 epochs.
|
| 119 |
+
|
| 120 |
+
| Experiment | Question | Finding |
|
| 121 |
+
| :---- | :---- | :---- |
|
| 122 |
+
| **Pooling Ablation** | Does attention help? | Cross-Attn wins in-dist (98.8%), overfits OOD (20%). Mean-pool more robust (80% OOD). |
|
| 123 |
+
| **Head Ablation** | Does MLP help? | MLP helps slightly on synthetic (+8%); mean+linear generalizes better OOD. |
|
| 124 |
+
| **Encoder Scaling** | Bigger \= better? | Tiny optimal. Base helps simple heads (+5%), hurts complex (−11%). |
|
| 125 |
+
| **Dataset Scaling** | Does real data fix overfitting? | Yes, Cross-Attn goes 1/5 → 5/5 on real Hindi. |
|
| 126 |
+
| **Semantic Solo** | Can text detect pauses? | No, 100% False Alarm Rate. |
|
| 127 |
+
| **Cross-Lingual** | Does Chinese turn-data help Hindi? | Yes for simpler models (mean+MLP 4/5 vs 2/5 synthetic). |
|
| 128 |
+
| **ASR Quality** | Is Whisper-Tiny good enough for Hindi? | No, 2/4 clips catastrophic. |
|
| 129 |
+
|
| 130 |
+
## 7. Evaluation & Results
|
| 131 |
+
|
| 132 |
+
### 7.1 Acoustic Branch \- Evaluation & Results
|
| 133 |
+
|
| 134 |
+
The acoustic branch listens to how speech sounds (pitch, pause, breath) and runs on frozen whisper\_tiny embeddings through a trainable pooling+head. It is the speed-first, always-on signal.
|
| 135 |
+
|
| 136 |
+
#### 7.1.1 The Overfitting Lesson (Chart 4, Chart 5\)
|
| 137 |
+
|
| 138 |
+
| Test | Mean+Linear | Mean+MLP | Cross-Attn+Linear |
|
| 139 |
+
| :---- | :---- | :---- | :---- |
|
| 140 |
+
| In-Distribution | 68.4% | 76.3% | 98.8% |
|
| 141 |
+
| Out-of-Distribution | **80% (4/5)** | 40% (2/5) | 20% (1/5) |
|
| 142 |
+
|
| 143 |
+
Pool+head latency is negligible (\<0.1ms) for every variant. What works: on the real-human OOD set, the simple Mean+Linear model is the most robust (80%), and crucially it achieves a 0.0 False-Alarm Rate on mid-sentence pauses in the pilot \- it never interrupts a thinking user. The catch: the high-capacity Cross-Attention model scored 98.8% in-distribution but only 20% OOD, because it memorized TTS voice artifacts.
|
| 144 |
+
|
| 145 |
+

|
| 146 |
+
|
| 147 |
+

|
| 148 |
+
|
| 149 |
+
#### 7.1.2 Encoder Comparison: Tiny vs Base
|
| 150 |
+
|
| 151 |
+
| Model | Tiny | Base | Δ |
|
| 152 |
+
| :---- | :---- | :---- | :---- |
|
| 153 |
+
| Mean+Linear | 68.4% | 73.4% | \+5.0% |
|
| 154 |
+
| Mean+MLP | 76.3% | 76.8% | \+0.5% |
|
| 155 |
+
| Cross-Attn+Linear | 98.8% | 87.6% | \-11.3% |
|
| 156 |
+
|
| 157 |
+
Bigger encoders help the simple head slightly but hurt the complex one. whisper\_tiny stays the optimal choice for latency.
|
| 158 |
+
|
| 159 |
+
#### 7.1.3 The Dataset Effect: Synthetic vs Real vs Cross-Lingual (Chart 6\)
|
| 160 |
+
|
| 161 |
+
| Architecture | Synthetic | Real Hindi | Cross-Lingual |
|
| 162 |
+
| :---- | :---- | :---- | :---- |
|
| 163 |
+
| Mean+Linear | 4/5 | 3/5 | 3/5 |
|
| 164 |
+
| Mean+MLP | 2/5 | 3/5 | 4/5 |
|
| 165 |
+
| Cross-Attn+Linear | 1/5 | **5/5** | 1/5 |
|
| 166 |
+
|
| 167 |
+
The big win: the same Cross-Attention architecture went from 1/5 → 5/5 OOD purely by training on real Hindi audio instead of synthetic TTS. Real audio also transfers across languages better than synthetic artifacts transfer within a language.
|
| 168 |
+
|
| 169 |
+

|
| 170 |
+
|
| 171 |
+

|
| 172 |
+
|
| 173 |
+
What we learned & should try (minimal):
|
| 174 |
+
|
| 175 |
+
* Real data fixes overfitting outright → deploy the real-Hindi-trained checkpoint (already built at 5/5; currently held separate by design).
|
| 176 |
+
* whisper\_tiny is enough; don\'t pay for a bigger encoder.
|
| 177 |
+
* Simplest architecture (Mean+Linear) is the safe production default; Cross-Attention is worth it only on real data.
|
| 178 |
+
|
| 179 |
+
### 7.2 Semantic Branch \- Evaluation & Results
|
| 180 |
+
|
| 181 |
+
The semantic branch reads the transcript and judges if the sentence feels complete. It catches explicit cues (\"wait\", \"hold on\") that acoustics alone miss \- but it depends entirely on ASR quality.
|
| 182 |
+
|
| 183 |
+
#### 7.2.1 The \"Pizza Problem\" \- Proven (Chart 2\)
|
| 184 |
+
|
| 185 |
+
| Model | False Alarm Rate | Miss Rate |
|
| 186 |
+
| :---- | :---- | :---- |
|
| 187 |
+
| Qwen2.5-0.5B (Text) | **100%** | 10% |
|
| 188 |
+
| LiveKit EOU (Fusion) | 40% | 60% |
|
| 189 |
+
|
| 190 |
+
Text-only thought every truncated clip was complete (the cut lands at a clause boundary → grammatically complete transcript). What works: fusing with the acoustic branch drops False Alarms from 100% to 40% \- the acoustic veto correctly overrides the premature \"complete\" verdict.
|
| 191 |
+
|
| 192 |
+

|
| 193 |
+
|
| 194 |
+
#### 7.2.2 Latency Breakdown (Chart 3\)
|
| 195 |
+
|
| 196 |
+
| Component | Time | % of Semantic |
|
| 197 |
+
| :---- | :---- | :---- |
|
| 198 |
+
| ASR (Whisper-Tiny) | 1,476ms | 60% |
|
| 199 |
+
| LLM (Qwen2.5-0.5B) | 965ms | 40% |
|
| 200 |
+
| **Total Semantic** | **2,441ms** | 100% |
|
| 201 |
+
|
| 202 |
+
The acoustic branch is 50,000× faster. The semantic branch is blocked by ASR \- you can\'t read text until it exists.
|
| 203 |
+
|
| 204 |
+

|
| 205 |
+
|
| 206 |
+
#### 7.2.3 The Whisper-Tiny Hindi Problem
|
| 207 |
+
|
| 208 |
+
All 4 real Hindi pilot clips transcribed by Whisper-Tiny were broken: hindi\_fleurs\_1524 hallucinated into French loops; hindi\_fleurs\_1526 degenerated to \"hehehehe…\". This fully explains the Hindi Miss Rate hitting 1.0 in fusion. What works: faster-whisper large-v3 produced coherent Devanagari Hindi on all 4 \- the fix is real, just not yet wired in.
|
| 209 |
+
What we learned & should try (minimal):
|
| 210 |
+
|
| 211 |
+
* Text-only detection fails alone → always fuse with the acoustic branch (already done; it cuts False Alarms 100%→40%).
|
| 212 |
+
* ASR is the bottleneck, not the LLM → swap in a stronger Hindi ASR (large-v3 tested and works) and stream the transcript to remove the 2.4s block.
|
| 213 |
+
* A CTC-based streaming path is the clear way forward.
|
| 214 |
+
|
| 215 |
+
## 8. Deployment / Product
|
| 216 |
+
|
| 217 |
+
The entire pipeline is deployed into a **real-time Gradio dashboard**:
|
| 218 |
+
|
| 219 |
+
* **Streams microphone audio** \- re-scores the growing buffer at every cadence tick (default 100ms) for active presets.
|
| 220 |
+
* Plots probability against the waveform to easily visaulise the model output with respect to the waveform.
|
| 221 |
+
* There is a sample of 30 real demo clips (10 en / 10 hi / 10 hinglish) that replay through the same live loop. You can try directly.
|
| 222 |
+
|
| 223 |
+
Note: short-window models jump erratically that is there would be high spikes which is hard to comprehend in visualisation hence added EMA smoothing.
|
| 224 |
+
|
| 225 |
+
## 9. Limitations
|
| 226 |
+
|
| 227 |
+
* **Small Sample Size** – With only 10 truncation and 5 OOD clips, the results show a clear direction but lack full statistical weight.
|
| 228 |
+
* **ASR Bottleneck** – The semantic branch is limited by Whisper-Tiny, which struggles with specific Hindi dialects and can cause the logic to fail.
|
| 229 |
+
* **Artificial Training Data** – Most heads were trained on TTS audio. While real data fixes the overfitting (scoring 5/5), those specific checkpoints haven't been deployed in the main dashboard yet.
|
| 230 |
+
* **Independent Branches** – There is no joint training between paths; they currently rely on a rule-based fusion layer to vote.
|
| 231 |
+
* **Streaming Simulation** – The current streaming module works by replaying growing audio prefixes rather than using a true live, frame-by-frame decoder.
|
| 232 |
+
|
| 233 |
+
## 10. Future Work
|
| 234 |
+
|
| 235 |
+
**Already built vs genuinely future (the word "streaming" appears in both):**
|
| 236 |
+
|
| 237 |
+
* *Built:* live dashboard streams mic audio; a streaming semantic **preset** exists (CTC-based).
|
| 238 |
+
* *Future:* make streaming the **default** semantic path; **unify** the two branches into one trainable model (FastTurn's actual design) instead of post-hoc weighted-vote; build a real production frame-synchronous decoder.
|
| 239 |
+
|
| 240 |
+
### Phase 2: Fine-Tune & Unified Fusion
|
| 241 |
+
|
| 242 |
+
| Component | Plan | Why |
|
| 243 |
+
| :---- | :---- | :---- |
|
| 244 |
+
| **Base Model** | Qwen3-0.6B | Smaller than alternatives; in head-to-head turn-completion tests it scored 6/8 vs Qwen2.5-0.5B's 3/8. |
|
| 245 |
+
| **Dataset** | Real Hindi/Hinglish | No ready-made set exists. |
|
| 246 |
+
| **Budget** | Stream Easy-Turn shards | \~11.4GB to low tens of MB. |
|
| 247 |
+
| **Fusion** | Trainable head over embeddings | Unifies streams. |
|
| 248 |
+
|
| 249 |
+
### Phase 3: Streaming & ASR Upgrades
|
| 250 |
+
|
| 251 |
+
| Component | Plan | Why |
|
| 252 |
+
| :---- | :---- | :---- |
|
| 253 |
+
| **Streaming Default** | CTC incremental path | Removes 2.4s block. |
|
| 254 |
+
| **Unified Fusion** | One trainable model | Matches FastTurn design. |
|
| 255 |
+
| **Hindi ASR** | faster-whisper large-v3 | Fixes Hindi clips. |
|
| 256 |
+
| **Eval Set** | Scale N=10 to N=100 | Statistical power. |
|
| 257 |
+
|
| 258 |
+
The throughline: turn detection for Hinglish needs both the ears (acoustics) and the brain (semantics). The acoustic branch is already fast and interruption-free enough to ship; the semantic side is fundamentally a latency and ASR-quality problem waiting for a streaming, unified fix.
|
| 259 |
+
|
frontend/layout.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
from __future__ import annotations
|
|
|
|
| 2 |
import json
|
|
|
|
| 3 |
import time
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
import soundfile as sf
|
| 6 |
import config
|
|
@@ -35,9 +38,20 @@ def _sample_clip_choices() -> list[tuple[str, str]]:
|
|
| 35 |
|
| 36 |
def _all_public_presets() -> list[dict]:
|
| 37 |
return live.all_public_presets()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
def _preset_display_order() -> list[str]:
|
| 40 |
-
return [live.DISPLAY_NAME.get(p['label'], p['label']) for p in
|
| 41 |
|
| 42 |
def _default_active_display_names() -> list[str]:
|
| 43 |
target = 'Whisper-Tiny + Mean-Pool + Linear (trained)'
|
|
@@ -58,63 +72,86 @@ def _param_relevance(active_display_names: list[str]) -> tuple[bool, bool]:
|
|
| 58 |
semantic_relevant = any((live.uses_semantic_temperature(label) for label in labels))
|
| 59 |
return (acoustic_relevant, semantic_relevant)
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def build_app() -> gr.Blocks:
|
| 62 |
with gr.Blocks(title='Turn Detection - Live Dashboard') as demo:
|
| 63 |
-
gr.Markdown('# Turn Detection - Live Dashboard\nSpeak, or replay a clip, and watch how different models judge whether the speaker is **done talking** vs. **still going** - plotted directly against the waveform as audio arrives.')
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
gr.
|
| 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 |
return demo
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
+
import base64
|
| 3 |
import json
|
| 4 |
+
import re
|
| 5 |
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
import gradio as gr
|
| 8 |
import soundfile as sf
|
| 9 |
import config
|
|
|
|
| 38 |
|
| 39 |
def _all_public_presets() -> list[dict]:
|
| 40 |
return live.all_public_presets()
|
| 41 |
+
FUTURE_PRESET_LABELS = ['Easy Turn (unavailable)']
|
| 42 |
+
|
| 43 |
+
def _display_presets() -> list[dict]:
|
| 44 |
+
presets = live.all_public_presets()
|
| 45 |
+
seen = {p['label'] for p in presets}
|
| 46 |
+
for label in FUTURE_PRESET_LABELS:
|
| 47 |
+
if label not in seen:
|
| 48 |
+
match = next((p for p in list_presets() if p['label'] == label), None)
|
| 49 |
+
if match:
|
| 50 |
+
presets.append(match)
|
| 51 |
+
return presets
|
| 52 |
|
| 53 |
def _preset_display_order() -> list[str]:
|
| 54 |
+
return [live.DISPLAY_NAME.get(p['label'], p['label']).replace(' (unavailable)', '') for p in _display_presets()]
|
| 55 |
|
| 56 |
def _default_active_display_names() -> list[str]:
|
| 57 |
target = 'Whisper-Tiny + Mean-Pool + Linear (trained)'
|
|
|
|
| 72 |
semantic_relevant = any((live.uses_semantic_temperature(label) for label in labels))
|
| 73 |
return (acoustic_relevant, semantic_relevant)
|
| 74 |
|
| 75 |
+
def _load_doc_markdown() -> str:
|
| 76 |
+
root = Path(__file__).resolve().parent.parent
|
| 77 |
+
doc_path = root / 'docs' / 'Turn Detection - Aman.md'
|
| 78 |
+
if not doc_path.exists():
|
| 79 |
+
return '_Documentation is not included in this deployment._'
|
| 80 |
+
text = doc_path.read_text(encoding='utf-8')
|
| 81 |
+
for a, b in [('\\"', '"'), ('\\&', '&'), ('\\'', "'"), ('\\<', '<'), ('\\>', '>')]:
|
| 82 |
+
text = text.replace(a, b)
|
| 83 |
+
|
| 84 |
+
def repl(m: re.Match) -> str:
|
| 85 |
+
alt, src = (m.group(1), m.group(2).strip())
|
| 86 |
+
if src.startswith(('http', 'data:')):
|
| 87 |
+
return m.group(0)
|
| 88 |
+
cand = (doc_path.parent / src).resolve()
|
| 89 |
+
if cand.exists():
|
| 90 |
+
b64 = base64.b64encode(cand.read_bytes()).decode('ascii')
|
| 91 |
+
return f''
|
| 92 |
+
return m.group(0)
|
| 93 |
+
return re.sub('!\\[([^\\]]*)\\]\\(([^)]+)\\)', repl, text)
|
| 94 |
+
|
| 95 |
def build_app() -> gr.Blocks:
|
| 96 |
with gr.Blocks(title='Turn Detection - Live Dashboard') as demo:
|
| 97 |
+
gr.Markdown('# Turn Detection - Live Dashboard\nSpeak, or replay a clip, and watch how different models judge whether the speaker is **done talking** vs. **still going** - plotted directly against the waveform as audio arrives. Open the **Documentation** tab for the full write-up, methodology and results.')
|
| 98 |
+
with gr.Tabs():
|
| 99 |
+
with gr.Tab('Live Dashboard', id='live'):
|
| 100 |
+
session_state = gr.State(live.new_session_state)
|
| 101 |
+
active_state = gr.State(_default_active_display_names())
|
| 102 |
+
with gr.Row():
|
| 103 |
+
threshold_slider = gr.Slider(0.0, 1.0, value=live.DEFAULT_DECISION_THRESHOLD, step=0.01, label='Decision threshold - probability above this = "complete"')
|
| 104 |
+
cadence_slider = gr.Slider(0, 5000, value=live.DEFAULT_CADENCE_MS, step=100, label='Update cadence (ms) - how often each model re-checks (slower models may still lag behind this)')
|
| 105 |
+
smoothing_slider = gr.Slider(0.0, 0.9, value=0.5, step=0.05, label="Smoothing - damps short swings on the chart (raw points still shown faintly; doesn't change what's logged)")
|
| 106 |
+
with gr.Row():
|
| 107 |
+
acoustic_weight_slider = gr.Slider(0.0, 1.0, value=0.6, label='Acoustic weight - audio tone vs. sentence grammar (fusion models only)', interactive=False)
|
| 108 |
+
temperature_slider = gr.Slider(0.0, 1.0, value=0.2, label='Semantic temperature - how deterministic the language judgment is (LLM-based models only)', interactive=False)
|
| 109 |
+
with gr.Row():
|
| 110 |
+
with gr.Column(scale=1, min_width=250):
|
| 111 |
+
gr.Markdown('### Models to compare')
|
| 112 |
+
gr.Markdown('*Greyed models are not built yet (future work).*')
|
| 113 |
+
default_active = _default_active_display_names()
|
| 114 |
+
preset_checkboxes = []
|
| 115 |
+
for preset in _display_presets():
|
| 116 |
+
label = preset['label']
|
| 117 |
+
disp = live.DISPLAY_NAME.get(label, label).replace(' (unavailable)', '')
|
| 118 |
+
available = bool(preset.get('available'))
|
| 119 |
+
cb_label = disp if available else f'{disp} (future)'
|
| 120 |
+
preset_checkboxes.append(gr.Checkbox(label=cb_label, value=disp in default_active, interactive=available))
|
| 121 |
+
with gr.Column(scale=2):
|
| 122 |
+
gr.Markdown('### Audio')
|
| 123 |
+
mic = gr.Audio(sources=['microphone'], streaming=True, type='numpy', label='Record')
|
| 124 |
+
gr.Markdown('*Recordings are saved privately to improve the model - never played back or shown to others.*')
|
| 125 |
+
clear_btn = gr.Button('Clear / reset')
|
| 126 |
+
gr.Markdown('**...or replay a clip in real time** (paced to its real duration, not dumped in at once) - 10 real English, 10 Hindi, 10 Hinglish')
|
| 127 |
+
replay_clip_dropdown = gr.Dropdown(choices=_sample_clip_choices(), value=None, label='Sample clip')
|
| 128 |
+
replay_upload = gr.Audio(sources=['upload'], type='filepath', label='...or upload a recording')
|
| 129 |
+
replay_btn = gr.Button('Replay in real time')
|
| 130 |
+
gr.Markdown('### Waveform + live probability, on one timeline')
|
| 131 |
+
chart = gr.Plot(value=live.render_chart(live.new_session_state(), [], live.DEFAULT_DECISION_THRESHOLD), label=None)
|
| 132 |
+
with gr.Accordion('History (this session)', open=False):
|
| 133 |
+
log_table = gr.Dataframe(headers=LOG_HEADERS, value=[], label=None)
|
| 134 |
+
for cb in preset_checkboxes:
|
| 135 |
+
cb.change(_on_preset_change, inputs=preset_checkboxes, outputs=[active_state, acoustic_weight_slider, temperature_slider])
|
| 136 |
|
| 137 |
+
def replay_clip(clip_filename, uploaded_path, active_display_names, acoustic_weight, temperature, threshold, cadence_ms, smoothing):
|
| 138 |
+
path = uploaded_path or (str(config.SAMPLE_CLIPS_DIR / clip_filename) if clip_filename else None)
|
| 139 |
+
if not path:
|
| 140 |
+
yield (live.new_session_state(), live.render_chart(live.new_session_state(), [], threshold, smoothing), [])
|
| 141 |
+
return
|
| 142 |
+
audio, sr = sf.read(path, dtype='float32')
|
| 143 |
+
if audio.ndim > 1:
|
| 144 |
+
audio = audio.mean(axis=1)
|
| 145 |
+
state = live.new_session_state()
|
| 146 |
+
for chunk in live.chunk_audio(audio, sr, chunk_seconds=1.0):
|
| 147 |
+
chunk_duration_s = len(chunk) / sr
|
| 148 |
+
state, fig, log_rows = live.process_chunk(state, (sr, chunk), active_display_names, acoustic_weight, temperature, threshold, cadence_ms, smoothing)
|
| 149 |
+
yield (state, fig, log_rows)
|
| 150 |
+
time.sleep(chunk_duration_s)
|
| 151 |
+
replay_btn.click(replay_clip, inputs=[replay_clip_dropdown, replay_upload, active_state, acoustic_weight_slider, temperature_slider, threshold_slider, cadence_slider, smoothing_slider], outputs=[session_state, chart, log_table])
|
| 152 |
+
mic.stream(fn=live.process_chunk, inputs=[session_state, mic, active_state, acoustic_weight_slider, temperature_slider, threshold_slider, cadence_slider, smoothing_slider], outputs=[session_state, chart, log_table], stream_every=1.0, time_limit=None)
|
| 153 |
+
clear_btn.click(live.clear_session, inputs=session_state, outputs=[session_state, chart, log_table])
|
| 154 |
+
with gr.Tab('Documentation', id='docs'):
|
| 155 |
+
gr.HTML('<style>.doc-md { max-height: 78vh; overflow: auto; padding-right: 16px; }.doc-md img { max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 6px; margin: 8px 0; }</style>')
|
| 156 |
+
gr.Markdown(_load_doc_markdown(), elem_classes=['doc-md'])
|
| 157 |
return demo
|
misc/figures/chart1_branch_latency.png
ADDED
|
misc/figures/chart2_pizza_problem.png
ADDED
|
misc/figures/chart3_latency_breakdown.png
ADDED
|
misc/figures/chart4_in_distribution.png
ADDED
|
misc/figures/chart5_ood.png
ADDED
|
misc/figures/chart6_dataset_effect.png
ADDED
|
misc/figures/chart7_preset_comparison.png
ADDED
|