"""Gradio app — voice triage & intake agent for code-switched African speech. Tabs: 1. Triage — audio in, structured record + SATS tier out 2. Compare — same audio through all 3 ASR models, side by side 3. Benchmark — run the eval set, produce the report table """ import json import os import gradio as gr from asr_comparators import build_registry from triage_agent import run as run_agent from triage_schema import Tier TIER_COLOR = { "RED": "#c0392b", "ORANGE": "#e67e22", "YELLOW": "#f1c40f", "GREEN": "#27ae60", "BLUE": "#3498db", } MODELS = build_registry(include_heavy=False) MODEL_NAMES = [m.name for m in MODELS] or [""] def _card(res) -> str: c = TIER_COLOR.get(res.tier.value, "#777") flags = "".join(f"
  • {f.replace('_',' ')}
  • " for f in res.red_flags) or "
  • none detected
  • " reasons = "".join(f"
  • {r}
  • " for r in res.reasons) return f"""
    {res.tier.value}
    {res.disposition}
    Red flags Why
    ⚠ Decision support only. A qualified clinician must confirm every tier.
    """ def triage(audio_path, model_name, lang): if not audio_path: return "Upload or record audio first.", "", "" model = next((m for m in MODELS if m.name == model_name), None) if model is None: return "No ASR model available. Set INTRON_API_KEY or install faster-whisper.", "", "" try: tr = model.transcribe(audio_path, language_hint=lang or None) except Exception as e: return f"ASR failed: {e}", "", "" try: rec, res, q = run_agent(tr.text, asr_model=tr.model_name) except Exception as e: return tr.text, f"Extraction failed: {e}", "" follow = f"**Next question:** {q}" if q else "**Intake complete** — no further questions." return ( f"**[{tr.model_name}]** ({tr.latency_s}s)\n\n{tr.text}", _card(res), follow + "\n\n```json\n" + json.dumps(rec.to_dict(), indent=2, ensure_ascii=False) + "\n```", ) def compare(audio_path, lang): if not audio_path: return "Upload audio first." rows = ["| Model | Latency | Tier | Transcript |", "|---|---|---|---|"] for m in MODELS: try: tr = m.transcribe(audio_path, language_hint=lang or None) _, res, _ = run_agent(tr.text, asr_model=m.name) tier = res.tier.value txt = tr.text[:180].replace("|", "\\|").replace("\n", " ") rows.append(f"| {m.name} | {tr.latency_s}s | **{tier}** | {txt} |") except Exception as e: rows.append(f"| {m.name} | — | ERROR | {str(e)[:120]} |") rows.append("") rows.append("_Differing tiers across models on the same audio is the finding worth reporting._") return "\n".join(rows) with gr.Blocks(title="Voice Triage — Code-Switched African Speech") as demo: gr.Markdown( "# 🩺 Voice Triage for Code-Switched African Speech\n" "Speak naturally in Pidgin, Yoruba, Hausa or English. The agent extracts a " "structured intake record, asks for what's missing, and assigns a **SATS** triage tier.\n\n" "*Prototype. Decision support only — never a substitute for a clinician.*" ) with gr.Tab("Triage"): with gr.Row(): with gr.Column(): a1 = gr.Audio(sources=["upload", "microphone"], type="filepath", label="Patient audio (≤120s)") m1 = gr.Dropdown(MODEL_NAMES, value=MODEL_NAMES[0], label="ASR model") l1 = gr.Textbox(value="en", label="Language hint (en, sw, yo, ha…)") b1 = gr.Button("Run triage", variant="primary") with gr.Column(): o_card = gr.HTML(label="Triage") o_tr = gr.Markdown(label="Transcript") o_rec = gr.Markdown(label="Record") b1.click(triage, [a1, m1, l1], [o_tr, o_card, o_rec]) with gr.Tab("Compare models"): a2 = gr.Audio(sources=["upload", "microphone"], type="filepath", label="Audio") l2 = gr.Textbox(value="en", label="Language hint") b2 = gr.Button("Run all models", variant="primary") o2 = gr.Markdown() b2.click(compare, [a2, l2], o2) with gr.Tab("Benchmark"): gr.Markdown( "Run offline for the report:\n\n" "```bash\npython run_bench.py --manifest eval.jsonl --out report.md\n```\n\n" "Reports WER, code-switch-boundary WER, clinical entity recall, " "negation flips, and downstream triage agreement." ) if __name__ == "__main__": demo.launch(theme=gr.themes.Soft())