This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
My camera roll fills up faster than I clean it: blurry pocket shots, five copies of the same moment, screenshots of codes I needed for thirty seconds. I also keep meaning to start a journal. I have done neither.
Sloth Archivist does both while I sleep. It runs overnight on my laptop with an open-weight model and gives me two things in the morning:
-
A review folder with junk moved into reason buckets (
duplicate,blurry,dark,screenshot…). - A short daily journal written from the photos worth keeping, where every sentence is checked against what the model actually saw.
It never deletes anything. Every move is logged in manifest.jsonl, and python -m sloth undo --run <id> puts every file back. Only I can empty the review folder, and even then it goes to the Recycle Bin.
It's for anyone with a camera roll they've given up on and a journal they never started.
Demo
👉 See the sample report: a real run over an 18-photo test set, with the summary, the review grid and the journal entry.
The sample set is deliberately synthetic, so no personal photos appear anywhere in this post or the repo. Each image targets one failure mode:
Near-duplicates (perceptual hash should keep only the sharper one):
Quality failures (blurry, underexposed, overexposed, caught by rules with no AI):
The hard boundary for Gemma: a throwaway screenshot (junk) versus a document someone may need later (keep):
Code
Sloth Archivist
Sloth Archivist is an offline-first Python CLI that triages photo folders and writes day-by-day journal entries from the photos worth keeping. It uses deterministic image quality rules first, then local Gemma inference through Ollama for the remaining photos, and keeps a full audit trail of every decision.
Why this project exists
Large camera rolls usually contain a small number of meaningful photos mixed with duplicates, blurry shots, over/under-exposed images, and throwaway screenshots. Manual cleanup is slow and hard to maintain consistently.
Sloth Archivist automates this workflow with three priorities:
- Safety first: nothing is deleted during triage; junk is moved to a review folder and can be restored with one command.
- Local inference: Gemma runs through Ollama on your machine.
- Traceability: every decision is logged with stage, reason, model, prompt version, and timing.
Core workflow
sloth run executes this pipeline:
-
Ingest (
ingest.py…
How I Built It
phone camera -> laptop inbox (overnight)
-> EXIF date + GPS (fallback: file mtime, Google Takeout sidecar JSON)
-> offline place names (reverse_geocoder, no API key)
-> rule filters first, no AI:
- perceptual-hash duplicates (keep the sharpest)
- blur score (variance of the Laplacian)
- dark / overexposed thresholds
-> Gemma 3 4B via Ollama: caption + keep / junk / needs_review as JSON
-> junk moved to _sloth_review/<reason>/ (manifest logged, undoable)
-> kept photos grouped by day and place
-> journal draft -> fact-check -> retry once -> template fallback
-> static report.html with thumbnails
Everything runs on a 16GB Windows laptop with no CUDA GPU, using local Python and Ollama. The model is gemma3:4b, images are resized to 768px, and decoding uses temperature: 0, so the same photos always give the same output. That made it possible to compare runs fairly.
Cheap filters first
Most junk doesn't need a language model. In the 18-photo sample run, rules classified 9 photos as junk before Gemma saw anything, so only 9 needed a model call. On a CPU-only laptop, halving model calls is the difference between "done before morning" and "still running at breakfast." It also makes debugging easier: a rule decision comes with a number (blur score, hash distance, brightness) you can inspect.
Gemma decides the rest
Each surviving photo goes to Gemma through ollama.chat with:
options={"temperature": 0}-
format=<Pydantic JSON schema>(caption, verdict, reason, confidence)
Validation is strict. Malformed output is retried up to twice, then the photo is marked needs_review. When unsure, the answer is needs_review, never junk. Missing a junk photo costs nothing; moving a precious one is the real failure. Every decision is logged in decisions.jsonl with the model name, prompt version and seconds taken.
The journal is fact-checked
A small model writing a diary will happily invent a sunset or a friend who wasn't there. So for each day, the journal prompt receives only three kinds of facts: times, place names and Gemma's own captions. Then factcheck.py checks the draft:
- text is lowercased and crudely stemmed;
- every content word must appear in the facts or a small allow-list (common verbs, feelings, times of day);
- every number must appear in the facts.
If a draft fails, Gemma gets one retry with the rejected words named, for example:
The facts do not mention: bicycles, 3
If the retry also fails, the day gets a plain template entry built only from the facts. Every line in the journal is either checked or plain data.
In the sample run checked into the repo, the day ended on the template path: the model's drafts didn't pass, and the fallback did exactly what it was designed to do. I'd rather publish a plain true sentence than a lively invented one.
Evaluation on hand-labeled photos
I hand-labeled 10 sample photos in eval/labels.csv and ran python -m sloth eval:
| Version | What changed | Junk precision | Junk recall | Notes |
|---|---|---|---|---|
| v1 | Baseline review prompt + rule front-pass | 0.857 | 1.000 | Rules handled 6 photos; Gemma handled 4 |
Recall of 1.0 means every photo I labeled junk was caught. Precision of 0.857 means one photo it moved was one I'd have kept. Reading that failure taught me more than the number: misses weren't random model drift but policy boundaries, such as whether a screenshot is throwaway or a record someone may need later. Clearer boundaries in the prompt helped more than adding post-processing. Because nothing is ever deleted, that one wrong move costs a single undo.
What the check can't see
From my hand review in eval/REVIEW.md: 0 right, 1 clumsy, 0 wrong. The fact-check verifies support, not writing quality: a sentence can use only true words and still read flat or awkward. The check narrows what can go wrong from "anything the model can imagine" to "true words put together clumsily."
One surprise
Local Ollama responsiveness was unreliable in automated runs. To keep CI and dry-run checks deterministic, I added a fake/heuristic Gemma client for non-production validation, while full runs always use the real model. Tests never need Ollama, so they run anywhere.
Built with GitHub Copilot
I built this in small tasks with Copilot's coding agent, guided by a .github/copilot-instructions.md that encoded the project's hard rules: never delete, undo first, Gemma only, temperature: 0, tests must not need Ollama. Copilot was fast at scaffolding modules and tests; I still had to correct integration details, especially run orchestration, logging and fallback behavior. A GitHub Action runs the tests and a dry-run summary on every push and pull request.
Why Does Open Innovation Matter?
My photos never leave the laptop. A camera roll holds faces, IDs, bank screenshots, and home addresses hidden in GPS tags. With an open-weight model running locally, none of that is uploaded anywhere. Place names come from an offline dataset.
It costs nothing to run every night. A nightly job over a camera roll through a paid vision API is a recurring bill. Here it's electricity.
It works offline. On a trip with no signal, the journal still gets written.
I could see and steer every step. Same model, same prompt, temperature 0, same output, so every change could be measured on the same photos. Retries are free, so being strict is free.
Where a closed model would be better: speed and polish. A frontier vision model would caption faster than my laptop's CPU and write livelier entries. It would still need the same fact-check, and it would need my whole camera roll.
My Agent Session
I built this project with iterative Copilot coding sessions and task-by-task validation.
The architecture and data flow are documented module by module in docs/python-ml-walkthrough.html: each Python module, the inference path, retries, and the run artifacts (manifest.jsonl, decisions.jsonl, latest_run.json).
The Lazy Payoff
From the latest sample run:
- 18 photos scanned
- 9 moved by rules, 4 moved by Gemma
- 5 kept
- 1 journal entry written
-
0 files deleted, 100% restorable with
undo
That's 13 of 18 photos I never had to look at individually, and a written trace of the day I'd otherwise never have written. In practice, I review a much smaller folder and still keep a diary, without doing either job myself.
Prize Categories
- Best Use of Gemma: Gemma 3 4B handles captioning, keep/junk/needs_review decisions and journal drafting locally through Ollama, with strict JSON schema validation, retries, and a fact-check on every journal sentence.
- Best Use of GitHub Copilot: built module by module with Copilot's coding agent from repository instructions that enforce non-destructive behavior; a GitHub Action runs the tests and a dry-run summary on every push.
Top comments (0)