# slothsort 🦥 — a local Gemma that tidies my Downloads folder while I sleep

> Source: <https://dev.to/sotaro/slothsort-a-local-gemma-that-tidies-my-downloads-folder-while-i-sleep-7ma>
> Published: 2026-10-23 09:59:25+00:00

*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)*

My 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.

**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:

``` php
Code/
   script (1).py
     -> 2026-09-26 hacker-news-top-stories.py
Documents/
   Doc1.docx
     -> 2026-10-15 quarterly-report-q3-2026.docx
   Document (3).docx
     -> 2026-10-13 acme-corp-frontend-developer-cover.docx
Invoices & Receipts/
   download.txt
     -> 2026-10-12 apartment-4b-lease-renewal.txt
   invoice_8812(1).txt
     -> 2026-10-07 tokopedia-invoice-wh-200.txt

20 file(s), 10 folder(s). Roughly 8 minute(s) of clicking you won't have to do.
This was a dry run. Add --apply to move the files.
```

`download.txt` tells you nothing. Its *content* tells you it's the lease renewal for apartment 4B. That's the whole trick.

Who 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.**

Because the point is that you *don't* supervise it, most of my effort went into making it safe to ignore:

`--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.
A 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.

**Tidy your Downloads folder without lifting a finger, or sending a single byte to the cloud.**

slothsort 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:

``` php
Code/
   script (1).py
     -> 2026-09-26 hacker-news-top-stories.py   (Python script scrapes hacker news front page titles.)
Documents/
   Doc1.docx
     -> 2026-10-15 quarterly-report-q3-2026.docx   (Report details revenue growth and board meeting preparation.)
   Document (3).docx
     -> 2026-10-13 acme-corp-frontend-developer-cover.docx   (Clearly a cover letter for a developer role.)
Invoices & Receipts/
   download.txt
     -> 2026-10-12 apartment-4b-lease-renewal.txt   (Confirms lease renewal details with apartment address.)
   invoice_8812(1).txt
     -> 2026-10-07 tokopedia-invoice-wh-200.txt   (Clearly an invoice from Tokopedia for headphones.)
Screenshots/
   Screenshot 2026-10-01 101010.png
     -> screenshot-2026-10-01-101010.png   (matched by file extension)
20 file(s), 10 folder(s). Roughly 8 minute(s) of clicking you won't have to do.
This was a dry run. Add
```

…
One 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:

```
python -m unittest -v test_slothsort
# Ran 13 tests ... OK
```

**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.

The first real run failed on every single file with an HTTP 500. Ollama's log said:

```
CUDA error: the provided PTX was compiled with an unsupported toolchain.
```

My 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.

Once 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**.

The 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**:

```
ALLOWED = {
    "Documents": ["Documents", "Invoices & Receipts"],
    "Spreadsheets": ["Spreadsheets", "Invoices & Receipts"],
    "Images": ["Images", "Screenshots", "Invoices & Receipts"],
}

def allowed_categories(ext):
    base = EXT_CATEGORY.get(ext)
    if base is None:
        return CATEGORIES          # unknown type: let the model decide
    return ALLOWED.get(base, [base])
```

A `.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.

For 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.

So 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.

`.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.
**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.

Look 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.

With an open-weight model running locally:

`localhost:11434`. Unplug the router and it still works.`--model` flag away.
A 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*.

I pointed it at my real Downloads folder:

`desktop.ini`, which it knows to leave alone).`-2`-style suffixes. Nothing was overwritten.
