# Drop in a PDF, Get Flashcards

> Source: <https://dev.to/ayushkawadkar29/drop-in-a-pdf-get-flashcards-4bdc>
> Published: 2026-10-23 10:32:07+00:00

*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.
