Add Insights Model Discovery and Fallback Handling
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user