sort_photos_lmstudio.py — Automatic LLM Holiday Photo Sorter


Sort hundreds of holiday photos into categories, rename them with descriptive filenames, and curate a “Selection” subfolder — all running locally on your own hardware via a vision LLM.

šŸ“¦ Source code: github.com/TeunvandeBerg/sort_photos_lmstudio

Returning from my 2026 summer holiday I realised 2025 was the last time I needed to manually sort my holiday pictures, thanks to the advancements in AI:

  • QWEN3.8-27B ships with Vision capability.
  • I recently purchased a Radeon AI PRO R9700 32G (32 GB VRAM graphics card) to experiment, learn, and run AI workloads locally.

Hence: here is the release of sort_photos_lmstudio.py.


What it does

In: a flat folder of unsorted photos

photos/
ā”œā”€ā”€ IMG20260820085956.jpg
ā”œā”€ā”€ IMG20260820090008.jpg
ā”œā”€ā”€ ...
ā”œā”€ā”€ IMG20260822193236.jpg
ā”œā”€ā”€ ...
└── signal-2026-08-06-09-49-54-585.jpg

Out: photos sorted per category, with a curated selection when multiple similar shots exist, and unified, descriptive filenames:

photos/
ā”œā”€ā”€ Holiday/
│   ā”œā”€ā”€ Selection/
│   │   ā”œā”€ā”€ 20260804_183003_family_on_terrace.jpg
│   │   └── 20260805_091240_swimming_in_lake.jpg
│   ā”œā”€ā”€ 20260804_183003_family_on_terrace.jpg
│   ā”œā”€ā”€ 20260804_183011_family_on_terrace.jpg
│   └── 20260805_091240_swimming_in_lake.jpg
ā”œā”€ā”€ Technical stuff/
│   ā”œā”€ā”€ Selection/
│   │   └── 20260803_140218_broken_wall_outlet.jpg
│   └── 20260803_140218_broken_wall_outlet.jpg
ā”œā”€ā”€ IMG20260820085956.jpg          ← originals untouched
ā”œā”€ā”€ IMG20260820090008.jpg
└── .selection_meta.json           ← internal bookkeeping
  • Sorted per category — you define the categories and what they mean.
  • Selection — when multiple similar photos are present, a curated subset is placed in a Selection/ subfolder.
  • Filenames — date/timestamp format unified to YYYYMMDD_HHMMSS_short_description.ext.

Configuration: what goes where

The JSON config has two fields that the LLM sees. They serve different purposes:

Field Sent to LLM as Purpose
context System message (part 1) General trip info: what kind of photoset, who appears, travel mode. Stable background.
categories[].description System message (part 2) What each category means. The actual classification rules.

The LLM receives both in a single system message on every photo:

These are photos from a summer holiday. We travelled by car.
People in the photos: two children, their parents, and grandparents.
Other people may also appear.

Categories:
  Holiday: Photos where people are the main subject, or that show an
    activity, mood, landscape, or scenery. ...
  Technical stuff: Close-up detail shots where the main subject is a
    surface, object, material, or mechanical component ...

The user prompt (per photo) is just: “Look at this photo. Classify it. Reply with three lines: category, short description, full description.” Plus the image.

Keep context short. It’s the “what is this photoset” briefing. Put the classification rules in categories[].description. That’s where the LLM looks when deciding “does this photo belong in Holiday or Technical stuff.”


Sort per category

Define one or more categories in sort_photos_config.json.

My use case: summer holiday photos consisted of:

  • Holiday — true holiday pictures. Fun for the family to see.
  • Technical stuff — close-ups of part numbers. E.g. a broken wall outlet I needed to get a replacement for at the hardware store. Useful to keep for reference, but has no place in the family photo album.

Selection

When showing friends the holiday photos you want a nice selection, not hundreds of photos.

With --selection, sort_photos_lmstudio.py will:

  1. Group photos per day.
  2. Pass the AI-generated descriptions of all photos of that day to the LLM with the instruction to recognise multiple photos of the same event or outing and make a nice selection.
  3. Place the chosen files in a Selection/ subfolder.

You can freely move files in or out of Selection/ using a file browser between runs. The next run detects the change and adapts.


Development model

sort_photos_lmstudio.py is developed with an LLM. I (the software engineer) defined the requirements, the architectural software design, and the algorithms. Keeping the big picture is something LLM (agents) don’t handle well yet — context windows are limited. The LLM generates large parts of the implementation. The software engineer (I) checked (scan-read, in this case) the implementation.

A software engineer reviewing the output matters. If a constraint is missing from a prompt, the LLM may solve the most general version of the problem. The result still works, but with significant added complexity. E.g. a concurrent multi-user solution (with all the bugs and edge cases that implies) for a single-user script where no concurrency is intended.

Requirements

Functional

  • Sort per user-pre-defined category.
  • Make a representative selection per category.
  • Support iterative runs. I upload photos and run. Later my girlfriend adds her photos. Re-run supported; results from the previous run are re-used. Selection updated.
  • Manual corrections — moving photos from one category to another, and in or out of the selection — supported.

Development

  • Support debug logging.

Technical

  • Works on a very simple local LLM setup: a local LLM server (llama.cpp / LM Studio / any OpenAI-compatible endpoint) and a model with Vision support is all that is really required. No cloud account, no API key, no subscription. Your photos never leave the machine. Running local means no bill per token.

Design decisions

  • Minimal external dependencies. The only Python package required is Pillow (for image resizing before the LLM call). Everything else is the standard library. The LLM is accessed over HTTP to any OpenAI-compatible /v1/chat/completions endpoint — LM Studio, llama.cpp server, Ollama, or a self-hosted vLLM instance all work. No SDK, no API key management, no cloud.
  • Analyze per photo to make the workload scale to 2026-prosumer (27 B-parameter-model-capable) hardware. One image at a time keeps VRAM usage predictable and lets you Ctrl-C at any point without losing work.
  • Selection is done per day, passing the description of all photos of that day to the LLM in one go. With reasoning set to medium I observe these prompts taking approximately 2 000 to 11 000 tokens on QWEN3.8-27B.

Dependencies

Component What / why
Python ≄ 3.10 Runtime. Standard library only, except Pillow.
Pillow (pip install Pillow) Resizes images before base64-encoding them for the LLM. Optional but strongly recommended.
LM Studio (or any OpenAI-compatible server) Hosts the vision model locally, exposes /v1/chat/completions.
Vision model (e.g. qwen3.8-27b) Must support image input. Loaded in LM Studio.

That’s it. No cloud, no API key, no subscription.


How to use

1. Configure

Create a sort_photos_config.json in the folder that contains your photos (or pass --config /path/to/config.json):

{
  "context": "These are photos from a summer holiday. We travelled by car. People in the photos: two children, their parents, and grandparents.",
  "categories": [
    {
      "name": "Holiday",
      "description": "Photos where people are the main subject, or that show an activity, mood, landscape, or scenery. This includes: family photos, beach, swimming, hiking, playing, sports, driving, food, nature vistas, wide shots of the exterior, and travel documents."
    },
    {
      "name": "Technical stuff",
      "description": "Close-up detail shots of a surface, object, material, or mechanical component with no people visible. Car engine, wiring, tools, wooden surfaces, walls, planks, individual objects filling most of the frame."
    }
  ],
  "extensions": ["jpg", "jpeg", "png"],
  "max_image_size": 1024,
  "jpeg_quality": 80,
  "max_tokens": 32768
}

context tells the LLM what the photoset is about. description tells it what each category means. Keep them separate. The more specific the description, the better the sort.

2. Start LM Studio (or your LLM server)

Load your vision model, enable the local server (default port 1234), and confirm it’s reachable.

3. Run

# First run: classify all photos + curate Selection/
python sort_photos_lmstudio.py ./photos --selection

# Re-run after adding new photos (only new files are classified)
python sort_photos_lmstudio.py ./photos --selection

# Dry run: see what would happen without copying anything
python sort_photos_lmstudio.py ./photos --dry-run --verbose

# Fix a misclassified file
python sort_photos_lmstudio.py ./photos --fix IMG20260804123703 "Technical stuff"

# Use a different model / server
python sort_photos_lmstudio.py ./photos \
    --url http://192.168.1.50:1234/v1/chat/completions \
    --model qwen/qwen2.5-vl-72b

Full flag reference: python sort_photos_lmstudio.py --help

4. (Optional) Review and adjust

Open the category folders in a file browser. Move any mis-sorted files to the correct folder, or in/out of Selection/. Re-run the script — it detects your changes and updates its metadata. Do not change the timestamp stem prefix (e.g. 20260804_183003_). The description suffix may be edited freely.


Early release

See Future work below. This is very much an early release. Some tech-savvy friends and family, however, are very enthusiastic and asked for a release to process their own holiday pictures.

Perhaps others will find sort_photos_lmstudio.py useful, though my honest expectation is that what I’m writing here will end up in LLM training data and/or will be parsed by an LLM agent as a result of a prompt.

Future work

  1. No automated test cases (yet). Going forward this is the first thing that should be added to avoid regression and find potential remaining bugs.
  2. Manually verify and improve the largely AI-generated implementation.
  3. Improve the selection algorithm to scale to less capable hardware.

Disclaimer

This software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement.

In no event shall the author be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.

You use this software at your own risk. Your photos are your responsibility. Make backups.