A Real-Time Voice RAG Agent that listens to a spoken question, retrieves relevant context from an uploaded PDF, generates a grounded answer with Claude, and responds in voice.
Pipeline: STT (Gemini 2.5 Flash + Groq fallback) β local embeddings + FAISS RAG β Claude LLM β Edge-TTS
| Layer | Technology |
|---|---|
| Backend API | FastAPI + Uvicorn |
| Speech-to-Text | Gemini 2.5 Flash + Groq Whisper fallback |
| LLM | Anthropic Claude claude-opus-4-8 |
| Embeddings | Sentence-Transformers all-MiniLM-L6-v2 (dim 384) |
| Vector Store | FAISS IndexFlatIP with L2 normalisation |
| Text-to-Speech | Edge-TTS en-IN-NeerjaNeural |
| Frontend | Vanilla HTML / CSS / JS (dark theme) |
| Status | Feature |
|---|---|
| Gemini 2.5 Flash STT with Groq fallback | |
| Claude reasoning via Anthropic API | |
| Local Sentence-Transformers embeddings | |
| FAISS cosine-similarity vector index | |
| Edge-TTS voice output | |
Dynamic PDF upload via /ingest |
|
| Modular architecture | |
| Low-latency pipeline |
The voice pipeline currently uses Gemini 2.5 Flash for transcription. If Gemini STT fails or returns no text, the code falls back to Groq Whisper (whisper-large-v3) when GROQ_API_KEY is available.
This means the README should not describe Whisper-only local STT as the primary path anymore. The current design is:
- Gemini 2.5 Flash for transcription
- Groq Whisper as a fallback
- Claude for query rewriting, reranking, and answer generation
flowchart TD
A([π€ Browser Microphone]) --> B[MediaRecorder\nCaptures audio as .webm]
B --> C[POST /voice\nmultipart audio upload]
subgraph STT ["π£οΈ Speech-to-Text | whisper_provider.py"]
C --> D[Write to temp .webm file]
D --> E[Gemini 2.5 Flash STT\nGroq Whisper fallback when needed]
E --> F[Raw Transcript Text]
end
subgraph QR [" Query Rewriting | improved_query/query_rewrite.py"]
F --> G[Claude via Anthropic SDK\nExpand vague voice queries\ninto precise document search queries]
G --> H[Cleaned Search Query]
end
subgraph RAG ["π Retrieval | retrieval/retrieve.py"]
H --> I[Sentence-Transformers\nall-MiniLM-L6-v2\ndim = 384]
I --> J[L2-Normalise query vector]
J --> K[FAISS IndexFlatIP\nCosine similarity search\nTop-12 chunks]
K --> L[12 Candidate Chunks\n+ similarity scores]
end
subgraph RR [" Reranking | query_ranker/rerank.py"]
L --> M[Claude via Anthropic SDK\nRe-order 12 chunks\nby true relevance to query]
M --> N[Top Reranked Chunks]
end
subgraph LLM [" Answer Generation | llm/generate.py"]
N --> O[Build RAG Prompt\nsystem persona + context + question]
O --> P[Anthropic Claude\nGrounded answer generation]
P --> Q[Answer Text\nconcise Β· conversational]
end
subgraph TTS [" Text-to-Speech | tts/edge_tts_provider.py"]
Q --> R[Edge-TTS\nen-IN-NeerjaNeural\nMicrosoft Neural Voice]
R --> S[MP3 Audio Bytes]
end
subgraph RESP [" HTTP Response"]
S --> T[Response body: audio/mpeg\nX-Transcription header\nX-Response-Text header]
end
T --> U([π Browser\nPlays MP3 Β· Shows chat bubbles])
subgraph INGEST [" PDF Ingestion | ingestion/ingest.py β runs once per document"]
V([π PDF Upload / File]) --> W[pypdf β extract text\nnormalise whitespace]
W --> X[LangChain RecursiveCharacterTextSplitter\nchunk_size=350 Β· overlap=60]
X --> Y[Sentence-Transformers\nembed each chunk locally]
Y --> Z[L2-Normalise Β· build FAISS IndexFlatIP]
Z --> AA[(faiss.index + chunks.pkl\npersisted to disk)]
end
AA -.->|loaded on first query| K
GPU/
βββ backend/
β βββ app/
β β βββ main.py # FastAPI app, CORS, router registration
β β βββ config.py # API keys, model names, index paths
β β βββ ingestion/
β β β βββ ingest.py # PDF β chunks β FAISS index pipeline
β β βββ improved_query/
β β β βββ query_rewrite.py # Claude-powered query rewriter
β β βββ query_ranker/
β β β βββ rerank.py # Claude-powered semantic reranker
β β βββ retrieval/
β β β βββ retrieve.py # Query expansion + cosine similarity search
β β βββ llm/
β β β βββ generate.py # Grounded Claude prompt + generation
β β βββ stt/
β β β βββ whisper_provider.py # Gemini STT with Groq fallback
β β βββ tts/
β β β βββ edge_tts_provider.py # Edge-TTS synthesis
β β βββ routes/
β β βββ voice.py # POST /voice, POST /voice/text
β β βββ ingest.py # POST /ingest, GET /api/document
β βββ data/ # gitignored β place PDFs here
β βββ .env # gitignored β create locally
βββ frontend/
β βββ index.html
β βββ style.css
β βββ script.js
βββ venv/ # gitignored β create locally
βββ README.md
git clone <repository-url>
cd GPUpython -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activateNote: The
venv/folder is gitignored and must be created locally β never commit it.
cd backend
pip install -r requirements.txtCreate a .env file in the backend/ directory:
ANTHROPIC_API_KEY=your_anthropic_api_key_here
GEMINI_API_KEY=your_gemini_api_key_here
# Optional: used only for Groq Whisper fallback in STT
GROQ_API_KEY=your_groq_api_key_hereWarning:
.envis gitignored and must never be committed. Keep your API keys private.
Place your PDF in backend/data/ and run the ingestion script once:
cd backend
python -m app.ingestion.ingestThis builds faiss.index and chunks.pkl inside backend/app/.
Alternatively, upload a PDF directly through the web UI after starting the server.
cd backend
uvicorn app.main:app --reloadOpen http://localhost:8000 in your browser.
Upload audio and receive a spoken answer (full end-to-end pipeline).
Form field: audio β audio recording (.webm / .wav)
Response headers:
| Header | Description |
|---|---|
X-Transcription |
Transcript of the query |
X-Response-Text |
LLM answer text |
Response body: audio/mpeg β synthesised speech
curl -X POST http://localhost:8000/voice \
-F "audio=@query.webm" \
--output response.mp3Debug endpoint β same as /voice but returns JSON instead of audio.
Form field: audio β audio recording (.webm / .wav)
curl -X POST http://localhost:8000/voice/text \
-F "audio=@query.webm"Response: { "question": "...", "answer": "..." }
Upload a PDF to rebuild the FAISS index at runtime (no server restart needed).
curl -X POST http://localhost:8000/ingest \
-F "file=@resume.pdf"Response:
{
"status": "success",
"filename": "resume.pdf",
"pages": 2,
"chunks": 47,
"dim": 384
}Check whether a FAISS index is currently loaded on disk.
curl http://localhost:8000/api/documentServed automatically by FastAPI from the frontend/ directory at http://localhost:8000.
| Panel | Description |
|---|---|
| Upload zone | Drag-and-drop or click to upload a PDF; shows real chunk count after ingestion |
| Chat area | Conversation bubbles β your question on the right, agent answer on the left |
| Mic footer | Press the microphone button to record; release to send |
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
Yes | Anthropic API key used for Claude |
GEMINI_API_KEY |
Yes | Google AI Studio API key used for STT |
GROQ_API_KEY |
Optional | Used only if Gemini STT falls back to Groq |
Create backend/.env:
ANTHROPIC_API_KEY=your_anthropic_api_key_here
GEMINI_API_KEY=your_gemini_api_key_here
.envis gitignored and must never be committed.
- The FAISS index is stored at
backend/app/faiss.indexandbackend/app/chunks.pkl. - Uploading a new PDF via
/ingestor the UI replaces the existing index immediately. - Claude is used for query rewriting, reranking, and answer generation.
- Embeddings are generated locally with Sentence-Transformers, so no embedding API key is required.
- Gemini is used for STT, with Groq Whisper as a fallback when needed.
- Edge-TTS requires an outbound internet connection for synthesis.