This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
Before exams, I used to spend hours turning lecture PDFs into notes I could actually revise from. [Project name] does that for me. I drop a PDF or notes file into a folder, and a local model produces a short summary, topic-wise key points, flashcards, and likely exam questions. It runs entirely on my laptop.
It’s for any student who has a pile of syllabus PDFs and would rather not read all of them twice.
Demo
Code
!/usr/bin/env python3
"""
study_sloth.py - a lazy, fully local study-notes assistant.
Drop a PDF / .txt / .md of lecture notes into the notes/ folder.
A local open-weight model (served by Ollama) writes, into output/:
_summary.md topic-wise revision summary
_flashcards.md Q/A flashcards
_questions.md likely exam questions with model answers
Nothing leaves your machine. No API key. Works offline once the model is pulled.
Setup:
1. Install Ollama: https://ollama.com
2. ollama pull gemma3:4b (or any other model, see --model)
3. pip install ollama pypdf
4. python study_sloth.py (watches notes/ forever)
python study_sloth.py --once (process what is there, then exit)
"""
import argparse
import time
from pathlib import Path
import ollama
from pypdf import PdfReader
DEFAULT_MODEL = "gemma3:4b"
CHUNK_CHARS = 3500 # size of each piece sent to the model
MAX_SUMMARY_CHARS = 12000 # cap on the combined summary fed to later steps
SUPPORTED = {".pdf", ".txt", ".md"}
SUMMARY_PROMPT = """You are helping a university student revise for an exam.
Summarize the lecture notes below as concise bullet points.
Keep important definitions, formulas, algorithms and complexities.
Use ONLY the text given. Do not add outside facts.
NOTES:
{text}
"""
FLASHCARD_PROMPT = """From the revision summary below, create {n} flashcards.
Use exactly this format, one after another:
Q:
A:
Cover the most important concepts. Use ONLY the summary.
SUMMARY:
{summary}
"""
QUESTION_PROMPT = """From the revision summary below, write {n} questions that are
likely to appear in a university theory exam. For each question give a model
answer suitable for a {marks}-mark question (about {points} clear points).
Format:
Q1.
Use ONLY the summary.
SUMMARY:
{summary}
"""
def ask(prompt: str, model: str) -> str:
"""Send one prompt to the local model and return the text reply."""
response = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
options={"temperature": 0.3},
)
return response["message"]["content"].strip()
def read_file(path: Path) -> str:
if path.suffix.lower() == ".pdf":
reader = PdfReader(str(path))
return "\n".join((page.extract_text() or "") for page in reader.pages)
return path.read_text(encoding="utf-8", errors="ignore")
def chunk_text(text: str, size: int = CHUNK_CHARS) -> list[str]:
"""Split text into chunks of roughly size characters, on line boundaries."""
chunks, current, length = [], [], 0
for line in text.splitlines():
line = line.strip()
if not line:
continue
if length + len(line) > size and current:
chunks.append("\n".join(current))
current, length = [], 0
current.append(line)
length += len(line) + 1
if current:
chunks.append("\n".join(current))
return chunks
def process_file(path: Path, out_dir: Path, model: str, marks: int) -> None:
start = time.time()
print(f"\n[+] Processing {path.name}")
text = read_file(path)
if len(text.strip()) < 50:
print(" Could not extract text (scanned PDF?). Skipping.")
return
chunks = chunk_text(text)
summaries = []
for i, chunk in enumerate(chunks, 1):
print(f" Summarizing part {i}/{len(chunks)}...")
summaries.append(ask(SUMMARY_PROMPT.format(text=chunk), model))
combined = "\n\n".join(summaries)
if len(combined) > MAX_SUMMARY_CHARS:
combined = combined[:MAX_SUMMARY_CHARS]
print(" Making flashcards...")
flashcards = ask(FLASHCARD_PROMPT.format(n=10, summary=combined), model)
print(" Making exam questions...")
questions = ask(
QUESTION_PROMPT.format(n=5, marks=marks, points=marks, summary=combined),
model,
)
out_dir.mkdir(parents=True, exist_ok=True)
stem = path.stem
(out_dir / f"{stem}_summary.md").write_text(
f"# Summary: {stem}\n\n{combined}\n", encoding="utf-8"
)
(out_dir / f"{stem}_flashcards.md").write_text(
f"# Flashcards: {stem}\n\n{flashcards}\n", encoding="utf-8"
)
(out_dir / f"{stem}_questions.md").write_text(
f"# Likely exam questions: {stem}\n\n{questions}\n", encoding="utf-8"
)
print(f" Done in {time.time() - start:.0f}s -> {out_dir}/")
def already_done(path: Path, out_dir: Path) -> bool:
return (out_dir / f"{path.stem}_summary.md").exists()
def scan(in_dir: Path, out_dir: Path, model: str, marks: int) -> None:
for path in sorted(in_dir.iterdir()):
if path.suffix.lower() in SUPPORTED and not already_done(path, out_dir):
try:
process_file(path, out_dir, model, marks)
except Exception as exc: # keep the watcher alive
print(f" Failed on {path.name}: {exc}")
def main() -> None:
parser = argparse.ArgumentParser(description="Local study-notes assistant")
parser.add_argument("--input", default="notes", help="folder to watch")
parser.add_argument("--output", default="output", help="folder for results")
parser.add_argument("--model", default=DEFAULT_MODEL, help="Ollama model name")
parser.add_argument("--marks", type=int, default=5, help="marks per exam answer")
parser.add_argument("--once", action="store_true", help="process once and exit")
args = parser.parse_args()
in_dir, out_dir = Path(args.input), Path(args.output)
in_dir.mkdir(parents=True, exist_ok=True)
print(f"Model: {args.model} | watching '{in_dir}/' | Ctrl+C to stop")
if args.once:
scan(in_dir, out_dir, args.model, args.marks)
return
while True:
scan(in_dir, out_dir, args.model, args.marks)
time.sleep(10)
if name == "main":
main()
How I Built It
Model: [e.g. Qwen / Llama, size and quantization] running through Ollama, so inference is local.
Pipeline: Python script that watches a folder, extracts text from PDFs with [library], splits it into chunks by heading, and sends each chunk to the model with prompts for summary, flashcards, and exam questions.
Output: Markdown files saved next to the original, e.g. Unit1_summary.md, Unit1_flashcards.md.
Hardware: [your laptop specs, and how long a typical PDF takes].
Why Does Open Innovation Matter?
Offline: It works in a hostel or on a train with no internet.
Private and free: My course material and notes never leave my machine, and there’s no API bill or rate limit during exam week.
Swappable: I can change models or edit the prompts to fit my needs. For example, I tuned the exam-question prompt to match [your university’s] 5-mark answer format, which a closed tool wouldn’t let me do.
[Add one honest comparison: where the local model was worse than a closed one, and how you worked around it.]
My Agent Session
The Lazy Payoff
Summarizing a unit by hand took me about [X hours]. Now it takes [Y minutes] of unattended processing while I do something else. I used it this week on [specific subject/unit], and [one concrete result, e.g. “it generated N flashcards I used to revise”].
Prize Categories
Best Use of Gemma: The whole project runs on [Gemma 3 4B] served locally through Ollama. Gemma reads each chunk of the lecture notes and generates the summaries, flashcards and exam-style questions. Because it runs on my own laptop, it works offline, my course material never leaves my machine, and I could tune the prompts to match my university's exam format.
Top comments (0)