feat(ccip): automation + reference quality — keep identity flowing hands-free (#114)
Works through the optional CCIP ideas + the "keep moving even if I forget" ask:
AUTOMATION (no button needed):
- Hourly beat auto-enqueues CCIP backfill — new images get embedded (and errored
ones retried) on their own; the queue never goes idle waiting for a click.
- CCIP auto-apply: a daily sweep tags confident matches (source='ccip_auto') so
identity tags keep flowing. ON by default (opt-out, like head auto-apply);
ml_settings.ccip_auto_apply_enabled + _threshold (0.92, above the suggest cut),
migration 0064. Vectorized (one matmul + reduceat per image), reversible, skips
already-applied/rejected. Switch + threshold in the GPU agent card; GET/PATCH
/api/ml/settings; auto_applied count in /api/ccip/overview.
REFERENCE QUALITY (the over-fire root cause):
- character_references now draws ONLY from single-character images — on a
multi-character image the tag is image-level, so every figure would otherwise
pollute each character's prototypes (a 2-char image tagged 'Velma' made
Daphne's figure a Velma reference). This is the contamination behind residual
over-firing.
- Cached on a cheap signature (char-tag count + ccip-region count/max-id) so the
reference load isn't redone on every modal open.
Tests: multi-character image not used as a reference; auto-apply tags a confident
match as ccip_auto.
NEXT (not done, confirmed): comic-panel cropping + SigLIP concept crops ("spot
interesting content").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""CCIP few-shot character matcher (#114). numpy cosine on stored vectors — no
|
||||
model needed, so it runs in CI with synthetic CCIP vectors."""
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.app.models import ImageRecord, ImageRegion, TagKind
|
||||
from backend.app.models.tag import image_tag
|
||||
@@ -103,3 +104,40 @@ async def test_threshold_gates_borderline_match(db):
|
||||
|
||||
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) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_character_image_not_used_as_reference(db):
|
||||
# A figure on a 2-character image is ambiguous (tag is image-level), so it
|
||||
# must NOT seed either character's prototypes — else it'd match both.
|
||||
raven = await TagService(db).find_or_create("Raven", TagKind.character)
|
||||
daphne = await TagService(db).find_or_create("Daphne", TagKind.character)
|
||||
multi = await _img(db, "j" * 64)
|
||||
await _figure(db, multi.id, _ccip(0))
|
||||
await _tag_image(db, multi.id, raven.id)
|
||||
await _tag_image(db, multi.id, daphne.id)
|
||||
query = await _img(db, "k" * 64)
|
||||
await _figure(db, query.id, _ccip(0)) # identical to the ambiguous figure
|
||||
await db.commit()
|
||||
assert await match_image(db, query.id) == [] # no clean references → nothing
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_apply_tags_confident_match(db):
|
||||
raven = await TagService(db).find_or_create("Raven", TagKind.character)
|
||||
ref = await _img(db, "l" * 64)
|
||||
await _figure(db, ref.id, _ccip(0))
|
||||
await _tag_image(db, ref.id, raven.id) # single-character reference
|
||||
query = await _img(db, "m" * 64)
|
||||
await _figure(db, query.id, _ccip(0)) # identical → cosine 1.0
|
||||
await db.commit()
|
||||
|
||||
from backend.app.tasks.ml import scheduled_ccip_auto_apply
|
||||
assert "applied=" in scheduled_ccip_auto_apply() # sync task, own session
|
||||
|
||||
rows = (await db.execute(
|
||||
select(image_tag.c.tag_id, image_tag.c.source).where(
|
||||
image_tag.c.image_record_id == query.id
|
||||
)
|
||||
)).all()
|
||||
assert (raven.id, "ccip_auto") in [(t, s) for t, s in rows]
|
||||
|
||||
Reference in New Issue
Block a user