Drop in a PDF, Get Flashcards A developer built study_sloth.py, a fully local study-notes assistant that watches a folder for PDF, .txt, or .md lecture notes and uses an open-weight model served by Ollama (default gemma3:4b) to generate a topic-wise revision summary, Q/A flashcards, and likely exam questions with model answers. The script chunks extracted text, runs summary, flashcard, and question prompts at temperature 0.3, and writes results to an output directory without sending data off the machine or requiring an API key. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady https://dev.to/challenges/hacktoberfest-week3-2026-10-19 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. """ 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 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: Use ONLY the 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 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 . 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. 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” . 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.