Støtte for flere bibs per bilde, EXIF-metadata og zoom i gjennomgang
Build & Deploy / build-and-deploy (push) Successful in 46s

- OCR: ny read_all_bibs() returnerer alle unike startnumre (≥2 sifre) per bilde
- Ingest: oppretter én passering per bib (ikke bare beste), ingen bib → needs_review
- image_tagger.py: skriv/les bib-metadata som JSON i EXIF UserComment (piexif)
- Ingest + resolve: tagger bildefilen med bibs automatisk og ved manuell bekreftelse
- API: POST /api/passages/{id}/reanalyze — re-kjør OCR på eksisterende bilde
- API: POST /api/passages/{id}/resolve oppdaterer nå EXIF med bekreftet bib
- races: ny kolonne bib_filter_enabled (med automatisk migrering) + per-løp toggle
- ReviewPage: Re-analyser-knapp og klikk-for-zoom med scroll/drag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 09:00:24 +01:00
parent 018f84efd8
commit 45f7a77171
8 changed files with 526 additions and 128 deletions
+58 -40
View File
@@ -64,33 +64,11 @@ def _bbox_area(bbox) -> float:
return (max(xs) - min(xs)) * (max(ys) - min(ys))
def _extract_bib_number(texts: list[tuple]) -> tuple[Optional[str], float, bool, float]:
def read_all_bibs(image_path: Path) -> list["OcrResult"]:
"""
Finn beste siffersekvens blant OCR-treff.
Returnerer (sifre, konfidens, partial, proximity_score).
proximity_score = areal av bounding box i piksler² (større = nærmere kamera).
"""
candidates = []
for (bbox, text, conf) in texts:
digits = re.sub(r"[^0-9]", "", text)
if digits:
candidates.append((digits, float(conf), _bbox_area(bbox)))
if not candidates:
return None, 0.0, False, 0.0
# Velg kandidat med høyest konfidens
best_digits, best_conf, best_area = max(candidates, key=lambda x: x[1])
partial = len(best_digits) < 2
return best_digits, best_conf, partial, best_area
def read_bib(image_path: Path) -> OcrResult:
"""
Les startnummer fra bildet.
Returnerer OcrResult. Aldri exception — fallback til konfidens 0 ved feil.
Les alle startnumre fra bildet.
Returnerer én OcrResult per unikt startnummer funnet (minst 2 sifre).
Tom liste hvis ingen funnet eller ved feil.
"""
try:
processed = _preprocess(image_path)
@@ -98,19 +76,59 @@ def read_bib(image_path: Path) -> OcrResult:
results = reader.readtext(processed, detail=1, paragraph=False)
raw_texts = [text for (_, text, _) in results]
digits, confidence, partial, proximity_score = _extract_bib_number(results)
return OcrResult(
digits=digits,
confidence=confidence,
partial=partial,
proximity_score=proximity_score,
raw_texts=raw_texts,
)
# Best konfidens + areal per unikt siffersekvens (minst 2 sifre)
best: dict[str, tuple[float, float]] = {} # digits -> (conf, area)
for (bbox, text, conf) in results:
digits = re.sub(r"[^0-9]", "", text)
if len(digits) < 2:
continue
area = _bbox_area(bbox)
if digits not in best or float(conf) > best[digits][0]:
best[digits] = (float(conf), area)
return [
OcrResult(
digits=digits,
confidence=conf,
partial=False,
proximity_score=area,
raw_texts=raw_texts,
)
for digits, (conf, area) in best.items()
]
except Exception as e:
return OcrResult(
digits=None,
confidence=0.0,
partial=False,
raw_texts=[f"ERROR: {e}"],
)
return [OcrResult(digits=None, confidence=0.0, partial=False, raw_texts=[f"ERROR: {e}"])]
def read_bib(image_path: Path) -> OcrResult:
"""
Les beste startnummer fra bildet (bakoverkompatibel).
Returnerer OcrResult. Aldri exception — fallback til konfidens 0 ved feil.
"""
bibs = read_all_bibs(image_path)
if not bibs or bibs[0].digits is None:
# Sjekk også enkle sifre (partial) for bakoverkompatibilitet
try:
processed = _preprocess(image_path)
reader = _get_reader()
results = reader.readtext(processed, detail=1, paragraph=False)
raw_texts = [text for (_, text, _) in results]
candidates = []
for (bbox, text, conf) in results:
digits = re.sub(r"[^0-9]", "", text)
if digits:
candidates.append((digits, float(conf), _bbox_area(bbox)))
if candidates:
best_digits, best_conf, best_area = max(candidates, key=lambda x: x[1])
return OcrResult(
digits=best_digits,
confidence=best_conf,
partial=len(best_digits) < 2,
proximity_score=best_area,
raw_texts=raw_texts,
)
except Exception:
pass
return OcrResult(digits=None, confidence=0.0, partial=False)
return max(bibs, key=lambda r: r.confidence)