docs+docker: README, CLAUDE.md, Dockerfile/compose (8084), M1 validation gate
This commit is contained in:
@@ -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/`.
|
||||
@@ -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/`.
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
|
||||
@@ -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}"
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user