Skip to content

Repository files navigation

StudySpace — AI-Powered Study Assistant

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.

Prerequisites

  • 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)

Setup

1. Install Ollama

macOS:

brew install ollama
brew services start ollama

Windows: 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 &

2. Install ffmpeg

macOS:

brew install ffmpeg

Windows: Download from https://ffmpeg.org/download.html and add to PATH.

Linux:

sudo apt install ffmpeg

3. Pull the required models

ollama 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)

4. Install Node.js dependencies

npm install

5. Set up the Python AI service

macOS / Linux:

cd ai-service
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Windows (Command Prompt):

cd ai-service
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt

Windows (PowerShell):

cd ai-service
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt

Note: Use Python 3.11–3.13. Python 3.14 is not compatible with ChromaDB.

6. Start MongoDB

Make sure MongoDB is running on localhost:27017.

macOS:

brew services start mongodb-community

Windows: MongoDB runs as a service automatically after installation. If not, start it with:

net start MongoDB

Or run manually:

mongod --dbpath C:\data\db

Linux:

sudo systemctl start mongod

Running

You need 3 terminals (or command prompts on Windows):

Terminal 1 — Python AI service

macOS / Linux:

cd ai-service
source .venv/bin/activate
uvicorn main:app --port 5000 --reload

Windows:

cd ai-service
.venv\Scripts\activate
uvicorn main:app --port 5000 --reload

Terminal 2 — Express server

node server.js

Terminal 3 — Ollama (if not running as a service)

ollama serve

On macOS (if started via brew services) and Windows (runs as background service by default), this terminal is not needed.

User Manual

1. Create a Study Room

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.

2. Upload 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.

3. Ask the AI a Question

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.

4. Take an Adaptive Exam

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.

Verifying the pipeline

Check service health

curl http://localhost:5000/health
# Expected: {"status":"ok","ollama":true}

Project Structure

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)

Supported File Types

Format Library Notes
PDF 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

Tech Stack

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

Switching LLM Models

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 --reload

Windows:

set LLM_MODEL=llama3.1
uvicorn main:app --port 5000 --reload

Recommended models for different use cases

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

Switching Vision Models

The default vision model is llava. You can swap it via environment variable:

VISION_MODEL=moondream2 uvicorn main:app --port 5000 --reload

Recommended vision models

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

Switching Whisper Models

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 --reload

Windows:

set WHISPER_MODEL=small
uvicorn main:app --port 5000 --reload

Available Whisper models

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

Environment Variables

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

Design and Implementation

Architecture Overview

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)

RAG Pipeline

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

Adaptive Exam Pipeline

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

Conversation Compaction

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

AI Model Choices and Rationale

Text Generation — qwen2.5 (via Ollama)

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 testing
  • mistral — good general performance but less consistent on educational Q&A formatting
  • phi3 — 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

Vision — llava (via Ollama)

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

Embeddings — nomic-embed-text (via Ollama)

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

Audio/Video Transcription — OpenAI Whisper (local)

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

Vector Database — ChromaDB

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

Text Splitting — LangChain RecursiveCharacterTextSplitter

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.

API Keys

No API keys are required. All models (Ollama, Whisper) run fully locally.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages