e6f128c894
Step 2 of milestone #128. _hygiene_excluded_ids (training_data.py) is the one shared predicate: images carrying any system tag are dropped from every OTHER concepts head training — not positives (a rough wip tagged as a character drags the head toward generic-sketch) and not rejection or sampled negatives (a wip OF character X is not evidence against X). A system tags own head trains on them unfiltered; that is what makes auto-flagging banners work. Selection is split out of train_head as the sklearn-free head_training_ids so CI (no sklearn) can pin the behavior. CCIP: reference prototypes skip hygiene-tagged images — a faceless wip figure region must never become an identity reference — and the ref cache signature now counts hygiene applications, since tagging an image wip changes the reference set without touching character/region counts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
"""Training hygiene (#128): system-tagged images are ABSENT from other
|
|
concepts' training data and from CCIP reference prototypes.
|
|
|
|
sklearn only exists in the ml image, so these pin head_training_ids (the
|
|
sklearn-free selection split out of train_head) rather than a full fit —
|
|
the exclusion lives entirely in that selection.
|
|
"""
|
|
import pytest
|
|
from sqlalchemy import insert, select
|
|
|
|
from backend.app.models import ImageRecord, ImageRegion, Tag, TagKind
|
|
from backend.app.models.tag import image_tag
|
|
from backend.app.services.ml import ccip
|
|
from backend.app.services.ml.heads import head_training_ids
|
|
from backend.app.services.ml.training_data import _hygiene_excluded_ids
|
|
from backend.app.services.tag_service import TagService
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
_CFG = {"min_positives": 2, "neg_ratio": 1}
|
|
|
|
|
|
async def _system_wip(db) -> Tag:
|
|
return (await db.execute(
|
|
select(Tag).where(Tag.is_system.is_(True), Tag.name == "wip")
|
|
)).scalar_one()
|
|
|
|
|
|
async def _img(db, sha, *, embedded=True):
|
|
rec = ImageRecord(
|
|
path=f"/images/{sha}.jpg", sha256=sha, size_bytes=1, mime="image/jpeg",
|
|
width=1, height=1, origin="imported_filesystem",
|
|
integrity_status="unknown",
|
|
siglip_embedding=([0.1] * 1152 if embedded else None),
|
|
)
|
|
db.add(rec)
|
|
await db.flush()
|
|
return rec
|
|
|
|
|
|
async def _apply(db, image_id, tag_id):
|
|
await db.execute(insert(image_tag).values(
|
|
image_record_id=image_id, tag_id=tag_id, source="manual",
|
|
))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hygiene_excluded_ids(db):
|
|
wip = await _system_wip(db)
|
|
flagged = await _img(db, "a" * 64)
|
|
await _img(db, "b" * 64)
|
|
await _apply(db, flagged.id, wip.id)
|
|
excluded = await db.run_sync(lambda s: _hygiene_excluded_ids(s))
|
|
assert excluded == {flagged.id}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_head_selection_drops_hygiene_from_both_sides(db):
|
|
"""A wip-tagged image of concept X is neither a positive NOR a sampled
|
|
negative for X — it is absent entirely."""
|
|
wip = await _system_wip(db)
|
|
concept = await TagService(db).find_or_create("concept_x", TagKind.general)
|
|
clean_a = await _img(db, "a" * 64)
|
|
clean_b = await _img(db, "b" * 64)
|
|
flagged_pos = await _img(db, "c" * 64) # X + wip: dropped positive
|
|
negative_pool = await _img(db, "d" * 64) # untagged: legit negative
|
|
flagged_pool = await _img(db, "e" * 64) # wip only: must not be sampled
|
|
for img in (clean_a, clean_b, flagged_pos):
|
|
await _apply(db, img.id, concept.id)
|
|
await _apply(db, flagged_pos.id, wip.id)
|
|
await _apply(db, flagged_pool.id, wip.id)
|
|
|
|
ids = await db.run_sync(lambda s: head_training_ids(s, concept.id, _CFG))
|
|
assert ids is not None
|
|
pos_ids, neg_ids = ids
|
|
assert set(pos_ids) == {clean_a.id, clean_b.id}
|
|
assert negative_pool.id in neg_ids
|
|
assert flagged_pos.id not in neg_ids
|
|
assert flagged_pool.id not in neg_ids
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_system_tags_own_head_keeps_hygiene_positives(db):
|
|
"""The wip head itself trains ON wip-tagged images — that's what makes
|
|
auto-flagging work."""
|
|
wip = await _system_wip(db)
|
|
one = await _img(db, "a" * 64)
|
|
two = await _img(db, "b" * 64)
|
|
await _img(db, "c" * 64) # negative pool
|
|
await _apply(db, one.id, wip.id)
|
|
await _apply(db, two.id, wip.id)
|
|
|
|
ids = await db.run_sync(lambda s: head_training_ids(s, wip.id, _CFG))
|
|
assert ids is not None
|
|
pos_ids, _ = ids
|
|
assert set(pos_ids) == {one.id, two.id}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ccip_references_skip_hygiene_images(db):
|
|
"""A wip's figure region must never become an identity prototype, even on
|
|
a single-character image."""
|
|
ccip._REF_CACHE.update(sig=None, refs=None)
|
|
wip = await _system_wip(db)
|
|
char = await TagService(db).find_or_create("Char A", TagKind.character)
|
|
|
|
clean = await _img(db, "a" * 64)
|
|
flagged = await _img(db, "b" * 64)
|
|
for img, slot in ((clean, 0), (flagged, 1)):
|
|
vec = [0.0] * 768
|
|
vec[slot] = 1.0
|
|
db.add(ImageRegion(
|
|
image_record_id=img.id, kind="figure",
|
|
rx=0.0, ry=0.0, rw=1.0, rh=1.0,
|
|
ccip_embedding=vec, embedding_version="ccip-test",
|
|
))
|
|
await _apply(db, img.id, char.id)
|
|
await _apply(db, flagged.id, wip.id)
|
|
await db.commit()
|
|
|
|
refs = await ccip.character_references(db)
|
|
vectors = refs.get(char.id, [])
|
|
assert len(vectors) == 1
|
|
assert float(vectors[0][0]) == pytest.approx(1.0)
|
|
assert float(vectors[0][1]) == pytest.approx(0.0)
|