vinoth3395/Veyra
Veyra — AI Interview Coach
A web app that runs a realistic, voice-based Product Manager mock interview anchored on the candidate's own CV, then returns a scored feedback report.
Flow: upload CV → review parsed details → device check → voice interview (the interviewer speaks, you answer out loud) → scored report (overall score, 5-axis radar, per-question feedback).
- Frontend: React + TypeScript + Vite (the "Veyra" UI)
- Backend: FastAPI, wrapping a reusable service + agent layer
- LLM: provider-agnostic via LangChain — Anthropic / OpenAI / Google Gemini
- Voice: Sarvam AI (text-to-speech + speech-to-text), routed through the backend
- Observability: PostHog LLM analytics on every model call
- Config: in-app Settings screen +
.env; all keys stay server-side
There is no database — CVs are processed in memory and not persisted; interview sessions live in memory and are lost on restart (a deliberate privacy choice). See the architecture page for the full picture.
What it does
- CV extraction — PyMuPDF (PDF) / python-docx (DOCX, incl. tables) → a LangChain structured-output chain → validated
ResumeExtraction, with dates and skills normalized. - Interview — a stateful, spoken interview driven by the
pm-past-work-interviewskill: anchors on one project and escalates from "what did you build" to "what actually happened", probing for specificity / ownership / outcome / reasoning, one question per turn. Tone and length are configurable. - Report — a structured, scored
InterviewReport: overall 0–100, band, a 5-axis breakdown (Clarity, Technical depth, Structure, Communication, Ownership), and per-question score + feedback. - Voice — questions are spoken via Sarvam TTS; answers are recorded in the browser in ~10s segments, converted to 16 kHz WAV, and transcribed via Sarvam STT. A type-to-answer fallback is always available.
- Settings (⚙ in the top bar) — change provider/model/keys, voice options, limits, and the agent prompts at runtime; changes apply without a restart.
It does not rank candidates, make hiring decisions, or infer sensitive attributes.
There is also a legacy Streamlit app (app.py) exposing the same three capabilities (CV / JD / interview) as a developer tool. It shares the services and agents layers unchanged.Project structure
backend/ FastAPI web API
main.py app + CORS + lifespan (PostHog flush)
deps.py settings + cached services
session_store.py in-memory interview sessions
sarvam.py Sarvam TTS/STT client
schemas.py request/response models
routers/ extract · interview · voice · admin (+ /config, /health)
frontend/ React + TS + Vite (the Veyra UI)
src/screens/ Landing · Config · Setup · Interview · Report · Settings
src/voice.ts Sarvam TTS playback + segmented recording → STT
src/api/client.ts typed API calls
config/
settings.py env-based config + provider aliasing
store.py runtime settings store (JSON over .env)
models/
resume.py CV extraction contract
job_description.py JD contract
interview.py scored InterviewReport contract
agents/
llm.py provider-agnostic chat-model factory (+ PostHog handler)
cv_extraction_agent.py CV structured-output chain
jd_parser_agent.py JD structured-output chain
interview_agent.py interview system prompt + report model
parsers/ PDF / DOCX text extraction + factory
prompts/ cv_extraction · jd_extraction · pm_interview_skill · report_instruction
services/ CV / JD / interview orchestration, validation, normalization
utils/ PII-safe logging, file validation, text cleanup, observability
tests/ pytest suite (runs offline; LLM & voice mocked)Setup
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .envFill in .env (keys are read server-side — the browser never sees them):
# LLM — pick one provider and set its key
LLM_PROVIDER=google # anthropic | openai | google
LLM_MODEL=gemini-flash-lite-latest # optional; blank = provider default
GOOGLE_API_KEY=... # or ANTHROPIC_API_KEY / OPENAI_API_KEY
# Voice (optional — enables the spoken interview)
SARVAM_API_KEY=...
SARVAM_LANGUAGE=en-IN
# LLM observability (optional)
POSTHOG_API_KEY=phc_...
POSTHOG_HOST=https://us.i.posthog.com
POSTHOG_PRIVACY_MODE=false # true redacts prompt/response contentProvider aliases: anthropic/claude, openai/gpt, google/gemini. Anything set here can also be changed later from the in-app Settings screen, which writes config/runtime_settings.json (layered on top of .env).
Run the web app
Terminal 1 — backend (from the repo root):
source .venv/bin/activate
uvicorn backend.main:app --reload --port 8000Terminal 2 — frontend:
cd frontend
npm install # first time only
npm run devOpen http://localhost:5173 in Chrome (voice needs microphone access; the device-check screen will prompt for it). Vite proxies /api to the backend on :8000, so there's no CORS setup in development.
Legacy Streamlit dev tool: streamlit run app.py.Settings screen
Click ⚙ in the top bar (available outside an active interview):
- Models & keys — provider, model, temperature, retries, and the LLM key
- Voice — Sarvam key, language, TTS speaker
- Limits — max file size, default questions, default tone
- Prompts — edit the CV / JD / interviewer / report prompts, with reset-to-default
Keys are masked and write-only: the UI shows only the last 4 characters and never receives the full key; enter a new value only to replace it. Saves apply immediately (the cached LLM service is rebuilt).
The Settings screen has no login — anyone who can reach the URL can change keys and prompts. It's intended for local/personal use. Add an auth gate before exposing the app beyond your machine.
Observability (PostHog)
When POSTHOG_API_KEY is set, every LLM call is captured via PostHog's LangChain callback handler (model, tokens, latency, cost, and — unless POSTHOG_PRIVACY_MODE is true — the prompt/response). Calls are tagged with a component property (cv_extraction, jd_extraction, interview, interview_report), and every interview shares one trace id. View them under LLM analytics in PostHog.
Test
pytestThe suite runs offline (LLM, voice, and PostHog are mocked or inert without keys).
Privacy notes
- CVs/JDs are processed in memory and not persisted; interview sessions are in-memory only.
- App logs contain operational metadata only (request id, timings, statuses) — never CV text or candidate PII.
- However, if
POSTHOG_PRIVACY_MODE=false, prompt/response content (including CV text and interview answers) is sent to PostHog. Set it totruefor metadata-only capture.
Known limitations / next steps
- In-memory sessions — lost on backend restart. Swap for Redis to persist.
- Voice lag — the transcript updates every ~10s (segmented REST STT). Sarvam's streaming API would give word-by-word real-time.
- No auth — add an admin token before hosting beyond localhost.
- Cost/quota — a full voice interview makes many LLM + Sarvam calls; use billed keys.
