<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ayush Kawadkar</title>
    <description>The latest articles on DEV Community by Ayush Kawadkar (@ayushkawadkar29).</description>
    <link>https://dev.to/ayushkawadkar29</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F99mvlsfu5tfj9m7ku25d.png</url>
      <title>DEV Community: Ayush Kawadkar</title>
      <link>https://dev.to/ayushkawadkar29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayushkawadkar29"/>
    <language>en</language>
    <item>
      <title>Drop in a PDF, Get Flashcards</title>
      <dc:creator>Ayush Kawadkar</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:32:07 +0000</pubDate>
      <link>https://dev.to/ayushkawadkar29/drop-in-a-pdf-get-flashcards-4bdc</link>
      <guid>https://dev.to/ayushkawadkar29/drop-in-a-pdf-get-flashcards-4bdc</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It’s for any student who has a pile of syllabus PDFs and would rather not read all of them twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;h1&gt;
  
  
  !/usr/bin/env python3
&lt;/h1&gt;

&lt;p&gt;"""&lt;br&gt;
study_sloth.py - a lazy, fully local study-notes assistant.&lt;/p&gt;

&lt;p&gt;Drop a PDF / .txt / .md of lecture notes into the &lt;code&gt;notes/&lt;/code&gt; folder.&lt;br&gt;
A local open-weight model (served by Ollama) writes, into &lt;code&gt;output/&lt;/code&gt;:&lt;br&gt;
    _summary.md     topic-wise revision summary&lt;br&gt;
    _flashcards.md  Q/A flashcards&lt;br&gt;
    _questions.md   likely exam questions with model answers&lt;/p&gt;

&lt;p&gt;Nothing leaves your machine. No API key. Works offline once the model is pulled.&lt;/p&gt;

&lt;p&gt;Setup:&lt;br&gt;
    1. Install Ollama: &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;https://ollama.com&lt;/a&gt;&lt;br&gt;
    2. ollama pull gemma3:4b          (or any other model, see --model)&lt;br&gt;
    3. pip install ollama pypdf&lt;br&gt;
    4. python study_sloth.py          (watches notes/ forever)&lt;br&gt;
       python study_sloth.py --once   (process what is there, then exit)&lt;br&gt;
"""&lt;/p&gt;

&lt;p&gt;import argparse&lt;br&gt;
import time&lt;br&gt;
from pathlib import Path&lt;/p&gt;

&lt;p&gt;import ollama&lt;br&gt;
from pypdf import PdfReader&lt;/p&gt;

&lt;p&gt;DEFAULT_MODEL = "gemma3:4b"&lt;br&gt;
CHUNK_CHARS = 3500          # size of each piece sent to the model&lt;br&gt;
MAX_SUMMARY_CHARS = 12000   # cap on the combined summary fed to later steps&lt;br&gt;
SUPPORTED = {".pdf", ".txt", ".md"}&lt;/p&gt;

&lt;p&gt;SUMMARY_PROMPT = """You are helping a university student revise for an exam.&lt;br&gt;
Summarize the lecture notes below as concise bullet points.&lt;br&gt;
Keep important definitions, formulas, algorithms and complexities.&lt;br&gt;
Use ONLY the text given. Do not add outside facts.&lt;/p&gt;

&lt;p&gt;NOTES:&lt;br&gt;
{text}&lt;br&gt;
"""&lt;/p&gt;

&lt;p&gt;FLASHCARD_PROMPT = """From the revision summary below, create {n} flashcards.&lt;br&gt;
Use exactly this format, one after another:&lt;/p&gt;

&lt;p&gt;Q: &lt;br&gt;
A: &lt;/p&gt;

&lt;p&gt;Cover the most important concepts. Use ONLY the summary.&lt;/p&gt;

&lt;p&gt;SUMMARY:&lt;br&gt;
{summary}&lt;br&gt;
"""&lt;/p&gt;

&lt;p&gt;QUESTION_PROMPT = """From the revision summary below, write {n} questions that are&lt;br&gt;
likely to appear in a university theory exam. For each question give a model&lt;br&gt;
answer suitable for a {marks}-mark question (about {points} clear points).&lt;br&gt;
Format:&lt;/p&gt;

&lt;h3&gt;
  
  
  Q1. 
&lt;/h3&gt;



&lt;p&gt;Use ONLY the summary.&lt;/p&gt;

&lt;p&gt;SUMMARY:&lt;br&gt;
{summary}&lt;br&gt;
"""&lt;/p&gt;

&lt;p&gt;def ask(prompt: str, model: str) -&amp;gt; str:&lt;br&gt;
    """Send one prompt to the local model and return the text reply."""&lt;br&gt;
    response = ollama.chat(&lt;br&gt;
        model=model,&lt;br&gt;
        messages=[{"role": "user", "content": prompt}],&lt;br&gt;
        options={"temperature": 0.3},&lt;br&gt;
    )&lt;br&gt;
    return response["message"]["content"].strip()&lt;/p&gt;

&lt;p&gt;def read_file(path: Path) -&amp;gt; str:&lt;br&gt;
    if path.suffix.lower() == ".pdf":&lt;br&gt;
        reader = PdfReader(str(path))&lt;br&gt;
        return "\n".join((page.extract_text() or "") for page in reader.pages)&lt;br&gt;
    return path.read_text(encoding="utf-8", errors="ignore")&lt;/p&gt;

&lt;p&gt;def chunk_text(text: str, size: int = CHUNK_CHARS) -&amp;gt; list[str]:&lt;br&gt;
    """Split text into chunks of roughly &lt;code&gt;size&lt;/code&gt; characters, on line boundaries."""&lt;br&gt;
    chunks, current, length = [], [], 0&lt;br&gt;
    for line in text.splitlines():&lt;br&gt;
        line = line.strip()&lt;br&gt;
        if not line:&lt;br&gt;
            continue&lt;br&gt;
        if length + len(line) &amp;gt; size and current:&lt;br&gt;
            chunks.append("\n".join(current))&lt;br&gt;
            current, length = [], 0&lt;br&gt;
        current.append(line)&lt;br&gt;
        length += len(line) + 1&lt;br&gt;
    if current:&lt;br&gt;
        chunks.append("\n".join(current))&lt;br&gt;
    return chunks&lt;/p&gt;

&lt;p&gt;def process_file(path: Path, out_dir: Path, model: str, marks: int) -&amp;gt; None:&lt;br&gt;
    start = time.time()&lt;br&gt;
    print(f"\n[+] Processing {path.name}")&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text = read_file(path)
if len(text.strip()) &amp;lt; 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) &amp;gt; 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 -&amp;gt; {out_dir}/")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;def already_done(path: Path, out_dir: Path) -&amp;gt; bool:&lt;br&gt;
    return (out_dir / f"{path.stem}_summary.md").exists()&lt;/p&gt;

&lt;p&gt;def scan(in_dir: Path, out_dir: Path, model: str, marks: int) -&amp;gt; None:&lt;br&gt;
    for path in sorted(in_dir.iterdir()):&lt;br&gt;
        if path.suffix.lower() in SUPPORTED and not already_done(path, out_dir):&lt;br&gt;
            try:&lt;br&gt;
                process_file(path, out_dir, model, marks)&lt;br&gt;
            except Exception as exc:  # keep the watcher alive&lt;br&gt;
                print(f"    Failed on {path.name}: {exc}")&lt;/p&gt;

&lt;p&gt;def main() -&amp;gt; None:&lt;br&gt;
    parser = argparse.ArgumentParser(description="Local study-notes assistant")&lt;br&gt;
    parser.add_argument("--input", default="notes", help="folder to watch")&lt;br&gt;
    parser.add_argument("--output", default="output", help="folder for results")&lt;br&gt;
    parser.add_argument("--model", default=DEFAULT_MODEL, help="Ollama model name")&lt;br&gt;
    parser.add_argument("--marks", type=int, default=5, help="marks per exam answer")&lt;br&gt;
    parser.add_argument("--once", action="store_true", help="process once and exit")&lt;br&gt;
    args = parser.parse_args()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
    main()&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Model: [e.g. Qwen / Llama, size and quantization] running through Ollama, so inference is local.&lt;br&gt;
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.&lt;br&gt;
Output: Markdown files saved next to the original, e.g. Unit1_summary.md, Unit1_flashcards.md.&lt;br&gt;
Hardware: [your laptop specs, and how long a typical PDF takes].&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Offline: It works in a hostel or on a train with no internet.&lt;br&gt;
Private and free: My course material and notes never leave my machine, and there’s no API bill or rate limit during exam week.&lt;br&gt;
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.&lt;br&gt;
[Add one honest comparison: where the local model was worse than a closed one, and how you worked around it.]&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;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”].&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: 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.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Receipt Sorter: a freelancer's tax-season tool that never leaves the laptop</title>
      <dc:creator>Ayush Kawadkar</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:20:26 +0000</pubDate>
      <link>https://dev.to/ayushkawadkar29/receipt-sorter-a-freelancers-tax-season-tool-that-never-leaves-the-laptop-3732</link>
      <guid>https://dev.to/ayushkawadkar29/receipt-sorter-a-freelancers-tax-season-tool-that-never-leaves-the-laptop-3732</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Receipt Sorter turns a messy folder of receipts and invoices into a clean CSV and a tidy, renamed folder structure. For each document it extracts the vendor, date, total, currency and a category (software, travel, meals, office, equipment, utilities, other). Files are copied into category folders and renamed like &lt;code&gt;2026-09-14_Notion_10.00.pdf&lt;/code&gt;. Anything it can't read confidently goes into a &lt;code&gt;needs_review.txt&lt;/code&gt; list instead of being silently guessed.&lt;/p&gt;

&lt;p&gt;Who would pay: freelancers, solo consultants and small studios who lose hours every tax season sorting receipts, and who often handle client documents they shouldn't upload to a random cloud service. A one-time license makes sense here because the tool has no running cost per document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;[PASTE YOUR VIDEO OR DEMO LINK HERE]&lt;/p&gt;

&lt;p&gt;The demo runs with Wi-Fi turned off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;import csv, shutil, sys&lt;br&gt;
from pathlib import Path&lt;/p&gt;

&lt;p&gt;import ollama&lt;br&gt;
from pydantic import BaseModel&lt;br&gt;
from pypdf import PdfReader&lt;/p&gt;

&lt;p&gt;MODEL = "qwen2.5:3b"&lt;br&gt;
CATEGORIES = "software, travel, meals, office, equipment, utilities, other"&lt;/p&gt;

&lt;p&gt;class Receipt(BaseModel):&lt;br&gt;
    vendor: str&lt;br&gt;
    date: str  # YYYY-MM-DD&lt;br&gt;
    amount: float&lt;br&gt;
    currency: str&lt;br&gt;
    category: str&lt;/p&gt;

&lt;p&gt;def read_text(path: Path) -&amp;gt; str:&lt;br&gt;
    if path.suffix.lower() == ".pdf":&lt;br&gt;
        return "\n".join(p.extract_text() or "" for p in PdfReader(path).pages)&lt;br&gt;
    return path.read_text(errors="ignore")&lt;/p&gt;

&lt;p&gt;def extract(text: str) -&amp;gt; Receipt:&lt;br&gt;
    prompt = (&lt;br&gt;
        "Extract the vendor, date (YYYY-MM-DD), total amount, currency code and "&lt;br&gt;
        f"category from this receipt. Category must be one of: {CATEGORIES}.\n\n"&lt;br&gt;
        + text[:6000]&lt;br&gt;
    )&lt;br&gt;
    r = ollama.chat(&lt;br&gt;
        model=MODEL,&lt;br&gt;
        messages=[{"role": "user", "content": prompt}],&lt;br&gt;
        format=Receipt.model_json_schema(),&lt;br&gt;
        options={"temperature": 0},&lt;br&gt;
    )&lt;br&gt;
    return Receipt.model_validate_json(r.message.content)&lt;/p&gt;

&lt;p&gt;def main(src: str, dst: str):&lt;br&gt;
    src, dst = Path(src), Path(dst)&lt;br&gt;
    dst.mkdir(exist_ok=True)&lt;br&gt;
    rows, failed = [], []&lt;br&gt;
    for f in sorted(src.iterdir()):&lt;br&gt;
        if f.suffix.lower() not in {".pdf", ".txt"}:&lt;br&gt;
            continue&lt;br&gt;
        rec = None&lt;br&gt;
        for _ in range(2):  # one retry if the JSON is bad&lt;br&gt;
            try:&lt;br&gt;
                rec = extract(read_text(f))&lt;br&gt;
                break&lt;br&gt;
            except Exception:&lt;br&gt;
                pass&lt;br&gt;
        if rec is None:&lt;br&gt;
            failed.append(f.name)&lt;br&gt;
            continue&lt;br&gt;
        folder = dst / rec.category&lt;br&gt;
        folder.mkdir(exist_ok=True)&lt;br&gt;
        vendor = "".join(c for c in rec.vendor if c.isalnum())[:20]&lt;br&gt;
        new_name = f"{rec.date}&lt;em&gt;{vendor}&lt;/em&gt;{rec.amount:.2f}{f.suffix}"&lt;br&gt;
        shutil.copy(f, folder / new_name)&lt;br&gt;
        rows.append({**rec.model_dump(), "file": new_name})&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;with open(dst / "receipts.csv", "w", newline="") as out:
    w = csv.DictWriter(
        out, fieldnames=["file", "vendor", "date", "amount", "currency", "category"]
    )
    w.writeheader()
    w.writerows(rows)
if failed:
    (dst / "needs_review.txt").write_text("\n".join(failed))
print(f"Sorted {len(rows)} files, {len(failed)} need review.")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == "&lt;strong&gt;main&lt;/strong&gt;":&lt;br&gt;
    main(sys.argv[1], sys.argv[2])&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Qwen2.5 3B, an open-weight model, running locally through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction:&lt;/strong&gt; text is pulled from each PDF with &lt;code&gt;pypdf&lt;/code&gt;, then sent to the model with a strict JSON schema (Ollama's structured output) so the reply always has the fields I need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; the reply is parsed with Pydantic. If it fails, the tool retries once. If it fails again, the file is listed for manual review rather than guessed at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; a CSV for the accountant plus category folders with consistent file names, all in about 70 lines of Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not done yet:&lt;/strong&gt; photographed receipts. That needs a vision model, which is the next thing I'd add.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; receipts and invoices often contain client names, project details and bank information. With a local open-weight model, none of it leaves the machine, so there is nothing to disclose and no vendor to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline:&lt;/strong&gt; the whole pipeline runs with no internet connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; there is no per-document API fee. That is what makes a one-time-price product viable, because a closed API would turn every receipt into a recurring cost I'd have to pass on to the customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control:&lt;/strong&gt; the model is a one-line setting. I can swap in a bigger one for accuracy, or a smaller one for speed on older laptops, and categories are just a list in the prompt that a user can edit for their own bookkeeping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in:&lt;/strong&gt; if a provider changes pricing or retires a model, the tool keeps working exactly as it did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
