feat: expand media discovery and AI workflows #8

Merged
LyAhn merged 25 commits from codex/5-discovery-features into main 2026-06-07 22:43:16 +00:00
3 changed files with 56 additions and 7 deletions
Showing only changes of commit 04ade3d5d6 - Show all commits
+1
View File
@@ -238,6 +238,7 @@ pub fn set_caption_detail(app_data_dir: &Path, detail: CaptionDetail) -> Result<
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
std::fs::write(path, detail.as_str())?; std::fs::write(path, detail.as_str())?;
CAPTION_SESSION_DIRTY.store(true, Ordering::Relaxed);
Ok(detail) Ok(detail)
} }
+22 -2
View File
@@ -693,9 +693,19 @@ pub fn reset_generated_captions(conn: &Connection, folder_id: Option<i64>) -> Re
WHERE folder_id = ?1", WHERE folder_id = ?1",
[folder_id], [folder_id],
)?; )?;
// Mark in-flight jobs cancelled so the worker skips writing back;
// delete all others.
tx.execute(
"UPDATE caption_jobs
SET status = 'cancelled', last_error = NULL, updated_at = datetime('now')
WHERE status = 'processing'
AND image_id IN (SELECT id FROM images WHERE folder_id = ?1)",
[folder_id],
)?;
tx.execute( tx.execute(
"DELETE FROM caption_jobs "DELETE FROM caption_jobs
WHERE image_id IN (SELECT id FROM images WHERE folder_id = ?1)", WHERE status NOT IN ('processing', 'cancelled')
AND image_id IN (SELECT id FROM images WHERE folder_id = ?1)",
[folder_id], [folder_id],
)?; )?;
} }
@@ -708,7 +718,17 @@ pub fn reset_generated_captions(conn: &Connection, folder_id: Option<i64>) -> Re
caption_error = NULL", caption_error = NULL",
[], [],
)?; )?;
tx.execute("DELETE FROM caption_jobs", [])?; // Same cancellation protocol as clear_caption_jobs.
tx.execute(
"UPDATE caption_jobs
SET status = 'cancelled', last_error = NULL, updated_at = datetime('now')
WHERE status = 'processing'",
[],
)?;
tx.execute(
"DELETE FROM caption_jobs WHERE status NOT IN ('processing', 'cancelled')",
[],
)?;
} }
} }
+33 -5
View File
@@ -24,6 +24,7 @@ interface Task {
stages: TaskStage[]; stages: TaskStage[];
hasFailedEmbeddings: boolean; hasFailedEmbeddings: boolean;
hasFailedTagging: boolean; hasFailedTagging: boolean;
hasFailedCaptions: boolean;
pendingMediaWork: number; pendingMediaWork: number;
embeddingProcessed: number; embeddingProcessed: number;
embeddingTotal: number; embeddingTotal: number;
@@ -148,16 +149,22 @@ export function BackgroundTasks() {
const taggingPending = jobs?.tagging_pending ?? 0; const taggingPending = jobs?.tagging_pending ?? 0;
const taggingReady = jobs?.tagging_ready ?? 0; const taggingReady = jobs?.tagging_ready ?? 0;
const taggingFailed = jobs?.tagging_failed ?? 0; const taggingFailed = jobs?.tagging_failed ?? 0;
const captionPending = jobs?.caption_pending ?? 0;
const captionReady = jobs?.caption_ready ?? 0;
const captionFailed = jobs?.caption_failed ?? 0;
const pendingMediaWork = thumbnailPending + metadataPending + embeddingPending + taggingPending; const pendingMediaWork = thumbnailPending + metadataPending + embeddingPending + taggingPending + captionPending;
const embeddingProcessed = embeddingReady + embeddingFailed; const embeddingProcessed = embeddingReady + embeddingFailed;
const embeddingTotal = embeddingProcessed + embeddingPending; const embeddingTotal = embeddingProcessed + embeddingPending;
const taggingProcessed = taggingReady + taggingFailed; const taggingProcessed = taggingReady + taggingFailed;
const taggingTotal = taggingProcessed + taggingPending; const taggingTotal = taggingProcessed + taggingPending;
const captionProcessed = captionReady + captionFailed;
const captionTotal = captionProcessed + captionPending;
const hasFailedEmbeddings = embeddingFailed > 0; const hasFailedEmbeddings = embeddingFailed > 0;
const hasFailedTagging = taggingFailed > 0; const hasFailedTagging = taggingFailed > 0;
const hasFailedCaptions = captionFailed > 0;
if (!index && pendingMediaWork === 0 && !hasFailedEmbeddings && !hasFailedTagging) return null; if (!index && pendingMediaWork === 0 && !hasFailedEmbeddings && !hasFailedTagging && !hasFailedCaptions) return null;
const stages: TaskStage[] = []; const stages: TaskStage[] = [];
@@ -209,6 +216,16 @@ export function BackgroundTasks() {
}); });
} }
if (captionPending > 0) {
const pct = captionTotal > 0 ? (captionProcessed / captionTotal) * 100 : 0;
stages.push({
label: "Captions",
detail: `${captionProcessed.toLocaleString()} / ${captionTotal.toLocaleString()}`,
progress: pct,
failed: false,
});
}
if (hasFailedEmbeddings && pendingMediaWork === 0) { if (hasFailedEmbeddings && pendingMediaWork === 0) {
stages.push({ stages.push({
label: "Failed", label: "Failed",
@@ -227,7 +244,16 @@ export function BackgroundTasks() {
}); });
} }
const snapshot = `${pendingMediaWork}:${embeddingFailed}:${taggingFailed}`; if (hasFailedCaptions && pendingMediaWork === 0) {
stages.push({
label: "Failed",
detail: `${captionFailed.toLocaleString()} captions`,
progress: null,
failed: true,
});
}
const snapshot = `${pendingMediaWork}:${embeddingFailed}:${taggingFailed}:${captionFailed}`;
return { return {
id: folder.id, id: folder.id,
@@ -235,6 +261,7 @@ export function BackgroundTasks() {
stages, stages,
hasFailedEmbeddings, hasFailedEmbeddings,
hasFailedTagging, hasFailedTagging,
hasFailedCaptions,
pendingMediaWork, pendingMediaWork,
embeddingProcessed, embeddingProcessed,
embeddingTotal, embeddingTotal,
@@ -262,6 +289,7 @@ export function BackgroundTasks() {
}], }],
hasFailedEmbeddings: false, hasFailedEmbeddings: false,
hasFailedTagging: false, hasFailedTagging: false,
hasFailedCaptions: false,
pendingMediaWork: 1, pendingMediaWork: 1,
embeddingProcessed: 0, embeddingProcessed: 0,
embeddingTotal: 0, embeddingTotal: 0,
@@ -275,7 +303,7 @@ export function BackgroundTasks() {
const primary = allTasks[0]; const primary = allTasks[0];
const extraCount = allTasks.length - 1; const extraCount = allTasks.length - 1;
const hasFailed = tasks.some((t) => (t.hasFailedEmbeddings || t.hasFailedTagging) && t.pendingMediaWork === 0); const hasFailed = tasks.some((t) => (t.hasFailedEmbeddings || t.hasFailedTagging || t.hasFailedCaptions) && t.pendingMediaWork === 0);
// Best progress bar value: use embedding progress if available (most informative), // Best progress bar value: use embedding progress if available (most informative),
// otherwise tagging progress, otherwise fall back to scanning progress, otherwise indeterminate. // otherwise tagging progress, otherwise fall back to scanning progress, otherwise indeterminate.
@@ -407,7 +435,7 @@ export function BackgroundTasks() {
const taskTaggingStage = task.stages.find((s) => s.label === "Tags"); const taskTaggingStage = task.stages.find((s) => s.label === "Tags");
const taskScanningStage = task.stages.find((s) => s.label === "Scanning"); const taskScanningStage = task.stages.find((s) => s.label === "Scanning");
const taskBarProgress = taskEmbeddingStage?.progress ?? taskTaggingStage?.progress ?? taskScanningStage?.progress ?? null; const taskBarProgress = taskEmbeddingStage?.progress ?? taskTaggingStage?.progress ?? taskScanningStage?.progress ?? null;
const taskHasFailed = (task.hasFailedEmbeddings || task.hasFailedTagging) && task.pendingMediaWork === 0; const taskHasFailed = (task.hasFailedEmbeddings || task.hasFailedTagging || task.hasFailedCaptions) && task.pendingMediaWork === 0;
return ( return (
<div key={task.id}> <div key={task.id}>