from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field, field_validator, model_validator Certainty = Literal["positive", "probable", "questionable"] LocalizationStatus = Literal[ "localized", "abstained", "rejected_by_quality_gate", "parser_error", ] class BBox(BaseModel): """Bounding box in normalized [y0, x0, y1, x1] space on [0, 1000].""" box_2d: list[int] label: str = Field(min_length=1) @field_validator("box_2d") @classmethod def validate_box(cls, value: list[int]) -> list[int]: if len(value) != 4: raise ValueError("box_2d must contain exactly four values: [y0, x0, y1, x1].") if any(c < 0 or c > 1000 for c in value): raise ValueError("box_2d coordinates must be normalized to [0, 1000].") return value @model_validator(mode="after") def validate_order(self) -> "BBox": y0, x0, y1, x1 = self.box_2d if not (x0 < x1 and y0 < y1): raise ValueError("box_2d must satisfy x0 < x1 and y0 < y1.") return self class Finding(BaseModel): finding: str = Field(min_length=1) anatomical_location: str = Field(min_length=1) certainty: Certainty class FindingDiscovery(BaseModel): findings: list[Finding] = Field(default_factory=list) class ViewLocalization(BaseModel): image_index: int image_path: str status: LocalizationStatus boxes: list[BBox] = Field(default_factory=list) candidate_boxes: list[BBox] = Field(default_factory=list) rejection_reasons: list[str] = Field(default_factory=list) class LocalizedFinding(BaseModel): finding: str anatomical_location: str certainty: Certainty localizations: list[ViewLocalization] = Field(default_factory=list) class DetectionResult(BaseModel): case_id: str | None = None input_images: list[str] findings: list[LocalizedFinding] = Field(default_factory=list)