Screen.Recording.2026-04-06.at.7.10.40_PM-compressed.mp4
StudySpace is an AI-powered study assistant integrated into a real-time chat application. Students upload their course materials — lecture slides, PDFs, recordings, or images — and the AI indexes everything into a searchable knowledge base. From there, they can generate practice questions, ask free-form questions about the content, and sit adaptive exams that respond to their performance. Everything runs locally with no API keys or internet connection required.
Multimodal document understanding. The platform does not treat documents as plain text. When a PDF or slide deck is uploaded, any embedded images are passed to a vision model (llava) which generates a natural-language description of each one — diagrams, charts, and figures become part of the searchable index. Audio and video files such as lecture recordings are transcribed locally using OpenAI Whisper, with timestamps preserved. Text from PDFs, Word documents, PowerPoints, and plain text files is extracted and chunked in parallel. All modalities — text, vision, and speech — are embedded into the same vector database, so a single query can retrieve relevant content regardless of what form it originally appeared in.
Adaptive exam pipeline. The exam feature goes beyond generating a fixed question set. Each question is labelled with a topic by the LLM. The pipeline tracks performance topic by topic — not just within the current session but across all past exams in the same study room — and steers each new question toward the student's persistent weak areas. Difficulty adjusts in real time: harder after a correct answer, easier after an incorrect one. Hints are available on request and, at the end of the session, the system produces a personalised summary identifying strong topics, weak topics, and specific pages from the uploaded material recommended for review.
Automatic conversation compression. LLMs operate within a fixed context window, meaning a long Q&A session would eventually cause earlier parts of the conversation to be silently dropped. StudySpace prevents this by monitoring token usage after each exchange. Once the conversation history exceeds 50% of the context window, the system automatically prompts the LLM to summarise the older turns into a compact block, while keeping the most recent exchanges verbatim. The result is a session that can run indefinitely without losing the thread of earlier questions and answers.
- Node.js v18+ (tested with v22)
- Python 3.11–3.13 (3.14 is not compatible with ChromaDB)
- MongoDB running locally on port 27017
- Ollama (local AI model runner)
- ffmpeg (audio/video processing)
macOS:
brew install ollama
brew services start ollamaWindows: Download and run the installer from https://ollama.com/download/windows
After installing, Ollama runs automatically as a background service.
Linux:
curl -fsSL https://ollama.com/install.sh | sh
ollama serve &macOS:
brew install ffmpegWindows: Download from https://ffmpeg.org/download.html and add to PATH.
Linux:
sudo apt install ffmpegollama pull nomic-embed-text # ~274MB — embedding model for vector search
ollama pull llava # ~4.7GB — vision LLM for describing images and diagrams
ollama pull qwen2.5 # ~4.7GB — LLM for question generation and Q&A (default)npm installmacOS / Linux:
cd ai-service
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtWindows (Command Prompt):
cd ai-service
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txtWindows (PowerShell):
cd ai-service
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txtNote: Use Python 3.11–3.13. Python 3.14 is not compatible with ChromaDB.
Make sure MongoDB is running on localhost:27017.
macOS:
brew services start mongodb-communityWindows: MongoDB runs as a service automatically after installation. If not, start it with:
net start MongoDBOr run manually:
mongod --dbpath C:\data\dbLinux:
sudo systemctl start mongodYou need 3 terminals (or command prompts on Windows):
macOS / Linux:
cd ai-service
source .venv/bin/activate
uvicorn main:app --port 5000 --reloadWindows:
cd ai-service
.venv\Scripts\activate
uvicorn main:app --port 5000 --reloadnode server.jsollama serveOn macOS (if started via brew services) and Windows (runs as background service by default), this terminal is not needed.
From the main page, click "+ New Study Room", enter a name, and confirm. Study rooms are separate from regular chat rooms and hold your uploaded documents.
Inside a study room, click "Upload Document" and select a file. Supported formats: PDF, DOCX, PPTX, TXT, PNG/JPG, MP3/MP4/WAV. The document is processed in the background — wait for its status to show "ready" before using it.
Click "Ask AI", type your question, and submit. The assistant retrieves the most relevant passages from your uploaded documents and answers based only on that material. Conversation history is preserved across the session.
Click "Start Exam", choose a question type and number of questions. The exam begins at difficulty level 2/5. Each question is labelled with a topic by the LLM. Correct answers raise the difficulty; incorrect ones lower it. The pipeline selects the next question's topic based on your weakest areas across current and past sessions in the room. For wrong or partial answers, up to 2 hints are available (not for true/false); each hint unlocks a reattempt. Reattempts are penalised — a correct answer on the second attempt scores 50%, third attempt 25%. A results summary (strong topics, weak topics, recommended pages to review) is shown at the end.
curl http://localhost:5000/health
# Expected: {"status":"ok","ollama":true}project-group-1/
├── server.js # Express server — file upload routes + existing chat
├── database.js # MongoDB methods (chat + document CRUD)
├── package.json # Node dependencies (express, multer, ws, etc.)
├── client/
│ ├── index.html # SPA shell with Study Tools nav item
│ ├── app.js # Frontend logic (Lobby, Chat, Profile, StudyView)
│ └── style.css # Styles including study view
├── uploads/ # Uploaded files stored here (gitignored)
└── ai-service/ # Python FastAPI microservice
├── main.py # /health, /ingest, /ask endpoints
├── document_parser.py # PDF/DOCX/TXT/image text extraction
├── vision_processor.py # llava image description calls
├── chunker.py # Text chunking with LangChain
├── embeddings.py # nomic-embed-text + nomic-embed-vision embeddings
├── vector_store.py # ChromaDB storage
├── question_generator.py # RAG retrieval + LLM question generation + Q&A
├── requirements.txt # Python dependencies
└── chroma_data/ # ChromaDB persistent storage (gitignored)
| Format | Library | Notes |
|---|---|---|
| PyMuPDF (fitz) | Text extraction with page numbers; embedded images described by llava; vector-only pages rendered and described | |
| DOCX | python-docx | Paragraph + embedded image extraction; images described by llava |
| PPTX | python-pptx | Text + images per slide; images described by llava |
| TXT | Built-in | Direct read |
| PNG / JPG / JPEG | llava | Vision model describes image for semantic search |
| MP3 / WAV / AAC / M4A / OGG / FLAC | Whisper | Audio transcribed with timestamps |
| MP4 / MOV / AVI / MKV | ffmpeg + Whisper | Audio track extracted via ffmpeg, then transcribed |
| Component | Technology |
|---|---|
| Web server | Express (Node.js) |
| File uploads | multer |
| Database | MongoDB |
| AI service | Python FastAPI |
| Text LLM | Qwen2.5 7B via Ollama (configurable) |
| Vision LLM | llava via Ollama (configurable) |
| Text chunking | LangChain RecursiveCharacterTextSplitter |
| Embeddings | nomic-embed-text via Ollama |
| Vector database | ChromaDB |
| PDF parsing | PyMuPDF |
| Audio/video transcription | OpenAI Whisper (local) |
| Video audio extraction | ffmpeg |
The default LLM is qwen2.5 (best for university studies). You can swap it by setting the LLM_MODEL environment variable:
# Pull the model first
ollama pull llama3.1
# Then start the Python service with that model
LLM_MODEL=llama3.1 uvicorn main:app --port 5000 --reloadWindows:
set LLM_MODEL=llama3.1
uvicorn main:app --port 5000 --reload| Model | Command | Size | Best for |
|---|---|---|---|
| qwen2.5 (default) | ollama pull qwen2.5 |
4.7 GB | General university studies, math, STEM |
| llama3.1 | ollama pull llama3.1 |
4.7 GB | Humanities, essays, general reasoning |
| mistral | ollama pull mistral |
4.4 GB | Fast, good for non-technical subjects |
| qwen2.5-math | ollama pull qwen2.5-math |
4.4 GB | Math-heavy courses |
| phi3 | ollama pull phi3 |
2.2 GB | Low-resource machines, fastest |
The default vision model is llava. You can swap it via environment variable:
VISION_MODEL=moondream2 uvicorn main:app --port 5000 --reload| Model | Command | Size | Notes |
|---|---|---|---|
| llava (default) | ollama pull llava |
4.7 GB | Best general-purpose vision |
| moondream2 | ollama pull moondream2 |
1.7 GB | Fastest, low-resource machines |
| llava-phi3 | ollama pull llava-phi3 |
2.9 GB | Good balance of speed and quality |
The default Whisper model is base. Larger models are more accurate but slower and require more RAM.
WHISPER_MODEL=small uvicorn main:app --port 5000 --reloadWindows:
set WHISPER_MODEL=small
uvicorn main:app --port 5000 --reload| Model | Size | Notes |
|---|---|---|
| tiny | ~39 MB | Fastest, lowest accuracy |
| base (default) | ~74 MB | Good balance for most use cases |
| small | ~244 MB | Better accuracy, still fast |
| medium | ~769 MB | High accuracy, slower |
| large | ~1.5 GB | Best accuracy, slowest |
| Variable | Default | Description |
|---|---|---|
LLM_MODEL |
qwen2.5 |
LLM for question generation and Q&A |
LLM_NUM_CTX |
8192 |
Context window size (tokens) |
COMPACT_AT_FRACTION |
0.5 |
Fraction of context window that triggers history compaction |
VISION_MODEL |
llava |
Vision LLM for describing images |
WHISPER_MODEL |
base |
Whisper model size for audio/video transcription |
Browser (client/app.js)
│ HTTP REST (port 3000)
│ WebSocket (port 8000)
│ file upload (multer)
▼
Node.js / Express (server.js)
│ HTTP REST (port 5000)
▼
Python FastAPI (ai-service/main.py)
├─── ChromaDB (vector embeddings, local)
├─── Ollama (LLM inference, local, port 11434)
│ ├── qwen2.5 (text generation)
│ ├── llava (image captioning)
│ └── nomic-embed-text (embeddings)
└─── ffmpeg ──► Whisper (video audio extraction → transcription)
MongoDB (port 27017) ◄── server.js (all persistent data — chat, docs, questions)
The pipeline has two phases — ingestion when a document is uploaded, and retrieval when the user asks a question or requests questions.
INGEST PHASE (on document upload)
──────────────────────────────────────────────────────────────
Uploaded file
│
▼
document_parser.py
├── PDF / DOCX / PPTX → extract text (with page numbers)
│ └── extract embedded images → llava → caption string
│
├── PNG / JPG / JPEG → llava (vision model)
│ └── generates captions
│
└── Audio / Video → Whisper (local transcription)
└── generates transcript
│
│ All outputs are plain text at this point:
│ • document text (modality: "text")
│ • image captions (modality: "image")
│ • transcripts (modality: "audio")
│
▼
chunker.py
├── image chunks → stored as-is (one chunk per image, no splitting)
├── audio chunks → stored as-is (one chunk per Whisper segment, timestamps preserved)
└── text chunks → RecursiveCharacterTextSplitter → overlapping chunks
(each chunk tagged with: document_id, page, modality)
│
▼`
embeddings.py
└── nomic-embed-text (via Ollama) → text embedding vector per chunk
(same embedding model used for all modalities —
vision and audio content is embedded as text after conversion)
│
▼
vector_store.py
└── ChromaDB → store (vector + original text + metadata)
metadata includes: document_id, filename, page_number, modality
RETRIEVAL PHASE (on question / Q&A request)
──────────────────────────────────────────────────────────────
User query or topic
│
▼
embeddings.py → embed the query
│
▼
ChromaDB → semantic similarity search → top-N matching chunks
│
▼
question_generator.py
└── build prompt: system instruction + retrieved chunks + query
│
▼
qwen2.5 (via Ollama) → structured JSON response
│
▼
Parse + validate → return questions or answer to user
The exam pipeline maintains a live session state and loops through questions. Each question is labelled with a topic by the LLM. Topic scores are tracked across sessions — when selecting the next question, the pipeline picks the topic the student is weakest on (combining current and past session history) and targets ChromaDB retrieval toward that area.
User starts exam (question type, count)
│
▼
Fetch past completed sessions → aggregate historical_topic_scores
│
▼
Initialise session state:
difficulty = 2/5
topic_scores = {}
historical_topic_scores = { topic: { total, count }, ... }
question_log = []
│
┌─────────────────────────────────────────────────────┐
│ EXAM LOOP │
│ │
│ Pick weakest topic (current + historical scores) │
│ Generate question at current difficulty │
│ (RAG query = weak topic → qwen2.5) │
│ LLM labels question with topic │
│ │ │
│ ▼ │
│ Present question to student │
│ │ │
│ ┌─────┴──────┐ │
│ │ │ │
│ Answer Hint request │
│ │ │ │
│ │ RAG retrieval → hint text │
│ │ │ │
│ └─────┬──────┘ │
│ │ │
│ ▼ │
│ qwen2.5 grades answer │
│ │ │
│ ┌────────────┬──────────────┐ │
│ Correct Partial Wrong │
│ score ≥ 0.7 score 0.3–0.69 score < 0.3 │
│ │ │ │ │
│ difficulty+1 unchanged difficulty-1 │
│ └─────────────┴─────────────┘ │
│ │ │
│ Update topic_scores, question_log │
│ Persist session state → MongoDB │
│ │ │
│ more questions? ──yes──► (loop back) │
│ │ no │
└───────────┼─────────────────────────────────────────┘
│
▼
Compile results:
├── overall score
├── strong topics (score ≥ 0.7)
├── weak topics (score < 0.7)
└── recommended pages to review (from failed questions)
│
▼
Return summary to client
To stay within the LLM's context window (default 8192 tokens), when conversation history exceeds 50% of the window the AI service asks the LLM to summarise older turns into a single block, keeping the last two turns verbatim.
User sends a question
│
▼
Load conversation history from MongoDB
│
▼
Count tokens used by history
│
├── < 50% of context window
│ │
│ ▼
│ Proceed normally
│
└── ≥ 50% of context window
│
▼
Keep last 2 turns verbatim
Send older turns to qwen2.5 → generate summary block
│
▼
Replace old turns with summary
│
▼
Continue with compacted history
│
▼
Build final prompt: system + compacted history + new question + context chunks
│
▼
qwen2.5 → answer → save updated history to MongoDB → return to client
Purpose: Question generation, Q&A answers, answer grading, exam turn evaluation, conversation summarisation.
Why qwen2.5:
- Strong instruction-following and JSON output reliability, which is critical for parsing structured question responses
- Competitive performance on academic/educational content among open models of similar size (~4.7 GB)
- Available directly through Ollama with no configuration beyond
ollama pull qwen2.5
Alternatives considered:
llama3— similar size, weaker structured JSON compliance in testingmistral— good general performance but less consistent on educational Q&A formattingphi3— smaller and faster but less accurate on multi-step reasoning needed for exam grading- Commercial APIs (OpenAI GPT-4, Anthropic Claude) — rejected to keep the application fully local and free to run, in line with the assignment's preference for open-source local models
Purpose: Generating text descriptions of images extracted from uploaded documents (PDFs, slides) so image content is indexed and searchable.
Why llava:
- Only open-source vision-language model readily available via Ollama at project time
- Produces coherent natural-language captions suitable for embedding
Alternatives considered:
- GPT-4 Vision — cloud-only, introduces API key dependency
- BLIP-2 — requires manual model hosting outside Ollama, adding setup complexity
Purpose: Converts text chunks and user queries into dense vectors for semantic similarity search in ChromaDB.
Why nomic-embed-text:
- Highest-quality open embedding model available natively through Ollama
- Consistent API with other Ollama models — no separate service needed
Alternatives considered:
sentence-transformers(all-MiniLM-L6-v2) — requires a separate Python process and model download outside Ollama- OpenAI
text-embedding-3-small— cloud API, introduces cost and key dependency
Purpose: Transcribes uploaded audio and video files into text so lecture recordings can be indexed and queried.
Why Whisper:
- State-of-the-art open-source transcription accuracy across accents and technical vocabulary
- Runs fully locally (no API key required despite the "OpenAI" name)
Alternatives considered:
- Vosk — lower accuracy on academic/technical speech
- Google Speech-to-Text — cloud API, key dependency
Purpose: Stores and queries document chunk embeddings for semantic retrieval.
Why ChromaDB:
- Embedded Python library — no separate server to manage
- Persistent local storage out of the box
- Simple API well-suited for per-document filtering
Alternatives considered:
- Pinecone — cloud-only, introduces API key and cost
- Weaviate / Qdrant — require running a separate server process, adding deployment complexity
Why: Splits documents with awareness of paragraph and sentence boundaries, reducing mid-sentence chunk breaks that degrade retrieval quality. LangChain was also recommended in the assignment guidelines.
Image and audio chunks are not passed through the splitter. Image chunks are one caption per image (splitting a caption would destroy its meaning). Audio chunks are one Whisper segment per chunk — each segment already has natural boundaries and carries a timestamp ([0.0s - 5.2s] text), which splitting would break.
No API keys are required. All models (Ollama, Whisper) run fully locally.