cd /news/ai-tools/drop-in-a-pdf-get-flashcards · home › topics › ai-tools › article
[ARTICLE · art-156025] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

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.

by read5 min views2 publishedOct 23, 2026

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

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.

── more in #ai-tools 4 stories · sorted by recency
── more on @ollama 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/drop-in-a-pdf-get-fl…] indexed:0 read:5min 2026-10-23 · —