diff --git a/src/ai/daily_summary_job.rs b/src/ai/daily_summary_job.rs index 9e8f280..200ff9a 100644 --- a/src/ai/daily_summary_job.rs +++ b/src/ai/daily_summary_job.rs @@ -1,8 +1,9 @@ use anyhow::Result; use chrono::{NaiveDate, Utc}; use opentelemetry::trace::Status; +use regex::Regex; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; use tokio::time::sleep; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -112,14 +113,20 @@ pub fn strip_summary_boilerplate(summary: &str) -> String { } } + // "Today, and I discussed …" for any contact name (one or two + // capitalised words), so no names are baked into the code. + static NAMED_OPENING: LazyLock = LazyLock::new(|| { + Regex::new( + r"^(?:[Tt]oday,?\s+)?\p{Lu}[\p{L}'’-]*(?:\s+\p{Lu}[\p{L}'’-]*)?\s+and\s+I\s+discussed", + ) + .expect("valid regex") + }); + if let Some(m) = NAMED_OPENING.find(&text) { + text = text[m.end()..].trim_start().to_string(); + } + // Remove common opening phrases that add no semantic value let opening_phrases = [ - "Today, Melissa and I discussed", - "Today, Amanda and I discussed", - "Today Melissa and I discussed", - "Today Amanda and I discussed", - "Melissa and I discussed", - "Amanda and I discussed", "Today, I discussed", "Today I discussed", "The conversation covered", @@ -376,3 +383,24 @@ async fn generate_and_store_daily_summary( result?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::strip_summary_boilerplate; + + #[test] + fn strips_a_named_opening_for_any_contact() { + for raw in [ + "Today, Sam and I discussed the trip.", + "Today Sam Lee and I discussed the trip.", + "Jordan and I discussed the trip.", + ] { + assert_eq!(strip_summary_boilerplate(raw), "the trip.", "{raw}"); + } + assert_eq!( + strip_summary_boilerplate("The weather and I discussed nothing"), + "The weather and I discussed nothing", + "a lowercase second word isn't a name" + ); + } +} diff --git a/src/handlers/video.rs b/src/handlers/video.rs index cd97d29..2be2227 100644 --- a/src/handlers/video.rs +++ b/src/handlers/video.rs @@ -263,8 +263,8 @@ fn is_valid_hash(s: &str) -> bool { /// /// Normalizing to forward slashes is essential on Windows: `file_scan` /// stores rel_paths forward-slash regardless of OS, but a raw strip of a -/// backslash Windows path (`Z:\...\pic\Melissa\clip.mp4`) yields -/// `Melissa\clip.mp4`. `get_exif_batch` does an exact match with no +/// backslash Windows path (`Z:\...\pic\album\clip.mp4`) yields +/// `album\clip.mp4`. `get_exif_batch` does an exact match with no /// normalization, so the backslash form misses and the handler falls back /// to re-hashing the entire file on every request. /// Response body for `POST /video/original`. @@ -716,19 +716,19 @@ mod tests { #[test] fn rel_path_for_lookup_normalizes_windows_separators() { // Windows: backslash root + backslash full path. The stored row is - // forward-slash (`Melissa/clip.mp4`), so without normalization the + // forward-slash (`album/clip.mp4`), so without normalization the // lookup misses and the handler re-hashes the whole file. assert_eq!( - rel_path_for_lookup(r"Z:\Media\pic\Melissa\clip.mp4", r"Z:\Media\pic"), - "Melissa/clip.mp4" + rel_path_for_lookup(r"Z:\Media\pic\album\clip.mp4", r"Z:\Media\pic"), + "album/clip.mp4" ); } #[test] fn rel_path_for_lookup_handles_unix_separators() { assert_eq!( - rel_path_for_lookup("/media/pic/Melissa/clip.mp4", "/media/pic"), - "Melissa/clip.mp4" + rel_path_for_lookup("/media/pic/album/clip.mp4", "/media/pic"), + "album/clip.mp4" ); } @@ -765,8 +765,8 @@ mod tests { // If the root doesn't prefix the path (e.g. a stale mount), we keep // the whole path but still normalize separators rather than panic. assert_eq!( - rel_path_for_lookup(r"D:\other\Melissa\clip.mp4", r"Z:\Media\pic"), - "D:/other/Melissa/clip.mp4" + rel_path_for_lookup(r"D:\other\album\clip.mp4", r"Z:\Media\pic"), + "D:/other/album/clip.mp4" ); }