Pipecat Β Β·Β Groq Llama 3.3 70B Β Β·Β AssemblyAI Universal-Streaming Β Β·Β Deepgram Aura-2 Β Β·Β Silero VAD
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.
- 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=eswith EN tolerance) and natively multilingual TTS - Observability built in β per-turn JSONL with STT/LLM/TTS timestamps;
/metricsendpoint - Beautiful, minimal UI β animated voice orb, live transcript, progress dashboard
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."
Two columns: what the spec asks for β what I shipped. Every line of the assignment brief, with a tick.
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 | 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.
| 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 |
| 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) |
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;
Sequence diagram, barge-in flow, and FSM state diagram β
docs/architecture.md. Raw.mmdsource βdocs/architecture.mmd.
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.
| 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.
- Python 3.11+, Node 18+
- API keys (all have free tiers, no card required):
- Groq β https://console.groq.com/keys
- AssemblyAI β https://www.assemblyai.com/dashboard/signup ($50 free credit)
- Deepgram β https://console.deepgram.com/ ($200 free credit)
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:8000cd frontend
npm install
cp .env.example .env.local
npm run dev
# Open http://localhost:3000Click the voice orb, grant mic permission, and say "Teach me how to greet people in Spanish."
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).
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.
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
cd backend
source .venv/bin/activate
pip install pytest pytest-asyncio httpx
pytest # 151 passingWhat'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 β€ P95for 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.
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.
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=elevenlabsin Render env vars to switch from Deepgram Aura-2 to ElevenLabs (requiresELEVENLABS_API_KEY). Default isdeepgram.
MIT β feel free to fork.
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.