Skip to content

feat(api): image map endpoints with cached UMAP projection and DBSCAN clusters - #37

Open
lstein wants to merge 1 commit into
feat/image-map-03-depsfrom
feat/image-map-04-projection
Open

feat(api): image map endpoints with cached UMAP projection and DBSCAN clusters#37
lstein wants to merge 1 commit into
feat/image-map-03-depsfrom
feat/image-map-04-projection

Conversation

@lstein

@lstein lstein commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR 4 of the Image Map stack (on the deps PR). Adds the serving layer:

  • Pure projection math (image_index/projection.py, lazy imports so the numba JIT never slows startup): PhotoMapAI-tuned UMAP (n_neighbors<=15, min_dist 0.05, cosine, seeded); 2-3-point galleries fall back to a PCA projection (umap-learn cannot fit them); per-request DBSCAN with eps clamped relative to the projection's coordinate span and a hard point cap — both bound sklearn's O(pairs) neighborhood memory.
  • Projection jobs on the index worker: deduplicated per user (last request wins, so admin promotions/demotions can't pin a stale scope), run when the index is quiescent, cached per (user_id, model_id). A failed fit caches an empty projection under the current scope hash — clients see "empty" instead of hot-looping recompute requests, and the next gallery change retries naturally.
  • /api/v1/image_map router:
    • GET /points?eps= — serves the cached projection, never blocks on a UMAP fit; clustering + response assembly run off the event loop (asyncio.to_thread). Stale caches are filtered against the caller's current accessible set before clustering, so un-shared/deleted images neither appear nor influence visible cluster labels via density-chaining.
    • POST /refresh (202), GET /status (index counts admin-only; projection point_count filtered to the current scope).
  • New image_map_projection_ready socket event routed to user:{id} + admin rooms in a single deduped emit.

An adversarial review of this diff was run; both HIGH findings (2-3-image permanent failure loop; event-loop-blocking DBSCAN with measured OOM at 30k points/eps=2.0), the pre-filter clustering leak, duplicate admin emits, scope-pinning dedup, and the stale point-count leak are all fixed above with regression tests. Accepted trade-offs (documented): projection fits are single-threaded (seeded) and can occupy the worker for large galleries; /points is unpaginated.

Testing

59 tests: projection math (blob recovery, seed determinism, tiny-gallery PCA fallback, eps clamp, size cap), worker projection jobs (compute+cache+event, failure self-healing, dedup semantics), router auth matrix (no cross-user name/coordinate/count leakage; hidden points cannot influence visible clusters), staleness flips, live re-clustering.

🤖 Generated with Claude Code

@lstein
lstein marked this pull request as draft August 2, 2026 22:23
@lstein
lstein force-pushed the feat/image-map-03-deps branch from 13a64aa to fdb7e82 Compare August 2, 2026 22:38
@lstein
lstein force-pushed the feat/image-map-04-projection branch from 236d5ef to 5091e5d Compare August 2, 2026 22:38
@lstein
lstein force-pushed the feat/image-map-04-projection branch from 5091e5d to ec55e63 Compare August 3, 2026 01:38
@lstein
lstein force-pushed the feat/image-map-03-deps branch from fdb7e82 to b86155a Compare August 3, 2026 01:38
@lstein
lstein force-pushed the feat/image-map-04-projection branch from ec55e63 to 9a46659 Compare August 3, 2026 02:26
@lstein
lstein marked this pull request as ready for review August 3, 2026 18:28
@lstein
lstein requested a review from Pfannkuchensack as a code owner August 3, 2026 18:28
@lstein
lstein force-pushed the feat/image-map-04-projection branch from 9a46659 to 589a09b Compare August 4, 2026 01:43
@lstein
lstein force-pushed the feat/image-map-03-deps branch from b86155a to 3b0243b Compare August 4, 2026 01:43
@lstein
lstein force-pushed the feat/image-map-04-projection branch from 589a09b to bbab55d Compare August 4, 2026 05:37
@lstein
lstein force-pushed the feat/image-map-03-deps branch from 3b0243b to 99ff815 Compare August 4, 2026 05:37
@lstein
lstein force-pushed the feat/image-map-04-projection branch from bbab55d to fe1b01c Compare August 4, 2026 14:58
@lstein
lstein force-pushed the feat/image-map-03-deps branch from 99ff815 to 086926d Compare August 4, 2026 14:58
@lstein
lstein force-pushed the feat/image-map-04-projection branch from fe1b01c to d1976a2 Compare August 4, 2026 22:32
@lstein
lstein force-pushed the feat/image-map-03-deps branch from 086926d to 178f253 Compare August 4, 2026 22:32
@lstein
lstein force-pushed the feat/image-map-04-projection branch from d1976a2 to 8754846 Compare August 5, 2026 00:31
@lstein
lstein force-pushed the feat/image-map-03-deps branch from 178f253 to 41592b3 Compare August 5, 2026 00:31
@lstein
lstein force-pushed the feat/image-map-04-projection branch from 8754846 to 285ea0b Compare August 5, 2026 01:18
… clusters

Adds the serving layer for the semantic image map:

- Pure projection math in `image_index/projection.py` (lazy imports so the
  numba JIT never slows app startup): PhotoMapAI-tuned UMAP
  (n_neighbors<=15, min_dist 0.05, cosine), per-request DBSCAN, and an
  order-insensitive scope hash fingerprinting the accessible image set.
- Projection jobs on the index worker: deduplicated per user, run when the
  index is quiescent, cached per (user_id, model_id), and announced with a
  new `image_map_projection_ready` event routed to the user's socket room
  (plus admins).
- New `/api/v1/image_map` router:
  - `GET /points?eps=` serves the cached projection with live DBSCAN
    clustering (eps adjustable without recompute) and never blocks on a
    UMAP fit; missing/stale caches enqueue a recompute. Stale caches are
    filtered against the current accessible set so un-shared or deleted
    images never leak out of an old projection.
  - `POST /refresh` (202) and `GET /status`.
- Admins get the all-images scope; regular users get their accessible set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lstein
lstein force-pushed the feat/image-map-04-projection branch from 285ea0b to 8984cd4 Compare August 5, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant