Merge pull request 'Release: SigLIP 2 default (new installs) + embedder model dropdown' (#157) from dev into main
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m26s
Build images / sign-extension (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / build-agent (push) Successful in 7s
Build images / build-ml (push) Successful in 8s
Build images / build-web (push) Successful in 11s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m26s
This commit was merged in pull request #157.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""default the embedder to SigLIP 2 — for FRESH installs only (#1203)
|
||||
|
||||
Make SigLIP 2 (so400m, 512px; a 1152-d drop-in) the default embedder. New
|
||||
installs start on it. An EXISTING library is NOT touched: flipping its stored
|
||||
embedder version would mark every embedding stale (the scorer is version-gated)
|
||||
and kill suggestions until a full re-embed+retrain — so an existing instance
|
||||
switches deliberately via Settings → GPU agent → Embedding model → Re-embed →
|
||||
Retrain. We detect "fresh" by the absence of any embedded image.
|
||||
|
||||
Revision ID: 0069
|
||||
Revises: 0068
|
||||
Create Date: 2026-06-30
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0069"
|
||||
down_revision: Union[str, None] = "0068"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_NEW_NAME = "google/siglip2-so400m-patch16-512"
|
||||
_NEW_VERSION = "siglip2-so400m-patch16-512"
|
||||
_OLD_NAME = "google/siglip-so400m-patch14-384"
|
||||
_OLD_VERSION = "siglip-so400m-patch14-384"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Fresh install (nothing embedded yet) → adopt SigLIP 2.
|
||||
op.execute(
|
||||
f"""
|
||||
UPDATE ml_settings SET
|
||||
embedder_model_name = '{_NEW_NAME}',
|
||||
embedder_model_version = '{_NEW_VERSION}'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM image_record WHERE siglip_embedding IS NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.alter_column("ml_settings", "embedder_model_name", server_default=_NEW_NAME)
|
||||
op.alter_column(
|
||||
"ml_settings", "embedder_model_version", server_default=_NEW_VERSION
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.alter_column("ml_settings", "embedder_model_name", server_default=_OLD_NAME)
|
||||
op.alter_column(
|
||||
"ml_settings", "embedder_model_version", server_default=_OLD_VERSION
|
||||
)
|
||||
@@ -23,6 +23,36 @@ _EDITABLE = (
|
||||
)
|
||||
|
||||
|
||||
# Supported embedders for the Settings dropdown — all 1152-d so a swap is a
|
||||
# drop-in (re-embed + retrain, no schema change). Server-authoritative so the UI
|
||||
# never free-types a model name.
|
||||
SUPPORTED_EMBEDDERS = (
|
||||
{
|
||||
"name": "google/siglip2-so400m-patch16-512",
|
||||
"version": "siglip2-so400m-patch16-512",
|
||||
"label": "SigLIP 2 · so400m · 512px (recommended)",
|
||||
"dim": 1152,
|
||||
},
|
||||
{
|
||||
"name": "google/siglip2-so400m-patch16-384",
|
||||
"version": "siglip2-so400m-patch16-384",
|
||||
"label": "SigLIP 2 · so400m · 384px (faster)",
|
||||
"dim": 1152,
|
||||
},
|
||||
{
|
||||
"name": "google/siglip-so400m-patch14-384",
|
||||
"version": "siglip-so400m-patch14-384",
|
||||
"label": "SigLIP 1 · so400m · 384px (original)",
|
||||
"dim": 1152,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ml_admin_bp.route("/embedder-models", methods=["GET"])
|
||||
async def embedder_models():
|
||||
return jsonify({"models": list(SUPPORTED_EMBEDDERS)})
|
||||
|
||||
|
||||
@ml_admin_bp.route("/settings", methods=["GET"])
|
||||
async def get_settings():
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -71,14 +71,16 @@ class MLSettings(Base):
|
||||
ccip_auto_apply_threshold: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.92
|
||||
)
|
||||
# Default = SigLIP 2 (so400m, 512px) for new installs (migration 0069);
|
||||
# existing libraries keep their stored value until the operator re-embeds.
|
||||
embedder_model_version: Mapped[str] = mapped_column(
|
||||
String(128), nullable=False, default="siglip-so400m-patch14-384"
|
||||
String(128), nullable=False, default="siglip2-so400m-patch16-512"
|
||||
)
|
||||
# The HF model NAME the embedder loads (server CPU embed + announced to the
|
||||
# GPU agent in the lease). Operator-settable so the embedder is a choice, not
|
||||
# a hardcode (#1190): set name + version together, then re-embed + retrain.
|
||||
embedder_model_name: Mapped[str] = mapped_column(
|
||||
String(128), nullable=False, default="google/siglip-so400m-patch14-384"
|
||||
String(128), nullable=False, default="google/siglip2-so400m-patch16-512"
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,8 +22,14 @@ export const useMLStore = defineStore('ml', () => {
|
||||
await api.post('/api/ml/backfill')
|
||||
}
|
||||
|
||||
// Server-authoritative list of supported embedders for the model dropdown.
|
||||
async function embedderModels() {
|
||||
const r = await api.get('/api/ml/embedder-models')
|
||||
return r.models || []
|
||||
}
|
||||
|
||||
return {
|
||||
settings, loading, error,
|
||||
loadSettings, patchSettings, triggerBackfill
|
||||
loadSettings, patchSettings, triggerBackfill, embedderModels
|
||||
}
|
||||
})
|
||||
|
||||
@@ -34,25 +34,34 @@ async def test_get_and_patch_settings(client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_embedder_model_settable_and_empty_rejected(client):
|
||||
# #1190: the embedder model name + version are operator-settable (a swap),
|
||||
# and neither may be blanked.
|
||||
async def test_embedder_model_default_settable_and_empty_rejected(client):
|
||||
# #1203: fresh installs default to SigLIP 2 (migration 0069, no embeddings).
|
||||
# The model is operator-settable (a swap) and neither field may be blanked.
|
||||
body = await (await client.get("/api/ml/settings")).get_json()
|
||||
assert body["embedder_model_name"] == "google/siglip-so400m-patch14-384"
|
||||
assert body["embedder_model_name"] == "google/siglip2-so400m-patch16-512"
|
||||
|
||||
ok = await client.patch("/api/ml/settings", json={
|
||||
"embedder_model_name": "google/siglip2-so400m-patch16-512",
|
||||
"embedder_model_version": "siglip2-so400m-patch16-512",
|
||||
"embedder_model_name": "google/siglip-so400m-patch14-384",
|
||||
"embedder_model_version": "siglip-so400m-patch14-384",
|
||||
})
|
||||
assert ok.status_code == 200
|
||||
out = await ok.get_json()
|
||||
assert out["embedder_model_name"] == "google/siglip2-so400m-patch16-512"
|
||||
assert out["embedder_model_version"] == "siglip2-so400m-patch16-512"
|
||||
assert out["embedder_model_name"] == "google/siglip-so400m-patch14-384"
|
||||
|
||||
bad = await client.patch("/api/ml/settings", json={"embedder_model_name": " "})
|
||||
assert bad.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_embedder_models_list(client):
|
||||
# #1203: the dropdown reads the supported-model list from the server.
|
||||
body = await (await client.get("/api/ml/embedder-models")).get_json()
|
||||
assert any(
|
||||
m["name"] == "google/siglip2-so400m-patch16-512" for m in body["models"]
|
||||
)
|
||||
assert all({"name", "version", "label"} <= set(m) for m in body["models"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_video_settings_default_and_patch(client):
|
||||
"""#747: video frame-sampling knobs are exposed + patchable."""
|
||||
|
||||
Reference in New Issue
Block a user