- Clojure 97.6%
- Makefile 1.3%
- Swift 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
test-support module: fake-tools builder encodes the tools seam argument/output protocol once — auge single and --ndjson shapes, apfel, apfel-tag, ffmpeg, ffprobe, tags binary, which/swiftc. A flag change in tools.clj now updates one fake, not one per test file. Closes architecture-review candidate 2. - all per-file op-dispatch fakes deleted; specs read as data - shared temp-dir / write-file! / with-temp-dir / fake-run / capturing-run replace byte-identical local copies - ndjson map handlers emit one JSON line per stdin path (the real protocol); fn handlers remain for per-path variance - unspecified programs fail loud so protocol drift fails tests - assertion content unchanged; 80 tests / 178 assertions green |
||
| mac/tags | ||
| src/clj_apt_reaction_image | ||
| test/clj_apt_reaction_image | ||
| .gitignore | ||
| CONTEXT.md | ||
| deps.edn | ||
| Makefile | ||
| README.md | ||
clj-apt-reaction-image
On-device reaction image organiser — rename and tag your maymays using Apple's built-in Vision and Foundation Models.
No cloud. No API keys. No Ollama. 100% on-device.
Tested on my M5 MacBook on 400+ images. Works.
What it does
Walks a directory of images and clips (GIFs, mp4/mov/m4v/webm/mkv videos), then for each one:
- Frame sampling — ffmpeg extracts N still frames for analysis (clips only; default 6)
- OCR — extracts visible text (
auge --ocr) - Classify — identifies objects in the scene (
auge --classify) - Detect — counts faces (
auge --faces) - Tag — generates Finder tags for Spotlight search (
apfel-tag) - Rename — creates a short kebab-case filename (
apfel) - Apply — renames the file and sets Finder tags
# Before: distracted-bf.jpg doge.jpg screenshot.png
# After: people-adult-clothing-jacket-c.jpg corgi-dog.jpg telegram-edit-view-window.png
Requirements
- macOS 26+ (Tahoe) on Apple Silicon
- Homebrew
- Apple Intelligence enabled in System Settings
Install
# Install on-device tools
make install
# Compile the Finder-tag helper
make compile
Usage
Dry run (preview changes without touching files):
make run IMAGES_DIR="~/iCloud/Pictures/maymays" ARGS="--dry-run"
Actually rename and tag:
make run IMAGES_DIR="~/iCloud/Pictures/maymays"
Or run directly with Clojure:
clojure -M:run organize --images-dir "~/iCloud/Pictures/maymays" --dry-run
Re-runs skip finished files. A file that already carries Finder tags is
treated as categorized and skipped, so re-running on the same directory only
picks up new additions. Force a full reprocess with -a / --all:
clojure -M:run organize --images-dir "~/iCloud/Pictures/maymays" --all
JSON output for scripting:
clojure -M:run organize --images-dir "~/iCloud/Pictures/maymays" --output json --dry-run
More frames for dense clips:
clojure -M:run organize --images-dir "~/iCloud/Videos/gifs" --frames 12 --dry-run
How it works
| Step | Tool | What it produces |
|---|---|---|
| Frame sampling | ffmpeg |
N still frames from each clip (default 6) |
| OCR | auge --ocr |
visible text from the image |
| Classification | auge --classify |
object/scene labels (top 5) |
| Face detection | auge --faces |
number of faces |
| Content tagging | apfel-tag |
Finder tags for search |
| Smart naming | apfel (Apple Foundation Model) |
short kebab-case filename |
| Read/set tags | mac/tags/main.swift |
reads and writes Finder tags via xattr |
All tools run Apple's on-device frameworks — Vision (VNRecognizeTextRequest,
VNClassifyImageRequest, VNDetectFaceRectanglesRequest) and FoundationModels.
Tests
make test
# or: clojure -M:test
Project structure
├── Makefile — install, compile, run, test
├── deps.edn — Clojure deps (just data.json)
├── src/
│ └── clj_apt_reaction_image/
│ └── core.clj — pipeline orchestrator
├── mac/
│ └── tags/
│ └── main.swift — Finder tag helper
└── test/
└── clj_apt_reaction_image/
└── core_test.clj — unit tests
Caveats
- Apple Intelligence must be enabled in System Settings for
apfelandapfel-tag. - Safety guardrails on Apple's Foundation Model may block images with people, falling back to label-based naming.
- Images with visible text (screenshots, memes, chat images) get the best filenames.
- Plain reaction images without text get descriptive-but-generic names from classification labels.
- Clips are judged by sampled frames only — a punchline that appears between
samples can be missed (raise
--framesfor dense clips). - webm and mkv have no native macOS support and are handled entirely through ffmpeg frame extraction.