feat(versions): link at ingest, hide in listings, version endpoints
XMP (PNG iTXt DerivedFrom, native chunk reader) then file-name convention links at ingest; unlinked name candidates are retried each tick. Versions are hidden from /photos (SQL for the recursive listing, hidden-row set for the walk and tag branches), /memories, reels, /photos/exif, the GPS summary, unified search and the media gauges while their original is reachable in the view; include_versions=true lists them. /photos gains the parallel arrays photo_display_paths / photo_version_counts, memories / search hits / /photos/exif rows gain display_path / version_count, all skipped when empty. GET /image/versions and PUT /image/versions/display (library optional). Tags and favorites on a version path bind to the original; faces, CLIP and duplicate grouping skip version hashes; insight generation redirects to the original. Orphan GC and the back-ref refresh cover the new tables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
044240037a
commit
080a75c336
24 files changed
+1614
-47
No files matched your search
+11
-1
@@ -82,6 +82,10 @@ pub struct SearchHit {
|
||||
/// Cosine similarity in [-1, 1]. In practice OpenAI CLIP returns
|
||||
/// 0.10–0.40 for the typical photo library.
|
||||
pub score: f32,
|
||||
/// `display_path` / `version_count` (#191); absent for a photo without
|
||||
/// versions. `rel_path` is always the original's.
|
||||
#[serde(flatten)]
|
||||
pub versions: crate::versions::VersionFields,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -230,11 +234,13 @@ pub async fn score_photos(
|
||||
/// to a row are skipped. Shared by both endpoints.
|
||||
pub fn resolve_hits(
|
||||
exif_dao: &Mutex<Box<dyn ExifDao>>,
|
||||
version_dao: &Mutex<Box<dyn crate::database::VersionDao>>,
|
||||
scored: &[(f32, String)],
|
||||
) -> Vec<SearchHit> {
|
||||
if scored.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let summaries = crate::versions::load_summaries(version_dao);
|
||||
let hashes: Vec<String> = scored.iter().map(|(_, h)| h.clone()).collect();
|
||||
let mut dao = exif_dao.lock().expect("exif dao");
|
||||
let path_map = dao.get_rel_paths_for_hashes(&hashes).unwrap_or_else(|e| {
|
||||
@@ -258,11 +264,15 @@ pub fn resolve_hits(
|
||||
.get(hash)
|
||||
.and_then(|paths| paths.first().cloned())
|
||||
.unwrap_or(row.file_path);
|
||||
let versions = crate::versions::VersionFields::from_summary(
|
||||
summaries.get(&(row.library_id, rel_path.clone())),
|
||||
);
|
||||
results.push(SearchHit {
|
||||
library_id: row.library_id,
|
||||
rel_path,
|
||||
content_hash: hash.clone(),
|
||||
score: *score,
|
||||
versions,
|
||||
});
|
||||
}
|
||||
results
|
||||
@@ -343,7 +353,7 @@ pub async fn search_photos(
|
||||
let end = (offset + limit).min(total_matching);
|
||||
scored.hits[offset..end].to_vec()
|
||||
};
|
||||
let results = resolve_hits(&exif_dao, &page);
|
||||
let results = resolve_hits(&exif_dao, &state.version_dao, &page);
|
||||
|
||||
Ok(HttpResponse::Ok().json(SearchResponse {
|
||||
query: q_text,
|
||||
|
||||
Reference in new issue
Block a user