feat(search): explicit filters on unified search; list camera bodies

The app's new search screen keeps tags, type, date, camera and place as
chips and sends free text to the unified endpoint. For the AI to add to
those chips instead of replacing them, the endpoint now takes them:

- /photos/search/unified accepts the /photos filters explicitly (tag_ids
  with All/Any, exclude_tag_ids, media_type, dates, camera make/model,
  gps circle) plus a recursive `path`. Explicit tags are their own
  predicate on top of the model's ANY-mode tags; single-valued filters
  replace the model's guess. `interpreted` echoes only what the model
  added, so the client doesn't show the user's own chips twice.
- interpret=false skips the LLM and uses q verbatim as the CLIP term,
  which replaces the app's separate "Semantic" mode with a visual match
  inside the user's filters.
- /image/tags/all also returns `cameras` (make + model with counts) so
  the camera picker can offer e.g. "Nexus 6" directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-15 21:09:06 -04:00
1 parent ed52a07d9d
commit 015ddd6c77
4 files changed
+511 -69

No files matched your search

+10 -1
View File
@@ -525,6 +525,15 @@ GET /photos?path=...&recursive=true&sort=DateTakenDesc&camera_make=Canon&gps_lat
// ExifDao::query_by_exif — shared by /photos, /photos/exif and unified search.
// A calendar-day range = that day's bounds in the owner's display zone.
// Natural-language search (LLM translates q into filters + a CLIP term)
GET /photos/search/unified?q=...&model=...&interpret=true
// Also accepts the /photos filters explicitly (tag_ids, exclude_tag_ids,
// tag_filter_mode, media_type, date_from/date_to, camera_make/camera_model,
// gps_lat/gps_lon/gps_radius_km) plus path (folder, recursive). They're
// AND-ed with what the model extracted; single-valued ones replace the
// model's guess, and `interpreted` only echoes what the model added.
// interpret=false skips the LLM: q is the CLIP term within the filters.
// Video streaming (HLS, the default playback path)
POST /video/generate (queues .m3u8 playlist + .ts segments, returns playlist_url)
GET /video/hls/<hash>/<file> (serves playlist / segments)
@@ -534,7 +543,7 @@ POST /video/original (body: {path, library}) -> {url, expires_at}
GET /video/original/stream?token=... (Range-capable original bytes)
// Tags
GET /image/tags/all
GET /image/tags/all (tags + counts, camera_makes, and cameras: make+model bodies)
POST /image/tags (add tag to file)
DELETE /image/tags (remove tag from file)
POST /image/tags/batch (bulk tag updates)