feat(ml): Settings → Tagging 'Crop proposers' card (#134 step 3)
Exposes the detector config (per-proposer enable + weights + confidence, caps, dedupe IoU) in Settings → Tagging, backed by MLSettings via /api/ml/settings. ml_admin adds the detector fields to _EDITABLE + GET payload + validation (conf 0..1, caps >=1, IoU 0..1). New CropProposersCard.vue (mirrors HeadsCard) with working defaults pre-filled, per-field live-save (no restart — the agent picks changes up on its next lease), weights-format help, switch-revert on error. Closes milestone #134: all three proposers are on out-of-the-box and tunable in the UI. Test: detector defaults GET + patch round-trip + range validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<MaintenanceTile
|
||||
icon="mdi-crop"
|
||||
title="Crop proposers (where to crop)"
|
||||
blurb="Detectors that decide WHERE to crop each image before embedding — general figures, anime/furry anatomy, and comic panels — so a tag can ground to a region, not just the whole image."
|
||||
:open="false"
|
||||
>
|
||||
<p class="fc-muted text-body-2 mb-4">
|
||||
Each proposer finds regions that the crop → SigLIP bag scores alongside the
|
||||
whole image; the best-scoring region is what grounds a tag. Weights accept
|
||||
an <strong>ultralytics name</strong> (<code>yolo11n.pt</code>), a
|
||||
<strong>URL</strong>, or <strong>hf_repo::file</strong>. Edits reach the GPU
|
||||
agent on its next lease — no restart. After turning a proposer on, re-process
|
||||
the library so existing images gain its regions.
|
||||
</p>
|
||||
|
||||
<div v-for="p in proposers" :key="p.key" class="fc-proposer">
|
||||
<div class="d-flex align-center mb-1" style="gap: 10px;">
|
||||
<v-icon size="18" :color="p.on ? 'accent' : undefined">{{ p.icon }}</v-icon>
|
||||
<span class="fc-section-h">{{ p.label }}</span>
|
||||
<v-switch
|
||||
v-model="p.on" :loading="busy" hide-details density="compact"
|
||||
color="success" class="ml-auto"
|
||||
@update:model-value="v => saveToggle(p, v)"
|
||||
/>
|
||||
</div>
|
||||
<p class="fc-muted text-body-2 mb-2">{{ p.help }}</p>
|
||||
<div class="d-flex flex-wrap mb-4" style="gap: 12px;">
|
||||
<v-text-field
|
||||
v-model="p.weights" label="Weights" density="compact" hide-details
|
||||
style="min-width: 300px; flex: 1;" :disabled="busy || !p.on"
|
||||
placeholder="name | URL | hf_repo::file"
|
||||
@change="save({ [`detector_${p.key}_weights`]: p.weights })"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="p.conf" label="Confidence" type="number"
|
||||
min="0" max="1" step="0.05" density="compact" hide-details
|
||||
style="max-width: 140px;" :disabled="busy || !p.on"
|
||||
@change="save({ [`detector_${p.key}_conf`]: Number(p.conf) })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fc-caps">
|
||||
<div class="fc-section-h mb-1">Caps</div>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Bound how many crops each image can produce — protects GPU time and
|
||||
storage. Dedupe IoU drops near-duplicate crops before embedding.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap" style="gap: 12px;">
|
||||
<v-text-field
|
||||
v-for="c in caps" :key="c.key"
|
||||
v-model.number="c.val" :label="c.label" type="number"
|
||||
:min="c.min" :max="c.max" :step="c.step || 1" density="compact"
|
||||
hide-details style="max-width: 165px;" :disabled="busy"
|
||||
@change="save({ [c.key]: Number(c.val) })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MaintenanceTile>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toast } from '../../utils/toast.js'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import MaintenanceTile from '../common/MaintenanceTile.vue'
|
||||
import { useMLStore } from '../../stores/ml.js'
|
||||
|
||||
const mlSettings = useMLStore()
|
||||
const busy = ref(false)
|
||||
const proposers = ref([])
|
||||
const caps = ref([])
|
||||
|
||||
// Static copy per proposer; the mutable state (on/weights/conf) is seeded from
|
||||
// settings on mount so the card reflects the live DB config (rule 26 — it works
|
||||
// untouched).
|
||||
const DEFS = [
|
||||
{
|
||||
key: 'person', label: 'Figures (person)', icon: 'mdi-account-outline',
|
||||
help: 'General figure detector for the Western/realistic art the anime ' +
|
||||
'detector misses → character identity + a whole-figure crop.',
|
||||
},
|
||||
{
|
||||
key: 'anatomy', label: 'Anatomy (booru)', icon: 'mdi-dog-side',
|
||||
help: 'Anime / furry / NSFW torso components (heads, busts, hips, …) → ' +
|
||||
'localized concept crops.',
|
||||
},
|
||||
{
|
||||
key: 'panel', label: 'Comic panels', icon: 'mdi-view-grid-outline',
|
||||
help: 'Splits a comic page into its panels → one concept crop each.',
|
||||
},
|
||||
]
|
||||
const CAP_DEFS = [
|
||||
{ key: 'detector_max_figures', label: 'Max figures', min: 1 },
|
||||
{ key: 'detector_max_components', label: 'Max components', min: 1 },
|
||||
{ key: 'detector_max_panels', label: 'Max panels', min: 1 },
|
||||
{ key: 'detector_max_regions', label: 'Max regions / job', min: 1 },
|
||||
{ key: 'detector_dedupe_iou', label: 'Dedupe IoU', min: 0, max: 1, step: 0.05 },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try { await mlSettings.loadSettings() } catch { /* non-fatal — defaults show */ }
|
||||
const s = mlSettings.settings || {}
|
||||
proposers.value = DEFS.map(d => ({
|
||||
...d,
|
||||
on: !!s[`detector_${d.key}_enabled`],
|
||||
weights: s[`detector_${d.key}_weights`] ?? '',
|
||||
conf: s[`detector_${d.key}_conf`] ?? 0.3,
|
||||
}))
|
||||
caps.value = CAP_DEFS.map(c => ({ ...c, val: s[c.key] ?? 0 }))
|
||||
})
|
||||
|
||||
async function save(patch, revert) {
|
||||
busy.value = true
|
||||
try {
|
||||
await mlSettings.patchSettings(patch)
|
||||
toast({ text: 'Saved', type: 'success' })
|
||||
} catch (e) {
|
||||
if (revert) revert()
|
||||
toast({ text: `Could not save: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function saveToggle (p, v) {
|
||||
// Revert the switch on failure so it never lies about the persisted state.
|
||||
save({ [`detector_${p.key}_enabled`]: !!v }, () => { p.on = !v })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
.fc-section-h {
|
||||
font-size: 13px; font-weight: 700; letter-spacing: 0.03em;
|
||||
text-transform: uppercase; color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
.fc-proposer {
|
||||
border-top: 1px solid rgb(var(--v-theme-surface-light)); padding-top: 14px;
|
||||
}
|
||||
.fc-proposer:first-of-type { border-top: none; padding-top: 0; }
|
||||
.fc-caps {
|
||||
border-top: 1px solid rgb(var(--v-theme-surface-light)); padding-top: 16px;
|
||||
}
|
||||
code {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: 0.85em;
|
||||
background: rgb(var(--v-theme-surface-light)); padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -36,6 +36,7 @@
|
||||
</p>
|
||||
<div class="fc-tile-stack">
|
||||
<MLThresholdSliders />
|
||||
<CropProposersCard />
|
||||
<HeadsCard />
|
||||
<AliasTable />
|
||||
</div>
|
||||
@@ -75,6 +76,7 @@ import MissingFileRepairCard from './MissingFileRepairCard.vue'
|
||||
import GpuTriageCard from './GpuTriageCard.vue'
|
||||
import DbMaintenanceCard from './DbMaintenanceCard.vue'
|
||||
import MLThresholdSliders from './MLThresholdSliders.vue'
|
||||
import CropProposersCard from './CropProposersCard.vue'
|
||||
import HeadsCard from './HeadsCard.vue'
|
||||
import GpuAgentCard from './GpuAgentCard.vue'
|
||||
import AliasTable from './AliasTable.vue'
|
||||
|
||||
Reference in New Issue
Block a user