c6f38b0dac
Lift recall on small/local concepts (glasses, cum, stomach-bulge, xray, lactation) that the whole-image SigLIP vector washes out: the GPU agent now embeds figure crops with SigLIP too, stored as kind='concept' regions, and the suggestion rail scores each image as a BAG (whole-image + every concept crop), taking each head's MAX over the bag. The whole-image vector is always in the bag, so this can never score lower than before. Model-agnostic by construction: the server ANNOUNCES the embedding model (HF name + version) in the lease, so the agent loads whatever the heads were trained in and stays in lock-step — a model swap is a server setting + a re-embed migration, never an agent change. - agent: model-agnostic CropEmbedder (torch/transformers get_image_features, fp16 on CUDA, inference-locked); worker branches on job.task — 'ccip' emits figure(CCIP)+concept(SigLIP) in one pass, 'siglip' emits concept-only so the back-catalogue backfill never churns figure/CCIP regions; torch cu124 + transformers in the image. - server: lease announces embed_model_name/embed_version; score_image is max-over-bag (version-filtered region embeddings); enqueue_gpu_backfill 'siglip' gates on a missing concept region (drains the back-catalogue, retries failures, no double-enqueue); daily siglip-backfill beat; UI button; /api/ccip/overview reports images_with_concept_siglip. - v1 scope: suggestion rail only — auto-apply stays whole-image (conservative; heads' thresholds were calibrated on whole-image). Bulk-apply bag = follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
125 lines
4.6 KiB
Python
125 lines
4.6 KiB
Python
"""CCIP / region observability API (#114) — read-only, analysis-shaped.
|
|
|
|
So the work can be checked through an API as the agent fills in vectors: overall
|
|
coverage (regions by kind, how many images have figure CCIP vectors, which
|
|
characters have enough reference examples to match on) + a per-image drill-down
|
|
(its regions + the CCIP character matches it would get). Mirrors the heads
|
|
metrics endpoint; no GPU, just reads what's stored.
|
|
"""
|
|
|
|
from quart import Blueprint, jsonify
|
|
from sqlalchemy import distinct, func, select
|
|
|
|
from ..extensions import get_session
|
|
from ..models import ImageRegion, Tag, TagKind
|
|
from ..models.tag import image_tag
|
|
from ..services.ml.ccip import match_image
|
|
|
|
ccip_bp = Blueprint("ccip", __name__, url_prefix="/api/ccip")
|
|
|
|
_FIGURE_KINDS = ("face", "figure")
|
|
|
|
|
|
@ccip_bp.route("/overview", methods=["GET"])
|
|
async def overview():
|
|
async with get_session() as session:
|
|
by_kind = dict(
|
|
(
|
|
await session.execute(
|
|
select(ImageRegion.kind, func.count()).group_by(ImageRegion.kind)
|
|
)
|
|
).all()
|
|
)
|
|
images_with_figure_ccip = (
|
|
await session.execute(
|
|
select(func.count(distinct(ImageRegion.image_record_id)))
|
|
.where(ImageRegion.kind.in_(_FIGURE_KINDS))
|
|
.where(ImageRegion.ccip_embedding.is_not(None))
|
|
)
|
|
).scalar_one()
|
|
# Concept-crop (SigLIP bag) coverage — how far the back-catalogue embed
|
|
# has progressed, so the max-over-bag scorer's reach is checkable.
|
|
images_with_concept_siglip = (
|
|
await session.execute(
|
|
select(func.count(distinct(ImageRegion.image_record_id)))
|
|
.where(ImageRegion.kind == "concept")
|
|
.where(ImageRegion.siglip_embedding.is_not(None))
|
|
)
|
|
).scalar_one()
|
|
# Per-character reference counts (no vectors loaded) — which characters
|
|
# have enough examples to match on.
|
|
ref_rows = (
|
|
await session.execute(
|
|
select(image_tag.c.tag_id, Tag.name, func.count())
|
|
.select_from(ImageRegion)
|
|
.join(
|
|
image_tag,
|
|
image_tag.c.image_record_id == ImageRegion.image_record_id,
|
|
)
|
|
.join(Tag, Tag.id == image_tag.c.tag_id)
|
|
.where(Tag.kind == TagKind.character)
|
|
.where(ImageRegion.kind.in_(_FIGURE_KINDS))
|
|
.where(ImageRegion.ccip_embedding.is_not(None))
|
|
.group_by(image_tag.c.tag_id, Tag.name)
|
|
.order_by(func.count().desc())
|
|
)
|
|
).all()
|
|
versions = [
|
|
v for (v,) in (
|
|
await session.execute(
|
|
select(distinct(ImageRegion.embedding_version))
|
|
)
|
|
).all() if v
|
|
]
|
|
auto_applied = (
|
|
await session.execute(
|
|
select(func.count()).select_from(image_tag).where(
|
|
image_tag.c.source == "ccip_auto"
|
|
)
|
|
)
|
|
).scalar_one()
|
|
return jsonify({
|
|
"regions_by_kind": by_kind,
|
|
"images_with_figure_ccip": images_with_figure_ccip,
|
|
"images_with_concept_siglip": images_with_concept_siglip,
|
|
"characters_with_references": len(ref_rows),
|
|
"character_references": [
|
|
{"tag_id": t, "name": n, "n_refs": c} for (t, n, c) in ref_rows
|
|
],
|
|
"embedding_versions": versions,
|
|
"auto_applied": auto_applied,
|
|
})
|
|
|
|
|
|
@ccip_bp.route("/images/<int:image_id>", methods=["GET"])
|
|
async def image_detail(image_id: int):
|
|
"""An image's stored regions + the CCIP character matches it would get —
|
|
for spot-checking the agent's output + the matcher."""
|
|
async with get_session() as session:
|
|
regions = (
|
|
await session.execute(
|
|
select(ImageRegion)
|
|
.where(ImageRegion.image_record_id == image_id)
|
|
.order_by(ImageRegion.id)
|
|
)
|
|
).scalars().all()
|
|
matches = await match_image(session, image_id)
|
|
return jsonify({
|
|
"image_id": image_id,
|
|
"regions": [
|
|
{
|
|
"id": r.id,
|
|
"kind": r.kind,
|
|
"bbox": [r.rx, r.ry, r.rw, r.rh],
|
|
"frame_time": r.frame_time,
|
|
"score": r.score,
|
|
"detector_version": r.detector_version,
|
|
"embedding_version": r.embedding_version,
|
|
"has_ccip": r.ccip_embedding is not None,
|
|
"has_siglip": r.siglip_embedding is not None,
|
|
}
|
|
for r in regions
|
|
],
|
|
"ccip_matches": matches,
|
|
})
|