Skip to content

Latest commit

Β 

History

36 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸŽ™οΈ VoiceTutor

A voice-first, hands-free Spanish tutor.

Pipecat Β Β·Β  Groq Llama 3.3 70B Β Β·Β  AssemblyAI Universal-Streaming Β Β·Β  Deepgram Aura-2 Β Β·Β  Silero VAD

python next.js pipecat license latency tests


Put the phone down. Talk. Learn Spanish.

VoiceTutor reimagines Duolingo as a voice-first tutor: lessons, quizzes, roleplay, and on-the-fly doubt clearing β€” all through natural spoken conversation, with sub-1.5 s end-to-end latency and real barge-in.

✨ Highlights

  • Four learning modes, all voice-entered: Teaching, Quiz, Conversation, Doubt
  • Sub-second response. Groq LLM + Deepgram Aura-2 TTS + tuned Silero VAD = ~1.0 s P50 end-to-end
  • Real barge-in. Speak over the agent and it stops in <250 ms
  • Semantic grading. Paraphrases are accepted ("I'd like coffee" ≑ "I would like a coffee, please")
  • Per-user persistence with FSRS-lite spaced repetition so weak words come back tomorrow
  • Multi-persona handoff β€” Teacher, Examiner, Companion β€” via prompt swap, single LLM
  • Code-switching aware STT (AssemblyAI language=es with EN tolerance) and natively multilingual TTS
  • Observability built in β€” per-turn JSONL with STT/LLM/TTS timestamps; /metrics endpoint
  • Beautiful, minimal UI β€” animated voice orb, live transcript, progress dashboard

🎬 30-second demo

Demo Video (Google Drive)

A learner says "Teach me how to order food in Spanish" β†’ tutor walks them through quisiera un cafΓ©, the learner interrupts with "wait, why is it la and not el?" β†’ tutor explains in English, then resumes the lesson. At the end, the tutor says "You have 3 words due for review tomorrow."


βœ… Spec Compliance Matrix

Two columns: what the spec asks for β†’ what I shipped. Every line of the assignment brief, with a tick.

Core constraint (Β§ 2)

Requirement Status
Voice is the primary interface βœ… Mic β†’ WebSocket β†’ Pipecat β†’ speakers
Minimal visual UI (status, transcript, progress) βœ… Voice orb Β· live transcript Β· progress panel
No core learning step requires tap / type / read βœ… Every mode enters and exits by voice

Four learning modes (Β§ 3)

Requirement Status
Teaching β€” "Teach me how to order food in Spanish" βœ… IntentRouter.INTENT_TEACH β†’ 5-phase lesson FSM
Quiz β€” "Quiz me on yesterday's vocabulary" βœ… IntentRouter.INTENT_QUIZ β†’ 5-question quiz + grading
Conversation Practice β€” "Let's roleplay at a restaurant in Paris" βœ… IntentRouter.INTENT_CONVO β†’ in-character roleplay
Doubt Resolution β€” "Wait, why is it 'la' and not 'el'?" βœ… Stack-pushed mode; answers in English; resumes exact step

Voice interaction (Β§ 4.1)

Requirement Status
Real-time full-duplex audio βœ… WebSocket carries PCM in + out
Streaming STT (input) βœ… AssemblyAI Universal-Streaming v3
Streaming TTS (output) βœ… Deepgram Aura-2 streaming (aura-2-carina-es)
Barge-in / interruption βœ… Silero VAD β†’ InterruptionFrame β†’ TTS cancel
Voice Activity Detection (VAD) βœ… Silero, tuned thresholds
E2E latency < 1.5 s (P50) βœ… Measured ~880–1010 ms
Latency documented βœ… Per-stage table in WRITEUP Β§ 4 + /metrics
Graceful silence handling βœ… VAD only fires above thresholds
Background noise handling βœ… min_volume=0.55 + confidence=0.60 reject AC / keyboard while catching browser mics
Disfluency handling ("um", restarts) βœ… Silero ignores, AssemblyAI tolerates

Teaching & pedagogy (Β§ 4.2)

Requirement Status
Structured lessons (objective β†’ explain β†’ example β†’ practice β†’ check) βœ… Lesson sub-FSM, one step per turn
Adaptive difficulty (repeat / simplify / advance) βœ… confidence_score 0..1 fed to LLM each turn
β‰₯ 3 lessons in curriculum βœ… 6 lessons (greetings, numbers, food, family, days/time, directions)
Curriculum hand-authored OR generated, defended βœ… Hand-authored, defended in WRITEUP Β§ 5
Pronunciation feedback specific (not "good job") βœ… 10 per-word + 12 phoneme-pattern hints

Quiz engine (Β§ 4.3)

Requirement Status
β‰₯ 3 question types βœ… Translation Β· Listening comprehension Β· Spoken response
Semantic grading (not exact-string) βœ… NFD-normalise β†’ exact β†’ variant β†’ Jaccard β†’ LLM fallback
Score tracked βœ… quiz_score / quiz_total + DB progress.score
End-of-quiz summary delivered as voice + on-screen βœ… LLM speaks summary + UI quiz chip

Doubt handling & memory (Β§ 4.4)

Requirement Status
Mid-lesson interrupt β†’ ask β†’ resume βœ… Doubt = stack push; restores mode + step + quiz index
Short-term: session mistakes βœ… SessionMemory.session_mistakes
Short-term: vocab introduced βœ… SessionMemory.introduced_vocab
Short-term: recent topics βœ… SessionMemory.recent_topics
Long-term file / SQLite persistence βœ… SQLite WAL, 4 tables
Per-user progress tracked βœ… progress table
Learned words tracked βœ… vocab_mastery table
Weak areas tracked βœ… weak_areas() query (lapses > 0)
Used to personalise next session βœ… greeting_for_returning_user reads weak + due vocab

Multilingual (Β§ 4.5)

Requirement Status
STT transcribes target language βœ… AssemblyAI language=Language.ES
STT transcribes native language too βœ… EN code-switching tolerated within ES mode
Same-utterance code-switching βœ… Language tag carried per frame
TTS native-sounding in both βœ… Deepgram Aura-2 aura-2-carina-es (native EN/ES code-switching)
Voice switching natural (no swap mid-sentence) βœ… Single voice handles both β€” zero artifact
Code-switching handling documented βœ… WRITEUP Β§ 8

Technical stack (Β§ 5.1)

Requirement Status
Orchestration: Pipecat OR LiveKit βœ… Pipecat (defended in WRITEUP D1)
LLM with function/tool calling βœ… Groq Llama 3.x + 4 tools
STT must be streaming, justified βœ… AssemblyAI Universal-Streaming v3 (WRITEUP D4)
TTS supports target lang + streams βœ… Deepgram Aura-2 (aura-2-carina-es)
VAD (Silero or pipeline-native) βœ… Silero
Frontend (anything minimal) βœ… Next.js 14, single page
Persistence (SQLite / Postgres / Redis / JSON) βœ… SQLite WAL

Architecture expectations (Β§ 5.2)

Requirement Status
Clear separation of concerns βœ… agent/ Β· curriculum/ Β· memory/ Β· transports/ Β· observability/
State management justified βœ… Hand-rolled FSM (defended in WRITEUP D7)
Tool calling: start_quiz() βœ… tools.py
Tool calling: grade_answer() βœ… tools.py
Tool calling: save_progress() βœ… tools.py
Tool calling: lookup_vocab() βœ… tools.py
Configurable system prompts βœ… agent/prompts.py β€” fully data-driven
Logs and traces every turn βœ… JSONL row per turn with all stage timestamps

Non-functional (Β§ 5.3)

Requirement Status
E2E voice latency P50 < 1500 ms βœ… Measured ~880–1010 ms
Interrupt-to-silence < 300 ms βœ… Measured ~150–250 ms
Cost per 5-min session estimated + documented βœ… $0 free / ~$0.10 paid (WRITEUP Β§ 10)
Crash resilience (survives STT/TTS/LLM failures) βœ… Pipecat retry + IntentRouter fallback
Per-turn STT text logged βœ… JSONL
Per-turn LLM input/output logged βœ… JSONL
Per-turn TTS latency logged βœ… JSONL
Per-turn total turn time logged βœ… JSONL + rolling /metrics

Deliverables (Β§ 7)

Requirement Status
Public GitHub repo + source + README + .env.example βœ… github.com/AyushCoder9/VoiceTutor
Working prototype runnable locally βœ… uvicorn + npm run dev
Demo video 3–5 min (lesson, quiz, doubt, code-switch, error recovery) βœ… Google Drive Folder
Technical write-up (2–4 pages, MD/PDF) βœ… WRITEUP.md
Architecture diagram βœ… 8 Mermaid diagrams in docs/architecture.md
Evaluation harness (scripted convos / unit tests) βœ… 151 tests incl. scripted-flow e2e

Bonus / stretch goals (Β§ 9)

Bonus Status
Pronunciation scoring (phoneme-level) 🟑 Heuristic phoneme hints; forced alignment defended as skipped
Spaced repetition (SM-2 / FSRS) βœ… FSRS-lite β€” ease / interval / lapses per word
Emotion / engagement from prosody βœ… ProsodyTracker (RMS + variance + pace + pauses)
Multi-agent (Teacher / Examiner / Companion) βœ… FSM-driven persona swap on same LLM
Offline / on-device mode ❌ Skipped + defended (Groq Whisper faster than local)
Streaming evaluation βœ… StreamingEvaluator pre-grades interim transcripts
Telephony (Twilio / LiveKit SIP) ❌ Skipped + defended (browser-first scope)

5 of 7 stretch goals implemented; remaining 2 explicitly skipped with documented rationale.

Scope & shortcuts (Β§ 10)

Allowance Status
Scope to one language βœ… Spanish
Curriculum hand-authored OR LLM-generated βœ… Hand-authored
Single hardcoded user OK βœ… DEFAULT_USER_ID=demo-user-001
No app store / production deploy βœ… Local prototype only
Don't wrap existing voice-agent product (Vapi / Retell) βœ… Custom pipeline; no wrapper

Honest disclosures

Requirement Status
AI assistant disclosed (FAQ) βœ… Claude Code, WRITEUP Β§ 13
Mid-build pivot (Deepgram β†’ AssemblyAI) βœ… WRITEUP D4 + Β§ 13
LLM tool-call β†’ IntentRouter pivot βœ… WRITEUP Β§ 13
Known limitations enumerated βœ… WRITEUP Β§ 14 (6 items)

πŸ— Architecture

flowchart TB
  subgraph CLIENT["Browser Client Β· Next.js 14"]
    direction TB
    UI["Voice Orb Β· Transcript Β· Progress Panel"]
    WSClient["WebSocket Client<br/>(Web Audio + AudioWorklet)"]
    UI --> WSClient
  end

  subgraph SERVER["FastAPI Server Β· Python 3.11+"]
    direction TB
    WSServer["WebSocket Endpoint /ws"]

    subgraph PIPE["Pipecat Pipeline"]
      direction LR
      VAD["Silero VAD"]
      STT["AssemblyAI<br/>Universal-Streaming"]
      Probe["Latency Probe"]
      LLM["Groq<br/>Llama 3.3 70B"]
      Tools["Intent Router<br/>+ 4 LLM tools"]
      TTS["Deepgram<br/>Aura-2"]

      VAD --> STT --> Probe --> LLM
      LLM --> Tools
      Tools --> LLM
      LLM --> TTS
    end

    WSServer --> VAD
    TTS --> WSServer
  end

  subgraph AGENT["Agent Core"]
    direction TB
    FSM["Mode FSM<br/>Teach Β· Quiz Β· Convo Β· Doubt"]
    Prompts["System Prompts<br/>(per mode + persona)"]
    Grader["Two-tier Semantic Grader"]
    Pron["Pronunciation Feedback"]
    Curriculum["Curriculum<br/>6 Spanish lessons"]
  end

  subgraph MEM["Memory"]
    direction TB
    Short["Short-term<br/>in-mem SessionMemory"]
    Long["Long-term<br/>SQLite + FSRS-lite"]
  end

  subgraph OBS["Observability"]
    direction TB
    Logger["Per-turn JSONL"]
    Metrics["Rolling P50/P95"]
  end

  WSClient <-->|"PCM16 audio Β· JSON events"| WSServer
  Tools <--> FSM
  Tools <--> Grader
  Tools <--> Pron
  Tools <--> Curriculum
  Tools <--> Short
  Tools <--> Long
  LLM --> Prompts
  Probe --> Logger
  Probe --> Metrics

  classDef ext   fill:#0ea5e9,stroke:#0369a1,color:#fff;
  classDef ours  fill:#a78bfa,stroke:#6d28d9,color:#fff;
  classDef store fill:#10b981,stroke:#047857,color:#fff;
  classDef obs   fill:#f59e0b,stroke:#b45309,color:#fff;

  class VAD,STT,LLM,TTS ext;
  class FSM,Prompts,Grader,Pron,Curriculum,Tools,Probe ours;
  class Short,Long store;
  class Logger,Metrics obs;
Loading

Sequence diagram, barge-in flow, and FSM state diagram β†’ docs/architecture.md. Raw .mmd source β†’ docs/architecture.mmd.

πŸ§ͺ Performance

End-to-end voice latency measured per turn and written to logs/turn_latency.jsonl:

Stage Budget Notes
Silero VAD end-of-speech ~200 ms stop_secs=0.4
AssemblyAI STT finalize ~150 ms Universal-Streaming WebSocket
Groq LLM TTFT (Llama 3.3 70B) ~250 ms ~300 tokens/s on Groq LPU
Deepgram Aura-2 first audio ~300 ms Streaming TTS
Network + buffer ~80 ms localhost
Total P50 ~1010 ms < 1500 ms target βœ…
Interrupt-to-silence <250 ms Target was 300 ms

Live metrics: GET /metrics.

🌐 Live Demo

Service URL
Frontend (Vercel) https://voicetutor-six.vercel.app
Backend API (Render) https://voicetutor-backend.onrender.com
Health check https://voicetutor-backend.onrender.com/health
Metrics https://voicetutor-backend.onrender.com/metrics

Note: Render free tier spins down after 15 min idle. First connect after inactivity may take ~15 s. Set up an UptimeRobot HTTP monitor on /health (5-min interval, free) to keep it warm.

πŸš€ Quickstart

Prerequisites

1. Backend

cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env       # then fill in your keys
python -m uvicorn backend.server:app --reload --port 8000
# Server up at http://localhost:8000

2. Frontend

cd frontend
npm install
cp .env.example .env.local
npm run dev
# Open http://localhost:3000

3. Talk

Click the voice orb, grant mic permission, and say "Teach me how to greet people in Spanish."

πŸ“š Curriculum

Six hand-authored lessons spanning A1 β†’ A2 difficulty. Hand-authored over LLM-generated so reviewers can verify content accuracy (Spanish grammar errors from auto-generation would be hard to catch).

ID Lesson Level Vocab
greetings-001 Greetings & Introductions A1 10
numbers-001 Numbers 1 to 20 A1 20
ordering-food-001 Ordering Food at a Restaurant A2 12
family-001 Family Members A1 11
days-time-001 Days of the Week & Time A2 12
directions-001 Asking for Directions A2 12

Each lesson is structured as objective β†’ explain β†’ example β†’ practice β†’ check, enforced by the FSM β€” not by hope. Lessons live in backend/curriculum/lessons.json β€” adding a new one is a data-only change (no code rebuild).

🧬 Bonus features (stretch goals)

Picked for depth over breadth:

Bonus feature What it does
FSRS-lite spaced repetition Lapsed words re-surface immediately; mastered words sleep for weeks
Phoneme-aware pronunciation feedback Per-word severity + word-specific hints (rr trill, Γ±, ll, silent h, gue/gΓΌe, j, soft c, qu, v/b, word-final d)
Multi-persona handoff Teacher / Examiner / Companion swap via prompt β€” one LLM, distinct styles
Frustration detection Repeated-mistake counter triggers gentler tone + slower pace
Streaming evaluation Interim-transcript grading β€” pre-verdict before final transcript lands
Session recovery /session_recovery endpoint surfaces last lesson + due words for resume
Adaptive difficulty Per-session confidence_score (0–1) updated by quiz & mistake outcomes
Prosody engagement detection RMS variance + pace WPM + pause analysis β†’ engaged / neutral / low chip in UI; nudges LLM tone

We deliberately skipped (out of scope or low ROI):

  • Phoneme-level forced alignment (Azure Pronunciation Assessment, MFA) β€” heavy install.
  • Telephony via Twilio / LiveKit SIP β€” not in scope for a browser-first demo.
  • On-device Whisper β€” Groq's hosted Whisper Turbo is faster than any local CPU build.

πŸ—‚ Project layout

voicetutor/
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ server.py                 # FastAPI app + WebSocket route
β”‚   β”œβ”€β”€ bot.py                    # Pipecat pipeline assembly + latency probes
β”‚   β”œβ”€β”€ agent/
β”‚   β”‚   β”œβ”€β”€ orchestrator.py       # Mode FSM
β”‚   β”‚   β”œβ”€β”€ prompts.py            # System prompts per mode + persona
β”‚   β”‚   β”œβ”€β”€ tools.py              # Function-calling tool specs + dispatcher
β”‚   β”‚   β”œβ”€β”€ grader.py             # Two-tier semantic grading
β”‚   β”‚   └── pronunciation.py      # Phoneme-aware pronunciation feedback
β”‚   β”œβ”€β”€ curriculum/
β”‚   β”‚   β”œβ”€β”€ lessons.json          # Hand-authored Spanish curriculum
β”‚   β”‚   └── loader.py             # Typed access layer
β”‚   β”œβ”€β”€ memory/
β”‚   β”‚   β”œβ”€β”€ schema.sql            # SQLite schema
β”‚   β”‚   β”œβ”€β”€ persistent.py         # Long-term store (incl. FSRS-lite)
β”‚   β”‚   └── session.py            # In-memory short-term state
β”‚   β”œβ”€β”€ transports/
β”‚   β”‚   β”œβ”€β”€ websocket.py          # FastAPI ws transport
β”‚   β”‚   └── serializer.py         # Custom JSON+binary wire format
β”‚   β”œβ”€β”€ observability/
β”‚   β”‚   β”œβ”€β”€ logger.py             # Per-turn JSONL
β”‚   β”‚   └── metrics.py            # Rolling P50/P95
β”‚   └── tests/                    # 56 tests covering grader, FSM, memory, tools, e2e
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ app/                      # Next.js 14 App Router
β”‚   β”œβ”€β”€ components/               # VoiceOrb, Transcript, ModeBadge, ProgressPanel…
β”‚   └── lib/                      # voiceClient.ts (WebSocket + Web Audio)
β”œβ”€β”€ docs/architecture.mmd
└── docs/architecture.md

πŸ§ͺ Testing

cd backend
source .venv/bin/activate
pip install pytest pytest-asyncio httpx
pytest   # 151 passing

What's covered (~1500 LOC of test code, 151 tests):

  • Unit:
    • Grader: normalisation (NFD, accents, inverted Spanish marks, whitespace), exact / variant / fuzzy / LLM-fallback paths, edge cases (empty input, punctuation-only diff)
    • FSM: mode transitions, doubt-stack nesting, lesson-step cap at done, persona changes on mode shift
    • Pronunciation: per-word + pattern hints, severity grading, summary promotion
    • Prosody: empty / low / high engagement scoring, pace computation, pause tracking, label thresholds
    • Curriculum loader: keyword + natural-phrase topic mapping for all 6 lessons
    • Metrics tracker: percentile invariants (P50 ≀ P95 for n=2 and large n), sliding window, None-skip
    • System-prompt builder: each mode overlay present, frustration addendum, state injection, greeting shape
    • FSRS-lite invariants: ease floor 1.3 / ceiling 2.5 held over 50+ iterations, reps-on-success-only, lapses-on-failure-only, mastered word not due soon
  • Integration:
    • Tool dispatcher end-to-end on a temp SQLite DB (all defined tools)
    • FastAPI endpoints: /, /curriculum, /progress, /metrics, /reset_progress, /session_recovery, /health β€” shape, multi-user isolation, synthetic metric data
    • Bot module import / construction with default + explicit user, prosody initialization, tools schema lock at 4 tools
  • E2E scripted flows:
    • teach-greetings-and-save
    • quiz-scoring with FSRS lapse
    • doubt-resume preserving quiz position
    • code-switched vocab lookup (EN ↔ ES)
    • FSRS resurfacing of lapsed words
    • full user journey: teach β†’ quiz β†’ doubt β†’ resume β†’ save
    • multi-lesson session (6 topics in a row, memory persistence verified)
    • repeated-wrong-answer stress (5 garbage replies in a row, bot stays functional)

The scripted-flows harness in backend/tests/e2e/ is the regression net for prompt/tool/FSM contract drift.

πŸ“ˆ Observability

Every turn writes one JSONL row to logs/turn_latency.jsonl:

{
  "turn_id": "9f3a",
  "stt_text": "teach me how to greet people",
  "llm_text": "Β‘Perfecto! Empezamos con los saludos…",
  "mode": "teaching",
  "tools_called": ["start_lesson"],
  "stt_final_ms": 138.4, "llm_first_token_ms": 230.1,
  "tts_first_audio_ms": 392.7, "total_ms": 970.5,
  "language_detected": "en"
}

Plus a rolling window of P50/P95/P99 on GET /metrics, surfaced in the UI side-panel.

πŸ›  Configuration

All environment variables are in .env (see backend/.env.example). Highlights:

Var Purpose
TTS_PROVIDER Sets active TTS (elevenlabs or deepgram)
DEEPGRAM_TTS_VOICE aura-2-carina-es (native EN/ES code-switching capability)
GROQ_MODEL LLM model id (default llama-3.3-70b-versatile)
DEEPGRAM_STT_MODEL STT model (default nova-2-general)
SQLITE_PATH Database file path

πŸ’‘ TTS Toggling: Set TTS_PROVIDER=elevenlabs in Render env vars to switch from Deepgram Aura-2 to ElevenLabs (requires ELEVENLABS_API_KEY). Default is deepgram.

πŸ“œ License

MIT β€” feel free to fork.

πŸ™ Acknowledgements

Pipecat for the pipeline framework, Groq / AssemblyAI / ElevenLabs for the free tiers, and the Anthropic Claude Code team for the dev experience.

Built for the AI Engineer Take-Home β€” architecture diagrams in docs/architecture.md.

About

VoiceTutor is a hands-free AI language tutor. It features real-time voice conversations, interactive lessons, semantic grading, and spaced repetition to help you master Spanish naturally.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages