"""Region read/write for the crop pipeline (#114). The GPU agent's results endpoint calls replace_regions() to store a freshly detected/embedded set; the character matcher + concept-bag scorer read via get_regions(). Replacement is scoped BY KIND so the figure pipeline and the concept pipeline don't clobber each other. """ from typing import Any from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession from ...models import ImageRegion class RegionService: def __init__(self, session: AsyncSession): self.session = session async def get_regions( self, image_id: int, kinds: list[str] | None = None ) -> list[ImageRegion]: stmt = select(ImageRegion).where(ImageRegion.image_record_id == image_id) if kinds: stmt = stmt.where(ImageRegion.kind.in_(kinds)) return list( (await self.session.execute(stmt.order_by(ImageRegion.id))).scalars() ) async def replace_regions( self, image_id: int, kinds: list[str], regions: list[dict[str, Any]] ) -> int: """Replace this image's regions OF THE GIVEN KINDS with `regions` (a re-detect/re-propose supersedes the prior set without touching other kinds). Each region dict: {kind, bbox:(x,y,w,h), score?, detector_version?, crop_version?, embedding_version?, ccip_embedding?, siglip_embedding?}. Returns the number inserted.""" await self.session.execute( delete(ImageRegion) .where(ImageRegion.image_record_id == image_id) .where(ImageRegion.kind.in_(kinds)) ) n = 0 for r in regions: rx, ry, rw, rh = r["bbox"] self.session.add(ImageRegion( image_record_id=image_id, kind=r["kind"], frame_time=r.get("frame_time"), rx=rx, ry=ry, rw=rw, rh=rh, score=r.get("score"), detector_version=r.get("detector_version"), crop_version=r.get("crop_version"), embedding_version=r.get("embedding_version"), ccip_embedding=r.get("ccip_embedding"), siglip_embedding=r.get("siglip_embedding"), )) n += 1 return n