跳到正文
DEV Community· Ayush Kawadkar·· 3 小時前AI 評分44

拖入 PDF 即可生成學習閃卡

Drop in a PDF, Get Flashcards

AI 導讀

這個本地學習筆記助理會讀取 PDF、TXT 或 Markdown 課堂筆記,生成摘要、分主題重點、閃卡及可能的考試題目。程式透過 Ollama 呼叫本機模型 gemma3:4b,並可改用其他模型;檔案內容不會離開電腦,模型下載後亦可離線使用。

正文

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.

來源:DEV Community · dev.to