DocSpeak
Ask questions of any PDF or Word file and hear the grounded answer.
- Google Gemini 2.0 Flash
- ChromaDB
- Sentence-Transformers (all-MiniLM-L6-v2)
- LangChain
- Status
- Public, not archived, no license. Nine commits from 2025-05-15 to 2025-07-09; the initial build landed in a single session on 2025-05-15 and the only later change added a GitHub Actions workflow. Effectively a finished prototype rather than actively developed software.
By the numbers · 6
1,000 / 200 characters
Chunk size and overlap
top-3
Chunks retrieved per query
temp 0.3 / 500-token cap
Answer generation settings
23,870 bytes across 3 modules
Python source size
10
Declared runtime dependencies
9 commits, 1 contributor
Repository history
Summary
DocSpeak is a document question-answering application that ingests PDF and Word files, indexes them locally, and answers questions grounded in the passages it retrieves. Embedding and vector storage run on the host, sentence-transformers for vectors, a persistent ChromaDB collection for the index, so each question sends only the three retrieved excerpts to Google Gemini rather than the whole corpus. Every answer is returned as text and as synthesized speech.
The problem
Long reports, manuals and contracts hold answers that are expensive to find. Someone with a specific question usually has to skim dozens of pages, and the answer they end up quoting is only as trustworthy as their memory of where it came from. DocSpeak narrows that gap: a reader hands over a document, asks a question in plain language, and gets back an answer drawn only from that document, plus a spoken version for anyone who would rather listen than read.
Approach
Ingestion normalizes both supported formats onto one path: an uploaded DOCX is converted to PDF with docx2pdf, then every document is loaded through LangChain's PyPDFLoader, so downstream code only ever handles a single representation.
RecursiveCharacterTextSplitter splits loaded pages into 1,000-character chunks with 200-character overlap, and each chunk carries its source filename and page number as ChromaDB metadata.
Chunks are embedded locally with sentence-transformers all-MiniLM-L6-v2 and written to a persistent ChromaDB collection named 'pdf_documents' on disk, so the index survives process restarts.
process_documents() short-circuits when the collection already holds chunks, making startup idempotent instead of re-embedding the corpus on every launch.
Query time retrieves the top three chunks by similarity and formats them into a numbered context block that names the source file and page for each excerpt, so the model sees provenance alongside content.
The prompt instructs the model to answer only from the supplied context and to say so when the context is insufficient, an explicit abstention instruction rather than open-ended generation.
Generation runs through a hand-written GoogleGeminiWrapper against gemini-2.0-flash at temperature 0.3 with a 500-token cap; the same wrapper also exposes a stateful multi-turn chat session and a model-listing helper.
Each answer is piped through gTTS into an MP3 the Gradio interface plays back, and conversation history can be cleared or exported to a text file from the same UI.
Architecture
Gradio uploaddocx2pdf normalization (DOCX only)PyPDFLoaderRecursiveCharacterTextSplitter (1000 chars / 200 overlap)all-MiniLM-L6-v2 embeddingspersistent ChromaDB collection 'pdf_documents'top-3 similarity querygrounded prompt assembly with source and page attributionGemini 2.0 Flash (temp 0.3, 500-token cap)answer textgTTS MP3 playback + exportable chat history
| Component | Role |
|---|---|
| app.py, Gradio interface | Upload widget for PDF/DOCX, question box, answer textbox, audio player, and buttons to clear or export chat history; wires each control to a handler and converts DOCX uploads before indexing. |
| rag.py, RAGSystem class | Owns the whole pipeline: document loading and chunking, ChromaDB collection lifecycle, top-k retrieval, context formatting with source/page provenance, prompt construction, and response generation. |
| gemini_wrapper.py, GoogleGeminiWrapper | Thin Google Generative AI client exposing single-turn ask(), a stateful multi-turn chat() backed by a Gemini chat session, reset_conversation(), and list_available_models(); catches exceptions and returns them as strings. |
| ChromaDB persistent collection 'pdf_documents' | On-disk vector store holding chunk text, embeddings, and {source, page} metadata; queried by similarity at request time. |
| SentenceTransformerEmbeddingFunction (all-MiniLM-L6-v2) | Local embedding model used both to index chunks and to embed incoming queries, keeping the two in the same vector space. |
| gTTS text-to-speech | Converts each generated answer to response_audio.mp3 for playback in the Gradio audio component. |
| .github/workflows/python-app.yml | GitHub Actions CI on push and PR to main: Python 3.10 setup, dependency install, flake8 (hard-failing on E9/F63/F7/F82, advisory otherwise), and a pytest step. |
Trade-offs
Chose
Local sentence-transformers embeddings (all-MiniLM-L6-v2)
Over
A hosted embedding API
Embeddings are computed at both ingest and query time; running them in-process keeps per-query cost limited to generation alone and avoids a second vendor dependency. rag.py instantiates SentenceTransformer directly and passes SentenceTransformerEmbeddingFunction to ChromaDB.
Chose
ChromaDB PersistentClient writing to a local directory
Over
A hosted or server-mode vector database
The application is a single-process Gradio deployment serving one index; an on-disk persistent client gives restart durability without operating a separate service. rag.py: chromadb.PersistentClient(path=db_directory).
Chose
Converting DOCX to PDF before ingestion
Over
Adding a second document-loader branch for Word files
One PyPDFLoader path then serves both formats, keeping chunking, metadata and page attribution identical regardless of what the user uploaded. app.py upload_and_process() runs docx2pdf.convert when the extension is .docx.
Chose
A hand-written Gemini client wrapper with explicit prompt assembly
Over
LangChain's retrieval-chain abstractions
Retrieval, context formatting and the abstention instruction are written out in rag.py.generate_response where they can be read and changed directly; LangChain is used only for the PDF loader and the text splitter.
Chose
Skipping ingestion entirely when the collection is non-empty
Over
Re-embedding on every startup
It makes restarts fast and cheap, at the cost of a stale index when documents are added later, process_documents() returns early if self.collection.count() > 0.
At scale
Three Python modules totaling 23,870 bytes: rag.py (12,079 B), gemini_wrapper.py (6,306 B), app.py (5,485 B).
Six files in the repository on a single branch (main); GitHub reports repository size 24 KB.
Ten runtime dependencies declared in requirements.txt.
Nine commits between 2025-05-15 and 2025-07-09, all attributed to a single contributor.
Retrieval is backed by one ChromaDB collection ('pdf_documents'), no namespacing, sharding, or multi-tenant separation.
No test files exist in the repository, although the GitHub Actions workflow includes a pytest step.
No sample document corpus is committed; the code expects a local 'material' directory that is not in the repo.
My role
Sole author. The GitHub contributors API attributes all 9 commits on the only branch to munib123, covering the retrieval pipeline (rag.py), the Gemini client wrapper (gemini_wrapper.py), the Gradio interface and custom CSS (app.py), and the CI workflow.
