- Python 100%
| digikam_nextcloud | ||
| .gitignore | ||
| config.example.yaml | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
| sync_faces.py | ||
digikam-nextcloud
One-way sync of face regions from digiKam → Nextcloud Recognize.
digiKam is the source of truth. Nothing is written back to digiKam.
Layout
digikam_nextcloud/ # Python package
cli.py # argparse / main
sync.py # orchestration
digikam.py # digiKam SQLite reader
nextcloud_db.py # SQL backend (+ SSH tunnel)
nextcloud_http.py # WebDAV / Recognize DAV backend
matching.py # file + region matching
models.py # dataclasses + backend protocol
session_state.py # cancel/resume session files
…
sync_faces.py # thin CLI wrapper
config.example.yaml
python sync_faces.py --config config.yaml
# or
python -m digikam_nextcloud --config config.yaml
What it does
- Reads named face rectangles from digiKam’s
digikam4.db - Matches those files to Nextcloud
- Compares regions with Recognize faces via IoU
- Assigns overlapping faces to digiKam people
- Optionally inserts digiKam-only regions (DB backend only)
- Reports conflicts when the same box has different names
Default mode is dry-run. Pass --apply to write.
Backends
Default / preferred: http (WebDAV + Recognize DAV with an app password).
| Backend | How it connects | Insert new face boxes | Assign / rename people |
|---|---|---|---|
http (default) |
Nextcloud WebDAV + Recognize DAV | No | Yes (MOVE / MKCOL) |
db |
SQL (+ optional SSH tunnel) | Yes | Yes |
HTTP / WebDAV (preferred)
Tuned for large libraries (keep-alive connections, concurrent workers, targeted PROPFIND only on digiKam-faced images — no full tree scan):
PROPFIND Depth:0on each mapped file path +nc:face-detectionsMKCOLpeople under/remote.php/dav/recognize/{user}/faces/{Person}MOVEdetections into person collections
export NEXTCLOUD_PASSWORD='your-app-password'
python sync_faces.py --config config.yaml \
--http-workers 24 --batch-size 500 --no-insert-missing
Configure path_maps so digiKam filesystem paths become Nextcloud paths
(e.g. /home/you/Nextcloud/Photos/… → Photos/…).
Recognize 403 / X-Recognize-Api-Key
Modern Recognize requires a server-generated API key for
/remote.php/dav/recognize/… (create people, MOVE/assign faces).
This tool will try to auto-fetch the key from the Photos app page
(initial-state-photos-recognizeApiKey, valid ~24h). That needs the Photos
app enabled.
If you still get 403:
# Admin: turn off the gate (simplest for self-hosted)
occ config:app:set recognize require_api_key --value false
# Or pass a key (from Photos page HTML, expires ~24h)
export RECOGNIZE_API_KEY='…'
# config: recognize_api_key: "…"
HTTP limits
- Cannot create new face rectangles → run Recognize detect first, then assign
- Unclustered detections (
cluster_idNULL) are not MOVE-able until clustered - Use a Nextcloud app password
DB over SSH (optional)
When you need inserts or raw SQL access:
python sync_faces.py --config config.yaml --backend db \
--ssh-host nextcloud-server.example.com \
--ssh-user cliff \
--ssh-identity ~/.ssh/id_ed25519
Requirements
- Python 3.10+
- digiKam SQLite core DB (
digikam4.db) - OpenSSH client (for SSH tunnel)
- DB drivers as needed:
PyMySQL/psycopg2-binary
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Configuration
cp config.example.yaml config.yaml
# edit digikam_db, backend, DSN or URL, nc_user, path_maps, ssh
Path matching
| digiKam path | Nextcloud path | path_map |
|---|---|---|
/mnt/nas/Photos/2024/a.jpg |
files/Photos/2024/a.jpg |
from: /mnt/nas/Photos → to: Photos |
Fallback: unique filename + file size.
Coordinates
| System | Box units |
|---|---|
digiKam tagRegion |
Pixels |
| Recognize | Relative 0–1 |
Usage
# Dry-run
python sync_faces.py --config config.yaml
# Write
python sync_faces.py --config config.yaml --apply
# JSON report
python sync_faces.py --config config.yaml --json-out report.json
# Only name existing detections
python sync_faces.py --config config.yaml --no-insert-missing --apply
# Report conflicts without overwriting NC names
python sync_faces.py --config config.yaml --leave-conflicts
# Single person only (tests / quick updates — skips full-library scan)
python sync_faces.py --config config.yaml --only-person Alice
python sync_faces.py --config config.yaml --only-person Alice --apply
# Cancel / resume (large libraries)
# Every run creates a session file under .sync_sessions/ and prints the id.
# Ctrl+C stops after the current image; re-run with --resume to continue.
python sync_faces.py --config config.yaml --apply
# … cancel with Ctrl+C …
python sync_faces.py --config config.yaml --apply --resume 20260722-120000-abc123
python sync_faces.py --list-sessions
python sync_faces.py --config config.yaml --apply --no-session # opt out
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success, no conflicts |
| 1 | Completed with conflicts reported |
| 2 | Usage / config error |
| 130 | Cancelled (SIGINT/SIGTERM); session saved for --resume |
Conflict policy
Overlapping boxes (IoU ≥ threshold) with different person names.
Default: conflict is reported and digiKam wins on --apply.
--leave-conflicts: report only; leave Nextcloud unchanged.
Large libraries (100k–500k+ faces)
The pipeline is batched so memory stays roughly O(batch_size), not O(total faces).
| Setting | Default | Notes |
|---|---|---|
backend |
http |
Preferred |
http_workers |
16 | Concurrent keep-alive WebDAV; try 24–32 if the server allows |
batch_size |
500 | digiKam images per batch (memory ≈ O(batch)) |
insert_missing |
false at scale |
Assign names only |
limit_images |
none | Smoke-test subset |
only_person / --only-person |
none | One digiKam person only (fast tests / targeted updates) |
Session / --resume |
on (.sync_sessions/) |
Skip already-processed image ids after cancel |
| Report detail | capped (~2000 actions) | Counters still cover the full run |
Cancel / resume
Long runs write progress to .sync_sessions/<session-id>.json (override with
state_dir / --state-dir / env DIGIKAM_NEXTCLOUD_STATE_DIR).
| Flag | Meaning |
|---|---|
| (default) | Auto-create a session id; print it at start |
--session-id ID |
Use this id (create or resume if the file exists) |
--resume ID |
Resume an existing session (error if missing) |
--list-sessions |
Show sessions and progress |
--no-session |
Do not persist progress |
On Ctrl+C, the current image finishes, state is flushed, status becomes
cancelled, and exit code 130 is returned. Resume skips digiKam image ids
already recorded in the session file.
# Smoke test
python sync_faces.py --config config.yaml --limit-images 200 -v
# One person only (targeted index — no full library scan)
python sync_faces.py --config config.yaml --only-person Alice -v
# Full assign-only over HTTP (preferred)
python sync_faces.py --config config.yaml \
--no-insert-missing --batch-size 500 --http-workers 24 --apply
Progress looks like:
INFO digiKam totals: 180000 images with face tags, 542098 tagRegion rows
INFO Progress: 5000 / 180000 images (2.8%), … 40.0 img/s, ETA 4375s
Safety
- Dry-run by default
- digiKam DB opened read-only
- Skips Unknown / Unconfirmed / Ignored
- One-way only
License
See LICENSE.