refactor: strip AI pipeline to binary photo/other classifier
Drops face recognition, OCR, object detection, and semantic embeddings. The sole remaining vision task is a CLIP-based binary classifier (photography vs other); photos in "other" get needs_review=true so screenshots, documents, memes and scans can be triaged from a new filter pill in the UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,6 @@ import {
|
||||
FolderSearch,
|
||||
Shield,
|
||||
Brain,
|
||||
ScanText,
|
||||
UserSquare2,
|
||||
Boxes,
|
||||
Tags as TagsIcon,
|
||||
RotateCcw,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
@@ -1107,56 +1103,20 @@ interface AiFeaturesTabProps {
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
// Flags are keyed by the backend's canonical name ("vision.enabled",
|
||||
// "vision.ocr.enabled", ...). The metadata here just adds presentation
|
||||
// (label, short description, icon) so the tab layout stays data-driven.
|
||||
const FLAG_META: Array<{
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
icon: React.ReactNode
|
||||
// Optional "run this backfill" hook — lets the user kick off a stage's
|
||||
// backfill right from the toggle row without hopping to a separate UI.
|
||||
backfillTask?: 'embed' | 'ocr' | 'detect' | 'faces' | 'classify'
|
||||
}> = [
|
||||
{
|
||||
id: 'vision.enabled',
|
||||
label: 'Vision pipeline (master switch)',
|
||||
label: 'Vision classifier',
|
||||
description:
|
||||
'When off, every AI stage below is skipped — including newly uploaded photos. ' +
|
||||
'Existing results stay intact.',
|
||||
'Binary photo-vs-other classifier. Flags screenshots, documents, memes and ' +
|
||||
'scans with "needs review" so they can be triaged.',
|
||||
icon: <Sparkles className="h-3.5 w-3.5" />,
|
||||
},
|
||||
{
|
||||
id: 'vision.ocr.enabled',
|
||||
label: 'Text recognition (OCR)',
|
||||
description: 'Extract printed / handwritten text from photos so it becomes searchable.',
|
||||
icon: <ScanText className="h-3.5 w-3.5" />,
|
||||
backfillTask: 'ocr',
|
||||
},
|
||||
{
|
||||
id: 'vision.detector.enabled',
|
||||
label: 'Object detection',
|
||||
description: 'Tag photos with detected objects (person, car, dog, …) via YOLOv8n.',
|
||||
icon: <Boxes className="h-3.5 w-3.5" />,
|
||||
backfillTask: 'detect',
|
||||
},
|
||||
{
|
||||
id: 'vision.faces.enabled',
|
||||
label: 'Face recognition',
|
||||
description:
|
||||
'Find and cluster faces across the library (RetinaFace + ArcFace). ' +
|
||||
'Expensive on big libraries — disable if you don\'t need the People view.',
|
||||
icon: <UserSquare2 className="h-3.5 w-3.5" />,
|
||||
backfillTask: 'faces',
|
||||
},
|
||||
{
|
||||
id: 'vision.classifier.enabled',
|
||||
label: 'Content classification',
|
||||
description: 'Zero-shot CLIP tags for scenes / activities (beach, wedding, …).',
|
||||
icon: <TagsIcon className="h-3.5 w-3.5" />,
|
||||
backfillTask: 'classify',
|
||||
},
|
||||
]
|
||||
|
||||
function AiFeaturesTab({ busy, runAction }: AiFeaturesTabProps) {
|
||||
@@ -1183,12 +1143,11 @@ function AiFeaturesTab({ busy, runAction }: AiFeaturesTabProps) {
|
||||
queryClient.invalidateQueries({ queryKey: ['features'] })
|
||||
}
|
||||
|
||||
type BackfillTask = 'embed' | 'ocr' | 'detect' | 'faces' | 'classify' | null
|
||||
const runBackfill = (task: BackfillTask) =>
|
||||
const runBackfill = () =>
|
||||
runAction(
|
||||
`ai-backfill:${task ?? 'all'}`,
|
||||
() => adminApi.triggerAiBackfill({ task }),
|
||||
task ? `Backfill queued for ${task}` : 'Full backfill queued',
|
||||
`ai-backfill:all`,
|
||||
() => adminApi.triggerAiBackfill({}),
|
||||
'Classifier backfill queued',
|
||||
(r) => `Celery task ${r.task_id}`,
|
||||
)
|
||||
|
||||
@@ -1286,17 +1245,6 @@ function AiFeaturesTab({ busy, runAction }: AiFeaturesTabProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{meta.backfillTask && state.effective && !masterOff && (
|
||||
<div className="mt-2">
|
||||
<ActionButton
|
||||
loading={!!busy[`ai-backfill:${meta.backfillTask}`]}
|
||||
onClick={() => runBackfill(meta.backfillTask!)}
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
Run {meta.backfillTask} backfill
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -1306,35 +1254,17 @@ function AiFeaturesTab({ busy, runAction }: AiFeaturesTabProps) {
|
||||
|
||||
<Section icon={<Cpu className="h-4 w-4" />} title="Manual pipeline triggers">
|
||||
<p className="text-xs text-text-muted">
|
||||
Run a full pass across the enabled stages, recompute face
|
||||
clusters, or force a fresh filesystem scan. All three are safe
|
||||
to run repeatedly — the backfill only touches photos that
|
||||
don\'t yet have a given output, and the rescan skips files
|
||||
that are already indexed.
|
||||
Run the classifier over any photos that haven't been classified yet,
|
||||
or force a fresh filesystem scan. Both are safe to run repeatedly.
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<ActionButton
|
||||
loading={!!busy['ai-backfill:all']}
|
||||
onClick={() => runBackfill(null)}
|
||||
onClick={() => runBackfill()}
|
||||
disabled={masterOff}
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Run full vision backfill
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
loading={!!busy['recluster']}
|
||||
onClick={() =>
|
||||
runAction(
|
||||
'recluster',
|
||||
() => adminApi.triggerFaceRecluster(),
|
||||
'Face recluster queued',
|
||||
(r) => `Celery task ${r.task_id}`,
|
||||
)
|
||||
}
|
||||
disabled={masterOff || !flags['vision.faces.enabled']?.effective}
|
||||
>
|
||||
<UserSquare2 className="h-4 w-4" />
|
||||
Recluster faces
|
||||
Run classifier backfill
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
loading={!!busy['rescan-full']}
|
||||
|
||||
Reference in New Issue
Block a user