feat(ml): default to SigLIP 2 (new installs) + model dropdown, no free-text (#1203)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m25s

- Migration 0069: new installs default to SigLIP 2 (so400m, 512px, 1152-d drop-in)
  — UPDATE applies ONLY where no image is embedded yet (fresh install), so an
  existing library is NOT silently invalidated; it switches deliberately via the
  dropdown → Re-embed → Retrain. Column server_defaults moved to SigLIP 2.
- GET /api/ml/embedder-models: server-authoritative supported list (SigLIP 2 512
  recommended / 384 faster / SigLIP 1 384 original) so the UI never free-types.
- GpuAgentCard: the two name/version text fields → a single model dropdown;
  Save sets name+version from the picked option (the current model is always
  selectable even if off-list).
- embedder.py DEFAULT_MODEL_NAME unchanged (stays the baked local-dir SigLIP 1)
  to avoid a local-dir/weights mismatch; SigLIP 2 loads by HF name, cached on the
  ml-worker's persistent HF_HOME.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
2026-06-30 16:29:27 -04:00
parent 80f8eb4756
commit 359bc5a283
6 changed files with 133 additions and 31 deletions
@@ -117,15 +117,12 @@
the suggest cut; 0.92 recommended.
</p>
<!-- Embedding model (advanced) -->
<div v-if="ml.settings" class="fc-section-h mt-5 mb-1">Embedding model (advanced)</div>
<!-- Embedding model -->
<div v-if="ml.settings" class="fc-section-h mt-5 mb-1">Embedding model</div>
<div v-if="ml.settings">
<v-text-field
v-model="modelName" label="HF model name" density="compact" hide-details
variant="outlined" class="mb-2"
/>
<v-text-field
v-model="modelVersion" label="Version stamp" density="compact" hide-details
<v-select
v-model="selectedModel" :items="modelItems" item-title="label"
item-value="name" label="Model" density="compact" hide-details
variant="outlined"
/>
<div class="d-flex mt-3" style="gap:8px">
@@ -139,12 +136,11 @@
>Re-embed library (GPU)</v-btn>
</div>
<p class="fc-muted text-caption mt-2 mb-0">
Changing the model means a DIFFERENT embedding space. After saving a new
model + version, run <b>Re-embed library</b> (the GPU agent re-embeds
whole images + concept crops), then <b>Retrain heads</b>. Suggestions
degrade until both finish. SigLIP 2 (<code>google/siglip2-so400m-patch16-512</code>,
version <code>siglip2-so400m-patch16-512</code>) is a 1152-d drop-in at
512px no schema change.
Switching the model is a DIFFERENT embedding space. After <b>Save model</b>,
run <b>Re-embed library</b> (the GPU agent re-embeds whole images + concept
crops), then <b>Retrain heads</b> suggestions degrade until both finish.
SigLIP 2 (512px) is a 1152-d drop-in over SigLIP 1; new installs default to
it. Your existing library stays on its current model until you re-embed.
</p>
</div>
</MaintenanceTile>
@@ -173,8 +169,8 @@ const savingThreshold = ref(false)
const autoApply = ref(true)
const autoThreshold = ref(0.92)
const savingAuto = ref(false)
const modelName = ref('')
const modelVersion = ref('')
const modelItems = ref([])
const selectedModel = ref(null)
const savingModel = ref(false)
const reembedding = ref(false)
const queue = ref({ pending: 0, leased: 0, done: 0, error: 0 })
@@ -204,18 +200,26 @@ onMounted(async () => {
autoThreshold.value = ml.settings.ccip_auto_apply_threshold
}
if (ml.settings?.embedder_model_name != null) {
modelName.value = ml.settings.embedder_model_name
modelVersion.value = ml.settings.embedder_model_version
const items = await ml.embedderModels()
// Make sure the current model is selectable even if it's not in the list.
const cur = ml.settings.embedder_model_name
if (!items.some((m) => m.name === cur)) {
items.push({ name: cur, version: ml.settings.embedder_model_version, label: `${cur} (current)` })
}
modelItems.value = items
selectedModel.value = cur
}
} catch { /* non-fatal */ }
})
async function onSaveModel() {
const opt = modelItems.value.find((m) => m.name === selectedModel.value)
if (!opt) return
savingModel.value = true
try {
await ml.patchSettings({
embedder_model_name: modelName.value.trim(),
embedder_model_version: modelVersion.value.trim(),
embedder_model_name: opt.name,
embedder_model_version: opt.version,
})
toast({ text: 'Embedding model saved — now Re-embed library, then Retrain heads', type: 'success' })
} catch (e) {