Spaces:
Running on Zero
Running on Zero
| title: Tiny Turn Detector | |
| emoji: ποΈ | |
| colorFrom: blue | |
| colorTo: purple | |
| sdk: gradio | |
| app_file: app.py | |
| pinned: false | |
| python_version: "3.12.12" | |
| # Tiny Turn Detector | |
| A small, audio-based turn detection model for conversational voice AI β | |
| built for the Shiprocket Data Scientist assessment. | |
| ## Project | |
| This project builds a tiny, audio-native model that decides whether a | |
| speaker has finished their turn (END) or is mid-pause / hesitating and | |
| likely to continue (CONTINUE). The chosen final architecture is a frozen | |
| Whisper Tiny encoder, mean-pooled, feeding a small Logistic Regression | |
| classifier β arrived at through a documented ladder of experiments | |
| (energy/silence baseline β classical acoustic features β temporal | |
| features β Whisper Tiny), not assumed from the outset. See | |
| `docs/RESULTS.md` for the full evidence trail and `experiments/EXPERIMENTS.md` | |
| for the complete experiment log. | |
| ## Problem | |
| Turn detection is different from voice activity detection (VAD). VAD | |
| answers "is there speech energy right now?" β a memoryless, frame-level | |
| question. Turn detection answers "given everything heard so far, has the | |
| speaker's intent shifted to yielding the floor?" This requires | |
| distinguishing a **mid-utterance pause** (silence, but the speaker intends | |
| to continue β hesitation, a filler word, a connective word like "and"/ | |
| "but") from a **true end-of-turn silence**. Getting this wrong in either | |
| direction has a real cost in a live voice agent: ending a turn too early | |
| interrupts the user (premature END); waiting too long makes the agent | |
| feel slow and unresponsive (delayed END / false CONTINUE). Both error | |
| types are measured and reported separately throughout this project. | |
| ## Dataset | |
| [`pipecat-ai/smart-turn-data-v3.2-train`](https://huggingface.co/datasets/pipecat-ai/smart-turn-data-v3.2-train) | |
| β the real training dataset behind Pipecat's open-source Smart Turn v3.2 | |
| model (270,946 rows, 41.4GB, Whisper Tiny + linear classifier reference | |
| architecture). Schema, label semantics (`endpoint_bool=true`βEND, | |
| `false`βCONTINUE), and provenance were confirmed directly from the | |
| dataset's own README and the upstream project's documentation β see | |
| `docs/INITIAL_ANALYSIS.md`. | |
| **The official test set, [`pipecat-ai/smart-turn-data-v3.2-test`](https://huggingface.co/datasets/pipecat-ai/smart-turn-data-v3.2-test), | |
| was reserved throughout this project and was never used for tuning, | |
| model selection, or any experiment reported here.** All development and | |
| validation splits used in this project were carved from the training | |
| distribution β this is a deliberate, stated methodological choice, not an | |
| oversight, and it means no result in this repository represents true | |
| held-out-test performance in the strictest sense. | |
| **Our small development/validation samples (90β300 clips) do not | |
| represent the complete 270,946-row dataset.** Every result in this | |
| project states its exact sample size and source. See | |
| `docs/LANGUAGE_ANALYSIS.md` for a Known/Inferred/Not-measurable breakdown | |
| of what could and couldn't be determined about language coverage | |
| (including Hindi/Hinglish) from the available metadata. | |
| ## Approach | |
| 1. **16kHz mono audio normalization** β matches the dataset's native | |
| format (confirmed uniform across every clip inspected) and Whisper's | |
| expected input. | |
| 2. **Whisper Tiny frozen encoder** β chosen based on measured evidence | |
| (EXP-004), not assumed from the assessment brief's suggestion alone. | |
| 3. **Mean pooling** over the encoder's time axis, producing a fixed | |
| 384-dimensional representation per clip. | |
| 4. **Logistic Regression** β the smallest classifier that performed well | |
| in testing (92 parameters, ~2.9KB). | |
| 5. **Threshold / debounce / hysteresis decision layer** β prevents | |
| committing to END on a single uncertain pause; configurable, not a | |
| second trained model. See `docs/architecture.md`. | |
| ## Experiments | |
| | Experiment | F1 | Key finding | | |
| |---|---:|---| | |
| | Energy baseline | 0.400 | Weak | | |
| | Acoustic features (Logistic Regression) | 0.575 | Strong improvement over energy baseline | | |
| | Temporal acoustic features | +0.112 (delta) | Recent context matters | | |
| | Whisper Tiny + Logistic Regression | 0.693 | Strongest measured approach | | |
| **Phase 3 (acoustic experiments) and Phase 4 (Whisper experiment) used | |
| different development samples** β same dataset, same general sampling | |
| methodology, independent draws, not a paired comparison. The +0.118 F1 | |
| difference between the acoustic and Whisper approaches is best described | |
| as a **directional improvement on independent samples drawn from the same | |
| Smart Turn training distribution**, not a controlled paired experiment. | |
| Full numbers, sample sizes, and this caveat repeated in context: | |
| `docs/RESULTS.md`. | |
| ## Filler analysis | |
| The acoustic-only model's error analysis (Phase 3) found that **73% of | |
| its false-END errors had filler metadata present** (`midfiller` or | |
| `endfiller` = True) β its dominant failure mode was exactly the | |
| assessment brief's named hard case: a pause that acoustically resembles | |
| an ending but linguistically isn't, because it's preceded by a filler | |
| word. The Whisper-based model's filler-flagged-clip F1 (0.722, n=35) was | |
| notably higher than the acoustic model's (0.529, n=39) on this specific | |
| slice, and its false-END errors were less filler-associated (56% vs. | |
| 73%). **This filler-subset result should be treated as directional due to | |
| the modest sample sizes involved (35β39 clips) and the fact that it | |
| compares different samples** β see `docs/ERROR_ANALYSIS.md` for the full, | |
| unglossed breakdown, including what was explicitly too small to trust | |
| (the `no_filler_known` slice, n=7). | |
| ## Latency | |
| **~15.6ms per clip**, measured on a Colab T4 GPU (preprocessing + Whisper | |
| encoder forward pass + Logistic Regression classifier, warm calls). This | |
| is hardware-dependent β the acoustic-only baseline's 12.2ms figure was | |
| CPU-measured, so the two are not directly comparable. No CPU-measured | |
| Whisper latency exists yet for this project; do not assume the GPU figure | |
| transfers to CPU deployment. | |
| ## Limitations | |
| - Development/validation sample sizes are small (75β300 clips per | |
| experiment) β not enough for tight statistical confidence. | |
| - Phase 3 (acoustic) and Phase 4 (Whisper) experiments used different | |
| samples β not a paired, controlled comparison. | |
| - No transcripts exist anywhere in the dataset (`spoken_text` is null for | |
| every row) β no claim in this project relies on knowing what was | |
| actually said. | |
| - No true conversation-level endpoint timestamps exist in the dataset β | |
| this project measures classification accuracy and false-END/ | |
| false-CONTINUE rates, not true wall-clock endpoint latency. | |
| - Hindi/Hinglish code-switching could not be directly verified from | |
| dataset metadata (no transcripts, no code-switch label) β **this | |
| project does not claim Hinglish robustness.** | |
| - GPU-measured Whisper latency differs from CPU deployment latency, which | |
| hasn't been separately measured. | |
| - The official test set was never touched β no number here represents | |
| true held-out-test performance in the strictest sense. | |
| ## Future work | |
| - Streaming incremental inference (avoid full-buffer recomputation on | |
| every triggering event) | |
| - Training/evaluating on a much larger sample of the full training set | |
| - A controlled Hinglish challenge set with ASR-verified code-switched content | |
| - Final evaluation on the official held-out test set, once the | |
| architecture is fully locked | |
| --- | |
| ## Project structure | |
| ``` | |
| turn-detection/ | |
| βββ app.py # Gradio demo | |
| βββ README.md | |
| βββ requirements.txt | |
| βββ .gitignore | |
| β | |
| βββ src/turn_detector/ | |
| β βββ __init__.py | |
| β βββ inference.py # TurnDetector β final inference module | |
| β βββ audio_io.py # audio loading/resampling (soundfile or ffmpeg fallback) | |
| β βββ features.py # classical acoustic features (EXP-001/002/003b) | |
| β βββ baseline_energy.py # EXP-001 | |
| β βββ baseline_classifier.py # EXP-002 | |
| β βββ splits.py # random / source-aware split utilities | |
| β βββ evaluation.py # shared metrics | |
| β βββ data.py # HF streaming + stratified sampling | |
| β βββ parquet_reader.py # pure-Python Parquet reader (built due to sandbox network limits) | |
| β | |
| βββ models/ | |
| β βββ whisper_classifier.joblib # trained Logistic Regression head (real, from EXP-004) | |
| β βββ model_metadata.json | |
| β | |
| βββ notebooks/ | |
| β βββ EXP004_whisper_baseline.ipynb # self-contained Colab notebook (Whisper Tiny run) | |
| β | |
| βββ scripts/ | |
| β βββ inspect_dataset.py | |
| β βββ create_dev_subset.py | |
| β βββ phase3_inventory_and_sample.py | |
| β βββ phase3_run_baselines.py | |
| β | |
| βββ experiments/ | |
| β βββ EXPERIMENTS.md # full experiment log | |
| β | |
| βββ artifacts/exp004/ # real EXP-004 outputs (results, embeddings, sample metadata) | |
| β | |
| βββ docs/ | |
| β βββ INITIAL_ANALYSIS.md | |
| β βββ LANGUAGE_ANALYSIS.md | |
| β βββ PHASE2_REPORT.md | |
| β βββ PHASE3_REAL_AUDIO_VALIDATION.md | |
| β βββ ERROR_ANALYSIS.md | |
| β βββ RESULTS.md | |
| β βββ architecture.md | |
| β | |
| βββ tests/ | |
| βββ test_features_and_baselines.py # 31 tests, synthetic audio | |
| βββ test_inference.py # 20 tests, real classifier + real embeddings | |
| ``` | |
| ## Whisper weights | |
| Whisper Tiny's weights (`openai/whisper-tiny`) are **not** committed to | |
| this repository β they're loaded at runtime via | |
| `transformers.WhisperModel.from_pretrained("openai/whisper-tiny")`, which | |
| downloads and caches them (~151MB) on first use. This requires network | |
| access to Hugging Face. Only the trained Logistic Regression head | |
| (`models/whisper_classifier.joblib`, ~14KB) is committed, since that's the | |
| part actually trained in this project. | |
| ## Running the demo | |
| ### Hugging Face Spaces | |
| This project is configured for a **Gradio ZeroGPU Space**. The GPU is allocated | |
| only when the `predict_turn` inference callback runs; the Whisper Tiny model | |
| and classifier are used for real inference. The Space does not require a paid | |
| dedicated GPU tier. | |
| For local/cloud environments: | |
| ```bash | |
| pip install -r requirements.txt | |
| python app.py | |
| ``` | |
| Whisper Tiny weights are downloaded from Hugging Face on first inference. | |
| ## Running tests | |
| ```bash | |
| python -m pytest tests/ -v | |
| ``` | |
| 51 tests total: 31 against synthetic audio (feature-extraction math, | |
| baseline logic), 20 against the real trained classifier and real Whisper | |
| embeddings from the actual EXP-004 Colab run (audio validation, decision | |
| logic, and end-to-end classifier-stage prediction) β the Whisper encoder | |
| stage itself is not testable in this project's own dev sandbox (see | |
| above), and tests that would require it fail informatively rather than | |
| being silently skipped or faked. | |