Add Insights Model Discovery and Fallback Handling

This commit is contained in:
Cameron
2026-01-03 20:27:34 -05:00
parent 1171f19845
commit cf52d4ab76
10 changed files with 419 additions and 80 deletions

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use chrono::Utc;
use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::fs::File;
use std::sync::{Arc, Mutex};
use crate::ai::ollama::OllamaClient;
@@ -8,6 +9,7 @@ use crate::ai::sms_client::SmsApiClient;
use crate::database::models::InsertPhotoInsight;
use crate::database::{ExifDao, InsightDao};
use crate::memories::extract_date_from_filename;
use crate::utils::normalize_path;
#[derive(Deserialize)]
struct NominatimResponse {
@@ -20,9 +22,7 @@ struct NominatimAddress {
city: Option<String>,
town: Option<String>,
village: Option<String>,
county: Option<String>,
state: Option<String>,
country: Option<String>,
}
#[derive(Clone)]
@@ -31,6 +31,7 @@ pub struct InsightGenerator {
sms_client: SmsApiClient,
insight_dao: Arc<Mutex<Box<dyn InsightDao>>>,
exif_dao: Arc<Mutex<Box<dyn ExifDao>>>,
base_path: String,
}
impl InsightGenerator {
@@ -39,12 +40,14 @@ impl InsightGenerator {
sms_client: SmsApiClient,
insight_dao: Arc<Mutex<Box<dyn InsightDao>>>,
exif_dao: Arc<Mutex<Box<dyn ExifDao>>>,
base_path: String,
) -> Self {
Self {
ollama,
sms_client,
insight_dao,
exif_dao,
base_path,
}
}
@@ -69,16 +72,35 @@ impl InsightGenerator {
None
}
/// Generate AI insight for a single photo
pub async fn generate_insight_for_photo(&self, file_path: &str) -> Result<()> {
/// Generate AI insight for a single photo with optional custom model
pub async fn generate_insight_for_photo_with_model(
&self,
file_path: &str,
custom_model: Option<String>,
) -> Result<()> {
// Normalize path to ensure consistent forward slashes in database
let file_path = normalize_path(file_path);
log::info!("Generating insight for photo: {}", file_path);
// Create custom Ollama client if model is specified
let ollama_client = if let Some(model) = custom_model {
log::info!("Using custom model: {}", model);
OllamaClient::new(
self.ollama.primary_url.clone(),
self.ollama.fallback_url.clone(),
model.clone(),
Some(model), // Use the same custom model for fallback server
)
} else {
self.ollama.clone()
};
// 1. Get EXIF data for the photo
let otel_context = opentelemetry::Context::new();
let exif = {
let mut exif_dao = self.exif_dao.lock().expect("Unable to lock ExifDao");
exif_dao
.get_exif(&otel_context, file_path)
.get_exif(&otel_context, &file_path)
.map_err(|e| anyhow::anyhow!("Failed to get EXIF: {:?}", e))?
};
@@ -88,17 +110,33 @@ impl InsightGenerator {
} else {
log::warn!("No date_taken in EXIF for {}, trying filename", file_path);
extract_date_from_filename(file_path)
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);
File::open(&full_path)
.and_then(|f| f.metadata())
.and_then(|m| m.created().or(m.modified()))
.map(|t| DateTime::<Utc>::from(t).timestamp())
.inspect_err(|e| {
log::warn!(
"Failed to get file timestamp for insight {}: {}",
file_path,
e
)
})
.ok()
})
.unwrap_or_else(|| Utc::now().timestamp())
};
let date_taken = chrono::DateTime::from_timestamp(timestamp, 0)
let date_taken = DateTime::from_timestamp(timestamp, 0)
.map(|dt| dt.date_naive())
.unwrap_or_else(|| Utc::now().date_naive());
// 3. Extract contact name from file path
let contact = Self::extract_contact_from_path(file_path);
let contact = Self::extract_contact_from_path(&file_path);
log::info!("Extracted contact from path: {:?}", contact);
// 4. Fetch SMS messages for the contact (±1 day)
@@ -124,7 +162,7 @@ impl InsightGenerator {
let sms_summary = if !sms_messages.is_empty() {
match self
.sms_client
.summarize_context(&sms_messages, &self.ollama)
.summarize_context(&sms_messages, &ollama_client)
.await
{
Ok(summary) => Some(summary),
@@ -157,13 +195,11 @@ impl InsightGenerator {
);
// 7. Generate title and summary with Ollama
let title = self
.ollama
let title = ollama_client
.generate_photo_title(date_taken, location.as_deref(), sms_summary.as_deref())
.await?;
let summary = self
.ollama
let summary = ollama_client
.generate_photo_summary(date_taken, location.as_deref(), sms_summary.as_deref())
.await?;
@@ -176,7 +212,7 @@ impl InsightGenerator {
title,
summary,
generated_at: Utc::now().timestamp(),
model_version: self.ollama.model.clone(),
model_version: ollama_client.primary_model.clone(),
};
let mut dao = self.insight_dao.lock().expect("Unable to lock InsightDao");