Photo viewer MVP with:
- Go API server
- React frontend embedded into one Go binary
- Playwright e2e test flow
Copy one of:
.env.examplefor local run.env.test.exampleformake test
At minimum configure S3 values:
S3_ENDPOINTS3_BUCKETS3_ACCESS_KEYS3_SECRET_KEY
Optional tuning:
- Recommendation feature:
RECO_TOPK_DEFAULTdefault recommendation count (default12).RECO_TOPK_MAXmax recommendation count (default48).
- Recommender service:
RECOMMENDER_ENDPOINTendpoint for the Rust recommender service (examplehttp://127.0.0.1:18081). Leave empty to disable recommender only whenRECOMMENDER_REQUIRED=false.RECOMMENDER_REQUIREDwhether startup must fail when recommender is unavailable (defaulttrue; viewer Docker image setsfalse). Whenfalseand endpoint is empty, recommender is disabled.RECOMMENDER_CONCURRENCYnumber of background embedding workers (0means auto, default is adaptive3x GOMAXPROCS, min8, max64).RECOMMENDER_REQUEST_TIMEOUT_SECONDStimeout per embed request (default120).
SIGLIP2_MODEL_IDmodel identifier for the Rust worker (defaultgoogle/siglip2-base-patch16-224).HF_HOMEHugging Face cache root used by the Rust worker. In Docker runtime this is pre-populated at/tmp/hf-home.
Rust recommender service endpoints:
GET /pingGET /healthzPOST /embedwith JSON{"request_id","image_b64"}
Rust worker resolves model files through the Hugging Face cache (HF_HOME), downloading only if a required file is missing.
Recommendation vectors are persisted in each album's albums/<album-id>/index.json under an embeddings section.
Background embedding now runs album-by-album: workers pick a random album with missing vectors, embed all pending photos in that album, then persist metadata in one write.
Recommendation responses are cross-album only: photos from the same album as the query are excluded from results.
If no cross-album neighbors exist for an embedded query photo, recommendations return an empty items list.
Docker images are split by service:
runtime-viewer(defaultdocker build .) contains only the Go viewer server and frontend assets.runtime-recommendercontains only the Rust recommender service.- CI publishes viewer as
ghcr.io/<owner>/<repo>and recommender asghcr.io/<owner>/<repo>-recommender.
The recommender image prefetches config.json and model.safetensors for SIGLIP2_MODEL_ID at build time via the model-base stage, so pod startup does not require Hugging Face egress.
To use a different model in Docker, build with --build-arg SIGLIP2_MODEL_ID=<repo-id>.
To build an MKL-accelerated recommender for x86_64 images, pass --build-arg RECOMMENDER_ACCEL=mkl --platform linux/amd64.
RECOMMENDER_ACCEL defaults to none; if mkl is requested on non-amd64 targets, the Dockerfile falls back to a non-MKL recommender build.
make buildbuildsbin/recommenderin release mode, builds frontend assets, and compilesbin/viewerplusbin/album-dedupe-cleaner.make testruns fast Go unit/integration tests only (go test ./cmd/... ./internal/...) using values from.env.test.make test-fullruns the full regression pipeline:make build,make test, then Playwright e2e (screenshots saved tosamples/by default).make test-fullbinds the app toTEST_PORT(default18080) and setsE2E_BASE_URLautomatically.make runstartsbin/viewer(loads.envif present, does not rebuild binaries).make cleanremoves build outputs and dependency caches.
Batch duplicate cleanup binary:
bin/album-dedupe-cleaner plan --out ./dedupe-plan.jsoncreates a deletion plan (no deletes).bin/album-dedupe-cleaner apply --plan ./dedupe-plan.jsonvalidates the plan snapshot and deletes duplicate album prefixes.
- The server logs to stdout/stderr via Go's standard logger.
- Startup warmup now runs in background and streams each loaded album index once into both album cache and recommendation state.
- Request-scoped 500 errors now include request context including:
- request method/path
- request ID (Chi request ID middleware)
- remote IP
- raw query
- detailed internal error message
- Panics are logged with stack traces before the 500 response is returned.