fix: resolve media across libraries for video, metadata, and insights
The /video/generate and /image/metadata handlers assumed files live under the resolved library only, which broke when a mobile client passed no library (union mode) but the file lived in a non-primary library. Both now fall back to scanning every configured library for an existing file. InsightGenerator held a single base_path, so vision-model loads and filename-date fallbacks failed for non-primary libraries. It now takes Vec<Library> and probes each root in resolve_full_path. /image/metadata responses now carry library_id/library_name so the mobile viewer can surface which library a file belongs to. Thumbnail generation at startup is now spawned on a background thread so the HTTP server can accept traffic while large libraries backfill. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ use crate::database::{
|
||||
CalendarEventDao, DailySummaryDao, ExifDao, InsightDao, KnowledgeDao, LocationHistoryDao,
|
||||
SearchHistoryDao,
|
||||
};
|
||||
use crate::libraries::Library;
|
||||
use crate::memories::extract_date_from_filename;
|
||||
use crate::otel::global_tracer;
|
||||
use crate::tags::TagDao;
|
||||
@@ -52,7 +53,7 @@ pub struct InsightGenerator {
|
||||
// Knowledge memory
|
||||
knowledge_dao: Arc<Mutex<Box<dyn KnowledgeDao>>>,
|
||||
|
||||
base_path: String,
|
||||
libraries: Vec<Library>,
|
||||
}
|
||||
|
||||
impl InsightGenerator {
|
||||
@@ -67,7 +68,7 @@ impl InsightGenerator {
|
||||
search_dao: Arc<Mutex<Box<dyn SearchHistoryDao>>>,
|
||||
tag_dao: Arc<Mutex<Box<dyn TagDao>>>,
|
||||
knowledge_dao: Arc<Mutex<Box<dyn KnowledgeDao>>>,
|
||||
base_path: String,
|
||||
libraries: Vec<Library>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ollama,
|
||||
@@ -80,10 +81,25 @@ impl InsightGenerator {
|
||||
search_dao,
|
||||
tag_dao,
|
||||
knowledge_dao,
|
||||
base_path,
|
||||
libraries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `rel_path` against the configured libraries, returning the
|
||||
/// first root under which the file exists. Insights may be generated
|
||||
/// for any library — the generator itself doesn't know which — so we
|
||||
/// probe each root rather than trust a single `base_path`.
|
||||
fn resolve_full_path(&self, rel_path: &str) -> Option<std::path::PathBuf> {
|
||||
use std::path::Path;
|
||||
for lib in &self.libraries {
|
||||
let candidate = Path::new(&lib.root_path).join(rel_path);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract contact name from file path
|
||||
/// e.g., "Sarah/img.jpeg" -> Some("Sarah")
|
||||
/// e.g., "img.jpeg" -> None
|
||||
@@ -108,9 +124,13 @@ impl InsightGenerator {
|
||||
/// Resizes to max 1024px on longest edge to reduce context usage
|
||||
fn load_image_as_base64(&self, file_path: &str) -> Result<String> {
|
||||
use image::imageops::FilterType;
|
||||
use std::path::Path;
|
||||
|
||||
let full_path = Path::new(&self.base_path).join(file_path);
|
||||
let full_path = self.resolve_full_path(file_path).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"File '{}' not found under any configured library",
|
||||
file_path
|
||||
)
|
||||
})?;
|
||||
|
||||
log::debug!("Loading image for vision model: {:?}", full_path);
|
||||
|
||||
@@ -725,8 +745,7 @@ impl InsightGenerator {
|
||||
extract_date_from_filename(&file_path)
|
||||
.map(|dt| dt.timestamp())
|
||||
.or_else(|| {
|
||||
// Combine base_path with file_path to get full path
|
||||
let full_path = std::path::Path::new(&self.base_path).join(&file_path);
|
||||
let full_path = self.resolve_full_path(&file_path)?;
|
||||
File::open(&full_path)
|
||||
.and_then(|f| f.metadata())
|
||||
.and_then(|m| m.created().or(m.modified()))
|
||||
@@ -2455,7 +2474,7 @@ Return ONLY the summary, nothing else."#,
|
||||
extract_date_from_filename(&file_path)
|
||||
.map(|dt| dt.timestamp())
|
||||
.or_else(|| {
|
||||
let full_path = std::path::Path::new(&self.base_path).join(&file_path);
|
||||
let full_path = self.resolve_full_path(&file_path)?;
|
||||
File::open(&full_path)
|
||||
.and_then(|f| f.metadata())
|
||||
.and_then(|m| m.created().or(m.modified()))
|
||||
|
||||
Reference in New Issue
Block a user