feat(tagging): title-based WIP auto-tagging (#1458)
Auto-apply the `wip` system tag to posts whose TITLE explicitly declares
work-in-progress ("WIP" / "work in progress") — a deterministic, high-precision
complement to the image-based ML `wip` head. WIP images are excluded from the
Explore/gallery browse, so honouring the artist's own label keeps unfinished
pieces out of the main browse.
- services/wip_title.py: precision-first token-anchored matcher (swipe/wiped
never trip it) + sync apply helpers (source='wip_title', ON CONFLICT DO
NOTHING, chunked under the psycopg param ceiling).
- importer: live hook on FRESH import only (never on deep-scan/supersede), so a
manually-removed WIP tag is never re-applied by a routine re-scan.
- maintenance.backfill_wip_title_tags: operator-triggered back-catalogue sweep
(coarse SQL prefilter + regex confirm, keyset-paginated). Deliberately NOT a
beat — a periodic re-run would silently undo manual removals.
- ImportSettings.wip_title_tagging_enabled (default ON, migration 0085) gating
the live hook; GET/PATCH + POST /settings/wip-title/scan.
- Settings UI: toggle + "Scan existing posts" button.
- Tests: pure matcher unit tests + integration (apply idempotency, backfill
precision).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,35 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-5" />
|
||||
|
||||
<!-- Title-based WIP auto-tagging (task #1458). The switch gates the LIVE
|
||||
import hook; the button runs the one-off back-catalogue scan (an
|
||||
explicit action — it is deliberately not a scheduled sweep so it can't
|
||||
silently re-apply a WIP tag you removed by hand). -->
|
||||
<div class="fc-wip">
|
||||
<div class="fc-wip__title">WIP auto-tagging</div>
|
||||
<v-switch
|
||||
v-model="local.wip_title_tagging_enabled"
|
||||
label="Tag work-in-progress from post titles"
|
||||
density="compact" hide-details color="primary" @change="save"
|
||||
/>
|
||||
<div class="fc-help mb-3">
|
||||
When a post's title says <strong>“WIP”</strong> or
|
||||
<strong>“work in progress”</strong>, new imports get the
|
||||
<code>wip</code> tag automatically — keeping unfinished pieces out of
|
||||
the Explore browse. Applies to new imports; run the scan below to catch
|
||||
posts already in your library.
|
||||
</div>
|
||||
<v-btn
|
||||
variant="tonal" color="primary" size="small"
|
||||
:loading="store.wipScanBusy" prepend-icon="mdi-magnify"
|
||||
@click="store.scanWipTitles()"
|
||||
>
|
||||
Scan existing posts for WIP titles
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-alert v-if="store.settingsError" type="error" variant="tonal" class="mt-2" closable>
|
||||
{{ store.settingsError }}
|
||||
</v-alert>
|
||||
@@ -109,6 +138,7 @@ const local = reactive({
|
||||
skip_transparent: false, transparency_threshold: 0.9,
|
||||
skip_single_color: false, single_color_threshold: 0.95,
|
||||
phash_threshold: 10,
|
||||
wip_title_tagging_enabled: true,
|
||||
})
|
||||
|
||||
watch(() => store.settings, (s) => { if (s) Object.assign(local, s) }, { immediate: true })
|
||||
@@ -124,11 +154,18 @@ async function save() {
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
margin-top: 2px;
|
||||
}
|
||||
.fc-phash__title {
|
||||
.fc-phash__title,
|
||||
.fc-wip__title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
.fc-wip code {
|
||||
font-size: 0.85em;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
background: rgb(var(--v-theme-surface-variant));
|
||||
}
|
||||
/* Headroom so the tick labels (Exact/Strict/Default/Loose) aren't clipped. */
|
||||
.fc-phash__slider { margin-bottom: 18px; }
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@ export const useImportStore = defineStore('import', () => {
|
||||
const settings = ref(null)
|
||||
const settingsLoading = ref(false)
|
||||
const settingsError = ref(null)
|
||||
const wipScanBusy = ref(false)
|
||||
|
||||
async function loadSettings() {
|
||||
settingsLoading.value = true
|
||||
@@ -40,8 +41,25 @@ export const useImportStore = defineStore('import', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue the back-catalogue WIP-title scan (task #1458). New imports are
|
||||
// tagged live; this catches posts already in the library. Fire-and-forget —
|
||||
// the sweep runs on the maintenance worker and its run shows in Activity.
|
||||
async function scanWipTitles() {
|
||||
wipScanBusy.value = true
|
||||
try {
|
||||
const r = await api.post('/api/settings/wip-title/scan')
|
||||
toast({ text: 'Scanning existing posts for WIP titles…', type: 'success' })
|
||||
return r
|
||||
} catch (e) {
|
||||
toast({ text: `WIP scan failed: ${e.message}`, type: 'error' })
|
||||
throw e
|
||||
} finally {
|
||||
wipScanBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
settings, settingsLoading, settingsError,
|
||||
loadSettings, patchSettings,
|
||||
settings, settingsLoading, settingsError, wipScanBusy,
|
||||
loadSettings, patchSettings, scanWipTitles,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user