From d67b677b6af5367a76af3d845b2a9a7d425d860a Mon Sep 17 00:00:00 2001 From: mischa Date: Sat, 27 Jun 2026 17:37:22 +0200 Subject: [PATCH] docs+docker: README, CLAUDE.md, Dockerfile/compose (8084), M1 validation gate --- CLAUDE.md | 38 ++++++++++++++++++++++++++++++++ README.md | 37 +++++++++++++++++++++++++++++++ apps/trip-cluster/Dockerfile | 9 ++++++++ apps/trip-cluster/pyproject.toml | 3 +++ docker-compose.yml | 13 +++++++++++ docs/M1-validation-gate.md | 26 ++++++++++++++++++++++ 6 files changed, 126 insertions(+) create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 apps/trip-cluster/Dockerfile create mode 100644 docker-compose.yml create mode 100644 docs/M1-validation-gate.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..84b0f66 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,38 @@ +# CLAUDE.md + +## What this is + +A monorepo (M1) of an Immich photo-flow foundation + the `trip-cluster` app. See +`docs/ROADMAP.md` for milestones and `docs/superpowers/specs/2026-06-27-immich-photo-flow-design.md` +for the M1 design. + +## Architecture map + +- `shared/photoflow/immich` — **the only** Immich client (`client.py`) + `_pipeline/` tag + conventions (`pipeline.py`). +- `shared/photoflow/core` — **the only** SQLite owner: `store.py` (data-access) + `models.py`. +- `shared/photoflow/ui` — `base.html`, shared Jinja macros, `shared.js` grid+lightbox. +- `apps/trip-cluster/app` — `config.py`, `cli.py` (ingest|cluster|serve|apply), + `ingest.py`, `clustering.py` (pure), `coverage.py` (pure), `cluster_run.py`, + `review.py`, `writeback.py`, `routes/`, `templates/`, `static/app.js`. + +## Key invariants + +- **Immich is the source of truth**; SQLite is rebuildable. Only **applied** decisions + survive loss of SQLite (via `_pipeline/processed` + `writeback_log`). +- Content/trip tags are **never namespaced**; pipeline meta-tags nest under `_pipeline/`. +- Trip detection is **timestamp-first, density-adaptive**, anchored by existing trip tags, + refined by GPS. Existing trip tags are authoritative seeds. +- Write-back is **idempotent** (`writeback_log`) and needs **explicit confirmation**. +- Ingest is **scopeable** (`--from/--to`, `--tag`, `--subset`) and incremental (`updatedAfter`). +- trip-cluster serves on **8084**. + +## Dev commands + +```bash +.venv/bin/python -m pytest # unit + route (Immich mocked) +.venv/bin/python -m pytest apps/trip-cluster/tests/ui # Playwright +docker compose up # UI on :8084 +``` + +Write a failing test first (TDD). Mirrors the sibling apps in `/home/mischa/Projects/`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..381beaf --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# immich-photo-flow + +A monorepo of small tools that clean up and structure a large [Immich](https://immich.app) +library into travel "memories". **M1** ships the shared foundation +(`shared/photoflow/{immich,core,ui}`) and the `trip-cluster` app. + +## Layout + +- `shared/photoflow/immich` — the one Immich REST client + `_pipeline/` tag conventions. +- `shared/photoflow/core` — SQLite store + domain models (the only SQL). +- `shared/photoflow/ui` — base template, DaisyUI/Tailwind/Alpine/HTMX, shared grid+lightbox. +- `apps/trip-cluster` — CLI + Flask review UI (port 8084). + +## Dev setup + +```bash +python3.12 -m venv .venv +.venv/bin/pip install -e ./shared -e ./apps/trip-cluster \ + pytest==8.3.4 pytest-httpserver==1.1.0 pytest-playwright==0.6.2 +.venv/bin/python -m pytest # unit + route tests +.venv/bin/python -m playwright install chromium +.venv/bin/python -m pytest apps/trip-cluster/tests/ui # Playwright UI +``` + +## trip-cluster workflow + +```bash +cp .env.example .env # fill IMMICH_URL + IMMICH_API_KEY +cd apps/trip-cluster +python categorize.py ingest --tag "Italy 2019" # or --from/--to/--subset +python categorize.py cluster +python categorize.py serve # review at http://localhost:8084 +python categorize.py apply # write approved tags back (asks to confirm) +``` + +Immich is the source of truth; SQLite is a rebuildable working layer. Content/trip tags +are never namespaced; pipeline meta-tags live under `_pipeline/`. diff --git a/apps/trip-cluster/Dockerfile b/apps/trip-cluster/Dockerfile new file mode 100644 index 0000000..f1e26a5 --- /dev/null +++ b/apps/trip-cluster/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim +WORKDIR /srv +COPY shared/ ./shared/ +COPY apps/trip-cluster/ ./apps/trip-cluster/ +RUN pip install --no-cache-dir ./shared ./apps/trip-cluster +ENV DATA_DIR=/data +EXPOSE 8084 +WORKDIR /srv/apps/trip-cluster +CMD ["python", "categorize.py", "serve"] diff --git a/apps/trip-cluster/pyproject.toml b/apps/trip-cluster/pyproject.toml index f682724..645616d 100644 --- a/apps/trip-cluster/pyproject.toml +++ b/apps/trip-cluster/pyproject.toml @@ -11,3 +11,6 @@ dependencies = ["photoflow", "flask==3.1.0", "requests==2.32.3", "Pillow==11.0.0 [tool.setuptools.packages.find] where = ["."] include = ["app*"] + +[tool.setuptools.package-data] +"app" = ["templates/*.html", "static/*.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ad024c3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + trip-cluster: + build: + context: . + dockerfile: apps/trip-cluster/Dockerfile + ports: + - "8084:8084" + volumes: + - ./data:/data + env_file: .env + environment: + - DATA_DIR=/data + user: "${UID}:${GID}" diff --git a/docs/M1-validation-gate.md b/docs/M1-validation-gate.md new file mode 100644 index 0000000..69e81d8 --- /dev/null +++ b/docs/M1-validation-gate.md @@ -0,0 +1,26 @@ +# M1 Validation Gate + +Before widening past the hard sample, trip-cluster must clear a quantified bar on a +**deliberately hard slice** — a GPS-poor, multi-year, low-density window (a well-remembered +old trip plus its surrounding everyday photos), **not** the easy phone-era last trip. + +## Protocol + +1. `categorize ingest --from --to ` (or `--tag `). +2. `categorize cluster`, then `categorize serve`. +3. Hand-label the slice: the true trip boundaries + which surrounding photos are non-trip. +4. Compare candidate clusters against the labels. + +## Acceptance bar (record actual numbers per run) + +| Metric | Definition | Target | +|--------|------------|--------| +| Trip-boundary precision | proposed boundaries that are real | ≥ 0.8 | +| Trip-boundary recall | real boundaries proposed | ≥ 0.8 | +| Coverage-flag recall | in-window missing-tag assets surfaced | ≥ 0.9 | +| Over-split rate | extra clusters per real trip | ≤ 0.5 | +| False-cluster rate | clusters that are pure noise | ≤ 0.1 | + +If unmet, tune `--gap-factor` / seed-span / confidence thresholds — do **not** widen the +backlog. See the F4 open question (everyday cluster blow-up): observe the real cluster +count on a representative subset first, then choose the surfacing/collapsing strategy.