{"slug": "drop-in-a-pdf-get-flashcards", "title": "Drop in a PDF, Get Flashcards", "summary": "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.", "body_md": "*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)*\n\nBefore 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.\n\nIt’s for any student who has a pile of syllabus PDFs and would rather not read all of them twice.\n\n\"\"\"\n\nstudy_sloth.py - a lazy, fully local study-notes assistant.\n\nDrop a PDF / .txt / .md of lecture notes into the `notes/` folder.\n\nA local open-weight model (served by Ollama) writes, into `output/`:\n\n    _summary.md     topic-wise revision summary\n\n    _flashcards.md  Q/A flashcards\n\n    _questions.md   likely exam questions with model answers\n\nNothing leaves your machine. No API key. Works offline once the model is pulled.\n\nSetup:\n\n    1. Install Ollama: [https://ollama.com](https://ollama.com)\n\n    2. ollama pull gemma3:4b          (or any other model, see --model)\n\n    3. pip install ollama pypdf\n\n    4. python study_sloth.py          (watches notes/ forever)\n\n       python study_sloth.py --once   (process what is there, then exit)\n\n\"\"\"\n\nimport argparse\n\nimport time\n\nfrom pathlib import Path\n\nimport ollama\n\nfrom pypdf import PdfReader\n\nDEFAULT_MODEL = \"gemma3:4b\"\n\nCHUNK_CHARS = 3500          # size of each piece sent to the model\n\nMAX_SUMMARY_CHARS = 12000   # cap on the combined summary fed to later steps\n\nSUPPORTED = {\".pdf\", \".txt\", \".md\"}\n\nSUMMARY_PROMPT = \"\"\"You are helping a university student revise for an exam.\n\nSummarize the lecture notes below as concise bullet points.\n\nKeep important definitions, formulas, algorithms and complexities.\n\nUse ONLY the text given. Do not add outside facts.\n\nNOTES:\n\n{text}\n\n\"\"\"\n\nFLASHCARD_PROMPT = \"\"\"From the revision summary below, create {n} flashcards.\n\nUse exactly this format, one after another:\n\nQ: \n\nA: \n\nCover the most important concepts. Use ONLY the summary.\n\nSUMMARY:\n\n{summary}\n\n\"\"\"\n\nQUESTION_PROMPT = \"\"\"From the revision summary below, write {n} questions that are\n\nlikely to appear in a university theory exam. For each question give a model\n\nanswer suitable for a {marks}-mark question (about {points} clear points).\n\nFormat:\n\nUse ONLY the summary.\n\ndef ask(prompt: str, model: str) -> str:\n\n    \"\"\"Send one prompt to the local model and return the text reply.\"\"\"\n\n    response = ollama.chat(\n\n        model=model,\n\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n\n        options={\"temperature\": 0.3},\n\n    )\n\n    return response[\"message\"][\"content\"].strip()\n\ndef read_file(path: Path) -> str:\n\n    if path.suffix.lower() == \".pdf\":\n\n        reader = PdfReader(str(path))\n\n        return \"\\n\".join((page.extract_text() or \"\") for page in reader.pages)\n\n    return path.read_text(encoding=\"utf-8\", errors=\"ignore\")\n\ndef chunk_text(text: str, size: int = CHUNK_CHARS) -> list[str]:\n\n    \"\"\"Split text into chunks of roughly `size` characters, on line boundaries.\"\"\"\n\n    chunks, current, length = [], [], 0\n\n    for line in text.splitlines():\n\n        line = line.strip()\n\n        if not line:\n\n            continue\n\n        if length + len(line) > size and current:\n\n            chunks.append(\"\\n\".join(current))\n\n            current, length = [], 0\n\n        current.append(line)\n\n        length += len(line) + 1\n\n    if current:\n\n        chunks.append(\"\\n\".join(current))\n\n    return chunks\n\ndef process_file(path: Path, out_dir: Path, model: str, marks: int) -> None:\n\n    start = time.time()\n\n    print(f\"\\n[+] Processing {path.name}\")\n\n```\ntext = read_file(path)\nif len(text.strip()) < 50:\n    print(\"    Could not extract text (scanned PDF?). Skipping.\")\n    return\n\nchunks = chunk_text(text)\nsummaries = []\nfor i, chunk in enumerate(chunks, 1):\n    print(f\"    Summarizing part {i}/{len(chunks)}...\")\n    summaries.append(ask(SUMMARY_PROMPT.format(text=chunk), model))\n\ncombined = \"\\n\\n\".join(summaries)\nif len(combined) > MAX_SUMMARY_CHARS:\n    combined = combined[:MAX_SUMMARY_CHARS]\n\nprint(\"    Making flashcards...\")\nflashcards = ask(FLASHCARD_PROMPT.format(n=10, summary=combined), model)\n\nprint(\"    Making exam questions...\")\nquestions = ask(\n    QUESTION_PROMPT.format(n=5, marks=marks, points=marks, summary=combined),\n    model,\n)\n\nout_dir.mkdir(parents=True, exist_ok=True)\nstem = path.stem\n(out_dir / f\"{stem}_summary.md\").write_text(\n    f\"# Summary: {stem}\\n\\n{combined}\\n\", encoding=\"utf-8\"\n)\n(out_dir / f\"{stem}_flashcards.md\").write_text(\n    f\"# Flashcards: {stem}\\n\\n{flashcards}\\n\", encoding=\"utf-8\"\n)\n(out_dir / f\"{stem}_questions.md\").write_text(\n    f\"# Likely exam questions: {stem}\\n\\n{questions}\\n\", encoding=\"utf-8\"\n)\n\nprint(f\"    Done in {time.time() - start:.0f}s -> {out_dir}/\")\n```\n\ndef already_done(path: Path, out_dir: Path) -> bool:\n\n    return (out_dir / f\"{path.stem}_summary.md\").exists()\n\ndef scan(in_dir: Path, out_dir: Path, model: str, marks: int) -> None:\n\n    for path in sorted(in_dir.iterdir()):\n\n        if path.suffix.lower() in SUPPORTED and not already_done(path, out_dir):\n\n            try:\n\n                process_file(path, out_dir, model, marks)\n\n            except Exception as exc:  # keep the watcher alive\n\n                print(f\"    Failed on {path.name}: {exc}\")\n\ndef main() -> None:\n\n    parser = argparse.ArgumentParser(description=\"Local study-notes assistant\")\n\n    parser.add_argument(\"--input\", default=\"notes\", help=\"folder to watch\")\n\n    parser.add_argument(\"--output\", default=\"output\", help=\"folder for results\")\n\n    parser.add_argument(\"--model\", default=DEFAULT_MODEL, help=\"Ollama model name\")\n\n    parser.add_argument(\"--marks\", type=int, default=5, help=\"marks per exam answer\")\n\n    parser.add_argument(\"--once\", action=\"store_true\", help=\"process once and exit\")\n\n    args = parser.parse_args()\n\n```\nin_dir, out_dir = Path(args.input), Path(args.output)\nin_dir.mkdir(parents=True, exist_ok=True)\n\nprint(f\"Model: {args.model} | watching '{in_dir}/' | Ctrl+C to stop\")\nif args.once:\n    scan(in_dir, out_dir, args.model, args.marks)\n    return\n\nwhile True:\n    scan(in_dir, out_dir, args.model, args.marks)\n    time.sleep(10)\n```\n\nif **name** == \"**main**\":\n\n    main()\n\nModel: [e.g. Qwen / Llama, size and quantization] running through Ollama, so inference is local.\n\nPipeline: 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.\n\nOutput: Markdown files saved next to the original, e.g. Unit1_summary.md, Unit1_flashcards.md.\n\nHardware: [your laptop specs, and how long a typical PDF takes].\n\nOffline: It works in a hostel or on a train with no internet.\n\nPrivate and free: My course material and notes never leave my machine, and there’s no API bill or rate limit during exam week.\n\nSwappable: 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.\n\n[Add one honest comparison: where the local model was worse than a closed one, and how you worked around it.]\n\nSummarizing 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”].\n\n**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.", "url": "https://wpnews.pro/news/drop-in-a-pdf-get-flashcards", "canonical_source": "https://dev.to/ayushkawadkar29/drop-in-a-pdf-get-flashcards-4bdc", "published_at": "2026-10-23 10:32:07+00:00", "updated_at": "2026-10-23 10:42:59.598423+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "generative-ai", "ai-products"], "entities": ["Ollama", "gemma3:4b", "study_sloth.py", "pypdf", "Hacktoberfest"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/drop-in-a-pdf-get-flashcards", "markdown": "https://wpnews.pro/news/drop-in-a-pdf-get-flashcards.md", "text": "https://wpnews.pro/news/drop-in-a-pdf-get-flashcards.txt", "jsonld": "https://wpnews.pro/news/drop-in-a-pdf-get-flashcards.jsonld"}}