feat: nightly agentic pre-generation of memory reels
Implement end-to-end nightly pre-generation of memory reels with agentic
scripting that grounds narration in calendar, location, messages, and RAG.
Sections A-E from the plan:
A. Extract produce_reel pipeline core from run_reel_job with
ScripterMode::Fast/Agentic and progress callbacks.
B. Agentic scripter: factor run_readonly_tool_loop from the insight
generator, build read-only tool gate, prompt builder with GPS, and
generate_script_agentic with fallback to fast path.
C. Precomputed reels ledger (SQLite table + DAO), GET /reels/precomputed
handler with validity gate, GET /reels/by-key/{key}/video streaming,
and normalize_library_key helper.
D. Nightly scheduler: spawn_pregen_scheduler with configurable hour,
run_pregen_batch (day/week/month spans), pregen_one with dedup and
disk-check, secs_until_next_run_hour time math.
E. user_ai_prefs passive mirror table + DAO for param capture in
create_reel_handler and replay in the scheduler.
Also fixes resolve_library_param signature to take &[Library] and adds
resolve_library_param_state wrapper for AppState callers.
New files: migrations/2026-06-13-000000_add_precomputed_reels/,
migrations/2026-06-13-000010_add_user_ai_prefs/,
src/database/precomputed_reel_dao.rs,
src/database/user_ai_prefs_dao.rs
This commit is contained in:
1 parent
b30c8c16d0
commit
f707353807
26 files changed
+1825
-153
No files matched your search
+19
-5
@@ -207,6 +207,7 @@ fn form_photo_beats(
|
||||
date,
|
||||
insight_title: None,
|
||||
insight_summary: None,
|
||||
gps: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -255,6 +256,7 @@ pub fn form_beats(
|
||||
date: v.created,
|
||||
insight_title: None,
|
||||
insight_summary: None,
|
||||
gps: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -334,15 +336,20 @@ fn distinct_years(items: &[memories::MemoryItem], tz: Option<FixedOffset>) -> Ve
|
||||
years
|
||||
}
|
||||
|
||||
/// Background pass: fill each beat's cached insight (title + summary) from its
|
||||
/// lead photo, where one exists. Best-effort — a missing or errored lookup
|
||||
/// leaves the fields `None` and the scripter narrates from the date alone.
|
||||
/// Background pass: fill each beat's cached insight (title + summary) and
|
||||
/// GPS coordinates from its lead photo, where one exists. Best-effort — a
|
||||
/// missing or errored lookup leaves the fields `None` and the scripter
|
||||
/// narrates from the date alone.
|
||||
pub fn enrich(
|
||||
insight_dao: &Mutex<Box<dyn InsightDao>>,
|
||||
exif_dao: &Mutex<Box<dyn ExifDao>>,
|
||||
span_context: &opentelemetry::Context,
|
||||
beats: &mut [PlannedBeat],
|
||||
) {
|
||||
let Ok(mut dao) = insight_dao.lock() else {
|
||||
let Ok(mut insight_dao) = insight_dao.lock() else {
|
||||
return;
|
||||
};
|
||||
let Ok(mut exif_dao) = exif_dao.lock() else {
|
||||
return;
|
||||
};
|
||||
for beat in beats.iter_mut() {
|
||||
@@ -352,10 +359,17 @@ pub fn enrich(
|
||||
}
|
||||
None => continue,
|
||||
};
|
||||
if let Ok(Some(insight)) = dao.get_insight(span_context, &rel_path) {
|
||||
if let Ok(Some(insight)) = insight_dao.get_insight(span_context, &rel_path) {
|
||||
beat.insight_title = Some(insight.title);
|
||||
beat.insight_summary = Some(insight.summary);
|
||||
}
|
||||
// Enrich GPS from EXIF when the lead media is a photo.
|
||||
if let Some(SegmentMedia::Photo { .. }) = beat.media.first()
|
||||
&& let Ok(Some(exif)) = exif_dao.get_exif(span_context, &rel_path)
|
||||
&& let (Some(lat), Some(lon)) = (exif.gps_latitude, exif.gps_longitude)
|
||||
{
|
||||
beat.gps = Some((lat as f64, lon as f64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user