A real-time, non-interruptive mock-interview coaching web app.
Silent Interview Coach runs a candidate through a mock interview and measures both how they deliver answers (camera alignment, speaking pace, pauses) and what they say (LLM content scoring). Feedback is delivered without interrupting β subtle visual nudges during the answer, and a scored report at the end.
For the full deep-dive (WebSocket contract, concurrency model, per-service internals, data flow), see ARCHITECTURE.md. This README is the high-level map.
Two deployables connected by a single WebSocket (/ws/coach). All ML runs on
the Python backend; the browser only captures media and renders feedback.
βββββββββββββββββ Browser (Frontend) ββββββββββββββββββ
β Webcam + Mic β getUserMedia β
β videoPump.ts β OffscreenCanvas β JPEG @10fps β
β audioSource.ts β Int16 PCM 16 kHz (pcm-worklet) β
β β
β interview state machine Β· metricsStore (Zustand) β
β nudgeEngine.ts (live nudges) Β· ttsPlayer.ts (voice) β
β report.ts (final score, computed client-side) β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ
β ONE WebSocket /ws/coach
binary = media β text = JSON control + metrics
βΌ
βββββββββββββββββ Python Backend (FastAPI) ββββββββββββ
β CoachSession β one per connection β
β asyncio I/O + 5 single-thread executors: β
β vision β FaceService (MediaPipe Face Landmarker)
β audio β VadService (Silero VAD v5) β
β stt β SttService (faster-whisper tiny.en) β
β tts β TtsService (Piper, local ONNX) β
β llm β content_evaluator (Google Gemini) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Part | Stack | Responsibility |
|---|---|---|
| Frontend | Vite + React 19 + TypeScript, Tailwind v4, shadcn/ui, Framer Motion, Zustand | Capture camera + mic, run the interview state machine, render live nudges + final report, play the interviewer voice |
| Backend | Python 3.12, FastAPI, asyncio | All ML: face landmarks, voice-activity detection, speech-to-text, text-to-speech, LLM content scoring |
A single connection (/ws/coach) carries two frame kinds:
- Binary β
[1-byte channel tag][payload]:0x01video JPEG (clientβserver),0x02audio PCM (clientβserver),0x03TTS WAV (serverβclient). - Text (JSON) β control + metrics. Client sends
init,calibrate,speak,answerStart,answerEnd,stop; server sendsready,faceStatus,calibration,metrics,speech,answerFeedback, etc.
There is no WebRTC and no API gateway β just the one WebSocket.
| Service | Model | Emits |
|---|---|---|
| FaceService | MediaPipe Face Landmarker (478-pt) | cameraAlignment, runs detecting β calibrating β live |
| VadService | Silero VAD v5 | speaking flag; gates STT |
| SttService | faster-whisper tiny.en |
paceWpm (3s tick); assembles per-answer transcript |
| TtsService | Piper en_US-lessac-medium (local ONNX/CPU) |
interviewer speech WAV |
| content_evaluator | Google Gemini (gemini-flash-latest) |
answerFeedback β 5 sub-scores, once per answer |
- Per-answer content scoring runs on the backend (Gemini) on the assembled transcript only β never on video. Returns communication / clarity / confidence / technical + a justification.
- The composite final report is computed on the frontend (
report.ts), blending delivery metrics (alignment, pace, pause) with content scores. - Nudge timing is decided on the frontend (
nudgeEngine.ts), not by the LLM β the app, not the model, decides when to interrupt.
Backend (Python 3.11 or 3.12 β mediapipe/torch lack reliable 3.13 wheels):
cd Backend
py -3.12 -m venv .venv
.venv\Scripts\activate # Windows (POSIX: source .venv/bin/activate)
pip install -r requirements.txt
# Fetch the interviewer voice (~63 MB):
python -m piper.download_voices en_US-lessac-medium --data-dir models/piper
uvicorn app.main:app --port 8000Frontend:
cd Frontend
npm install
npm run dev # Vite dev server; proxies /ws β ws://localhost:8000Backend .env: GEMINI_API_KEY (content scoring). Optional: CONTENT_MODEL,
PIPER_VOICE, FACE_LANDMARKER_PATH, ALLOWED_ORIGINS.
Models on disk: models/face_landmarker.task, models/piper/en_US-lessac-medium.onnx.
This is a working local build, not yet production-hardened:
- No persistence β there is no database, ORM, or object storage. The report lives in the browser and is lost when the tab closes.
/ws/coachis unauthenticated β anyone reaching the port gets a full ML session. Must be gated before hosting.- Local-only deploy β
uvicorn+ Vite dev proxy; no CDN/edge, no metrics or error tracking wired up. - Privacy note β because ML moved to the backend, raw camera + mic leave the device. The earlier "A/V never leaves the browser" guarantee no longer holds.
- Backend:
cd Backend && .venv/Scripts/python -m pytest tests/ -q(13 tests). - Frontend:
cd Frontend && npx tsc -b && npm run build.
InterviewCoach/
βββ ARCHITECTURE.md # full system deep-dive
βββ Frontend/ # Vite + React 19 + TypeScript SPA
β βββ src/
β βββ coach/ # capture, WebSocket, TTS playback, orchestration
β βββ interview/ # state machine, room UI, questions, report
β βββ dashboard/ # results dashboard
β βββ state/ # Zustand metrics store
βββ Backend/ # Python 3.12 + FastAPI ML backend
βββ app/
βββ vision/ # FaceService, face geometry
βββ audio/ # VadService, STT, turn detection
βββ voice/ # Piper TTS
βββ llm/ # Gemini content evaluator