feat(ccip): tunable match threshold, default 0.85 (#114)
Live data showed the v1 flat 0.75 cosine over-fired — ~64% of matched images got
3-10 character guesses dominated by the most-referenced characters (a 27-ref
character clears a low bar on many images). A sweep showed 0.85 collapses the
noise (noisy multi-matches 47→3) while keeping the confident single-character
matches.
- ml_settings.ccip_match_threshold (migration 0063, default 0.85); match_image
reads it (override still accepted). DEFAULT_SIM_THRESHOLD fallback 0.75→0.85.
- Exposed in GET/PATCH /api/ml/settings (validated 0.5–0.999).
- Slider in the GPU agent card ("Character-match strictness") — tune live, no
redeploy, same observe-and-tune loop as auto-apply.
Test: a ~0.9-cosine figure matches at 0.85, dropped at 0.95.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -86,3 +86,20 @@ async def test_no_figure_vectors_means_no_match(db):
|
||||
query = await _img(db, "g" * 64)
|
||||
await db.commit()
|
||||
assert await match_image(db, query.id) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_threshold_gates_borderline_match(db):
|
||||
# A figure ~0.9 cosine from the reference: matched at 0.85, dropped at 0.95.
|
||||
raven = await TagService(db).find_or_create("Raven", TagKind.character)
|
||||
ref = await _img(db, "h" * 64)
|
||||
await _figure(db, ref.id, _ccip(0)) # e0
|
||||
await _tag_image(db, ref.id, raven.id)
|
||||
near = [0.0] * 768
|
||||
near[0], near[1] = 0.9, 0.4359 # |·|=1, cos(e0)=0.9
|
||||
query = await _img(db, "i" * 64)
|
||||
await _figure(db, query.id, near)
|
||||
await db.commit()
|
||||
|
||||
assert any(m["tag_id"] == raven.id for m in await match_image(db, query.id, 0.85))
|
||||
assert await match_image(db, query.id, 0.95) == []
|
||||
|
||||
Reference in New Issue
Block a user