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"])
|
@ml_admin_bp.route("/settings", methods=["GET"])
|
||||||
async def get_settings():
|
async def get_settings():
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|||||||
@@ -71,14 +71,16 @@ class MLSettings(Base):
|
|||||||
ccip_auto_apply_threshold: Mapped[float] = mapped_column(
|
ccip_auto_apply_threshold: Mapped[float] = mapped_column(
|
||||||
Float, nullable=False, default=0.92
|
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(
|
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
|
# 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
|
# 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.
|
# a hardcode (#1190): set name + version together, then re-embed + retrain.
|
||||||
embedder_model_name: Mapped[str] = mapped_column(
|
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(
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||||
|
|||||||
@@ -117,15 +117,12 @@
|
|||||||
the suggest cut; 0.92 recommended.
|
the suggest cut; 0.92 recommended.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Embedding model (advanced) -->
|
<!-- Embedding model -->
|
||||||
<div v-if="ml.settings" class="fc-section-h mt-5 mb-1">Embedding model (advanced)</div>
|
<div v-if="ml.settings" class="fc-section-h mt-5 mb-1">Embedding model</div>
|
||||||
<div v-if="ml.settings">
|
<div v-if="ml.settings">
|
||||||
<v-text-field
|
<v-select
|
||||||
v-model="modelName" label="HF model name" density="compact" hide-details
|
v-model="selectedModel" :items="modelItems" item-title="label"
|
||||||
variant="outlined" class="mb-2"
|
item-value="name" label="Model" density="compact" hide-details
|
||||||
/>
|
|
||||||
<v-text-field
|
|
||||||
v-model="modelVersion" label="Version stamp" density="compact" hide-details
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
<div class="d-flex mt-3" style="gap:8px">
|
<div class="d-flex mt-3" style="gap:8px">
|
||||||
@@ -139,12 +136,11 @@
|
|||||||
>Re-embed library (GPU)</v-btn>
|
>Re-embed library (GPU)</v-btn>
|
||||||
</div>
|
</div>
|
||||||
<p class="fc-muted text-caption mt-2 mb-0">
|
<p class="fc-muted text-caption mt-2 mb-0">
|
||||||
Changing the model means a DIFFERENT embedding space. After saving a new
|
Switching the model is a DIFFERENT embedding space. After <b>Save model</b>,
|
||||||
model + version, run <b>Re-embed library</b> (the GPU agent re-embeds
|
run <b>Re-embed library</b> (the GPU agent re-embeds whole images + concept
|
||||||
whole images + concept crops), then <b>Retrain heads</b>. Suggestions
|
crops), then <b>Retrain heads</b> — suggestions degrade until both finish.
|
||||||
degrade until both finish. SigLIP 2 (<code>google/siglip2-so400m-patch16-512</code>,
|
SigLIP 2 (512px) is a 1152-d drop-in over SigLIP 1; new installs default to
|
||||||
version <code>siglip2-so400m-patch16-512</code>) is a 1152-d drop-in at
|
it. Your existing library stays on its current model until you re-embed.
|
||||||
512px — no schema change.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</MaintenanceTile>
|
</MaintenanceTile>
|
||||||
@@ -173,8 +169,8 @@ const savingThreshold = ref(false)
|
|||||||
const autoApply = ref(true)
|
const autoApply = ref(true)
|
||||||
const autoThreshold = ref(0.92)
|
const autoThreshold = ref(0.92)
|
||||||
const savingAuto = ref(false)
|
const savingAuto = ref(false)
|
||||||
const modelName = ref('')
|
const modelItems = ref([])
|
||||||
const modelVersion = ref('')
|
const selectedModel = ref(null)
|
||||||
const savingModel = ref(false)
|
const savingModel = ref(false)
|
||||||
const reembedding = ref(false)
|
const reembedding = ref(false)
|
||||||
const queue = ref({ pending: 0, leased: 0, done: 0, error: 0 })
|
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
|
autoThreshold.value = ml.settings.ccip_auto_apply_threshold
|
||||||
}
|
}
|
||||||
if (ml.settings?.embedder_model_name != null) {
|
if (ml.settings?.embedder_model_name != null) {
|
||||||
modelName.value = ml.settings.embedder_model_name
|
const items = await ml.embedderModels()
|
||||||
modelVersion.value = ml.settings.embedder_model_version
|
// 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 */ }
|
} catch { /* non-fatal */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
async function onSaveModel() {
|
async function onSaveModel() {
|
||||||
|
const opt = modelItems.value.find((m) => m.name === selectedModel.value)
|
||||||
|
if (!opt) return
|
||||||
savingModel.value = true
|
savingModel.value = true
|
||||||
try {
|
try {
|
||||||
await ml.patchSettings({
|
await ml.patchSettings({
|
||||||
embedder_model_name: modelName.value.trim(),
|
embedder_model_name: opt.name,
|
||||||
embedder_model_version: modelVersion.value.trim(),
|
embedder_model_version: opt.version,
|
||||||
})
|
})
|
||||||
toast({ text: 'Embedding model saved — now Re-embed library, then Retrain heads', type: 'success' })
|
toast({ text: 'Embedding model saved — now Re-embed library, then Retrain heads', type: 'success' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -22,8 +22,14 @@ export const useMLStore = defineStore('ml', () => {
|
|||||||
await api.post('/api/ml/backfill')
|
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 {
|
return {
|
||||||
settings, loading, error,
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_embedder_model_settable_and_empty_rejected(client):
|
async def test_embedder_model_default_settable_and_empty_rejected(client):
|
||||||
# #1190: the embedder model name + version are operator-settable (a swap),
|
# #1203: fresh installs default to SigLIP 2 (migration 0069, no embeddings).
|
||||||
# and neither may be blanked.
|
# The model is operator-settable (a swap) and neither field may be blanked.
|
||||||
body = await (await client.get("/api/ml/settings")).get_json()
|
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={
|
ok = await client.patch("/api/ml/settings", json={
|
||||||
"embedder_model_name": "google/siglip2-so400m-patch16-512",
|
"embedder_model_name": "google/siglip-so400m-patch14-384",
|
||||||
"embedder_model_version": "siglip2-so400m-patch16-512",
|
"embedder_model_version": "siglip-so400m-patch14-384",
|
||||||
})
|
})
|
||||||
assert ok.status_code == 200
|
assert ok.status_code == 200
|
||||||
out = await ok.get_json()
|
out = await ok.get_json()
|
||||||
assert out["embedder_model_name"] == "google/siglip2-so400m-patch16-512"
|
assert out["embedder_model_name"] == "google/siglip-so400m-patch14-384"
|
||||||
assert out["embedder_model_version"] == "siglip2-so400m-patch16-512"
|
|
||||||
|
|
||||||
bad = await client.patch("/api/ml/settings", json={"embedder_model_name": " "})
|
bad = await client.patch("/api/ml/settings", json={"embedder_model_name": " "})
|
||||||
assert bad.status_code == 400
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_video_settings_default_and_patch(client):
|
async def test_video_settings_default_and_patch(client):
|
||||||
"""#747: video frame-sampling knobs are exposed + patchable."""
|
"""#747: video frame-sampling knobs are exposed + patchable."""
|
||||||
|
|||||||
Reference in New Issue
Block a user