Spaces:
Running on Zero
Running on Zero
Download visualize.py from Tonic/OCR-with-JEV: direct link, hf CLI and curl.
- Browser
- Download file 5.4 kB
-
https://huggingface.co/spaces/Tonic/OCR-with-JEV/resolve/main/visualize.py
- Command line
-
hf download hf://spaces/Tonic/OCR-with-JEV/visualize.py
-
curl -L -o visualize.py https://huggingface.co/spaces/Tonic/OCR-with-JEV/resolve/main/visualize.py
5.4 kB
| """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 | |
| 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""" | |
| <div class="jev-summary"> | |
| <div class="jev-stat"><b>{len(scored)}</b><span>scored</span></div> | |
| <div class="jev-stat accept"><b>{len(accepted)}</b><span>auto-accept ≥ {threshold:.2f}</span></div> | |
| <div class="jev-stat review"><b>{len(review)}</b><span>human review</span></div> | |
| <div class="jev-stat"><b>{avg:.3f}</b><span>mean score</span></div> | |
| <div class="jev-stat"><b>{len(errors)}</b><span>errors</span></div> | |
| <div class="jev-stat"><b>{len(skipped)}</b><span>not scored</span></div> | |
| </div> | |
| """.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"<!-- #{region.index} {region.label} score={region.score} -->\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 """ | |
| <div class="jev-legend"> | |
| <span class="pill accept">Auto-accept</span> | |
| <span class="pill borderline">Borderline</span> | |
| <span class="pill review">Needs review</span> | |
| <span class="pill skip">Not scored</span> | |
| </div> | |
| """.strip() | |