Build insight title from generated summary
This commit is contained in:
7
.idea/sqldialects.xml
generated
7
.idea/sqldialects.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="SqlDialectMappings">
|
|
||||||
<file url="file://$PROJECT_DIR$/migrations/2021-09-02-000740_create_tags/up.sql" dialect="GenericSQL" />
|
|
||||||
<file url="PROJECT" dialect="SQLite" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1011,18 +1011,7 @@ impl InsightGenerator {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// 10. Generate title and summary with Ollama (using multi-source context + image if supported)
|
// 10. Generate summary first, then derive title from the summary
|
||||||
let title = ollama_client
|
|
||||||
.generate_photo_title(
|
|
||||||
date_taken,
|
|
||||||
location.as_deref(),
|
|
||||||
contact.as_deref(),
|
|
||||||
Some(&combined_context),
|
|
||||||
custom_system_prompt.as_deref(),
|
|
||||||
image_base64.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let summary = ollama_client
|
let summary = ollama_client
|
||||||
.generate_photo_summary(
|
.generate_photo_summary(
|
||||||
date_taken,
|
date_taken,
|
||||||
@@ -1034,6 +1023,10 @@ impl InsightGenerator {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let title = ollama_client
|
||||||
|
.generate_photo_title(&summary, custom_system_prompt.as_deref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
log::info!("Generated title: {}", title);
|
log::info!("Generated title: {}", title);
|
||||||
log::info!("Generated summary: {}", summary);
|
log::info!("Generated summary: {}", summary);
|
||||||
|
|
||||||
|
|||||||
@@ -373,94 +373,25 @@ impl OllamaClient {
|
|||||||
Ok(cleaned)
|
Ok(cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a title for a single photo based on its context
|
/// Generate a title for a single photo based on its generated summary
|
||||||
pub async fn generate_photo_title(
|
pub async fn generate_photo_title(
|
||||||
&self,
|
&self,
|
||||||
date: NaiveDate,
|
summary: &str,
|
||||||
location: Option<&str>,
|
|
||||||
contact: Option<&str>,
|
|
||||||
sms_summary: Option<&str>,
|
|
||||||
custom_system: Option<&str>,
|
custom_system: Option<&str>,
|
||||||
image_base64: Option<String>,
|
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let location_str = location.unwrap_or("Unknown location");
|
let prompt = format!(
|
||||||
let sms_str = sms_summary.unwrap_or("No messages");
|
r#"Create a short title (maximum 8 words) for the following journal entry:
|
||||||
|
|
||||||
let prompt = if image_base64.is_some() {
|
{}
|
||||||
if let Some(contact_name) = contact {
|
|
||||||
format!(
|
|
||||||
r#"Create a short title (maximum 8 words) about this moment by analyzing the image and context:
|
|
||||||
|
|
||||||
Date: {}
|
Capture the key moment or theme. Return ONLY the title, nothing else."#,
|
||||||
Location: {}
|
summary
|
||||||
Person/Contact: {}
|
);
|
||||||
Messages: {}
|
|
||||||
|
|
||||||
Analyze the image and use specific details from both the visual content and the context above. The photo is from a folder for {}, so they are likely in or related to this photo. If limited information is available, use a simple descriptive title based on what you see.
|
|
||||||
|
|
||||||
Return ONLY the title, nothing else."#,
|
|
||||||
date.format("%B %d, %Y"),
|
|
||||||
location_str,
|
|
||||||
contact_name,
|
|
||||||
sms_str,
|
|
||||||
contact_name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
r#"Create a short title (maximum 8 words) about this moment by analyzing the image and context:
|
|
||||||
|
|
||||||
Date: {}
|
|
||||||
Location: {}
|
|
||||||
Messages: {}
|
|
||||||
|
|
||||||
Analyze the image and use specific details from both the visual content and the context above. If limited information is available, use a simple descriptive title based on what you see.
|
|
||||||
|
|
||||||
Return ONLY the title, nothing else."#,
|
|
||||||
date.format("%B %d, %Y"),
|
|
||||||
location_str,
|
|
||||||
sms_str
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if let Some(contact_name) = contact {
|
|
||||||
format!(
|
|
||||||
r#"Create a short title (maximum 8 words) about this moment:
|
|
||||||
|
|
||||||
Date: {}
|
|
||||||
Location: {}
|
|
||||||
Person/Contact: {}
|
|
||||||
Messages: {}
|
|
||||||
|
|
||||||
Use specific details from the context above. The photo is from a folder for {}, so they are likely related to this moment. If no specific details are available, use a simple descriptive title.
|
|
||||||
|
|
||||||
Return ONLY the title, nothing else."#,
|
|
||||||
date.format("%B %d, %Y"),
|
|
||||||
location_str,
|
|
||||||
contact_name,
|
|
||||||
sms_str,
|
|
||||||
contact_name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
r#"Create a short title (maximum 8 words) about this moment:
|
|
||||||
|
|
||||||
Date: {}
|
|
||||||
Location: {}
|
|
||||||
Messages: {}
|
|
||||||
|
|
||||||
Use specific details from the context above. If no specific details are available, use a simple descriptive title.
|
|
||||||
|
|
||||||
Return ONLY the title, nothing else."#,
|
|
||||||
date.format("%B %d, %Y"),
|
|
||||||
location_str,
|
|
||||||
sms_str
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let system = custom_system.unwrap_or("You are my long term memory assistant. Use only the information provided. Do not invent details.");
|
let system = custom_system.unwrap_or("You are my long term memory assistant. Use only the information provided. Do not invent details.");
|
||||||
|
|
||||||
let images = image_base64.map(|img| vec![img]);
|
|
||||||
let title = self
|
let title = self
|
||||||
.generate_with_images(&prompt, Some(system), images)
|
.generate_with_images(&prompt, Some(system), None)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(title.trim().trim_matches('"').to_string())
|
Ok(title.trim().trim_matches('"').to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/files.rs
12
src/files.rs
@@ -1471,9 +1471,7 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(RealFileSystem::new(temp_dir.to_str().unwrap().to_string())),
|
Data::new(RealFileSystem::new(temp_dir.to_str().unwrap().to_string())),
|
||||||
Data::new(Mutex::new(SqliteTagDao::default())),
|
Data::new(Mutex::new(SqliteTagDao::default())),
|
||||||
Data::new(Mutex::new(
|
Data::new(Mutex::new(Box::new(MockExifDao) as Box<dyn ExifDao>)),
|
||||||
Box::new(MockExifDao) as Box<dyn ExifDao>
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1518,9 +1516,7 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(FakeFileSystem::new(HashMap::new())),
|
Data::new(FakeFileSystem::new(HashMap::new())),
|
||||||
Data::new(Mutex::new(tag_dao)),
|
Data::new(Mutex::new(tag_dao)),
|
||||||
Data::new(Mutex::new(
|
Data::new(Mutex::new(Box::new(MockExifDao) as Box<dyn ExifDao>)),
|
||||||
Box::new(MockExifDao) as Box<dyn ExifDao>
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1581,9 +1577,7 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(FakeFileSystem::new(HashMap::new())),
|
Data::new(FakeFileSystem::new(HashMap::new())),
|
||||||
Data::new(Mutex::new(tag_dao)),
|
Data::new(Mutex::new(tag_dao)),
|
||||||
Data::new(Mutex::new(
|
Data::new(Mutex::new(Box::new(MockExifDao) as Box<dyn ExifDao>)),
|
||||||
Box::new(MockExifDao) as Box<dyn ExifDao>
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
57
src/main.rs
57
src/main.rs
@@ -1044,10 +1044,12 @@ fn cleanup_orphaned_playlists() {
|
|||||||
.filter(|e| e.file_type().is_file())
|
.filter(|e| e.file_type().is_file())
|
||||||
{
|
{
|
||||||
if let Some(entry_stem) = entry.path().file_stem()
|
if let Some(entry_stem) = entry.path().file_stem()
|
||||||
&& entry_stem == filename && is_video_file(entry.path()) {
|
&& entry_stem == filename
|
||||||
video_exists = true;
|
&& is_video_file(entry.path())
|
||||||
break;
|
{
|
||||||
}
|
video_exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !video_exists {
|
if !video_exists {
|
||||||
@@ -1078,27 +1080,27 @@ fn cleanup_orphaned_playlists() {
|
|||||||
{
|
{
|
||||||
let entry_path = entry.path();
|
let entry_path = entry.path();
|
||||||
if let Some(ext) = entry_path.extension()
|
if let Some(ext) = entry_path.extension()
|
||||||
&& ext.eq_ignore_ascii_case("ts") {
|
&& ext.eq_ignore_ascii_case("ts")
|
||||||
// Check if this .ts file belongs to our playlist
|
{
|
||||||
if let Some(ts_stem) = entry_path.file_stem() {
|
// Check if this .ts file belongs to our playlist
|
||||||
let ts_name = ts_stem.to_string_lossy();
|
if let Some(ts_stem) = entry_path.file_stem() {
|
||||||
if ts_name.starts_with(&*video_filename) {
|
let ts_name = ts_stem.to_string_lossy();
|
||||||
if let Err(e) = std::fs::remove_file(entry_path)
|
if ts_name.starts_with(&*video_filename) {
|
||||||
{
|
if let Err(e) = std::fs::remove_file(entry_path) {
|
||||||
debug!(
|
debug!(
|
||||||
"Failed to delete segment {}: {}",
|
"Failed to delete segment {}: {}",
|
||||||
entry_path.display(),
|
entry_path.display(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"Deleted segment: {}",
|
"Deleted segment: {}",
|
||||||
entry_path.display()
|
entry_path.display()
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1206,12 +1208,11 @@ fn playlist_needs_generation(video_path: &Path, playlist_path: &Path) -> bool {
|
|||||||
if let (Ok(video_meta), Ok(playlist_meta)) = (
|
if let (Ok(video_meta), Ok(playlist_meta)) = (
|
||||||
std::fs::metadata(video_path),
|
std::fs::metadata(video_path),
|
||||||
std::fs::metadata(playlist_path),
|
std::fs::metadata(playlist_path),
|
||||||
)
|
) && let (Ok(video_modified), Ok(playlist_modified)) =
|
||||||
&& let (Ok(video_modified), Ok(playlist_modified)) =
|
(video_meta.modified(), playlist_meta.modified())
|
||||||
(video_meta.modified(), playlist_meta.modified())
|
{
|
||||||
{
|
return video_modified > playlist_modified;
|
||||||
return video_modified > playlist_modified;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If we can't determine, assume it needs generation
|
// If we can't determine, assume it needs generation
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -156,20 +156,20 @@ async fn get_video_rotation(video_path: &str) -> i32 {
|
|||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(output) = output {
|
if let Ok(output) = output
|
||||||
if output.status.success() {
|
&& output.status.success() {
|
||||||
let rotation_str = String::from_utf8_lossy(&output.stdout);
|
let rotation_str = String::from_utf8_lossy(&output.stdout);
|
||||||
let rotation_str = rotation_str.trim();
|
let rotation_str = rotation_str.trim();
|
||||||
if !rotation_str.is_empty() {
|
if !rotation_str.is_empty()
|
||||||
if let Ok(rotation) = rotation_str.parse::<i32>() {
|
&& let Ok(rotation) = rotation_str.parse::<i32>()
|
||||||
if rotation != 0 {
|
&& rotation != 0 {
|
||||||
debug!("Detected rotation {}° from stream tag for {}", rotation, video_path);
|
debug!(
|
||||||
|
"Detected rotation {}° from stream tag for {}",
|
||||||
|
rotation, video_path
|
||||||
|
);
|
||||||
return rotation;
|
return rotation;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check display matrix side data (modern videos, e.g. iPhone)
|
// Check display matrix side data (modern videos, e.g. iPhone)
|
||||||
let output = tokio::process::Command::new("ffprobe")
|
let output = tokio::process::Command::new("ffprobe")
|
||||||
@@ -185,21 +185,22 @@ async fn get_video_rotation(video_path: &str) -> i32 {
|
|||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(output) = output {
|
if let Ok(output) = output
|
||||||
if output.status.success() {
|
&& output.status.success() {
|
||||||
let rotation_str = String::from_utf8_lossy(&output.stdout);
|
let rotation_str = String::from_utf8_lossy(&output.stdout);
|
||||||
let rotation_str = rotation_str.trim();
|
let rotation_str = rotation_str.trim();
|
||||||
if !rotation_str.is_empty() {
|
if !rotation_str.is_empty()
|
||||||
if let Ok(rotation) = rotation_str.parse::<f64>() {
|
&& let Ok(rotation) = rotation_str.parse::<f64>() {
|
||||||
let rotation = rotation.abs() as i32;
|
let rotation = rotation.abs() as i32;
|
||||||
if rotation != 0 {
|
if rotation != 0 {
|
||||||
debug!("Detected rotation {}° from display matrix for {}", rotation, video_path);
|
debug!(
|
||||||
|
"Detected rotation {}° from display matrix for {}",
|
||||||
|
rotation, video_path
|
||||||
|
);
|
||||||
return rotation;
|
return rotation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user