Merge pull request 'Release v26.05.25.3 — ML embedder SigLIP fix, import-UX, extension publish' (#13) from dev into main

This commit was merged in pull request #13.
This commit is contained in:
2026-05-25 17:56:50 -04:00
7 changed files with 78 additions and 30 deletions
+2
View File
@@ -47,6 +47,8 @@ async def status():
if active: if active:
payload["active_batch"] = { payload["active_batch"] = {
"id": active.id, "id": active.id,
"source_path": active.source_path,
"scan_mode": active.scan_mode,
"total_files": active.total_files, "total_files": active.total_files,
"imported": active.imported, "imported": active.imported,
"skipped": active.skipped, "skipped": active.skipped,
+13 -2
View File
@@ -34,10 +34,21 @@ class Embedder:
if self._model is not None: if self._model is not None:
return return
import torch import torch
from transformers import AutoModel, AutoProcessor from transformers import AutoModel, SiglipImageProcessor
self._torch = torch self._torch = torch
self._processor = AutoProcessor.from_pretrained(str(self._model_dir)) # FC's embedder only does IMAGE inference — never text. AutoProcessor
# loads the full processor including SiglipTokenizer, which requires
# the sentencepiece library at import time even if we never call it.
# SiglipImageProcessor loads ONLY preprocessor_config.json (image
# side) and skips the tokenizer config entirely. Operator hit the
# ImportError 2026-05-25 once the ml-worker started actually running
# tag_and_embed; switching to the image-only loader avoids the
# tokenizer dep without adding ~30 MB of unused C++ build to the
# lean ml-worker image.
self._processor = SiglipImageProcessor.from_pretrained(
str(self._model_dir)
)
self._model = AutoModel.from_pretrained(str(self._model_dir)) self._model = AutoModel.from_pretrained(str(self._model_dir))
self._model.eval() self._model.eval()
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "FabledCurator", "name": "FabledCurator",
"version": "1.0.0", "version": "1.0.1",
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.", "description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
"browser_specific_settings": { "browser_specific_settings": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "fabledcurator-extension", "name": "fabledcurator-extension",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"description": "Firefox extension for FabledCurator", "description": "Firefox extension for FabledCurator",
"scripts": { "scripts": {
@@ -7,7 +7,8 @@
indeterminate color="accent" size="20" indeterminate color="accent" size="20"
/> />
<span> <span>
Scanning {{ store.activeBatch.source_path }} {{ store.activeBatch.scan_mode === 'deep' ? 'Deep scanning' : 'Scanning' }}
{{ store.activeBatch.source_path || '/import' }}
imported {{ store.activeBatch.imported }}, imported {{ store.activeBatch.imported }},
skipped {{ store.activeBatch.skipped }}, skipped {{ store.activeBatch.skipped }},
failed {{ store.activeBatch.failed }} / failed {{ store.activeBatch.failed }} /
@@ -24,8 +25,12 @@
<p class="text-body-2 mb-3"> <p class="text-body-2 mb-3">
<span v-if="!store.activeBatch"> <span v-if="!store.activeBatch">
Run a quick scan of the import directory. Deep scan (pHash dedup, <strong>Quick scan</strong> walks <code>/import</code> and enqueues
archives) lands in FC-2d. new files only (skips paths already on a non-failed ImportTask).
<strong>Deep scan</strong> additionally chains a phash backfill
across the existing library use after bulk-imports to catch
near-duplicates that slipped through. Both modes route non-media
+ sidecar pairs through PostAttachment capture.
</span> </span>
<span v-else> <span v-else>
An active batch is in progress. Wait for it to finish, or click An active batch is in progress. Wait for it to finish, or click
@@ -34,15 +39,26 @@
</span> </span>
</p> </p>
<v-btn <div class="d-flex flex-wrap" style="gap: 12px;">
color="primary" rounded="pill" <v-btn
:disabled="!!store.activeBatch" color="primary" rounded="pill"
:loading="busy" :disabled="!!store.activeBatch"
@click="trigger" :loading="busy === 'quick'"
> @click="trigger('quick')"
<v-icon start>mdi-magnify-scan</v-icon> >
Quick scan <v-icon start>mdi-magnify-scan</v-icon>
</v-btn> Quick scan
</v-btn>
<v-btn
color="secondary" rounded="pill" variant="tonal"
:disabled="!!store.activeBatch"
:loading="busy === 'deep'"
@click="trigger('deep')"
>
<v-icon start>mdi-magnify-plus-outline</v-icon>
Deep scan
</v-btn>
</div>
<v-alert v-if="store.triggerError" type="error" variant="tonal" class="mt-3" closable> <v-alert v-if="store.triggerError" type="error" variant="tonal" class="mt-3" closable>
{{ store.triggerError }} {{ store.triggerError }}
@@ -56,12 +72,12 @@ import { ref } from 'vue'
import { useImportStore } from '../../stores/import.js' import { useImportStore } from '../../stores/import.js'
const store = useImportStore() const store = useImportStore()
const busy = ref(false) const busy = ref(null)
const clearing = ref(false) const clearing = ref(false)
async function trigger() { async function trigger(mode) {
busy.value = true busy.value = mode
try { await store.triggerScan() } catch {} finally { busy.value = false } try { await store.triggerScan(mode) } catch {} finally { busy.value = null }
} }
async function onClearStuck() { async function onClearStuck() {
+13 -7
View File
@@ -52,25 +52,31 @@ export const useImportStore = defineStore('import', () => {
} }
} }
async function triggerScan() { async function triggerScan(mode = 'quick') {
if (!['quick', 'deep', 'verify'].includes(mode)) {
throw new Error(`unsupported scan mode: ${mode}`)
}
triggerError.value = null triggerError.value = null
try { try {
await api.post('/api/import/trigger', { body: { mode: 'quick' } }) await api.post('/api/import/trigger', { body: { mode } })
// Acknowledge immediately so the click isn't invisible. scan_directory // Acknowledge immediately so the click isn't invisible. scan_directory
// can finalize the batch synchronously when every file in /import is // can finalize the batch synchronously when every file in /import is
// already on a non-failed ImportTask (operator-flagged 2026-05-25: // already on a non-failed ImportTask (operator-flagged 2026-05-25:
// 233k existing tasks → all paths in skip-set → files_seen=0 → // 233k existing tasks → all paths in skip-set → files_seen=0 →
// batch flashes 'running' for <100ms then 'complete' before the // batch flashes 'running' for <100ms then 'complete' before the
// first refreshStatus() lands; UI never sees the active state). // first refreshStatus() lands; UI never sees the active state).
window.__fcToast?.({ text: 'Scan triggered', type: 'success' }) const label = mode === 'deep'
? 'Deep scan triggered (pHash backfill chained)'
: mode === 'verify' ? 'Library verify triggered' : 'Quick scan triggered'
window.__fcToast?.({ text: label, type: 'success' })
await refreshStatus() await refreshStatus()
// Re-poll twice over ~5s to catch quick-finalize transitions and // Re-poll twice over ~5s to catch quick-finalize transitions and
// surface a result toast either way. // surface a result toast either way. Skip the "no new files" hint
// for 'verify' since it doesn't walk the import_root.
setTimeout(async () => { setTimeout(async () => {
await refreshStatus() await refreshStatus()
if (!activeBatch.value) { await loadTasks(true)
// Either scan completed with zero new files, or it never visibly if (!activeBatch.value && mode !== 'verify') {
// started. Fetch the freshest task to differentiate.
window.__fcToast?.({ window.__fcToast?.({
text: 'Scan complete — no new files (everything already on an ImportTask row)', text: 'Scan complete — no new files (everything already on an ImportTask row)',
type: 'info', type: 'info',
+16 -3
View File
@@ -1,6 +1,16 @@
<template> <template>
<v-container fluid class="py-6"> <v-container fluid class="py-6">
<v-tabs v-model="tab" color="accent" class="mb-4"> <!-- Sticky tabs: operator-flagged 2026-05-25 long Import / Maintenance
panels pushed the tab strip out of the viewport, forcing a scroll-
to-top just to change tab. AppShell's TopNav is 64px sticky, so the
tab strip lives directly under it. Background uses the theme surface
token so it visually merges with the page rather than the
translucent v-tabs default. -->
<v-tabs
v-model="tab" color="accent" class="mb-4"
style="position: sticky; top: 64px; z-index: 4;
background: rgb(var(--v-theme-surface));"
>
<v-tab value="overview">Overview</v-tab> <v-tab value="overview">Overview</v-tab>
<v-tab value="activity">Activity</v-tab> <v-tab value="activity">Activity</v-tab>
<v-tab value="import">Import</v-tab> <v-tab value="import">Import</v-tab>
@@ -28,11 +38,14 @@
</v-window-item> </v-window-item>
<v-window-item value="import"> <v-window-item value="import">
<!-- Order: trigger → recent tasks → filters. Tasks sit directly
below the trigger so operator sees hit/miss feedback without
scrolling past the filter card (operator-flagged 2026-05-25). -->
<ImportTriggerPanel /> <ImportTriggerPanel />
<v-divider class="my-6" /> <v-divider class="my-6" />
<ImportFiltersForm />
<v-divider class="my-6" />
<ImportTaskList /> <ImportTaskList />
<v-divider class="my-6" />
<ImportFiltersForm />
</v-window-item> </v-window-item>
<v-window-item value="maintenance"> <v-window-item value="maintenance">