feat: M1 foundation packages + trip-cluster app #1

Merged
m038 merged 19 commits from feat/m1-foundation-trip-cluster into master 2026-06-27 19:10:50 +02:00
6 changed files with 126 additions and 0 deletions
Showing only changes of commit d67b677b6a - Show all commits
+38
View File
@@ -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/`.
+37
View File
@@ -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/`.
+9
View File
@@ -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"]
+3
View File
@@ -11,3 +11,6 @@ dependencies = ["photoflow", "flask==3.1.0", "requests==2.32.3", "Pillow==11.0.0
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["."] where = ["."]
include = ["app*"] include = ["app*"]
[tool.setuptools.package-data]
"app" = ["templates/*.html", "static/*.js"]
+13
View File
@@ -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}"
+26
View File
@@ -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 <hard-window-start> --to <hard-window-end>` (or `--tag <old-trip>`).
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.