"""Draw Jev correctness overlays and review-routing summaries.""" from __future__ import annotations from dataclasses import dataclass from typing import Sequence from PIL import Image, ImageDraw, ImageFont @dataclass class ScoredRegion: index: int label: str category: str box: tuple[int, int, int, int] text: str formatted: str score: float | None verdict: str detail: str = "" def _font(size: int = 16) -> ImageFont.ImageFont: for name in ( "DejaVuSans-Bold.ttf", "Arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "C:\\Windows\\Fonts\\arialbd.ttf", "C:\\Windows\\Fonts\\arial.ttf", ): try: return ImageFont.truetype(name, size=size) except OSError: continue return ImageFont.load_default() def route(score: float | None, verdict: str, threshold: float) -> str: if verdict != "scored" or score is None: return "error" if score >= threshold: return "accept" if score >= max(0.0, threshold - 0.15): return "borderline" return "review" def route_color(route_name: str) -> tuple[int, int, int]: return { "accept": (22, 163, 74), "borderline": (202, 138, 4), "review": (220, 38, 38), "error": (107, 114, 128), "skip": (156, 163, 175), }.get(route_name, (107, 114, 128)) def draw_overlay( page: Image.Image, regions: Sequence[ScoredRegion], threshold: float, ) -> Image.Image: canvas = page.convert("RGB").copy() draw = ImageDraw.Draw(canvas, "RGBA") label_font = _font(15) for region in regions: x1, y1, x2, y2 = region.box if region.score is None and region.verdict == "skipped": color = route_color("skip") tag = "skip" else: name = route(region.score, region.verdict, threshold) color = route_color(name) if region.verdict == "scored" and region.score is not None: tag = f"{region.score:.2f}" else: tag = region.verdict outline = (*color, 230) fill = (*color, 36) draw.rectangle([x1, y1, x2, y2], outline=outline, width=3, fill=fill) badge = f"#{region.index} {tag}" tw = max(48, int(draw.textlength(badge, font=label_font)) + 10) th = 20 by1 = max(0, y1 - th - 2) draw.rectangle([x1, by1, x1 + tw, by1 + th], fill=(*color, 230)) draw.text((x1 + 5, by1 + 2), badge, fill=(255, 255, 255), font=label_font) return canvas def summary_html(regions: Sequence[ScoredRegion], threshold: float) -> str: scored = [r for r in regions if r.verdict == "scored" and r.score is not None] accepted = [r for r in scored if r.score >= threshold] review = [r for r in scored if r.score < threshold] errors = [r for r in regions if r.verdict not in {"scored", "skipped"}] skipped = [r for r in regions if r.verdict == "skipped"] avg = sum(r.score for r in scored) / len(scored) if scored else 0.0 return f"""
{len(scored)}scored
{len(accepted)}auto-accept ≥ {threshold:.2f}
{len(review)}human review
{avg:.3f}mean score
{len(errors)}errors
{len(skipped)}not scored
""".strip() def regions_table(regions: Sequence[ScoredRegion], threshold: float) -> list[list[str]]: rows: list[list[str]] = [] for region in regions: decision = ( "skipped" if region.verdict == "skipped" else route(region.score, region.verdict, threshold) ) score = f"{region.score:.4f}" if region.score is not None else "—" preview = region.text.replace("\n", " ") if len(preview) > 120: preview = preview[:117] + "…" rows.append( [ str(region.index), region.label, region.category, score, region.verdict, decision, preview, ] ) return rows def split_markdown(regions: Sequence[ScoredRegion], threshold: float) -> tuple[str, str]: accepted: list[str] = [] review: list[str] = [] for region in regions: if not region.formatted.strip(): continue block = f"\n{region.formatted}" if region.verdict == "scored" and region.score is not None and region.score >= threshold: accepted.append(block) elif region.verdict == "skipped": accepted.append(block) else: review.append(block) return "\n\n".join(accepted) or "_No auto-accepted regions._", "\n\n".join(review) or "_Nothing queued for review._" def legend_html() -> str: return """
Auto-accept Borderline Needs review Not scored
""".strip()