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:
@@ -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,
|
||||||
|
|||||||
@@ -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,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,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() {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user