What I Built
My Downloads folder is a landfill. IMG_4821.heic, final_v3.docx, a LICENSE with no context, installers from months ago. Every few weeks I promise myself I'll sort it, and every few weeks I don't.
So I built lazy-tidy: a tiny Python script that sorts a messy folder into Documents, Images, Videos, Audio, Code, Archives, Installers and Other. It previews by default and only moves files when I pass --apply.
The "slow and steady" part: it uses the cheapest decision-maker first. Known extensions are sorted instantly with a lookup table. Only files the table can't place (Makefile, README, data.bin, photo.raw) are handed to a local open-weight model, Gemma 3 4B running through Ollama on my laptop.
Code: https://github.com/naeemahmedpnl/lazy-tidy
Demo
Before, a folder with 18 mixed files:
Preview first, and every line shows who made the decision, [extension] or [model]:
$ python tidy.py ~/tidy-demo
IMG_4821.heic -> Images/ [extension]
LICENSE -> Documents/ [model]
Makefile -> Code/ [model]
README -> Documents/ [model]
data.bin -> Archives/ [model]
photo.raw -> Images/ [model]
...
Done: 12 by extension, 6 by local model.
Preview only. Re-run with --apply to move files.
After --apply:
How It Works
- Extension first. A dictionary maps common extensions to categories. It is instant, free, and never wrong. In my demo it handled 12 of 18 files without touching a model.
- Local model for the leftovers. Anything unknown goes to Gemma 3 4B via Ollama with a prompt that asks for JSON only:
r = ollama.chat(
model="gemma3:4b",
messages=[{"role": "user", "content": prompt}],
format="json",
)
cat = json.loads(r["message"]["content"]).get("category", "Other")
return cat if cat in CATEGORIES else "Other"
-
Guardrails. If the model returns something invalid or the call fails, the file goes to
Other. If a name already exists in the destination, the file gets a_dupsuffix instead of being overwritten. Nothing moves unless I pass--apply.
Why Open Innovation Mattered Here
It works with no internet. I turned Wi-Fi off and proved it with a failing ping before running the script:
$ ping -c 2 google.com
ping: cannot resolve google.com: Unknown host
$ time python tidy.py ~/tidy-offline
...
Done: 6 by extension, 6 by local model.
python tidy.py ~/tidy-offline 4.067 total
Twelve files, six of them classified by the model, in about 4 seconds on an M1 Pro, fully offline.
My file names stay on my machine. Downloads contains invoices, resumes and personal photos. Even with just file names, that is information I would rather not send to a server I don't control, just to decide that LICENSE is a document.
It costs nothing. No API key, no per-call billing, no rate limits. I can run it on a schedule forever.
It is swappable. The model is one line, MODEL = "gemma3:4b". Changing to another open model is a one-line edit, and I can compare how different models handle the awkward files.
A closed API would have worked for this task too. But for something that touches the contents of my personal folders and should run quietly in the background, running it locally was the better fit, not just a nice-to-have.
Honest Limitations
-
The model only sees file names, never contents. My test files were empty
touchfiles, so this was purely name-based. -
It makes mistakes.
data.binwent toArchives, andOtherwould have been the better answer. It made the same choice in both runs. -
The small model is a guess on ambiguous names.
READMEandnotes_2026landing inDocumentsis reasonable, but it is a guess. - No undo. That is exactly why preview is the default. Only top-level files are handled, not subfolders.
What's Next
Read a short text snippet for ambiguous files, add an undo log, and run it on a schedule so the folder never gets bad again.
The Lazy Payoff
One command, a few seconds, no internet, and a folder I had ignored for months is tidy. Do the work once, then let the sloth handle it.



Top comments (0)