discover · compare · collections · requirements · contribute · feed · watching · stats · help with your tokens
Ivamic/immich-steward · source
Snapshot 0 · 2026-11-04T12:47:28Z · Python · AGPL-3.0
Content fingerprint
1d92e4b58ff486a4178ebe8e250123502e6cb945cc32d5c90ff7a50daa791c97GitHub description at capture
Self-hosted companion for Immich that finds where photos without GPS were taken. Suggestions from photos taken close in time and from a local GeoCLIP model are shown on one map — nothing is written to Immich until you confirm.
README at capture
<p align="center">
<img src="docs/images/logo.svg" width="96" height="96" alt="Steward for Immich logo">
</p>
<h1 align="center">Steward for Immich</h1>
<p align="center">
Put your photos without GPS back on the map — several independent clues, one map, your pick.
</p>
<p align="center">
<a href="https://github.com/Ivamic/immich-steward/releases"><img src="https://img.shields.io/github/v/release/Ivamic/immich-steward?include_prereleases&sort=semver" alt="Release"></a>
<a href="https://github.com/Ivamic/immich-steward/actions/workflows/ci.yml"><img src="https://github.com/Ivamic/immich-steward/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Ivamic/immich-steward" alt="License: AGPL-3.0"></a>
<img src="https://img.shields.io/badge/self--hosted-Docker%20Compose-2496ED?logo=docker&logoColor=white" alt="Self-hosted with Docker Compose">
</p>
<p align="center">
<b>English</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.it.md">Italiano</a> · <a href="README.ru.md">Русский</a>
</p>
<p align="center">
<img src="docs/images/time-neighbors-route.png" alt="Photo card: the route between photos taken shortly before and after, with three suggested locations on the map and in the list">
</p>
> [!IMPORTANT]
> **Updating to 0.6 from 0.5 or older: one manual step, once.**
>
> **Why.** Up to 0.5, `compose.yaml` named the exact version of the app, so every update
> also needed `git pull`. From 0.6 on, `compose.yaml` follows the major version and reads all
> settings from `.env`: it stays the same, and every later update is just `docker compose pull`
> and `docker compose up -d`.
>
> **How.** In the folder of the app; your `.env` and your data stay as they are.
>
> Without the worker:
>
> ```sh
> curl -L -o compose.yaml https://github.com/Ivamic/immich-steward/releases/latest/download/compose.yaml
> docker compose pull
> docker compose up -d
> ```
>
> With the worker on the server:
>
> ```sh
> curl -L -o compose.yaml https://github.com/Ivamic/immich-steward/releases/latest/download/compose.yaml
> docker compose --profile worker pull
> docker compose --profile worker up -d
> ```
>
> If `.env` has a line `STEWARD_VERSION=…`, remove it first: it would keep the old version. [Details](docs/updating.md#coming-from-05-or-older).
> **Status: under active development.** An early version: reviewing photos without a location,
> suggestions from photos taken close in time, GeoCLIP and similar photos of your own library
> already work; the other modules are still to come. Expect bugs and changes between versions, and keep a backup of your Immich
> library before writing locations to it.
A self-hosted companion app for [Immich](https://immich.app). For each photo without a location,
several modules — photos taken close in time, the GeoCLIP neural network and similar photos
of the same place in your own library — suggest where it was taken, all on one map. You pick
one with a click or a key, or set the place of many photos at once. Nothing is written to
Immich until you do, and every change can be undone.
Typical use case: a family photo archive with scanned prints, old digital cameras
without GPS, or messenger downloads that lost their metadata.
## Idea
The app combines several independent **location detection modules**. Each module
proposes one or more candidate locations, and all candidates are shown together on a
map, color-coded by module and numbered by rank. You review them, pick one, or place a
point manually.
Available modules:
- **Time neighbors** — photos taken shortly before and after that already have a location. The
route between them is drawn on the map, colored by time; cameras with a wrong clock can be
excluded, or only the photo's own camera kept.
- **GeoCLIP** *(optional worker)* — worldwide image geolocation by a neural network, running
locally on the server CPU or on a GPU of another computer. Shown by confidence, with a gallery
filter for its confident guesses. It can also look at a part of the photo only: a building
behind a crowd, a sign, a mountain on the horizon.
- **Similar photos** *(optional worker)* — photos of the same place in your own library, with a
location: the same street, house, view or room, from another angle or in another season. Uses
[MegaLoc](https://github.com/gmberton/MegaLoc), a model made for recognizing places; on a real
family library, about half of its suggestions are right within 1 km, and more than eight
out of ten of its confident ones. The search always includes the locations you have set since.
A photo can be rotated in the app for a better look, the modules run on it rotated, and the
rotation can be saved to Immich.
To check a place by eye, satellite imagery can be shown under the map: Sentinel-2 by EOX
(no key), Esri World Imagery or MapTiler Satellite (with your own key). All of them are off
until you switch one on.
The gallery shows your photos by month and day and marks the ones with an easy answer: a photo
with a location taken minutes before or after, a confident GeoCLIP guess, a similar photo with a
location. Videos play in the gallery and in the photo card. Everything works on a phone too.
**Groups** gather the photos of one view by themselves, with a location or without: when the photos
with a location agree on a spot, the place can be set on the others in one step, after a look at
the map. A subject that moved over the years shows month by month, a spot that does not fit stands
out, and photos that tell nothing of the place (a pet, a document) can be marked, so that they
suggest nothing.
Planned:
- **Street-level panoramas** on the map, to check a place by eye.
- **Geometric verification** of similar photos (matching details, not only the overall look),
and finding by itself the photos that show no place at all: close-up portraits, pets, plants,
objects.
- **LLM reasoning** (Img2Loc-style) — a vision-language model reasons over the photo
plus the evidence collected by the other modules.
- **External services** — optional cloud recognizers, one module per service.
New modules can be added later as plugins.
## Screenshots
<table>
<tr>
<td width="50%"><img src="docs/images/geoclip-confident.png" alt="GeoCLIP suggests the Bay of Kotor with 78% confidence, in the dark theme"></td>
<td width="50%"><img src="docs/images/geoclip-and-time-neighbors.png" alt="Suggestions of three modules on one map: a GeoCLIP area, a time neighbor from another camera and similar photos"></td>
</tr>
<tr>
<td>GeoCLIP recognizes the place from the image alone: a confident guess for a camera without GPS (dark theme).</td>
<td>Several modules at once: the GeoCLIP area, a phone photo taken two hours later and similar photos agree on the city.</td>
</tr>
<tr>
<td colspan="2"><img src="docs/images/gallery.png" alt="Gallery of photos without a location, filtered to confident GeoCLIP guesses, with the filter chips and the timeline"></td>
</tr>
<tr>
<td colspan="2">The gallery: filters as chips, a badge on every photo with a location or a confident guess, the timeline on the right.</td>
</tr>
<tr>
<td width="50%"><img src="docs/images/processing.png" alt="Processing page: library counts, workers and the progress of each module"></td>
<td>Processing: the library and its sync, the workers and what they do, and the progress and failures of each module, with a pause for the background runs.</td>
</tr>
</table>
## Principles
- **Nothing is written to Immich automatically.** Coordinates are sent only after you
confirm a location. Every change can be undone.
- **Local first.** Default modules run on your own hardware. Modules that send images
to third-party services are never run automatically — only on an explicit request,
with a clear warning about what is sent and where.
- **Region-based search.** If the whole picture is misleading (for example, a crowd in front
of a monument), select a part of the image and run detection on that region only.
- **"Not applicable" flag.** Mark photos where a location makes no sense, so they never
show up in your to-do list again.
## Installation
Steward runs with Docker Compose next to your Immich server (Immich v3.x):
```sh
mkdir immich-steward && cd immich-steward
curl -L -o compose.yaml https://github.com/Ivamic/immich-steward/releases/latest/download/compose.yaml
curl -L -o example.env https://github.com/Ivamic/immich-steward/releases/latest/download/example.env
cp example.env .env # set STEWARD_IMMICH_URL, STEWARD_SECRET_KEY and DB_PASSWORD
docker compose up -d
```
The images are pulled from GitHub Container Registry (`ghcr.io/ivamic/immich-steward-core`,
`amd64` and `arm64`); they can also be
[built from source](docs/updating.md#building-the-images-from-source). Updating is
`docker compose pull && docker compose up -d`.
Then open `http://<your-server>:8780` and sign in with your Immich account.
GeoCLIP and similar photos run in an optional worker container (2 to 4 GB of RAM with both
models loaded): set `STEWARD_WORKER_TOKEN` in `.env` and start with
`docker compose --profile worker up -d`.
Workers can also run on other computers in your network: **Processing → Add a device** shows
the commands to copy ([more](docs/workers.md#a-worker-on-another-computer)).
The [documentation](docs/README.md) explains it all step by step:
[installation](docs/install.md), [using the app](docs/usage.md), [workers](docs/workers.md),
[configuration](docs/configuration.md), [updating](docs/updating.md),
[privacy](docs/privacy.md) and [troubleshooting](docs/troubleshooting.md).
## Architecture
- Backend: Python (FastAPI), PostgreSQL with pgvector.
- Runs with Docker Compose next to your Immich server.
- ML modules run in workers: separate processes that take jobs from the core over HTTP, on the
server CPU or on another computer with a GPU. Workers never see an Immich API key: the core
gives them the photos.
- Web UI: timeline gallery with filters, photo view with a map, keyboard-driven review.
- UI in English, German, French, Italian and Russian.
## Development
Repository layout:
| Path | Contents |
|---|---|
| `core/` | Core service: Python package `steward` (FastAPI), Alembic migrations, tests |
| `worker/` | Worker: Python package `steward_worker` that runs ML location modules (GeoCLIP, similar photos) |
| `frontend/` | Web UI: SvelteKit single-page app; translations in `frontend/messages/` |
| `docker/` | Dockerfiles of the core image (with the built web UI) and of the worker image (CPU); `.github/workflows/release.yml` publishes them for every version tag |
| `compose.yaml` | Core service, PostgreSQL with pgvector, optional worker (profile `worker`); attached to every release |
| `compose.build.yaml` | Builds the images from this source instead of pulling them |
| `docs/` | User documentation, starting at [docs/README.md](docs/README.md) |
Requirements: Python 3.12 with [uv](https://docs.astral.sh/uv/), Node.js 24 with pnpm, Docker.
Run all commands from the repository root unless stated otherwise.
```sh
cp .env.example .env # then set DB_PASSWORD and STEWARD_DATABASE_URL
# Database only, exposed on localhost
docker compose -f compose.yaml -f compose.dev.yaml up -d db
# Core service on http://127.0.0.1:8080
uv sync
uv run alembic -c core/alembic.ini upgrade head
uv run python -m steward --reload
# Web UI dev server on http://localhost:5173 (proxies /api to the core)
cd frontend && pnpm install && pnpm dev
# Optional: a worker with GeoCLIP and similar photos (PyTorch for the CPU; --extra cuda for an
# NVIDIA GPU). The core needs the same STEWARD_WORKER_TOKEN in .env.
uv sync --all-packages --extra geoclip --extra similar --extra cpu
STEWARD_WORKER_TOKEN=... uv run python -m steward_worker --core http://127.0.0.1:8080 --modules similar,geoclip
```
Checks (the same as in CI):
```sh
uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest
cd frontend && pnpm check && pnpm lint && pnpm test && pnpm check:licenses
```
Database integration tests run when `STEWARD_TEST_DATABASE_URL` is set.
Full stack in Docker (UI on http://localhost:8780 by default):
```sh
docker compose -f compose.yaml -f compose.build.yaml up -d --build
```
### Translations
UI strings live in `frontend/messages/<locale>.json`; English (`en.json`) is the source.
A new string must be added to every locale file at once — a test checks that all locales
have the same keys and placeholders.
The German, French and Italian translations were made with the help of AI and have not been
reviewed by native speakers yet. Corrections are very welcome: open a pull request against the
locale file or an issue with the wrong wording.
The README is translated too (`README.<locale>.md`), without the development sections. The
English README is the reference: a translation is outdated when `README.md` was changed in a
later commit than the translation.
## Feedback and contributing
- **Bugs** — open an [issue](https://github.com/Ivamic/immich-steward/issues) with the versions
of Steward and Immich and the steps to reproduce.
- **Ideas and questions** — start a thread in
[Discussions](https://github.com/Ivamic/immich-steward/discussions). Stories of how you use the
app and which photos it fails on are welcome too.
- **Pull requests** are welcome. For anything bigger than a small fix, please discuss it in an
issue or a discussion first, so that your work fits the plans of the project.
- **Security issues** — please report them privately, see [SECURITY.md](SECURITY.md).
## Support the project
Steward is free and developed in spare time. If it saved you hours of manual tagging,
you can support its development with a donation in cryptocurrency. Starring the repository,
reporting bugs and sharing the project help too.
| Currency | Network | Address |
|---|---|---|
| USDT | TRON (TRC-20) | `TAqGEvmDTw2ro4BKQmcPPtqK5xEMfNaiSX` |
| ETH, USDT and other tokens | Ethereum and EVM networks (Base, Polygon, Arbitrum…) | `0x783C192d69fE5E9DAfa1dC2c41FeE272122AE031` |
| SOL | Solana | `38gidbL2ocv5c6CK7zP7c3QWVHisYyqhnrMV3tuaYpgp` |
| BTC | Bitcoin | `bc1qsdys5hvx7vlxp25mz77hfgz2l35x8klc8z2npx` |
Please double-check the network before sending: funds sent over a different network may be lost.
## License
Copyright (C) 2026 Ivamic.
[GNU Affero General Public License v3.0 or later](LICENSE) (`AGPL-3.0-or-later`).
**Name and logo.** The name "Steward for Immich" and its logo (`docs/images/logo.svg`,
`frontend/src/lib/assets/favicon.svg` and the mark drawn in `frontend/src/lib/ui/Brand.svelte`)
are not licensed under the AGPL; all rights are reserved by the author. You may use them to refer
to this project. Forks and modified versions must use a different name and logo.
## AI assistance
This project is developed with the help of AI tools (Claude by Anthropic).
All code is reviewed and maintained by a human author.
## Disclaimer
This is an independent community project. It is not affiliated with or endorsed by the
Immich project or FUTO.