{"slug": "slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep", "title": "slothsort 🦥 — a local Gemma that tidies my Downloads folder while I sleep", "summary": "A developer built slothsort, an MIT-licensed single-file Python tool that uses Gemma 3 4B served locally through Ollama to read the contents of files in a messy folder and propose human-readable names and destination folders, with a dry-run default, an --undo that restores the last run even if interrupted, and skips for partial downloads and recently touched files. The tool runs entirely on-device, tested on a GTX 1050 Ti with 4 GB of VRAM and 32 GB of RAM, and ships with a demo-folder generator and 13 tests that use a fake Ollama server to run in under a second without a GPU.", "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\nMy Downloads folder had 41 files in it. `Doc1.docx`. `download.txt`. `script (1).py`. Two copies of `setup_v2.exe`. Every few weeks I'd open it, sigh, and close it again.\n\n**slothsort** is the sloth that does it for me. Point it at a messy folder and a small open-weight model running *on my own PC* reads each file, decides what it is, and proposes a home and a name a human can actually understand. This is real output from Gemma 3 4B:\n\n``` php\nCode/\n   script (1).py\n     -> 2026-09-26 hacker-news-top-stories.py\nDocuments/\n   Doc1.docx\n     -> 2026-10-15 quarterly-report-q3-2026.docx\n   Document (3).docx\n     -> 2026-10-13 acme-corp-frontend-developer-cover.docx\nInvoices & Receipts/\n   download.txt\n     -> 2026-10-12 apartment-4b-lease-renewal.txt\n   invoice_8812(1).txt\n     -> 2026-10-07 tokopedia-invoice-wh-200.txt\n\n20 file(s), 10 folder(s). Roughly 8 minute(s) of clicking you won't have to do.\nThis was a dry run. Add --apply to move the files.\n```\n\n`download.txt` tells you nothing. Its *content* tells you it's the lease renewal for apartment 4B. That's the whole trick.\n\nWho gets to be lazier? Anyone whose Downloads folder is a graveyard. Run it once to see the plan, add `--apply`, then schedule it nightly and forget it exists. **Set it up once, never think about it again.**\n\nBecause the point is that you *don't* supervise it, most of my effort went into making it safe to ignore:\n\n`--apply`.`--undo` puts the last run back exactly as it was, even if the run was interrupted halfway (every move is logged the moment it happens).`name-2.pdf`.`.crdownload`, `.part`, and anything touched in the last two minutes are skipped.\nA real session on a 21-file test folder: dry run, apply, look at the result, undo. Gemma's 114 seconds of reading are sped up; everything else is as it printed.\n\n**Tidy your Downloads folder without lifting a finger, or sending a single byte to the cloud.**\n\nslothsort looks at every file in a messy folder, asks a small open-weight model running on your own machine (Gemma, via [Ollama](https://ollama.com)) what it is, and proposes a folder and a human-readable name for it:\n\n``` php\nCode/\n   script (1).py\n     -> 2026-09-26 hacker-news-top-stories.py   (Python script scrapes hacker news front page titles.)\nDocuments/\n   Doc1.docx\n     -> 2026-10-15 quarterly-report-q3-2026.docx   (Report details revenue growth and board meeting preparation.)\n   Document (3).docx\n     -> 2026-10-13 acme-corp-frontend-developer-cover.docx   (Clearly a cover letter for a developer role.)\nInvoices & Receipts/\n   download.txt\n     -> 2026-10-12 apartment-4b-lease-renewal.txt   (Confirms lease renewal details with apartment address.)\n   invoice_8812(1).txt\n     -> 2026-10-07 tokopedia-invoice-wh-200.txt   (Clearly an invoice from Tokopedia for headphones.)\nScreenshots/\n   Screenshot 2026-10-01 101010.png\n     -> screenshot-2026-10-01-101010.png   (matched by file extension)\n20 file(s), 10 folder(s). Roughly 8 minute(s) of clicking you won't have to do.\nThis was a dry run. Add\n```\n\n…\nOne Python file, standard library only, MIT licensed. `make_demo_folder.py` builds a realistic messy folder so you can try it without risking your own files. Tests spin up a fake Ollama server, so they run in under a second with no GPU:\n\n```\npython -m unittest -v test_slothsort\n# Ran 13 tests ... OK\n```\n\n**The stack:** Gemma 3 4B, served locally by Ollama, called from a single Python script with nothing but the standard library. My machine is a desktop with a **GTX 1050 Ti (4 GB)** and 32 GB of RAM, which is a nice, honest \"normal person's computer\" to test on. It also turned out to be the most interesting part of the project.\n\nThe first real run failed on every single file with an HTTP 500. Ollama's log said:\n\n```\nCUDA error: the provided PTX was compiled with an unsupported toolchain.\n```\n\nMy NVIDIA driver was older than the CUDA build Ollama ships with. A closed API would never have shown me this, and it would never have been my problem either. But with open weights, the model was already sitting on my disk and could run on the CPU. One option, `\"num_gpu\": 0`, fixed it. So slothsort now does this itself: if the GPU can't load the model, it says so once, switches to the CPU, and carries on. On the CPU it takes about **10 to 17 seconds per readable file**, depending on how much text there is. That doesn't matter for a job that runs at 3 AM.\n\nOnce it ran, the names were great from the start. The folders were not. Gemma put the Tokopedia invoice in **Ebooks**, a Python script in **Screenshots**, and the VS Code installer in **Video**.\n\nThe fix was to stop asking the model a question the file extension already answers. slothsort sends Ollama a JSON schema whose `enum` is built **per file**:\n\n```\nALLOWED = {\n    \"Documents\": [\"Documents\", \"Invoices & Receipts\"],\n    \"Spreadsheets\": [\"Spreadsheets\", \"Invoices & Receipts\"],\n    \"Images\": [\"Images\", \"Screenshots\", \"Invoices & Receipts\"],\n}\n\ndef allowed_categories(ext):\n    base = EXT_CATEGORY.get(ext)\n    if base is None:\n        return CATEGORIES          # unknown type: let the model decide\n    return ALLOWED.get(base, [base])\n```\n\nA `.py` file can only ever be Code. A `.txt` can be a document *or* a receipt, and that's the judgement call worth spending a model on. Ollama's structured outputs constrain generation to the schema, so the model physically can't pick a folder outside the list. After this change, every file in my test folder landed somewhere sensible.\n\nFor a photo called `IMG_20260914_183022.jpg`, the model only sees the filename, and it cheerfully renamed it `img-2026-september-screenshot.jpg`. It wasn't a screenshot. Small models embellish when they have nothing to go on.\n\nSo files with no readable text now skip the model entirely and just get their names tidied (`setup_v2 (1).exe` → `setup-v2.exe`). It's more honest, and it more than halved the run time: **from 187 to 94 seconds** for the 20-file test folder.\n\n`.docx`, `.pptx` and `.xlsx` are just zipped XML, so the standard library can read them).`{category, name, reason}` at temperature 0, with the category limited by extension.`Category/YYYY-MM-DD descriptive-name.ext`, resolving clashes.`.slothsort/history.jsonl` before the next, so undo always knows the truth.\n**Scheduling** is just the OS: one `schtasks` line on Windows or one cron line on macOS/Linux. No daemon, no tray icon, nothing to keep running.\n\nLook at what my test folder held: a lease with a home address, a CV, a cover letter, an invoice, a spending export. A real Downloads folder is worse: bank statements, payslips, scanned IDs. That is exactly the stuff I would **never** upload to a cloud API just so a model can suggest a filename.\n\nWith an open-weight model running locally:\n\n`localhost:11434`. Unplug the router and it still works.`--model` flag away.\nA closed API would have been easier to prompt, honestly. But \"upload the contents of your Downloads folder to a third party every night\" is a non-starter, and that's the whole product. Open weights didn't make this project *better*; they made it *possible*.\n\nI pointed it at my real Downloads folder:\n\n`desktop.ini`, which it knows to leave alone).`-2`-style suffixes. Nothing was overwritten.", "url": "https://wpnews.pro/news/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep", "canonical_source": "https://dev.to/sotaro/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep-7ma", "published_at": "2026-10-23 09:59:25+00:00", "updated_at": "2026-10-23 10:11:36.838923+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "ai-agents", "developer-tools"], "entities": ["Gemma 3 4B", "Ollama", "NVIDIA", "GTX 1050 Ti", "slothsort", "Hacktoberfest"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep", "markdown": "https://wpnews.pro/news/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep.md", "text": "https://wpnews.pro/news/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep.txt", "jsonld": "https://wpnews.pro/news/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep.jsonld"}}