Spaces:
Running on Zero
Running on Zero
| """ | |
| Tiny Turn Detector — Gradio demo. | |
| SANDBOX NOTE: `gradio` and `torch`/`transformers` are not installed in the | |
| environment this project was developed in, and cannot be installed there | |
| (no network route to PyPI — see docs/INITIAL_ANALYSIS.md). This file is | |
| written to run in a normal environment with those installed | |
| (`pip install -r requirements.txt`); it has been validated here by static | |
| syntax/import-structure checking only, not by actually starting the | |
| Gradio server — see the final quality-check report for exactly what was | |
| and wasn't verified. | |
| Run with: | |
| python app.py | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent / "src")) | |
| import numpy as np | |
| import spaces | |
| try: | |
| import gradio as gr | |
| except ImportError as e: | |
| raise SystemExit( | |
| "gradio is not installed. Install with `pip install -r requirements.txt` " | |
| "in an environment with network access." | |
| ) from e | |
| from turn_detector.inference import TurnDetector, TurnDetectorConfig, InferenceError, AudioValidationError | |
| MODEL_INFO_TEXT = """ | |
| **Model:** Whisper Tiny (frozen encoder) + Logistic Regression | |
| **Representation:** mean-pooled encoder hidden states (384-dim) | |
| **Encoder params:** 8,208,384 (~32.8MB fp32) | |
| **Classifier params:** 92 (~2.9KB) | |
| **Measured F1 (75-clip real-audio validation, EXP-004):** 0.693 | |
| This is a research prototype validated on small real-audio samples from | |
| `pipecat-ai/smart-turn-data-v3.2-train`, **not** the official held-out test | |
| set. See `docs/RESULTS.md` and `docs/ERROR_ANALYSIS.md` for full, honestly | |
| caveated results — including known weaknesses (filler-associated pauses, | |
| small validation-set sizes, GPU-vs-CPU latency differences). | |
| """ | |
| EXPLANATION_TEXT = ( | |
| "Predicts whether the speaker appears to have completed their turn " | |
| "(END) or is likely still speaking / pausing mid-thought (CONTINUE), " | |
| "from audio alone. Trained and validated on real clips from one " | |
| "public turn-detection dataset — not claimed to generalize perfectly " | |
| "to all accents, languages, or code-switching patterns. In " | |
| "particular, Hindi-English (\"Hinglish\") code-switching could not be " | |
| "directly verified in the source dataset (no transcripts available), " | |
| "so robustness to it is not a claim this demo makes." | |
| ) | |
| _detector: TurnDetector | None = None | |
| def get_detector() -> TurnDetector: | |
| global _detector | |
| if _detector is None: | |
| _detector = TurnDetector(TurnDetectorConfig()) | |
| return _detector | |
| def predict_turn(audio) -> tuple[str, dict, str]: | |
| """Gradio callback. `audio` from gr.Audio(type="numpy") is either None | |
| or a (sample_rate, numpy_array) tuple. | |
| """ | |
| if audio is None: | |
| return "No audio provided.", {}, "" | |
| sr, array = audio | |
| array = np.asarray(array, dtype=np.float32) | |
| # gr.Audio can hand back int16 PCM depending on source; normalize to | |
| # [-1, 1] float32 if it looks like integer-range data. | |
| if np.abs(array).max() > 1.5: | |
| array = array / 32768.0 | |
| try: | |
| detector = get_detector() | |
| except InferenceError as e: | |
| return f"Model unavailable: {e}", {}, "" | |
| try: | |
| result = detector.predict(array, sr=sr) | |
| except AudioValidationError as e: | |
| return f"Invalid audio: {e}", {}, "" | |
| except InferenceError as e: | |
| return f"Inference failed: {e}", {}, "" | |
| decision_label = result["decision"] | |
| probs = {"END": result["end_probability"], "CONTINUE": result["continue_probability"]} | |
| detail = ( | |
| f"**Decision:** {decision_label}\n\n" | |
| f"**END probability:** {result['end_probability']:.3f}\n\n" | |
| f"**CONTINUE probability:** {result['continue_probability']:.3f}\n\n" | |
| f"**Inference latency:** {result['latency_ms']:.1f} ms " | |
| f"(this machine — see docs/RESULTS.md for the hardware caveat)" | |
| ) | |
| return decision_label, probs, detail | |
| def build_app() -> "gr.Blocks": | |
| with gr.Blocks(title="Tiny Turn Detector") as demo: | |
| gr.Markdown("# Tiny Turn Detector") | |
| gr.Markdown("**Audio-based END vs CONTINUE detection for conversational voice AI**") | |
| gr.Markdown(EXPLANATION_TEXT) | |
| with gr.Row(): | |
| with gr.Column(): | |
| audio_input = gr.Audio( | |
| sources=["upload", "microphone"], | |
| type="numpy", | |
| label="Audio input (WAV or microphone)", | |
| ) | |
| submit_btn = gr.Button("Detect", variant="primary") | |
| with gr.Column(): | |
| decision_output = gr.Label(label="Decision", elem_id="decision-output") | |
| probs_output = gr.Label(label="Probabilities", num_top_classes=2) | |
| detail_output = gr.Markdown() | |
| submit_btn.click( | |
| fn=predict_turn, | |
| inputs=[audio_input], | |
| outputs=[decision_output, probs_output, detail_output], | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown(MODEL_INFO_TEXT) | |
| return demo | |
| if __name__ == "__main__": | |
| app = build_app() | |
| app.launch() | |