refactor(ml): retire the Camie tagger + allowlist bulk-apply (#1189)
Heads + CCIP are the tag source and head auto-apply is the earned propagation.
The Camie tagger ran only to feed the allowlist bulk-apply (its ImagePrediction
rows had no other consumer), and the allowlist was a SECOND, un-earned auto-apply
path firing in parallel with heads on every accept — exactly the un-earned spray
the v2 pivot replaced. Retire both.
Behavior change: accepting a suggestion now applies the tag to THAT image only
(source='ml_accepted', a head-training positive) — it no longer allowlists +
fans the tag across the library via Camie. Propagation is heads' earned
auto-apply. (Loses instant cold-start propagation for booru-vocab tags; that was
un-earned and bypassed the precision gate.)
- tag_and_embed is now EMBED-ONLY (no Camie load/infer, no ImagePrediction
writes); backfill enqueues it for images with no embedding.
- Removed: services/ml/tagger.py, apply_allowlist_tags + helpers + daily beat +
every enqueue caller (accept/alias/merge/per-image), api/allowlist.py +
blueprint, ImagePrediction + TagAllowlist models/tables (migration 0067),
AllowlistTable.vue + allowlist store, the accept coverage-projection payload.
- AllowlistService gutted to accept/dismiss/undismiss/reject (the rejection store
the rail still needs); accept returns nothing, API returns {accepted, tag_id}.
- tag merge no longer repoints/triggers the allowlist; _keep_as_alias now keys on
ML-applied image_tag sources (incl. head_auto) instead of the allowlist.
- UI: MLBackfillCard relabelled to embedding-only; accept toast simplified;
MaintenancePanel drops the allowlist tile.
Left for a follow-up hygiene pass (now-inert, harmless): the dead settings
columns (tagger_store_floor, tagger_model_version, suggestion_threshold_*,
video_min_tag_frames), image_record.tagger_model_version, MLThresholdSliders
trim, and the Camie model download in download_models.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<MaintenanceTile
|
||||
icon="mdi-playlist-check"
|
||||
:title="`Allowlisted tags (${store.rows.length})`"
|
||||
blurb="Tags auto-applied to images that score above their threshold. Tune the
|
||||
threshold and see how many images it would cover."
|
||||
>
|
||||
<v-data-table-virtual
|
||||
:headers="headers" :items="store.rows" :loading="store.loading"
|
||||
height="360" density="compact" fixed-header
|
||||
no-data-text="No tags on the allowlist yet — accept a suggestion to add one."
|
||||
>
|
||||
<template #item.applied_count="{ item }">
|
||||
<span class="fc-num">{{ item.applied_count ?? '—' }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.min_confidence="{ item }">
|
||||
<div class="fc-thr">
|
||||
<v-text-field
|
||||
:model-value="item.min_confidence" type="number"
|
||||
density="compact" hide-details style="max-width: 100px;"
|
||||
:min="floor" max="1" step="0.05"
|
||||
:aria-label="`Auto-apply threshold for ${item.tag_name}`"
|
||||
@update:model-value="(v) => onThreshold(item, v)"
|
||||
/>
|
||||
<span
|
||||
v-if="proj[item.tag_id]"
|
||||
class="fc-thr__proj"
|
||||
:class="{ 'fc-thr__proj--loading': proj[item.tag_id].loading }"
|
||||
:title="`At ${proj[item.tag_id].threshold}, a sweep would cover this many images`"
|
||||
>≈ {{ proj[item.tag_id].count }} at {{ proj[item.tag_id].threshold }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.coverage_count="{ item }">
|
||||
<span class="fc-num" :title="`Images a sweep covers at ${item.min_confidence}`">
|
||||
{{ item.coverage_count ?? '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-delete" size="x-small" variant="text" color="error"
|
||||
:aria-label="`Remove ${item.tag_name} from the allowlist`"
|
||||
@click="store.remove(item.tag_id)"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table-virtual>
|
||||
<p class="fc-muted text-caption mt-2">
|
||||
<strong>Applied</strong> = images currently carrying the tag.
|
||||
<strong>Covers</strong> = images a sweep would auto-apply it to at the
|
||||
current threshold. Lower the threshold to cover more (less certain) images.
|
||||
</p>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive } from 'vue'
|
||||
import { useAllowlistStore } from '../../stores/allowlist.js'
|
||||
import { useMLStore } from '../../stores/ml.js'
|
||||
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
||||
|
||||
const store = useAllowlistStore()
|
||||
const ml = useMLStore()
|
||||
// min_confidence can't be set below the tagger store floor — predictions
|
||||
// below it aren't stored, so a lower threshold would behave identically to
|
||||
// the floor. The backend clamps too (#764).
|
||||
const floor = computed(() => ml.settings?.tagger_store_floor ?? 0.70)
|
||||
const headers = [
|
||||
{ title: 'Tag', key: 'tag_name', sortable: true },
|
||||
{ title: 'Kind', key: 'tag_kind', sortable: true, width: 100 },
|
||||
{ title: 'Applied', key: 'applied_count', sortable: true, width: 90 },
|
||||
{ title: 'Min confidence', key: 'min_confidence', sortable: false, width: 220 },
|
||||
{ title: 'Covers', key: 'coverage_count', sortable: true, width: 90 },
|
||||
{ title: '', key: 'actions', sortable: false, width: 56 }
|
||||
]
|
||||
|
||||
// Per-row live projection while the operator drags a threshold:
|
||||
// proj[tagId] = { threshold, count, loading }
|
||||
const proj = reactive({})
|
||||
|
||||
onMounted(() => {
|
||||
store.load()
|
||||
if (!ml.settings) ml.loadSettings()
|
||||
})
|
||||
|
||||
const debounces = {}
|
||||
function onThreshold(item, value) {
|
||||
const tagId = item.tag_id
|
||||
const v = Math.max(parseFloat(value), floor.value)
|
||||
if (!(v > 0 && v <= 1)) return
|
||||
const shown = Number(v.toFixed(2))
|
||||
// Optimistic live projection box (loading until the count returns).
|
||||
proj[tagId] = { threshold: shown, count: proj[tagId]?.count ?? '…', loading: true }
|
||||
if (debounces[tagId]) clearTimeout(debounces[tagId])
|
||||
debounces[tagId] = setTimeout(async () => {
|
||||
try {
|
||||
const { count } = await store.coverage(tagId, v)
|
||||
proj[tagId] = { threshold: shown, count, loading: false }
|
||||
} catch {
|
||||
delete proj[tagId] // drop the projection rather than show a wrong number
|
||||
}
|
||||
// Commit the new threshold (also refreshes the row's stored coverage_count).
|
||||
store.updateThreshold(tagId, v)
|
||||
}, 500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-num { font-variant-numeric: tabular-nums; }
|
||||
.fc-thr { display: flex; align-items: center; gap: 10px; }
|
||||
.fc-thr__proj {
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: rgb(var(--v-theme-accent));
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fc-thr__proj--loading { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
</style>
|
||||
@@ -2,12 +2,13 @@
|
||||
<MaintenanceTile
|
||||
icon="mdi-refresh"
|
||||
title="ML backfill"
|
||||
blurb="Re-run tagging + embeddings on images missing them."
|
||||
blurb="Compute SigLIP embeddings on images missing them."
|
||||
:open="busy"
|
||||
>
|
||||
<p class="text-body-2 mb-3">
|
||||
Re-run Camie + SigLIP on images missing predictions or embeddings
|
||||
for the current model versions. Safe to re-run.
|
||||
Compute the SigLIP embedding for any image that doesn't have one yet
|
||||
(CPU). Safe to re-run. To re-embed under a NEW model, use the GPU
|
||||
agent's "Re-embed library" instead.
|
||||
</p>
|
||||
<v-btn color="primary" rounded="pill" :loading="busy" @click="run">
|
||||
<v-icon start>mdi-refresh</v-icon> Run backfill now
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="fc-maint">
|
||||
<p class="fc-muted text-body-2 mb-5">
|
||||
One-off backfills, tagging config and storage tools. The ML backfill runs
|
||||
nightly; the allowlist auto-applies accepted tags. Click a tile to open it.
|
||||
One-off backfills, tagging config and storage tools. Heads train nightly
|
||||
and auto-apply earned tags. Click a tile to open it.
|
||||
</p>
|
||||
|
||||
<section class="fc-section">
|
||||
@@ -26,7 +26,6 @@
|
||||
<MLThresholdSliders />
|
||||
<HeadsCard />
|
||||
<GpuAgentCard />
|
||||
<AllowlistTable />
|
||||
<AliasTable />
|
||||
<TagEvalCard />
|
||||
</div>
|
||||
@@ -53,7 +52,6 @@ import DbMaintenanceCard from './DbMaintenanceCard.vue'
|
||||
import MLThresholdSliders from './MLThresholdSliders.vue'
|
||||
import HeadsCard from './HeadsCard.vue'
|
||||
import GpuAgentCard from './GpuAgentCard.vue'
|
||||
import AllowlistTable from './AllowlistTable.vue'
|
||||
import AliasTable from './AliasTable.vue'
|
||||
import TagEvalCard from './TagEvalCard.vue'
|
||||
import BackupCard from './BackupCard.vue'
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from '../composables/useApi.js'
|
||||
|
||||
export const useAllowlistStore = defineStore('allowlist', () => {
|
||||
const api = useApi()
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try { rows.value = await api.get('/api/allowlist') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function updateThreshold(tagId, minConfidence) {
|
||||
await api.patch(`/api/tags/${tagId}/allowlist`, {
|
||||
body: { min_confidence: minConfidence }
|
||||
})
|
||||
const r = rows.value.find(x => x.tag_id === tagId)
|
||||
if (r) {
|
||||
r.min_confidence = minConfidence
|
||||
// The committed threshold changed the covered pool — refresh the row's
|
||||
// coverage so the table stays truthful after a save.
|
||||
try { r.coverage_count = (await coverage(tagId, minConfidence)).count }
|
||||
catch { /* leave the stale count rather than blank it */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Live "at threshold T, a sweep would cover ~N images" projection for the
|
||||
// tuning dashboard. Returns { count, threshold }.
|
||||
async function coverage(tagId, threshold) {
|
||||
return api.get(`/api/tags/${tagId}/allowlist/coverage`, {
|
||||
params: { threshold }
|
||||
})
|
||||
}
|
||||
|
||||
async function remove(tagId) {
|
||||
await api.delete(`/api/tags/${tagId}/allowlist`)
|
||||
rows.value = rows.value.filter(x => x.tag_id !== tagId)
|
||||
}
|
||||
|
||||
return { rows, loading, load, updateThreshold, coverage, remove }
|
||||
})
|
||||
@@ -113,7 +113,7 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
})
|
||||
tagId = created.id
|
||||
}
|
||||
const res = await api.post(`/api/images/${imageId}/suggestions/accept`, {
|
||||
await api.post(`/api/images/${imageId}/suggestions/accept`, {
|
||||
body: { tag_id: tagId }
|
||||
})
|
||||
// Only drop from THIS image's category list — if the user navigated,
|
||||
@@ -121,23 +121,14 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
if (currentImageId === imageId) {
|
||||
_dropEverywhere(suggestion)
|
||||
}
|
||||
_acceptToast('Tagged', suggestion.display_name, res)
|
||||
_acceptToast('Tagged', suggestion.display_name)
|
||||
}
|
||||
|
||||
// One non-blocking toast for accept/alias. When the accept newly allowlisted
|
||||
// the tag, surface the coverage PROJECTION (#7) so the operator sees the
|
||||
// auto-apply reach without a blocking pre-accept preview — the apply itself
|
||||
// runs async, hence "~N".
|
||||
function _acceptToast(verb, displayName, res) {
|
||||
if (res?.allowlisted) {
|
||||
const n = res.projected_count
|
||||
toast({
|
||||
text: `${verb}: ${displayName} — allowlisted, auto-applying to ~${n} image${n === 1 ? '' : 's'}`,
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
toast({ text: `${verb}: ${displayName}`, type: 'success' })
|
||||
}
|
||||
// One non-blocking toast for accept/alias. The accepted tag is applied to this
|
||||
// image and feeds head training; head auto-apply handles propagation (earned),
|
||||
// so there's no instant fan-out to project.
|
||||
function _acceptToast(verb, displayName) {
|
||||
toast({ text: `${verb}: ${displayName}`, type: 'success' })
|
||||
}
|
||||
|
||||
async function aliasAccept(suggestion, canonicalTagId) {
|
||||
@@ -149,7 +140,7 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
// reappearing unaliased. raw_name is null only for centroid hits, which
|
||||
// can't be aliased (the UI hides the action for them).
|
||||
const aliasString = suggestion.raw_name ?? suggestion.display_name
|
||||
const res = await api.post(`/api/images/${imageId}/suggestions/alias`, {
|
||||
await api.post(`/api/images/${imageId}/suggestions/alias`, {
|
||||
body: {
|
||||
alias_string: aliasString,
|
||||
alias_category: suggestion.category,
|
||||
@@ -159,7 +150,7 @@ export const useSuggestionsStore = defineStore('suggestions', () => {
|
||||
if (currentImageId === imageId) {
|
||||
_dropEverywhere(suggestion)
|
||||
}
|
||||
_acceptToast('Aliased & tagged', suggestion.display_name, res)
|
||||
_acceptToast('Aliased & tagged', suggestion.display_name)
|
||||
}
|
||||
|
||||
// Remove the alias behind an aliased suggestion (the raw prediction reverts to
|
||||
|
||||
Reference in New Issue
Block a user