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
+110 -68
View File
@@ -21,7 +21,8 @@ from watchdog.events import FileSystemEventHandler, FileCreatedEvent
from watchdog.observers import Observer
from exif_parser import ExifError, parse_image
from ocr import read_bib
from image_tagger import write_bib_tags
from ocr import read_all_bibs
from passage_log import log_passage
from profile_db import get_or_create_athlete, init_db
@@ -70,56 +71,73 @@ async def process_image(path: Path) -> None:
return
# --- OCR ---
ocr = read_bib(path)
logger.debug("OCR: digits=%s conf=%.2f", ocr.digits, ocr.confidence)
bibs = read_all_bibs(path)
logger.debug("OCR: %d startnumre funnet", len(bibs))
# --- Flytt til processed/ ---
dest = _destination_path(path, meta.timestamp_utc)
shutil.move(str(path), str(dest))
logger.info("Flyttet til: %s", dest)
# --- Bestem konfidens og review-flagg ---
confidence = ocr.confidence
needs_review = False
review_note = None
id_method = "bib_ocr"
if ocr.digits is None or confidence < MIN_AUTO_CONFIDENCE:
needs_review = True
review_note = "number_unreadable" if ocr.digits is None else "low_confidence"
id_method = "bib_ocr_uncertain"
# --- Koble mot profil-DB ---
profile_id = None
bib_number = ocr.digits
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row
await init_db(db)
if bib_number and not needs_review:
profile_id = await get_or_create_athlete(db, bib_number)
if not bibs:
# Ingen bib funnet — legg til manuell gjennomgang
await log_passage(
db,
profile_id=None,
bib_number=None,
station=meta.station or "unknown",
timestamp_utc=meta.timestamp_utc,
gps_lat=meta.gps_lat,
gps_lon=meta.gps_lon,
gps_alt=meta.gps_alt,
confidence=0.0,
proximity_score=0.0,
id_method="bib_ocr_uncertain",
source_image=str(dest),
needs_review=True,
review_note="number_unreadable",
)
logger.info("Passering logget (ingen bib): station=%s", meta.station)
else:
for ocr in bibs:
confidence = ocr.confidence
needs_review = confidence < MIN_AUTO_CONFIDENCE
id_method = "bib_ocr" if not needs_review else "bib_ocr_uncertain"
review_note = "low_confidence" if needs_review else None
await log_passage(
db,
profile_id=profile_id,
bib_number=bib_number,
station=meta.station or "unknown",
timestamp_utc=meta.timestamp_utc,
gps_lat=meta.gps_lat,
gps_lon=meta.gps_lon,
gps_alt=meta.gps_alt,
confidence=confidence,
proximity_score=ocr.proximity_score,
id_method=id_method,
source_image=str(dest),
needs_review=needs_review,
review_note=review_note,
)
logger.info(
"Passering logget: bib=%s station=%s needs_review=%s",
bib_number, meta.station, needs_review,
)
profile_id = None
if ocr.digits and not needs_review:
profile_id = await get_or_create_athlete(db, ocr.digits)
await log_passage(
db,
profile_id=profile_id,
bib_number=ocr.digits,
station=meta.station or "unknown",
timestamp_utc=meta.timestamp_utc,
gps_lat=meta.gps_lat,
gps_lon=meta.gps_lon,
gps_alt=meta.gps_alt,
confidence=confidence,
proximity_score=ocr.proximity_score,
id_method=id_method,
source_image=str(dest),
needs_review=needs_review,
review_note=review_note,
)
logger.info(
"Passering logget: bib=%s station=%s needs_review=%s",
ocr.digits, meta.station, needs_review,
)
# Skriv alle funne bibs til EXIF-metadata i filen
found_bibs = [ocr.digits for ocr in bibs if ocr.digits]
if found_bibs:
write_bib_tags(dest, found_bibs, station=meta.station or "unknown")
async def process_image_with_override(
@@ -148,39 +166,63 @@ async def process_image_with_override(
except ExifError:
timestamp = datetime.now(timezone.utc)
ocr = read_bib(path)
bibs = read_all_bibs(path)
dest = _destination_path(path, timestamp)
shutil.move(str(path), str(dest))
confidence = ocr.confidence
needs_review = ocr.digits is None or confidence < MIN_AUTO_CONFIDENCE
id_method = "bib_ocr" if not needs_review else "bib_ocr_uncertain"
review_note = None if not needs_review else (
"number_unreadable" if ocr.digits is None else "low_confidence"
)
if not bibs:
await log_passage(
db,
race_id=race_id,
profile_id=None,
bib_number=None,
station=station_name,
timestamp_utc=timestamp,
gps_lat=gps_lat or 0.0,
gps_lon=gps_lon or 0.0,
gps_alt=gps_alt,
confidence=0.0,
proximity_score=0.0,
id_method="bib_ocr_uncertain",
source_image=str(dest),
needs_review=True,
review_note="number_unreadable",
)
logger.info("Passering logget (ingen bib): station=%s", station_name)
else:
for ocr in bibs:
confidence = ocr.confidence
needs_review = confidence < MIN_AUTO_CONFIDENCE
id_method = "bib_ocr" if not needs_review else "bib_ocr_uncertain"
review_note = "low_confidence" if needs_review else None
profile_id = None
if ocr.digits and not needs_review:
profile_id = await _get_or_create(db, ocr.digits)
profile_id = None
if ocr.digits and not needs_review:
profile_id = await _get_or_create(db, ocr.digits)
await log_passage(
db,
race_id=race_id,
profile_id=profile_id,
bib_number=ocr.digits,
station=station_name,
timestamp_utc=timestamp,
gps_lat=gps_lat or 0.0,
gps_lon=gps_lon or 0.0,
gps_alt=gps_alt,
confidence=confidence,
proximity_score=ocr.proximity_score,
id_method=id_method,
source_image=str(dest),
needs_review=needs_review,
review_note=review_note,
)
logger.info("Passering logget: bib=%s station=%s", ocr.digits, station_name)
await log_passage(
db,
race_id=race_id,
profile_id=profile_id,
bib_number=ocr.digits,
station=station_name,
timestamp_utc=timestamp,
gps_lat=gps_lat or 0.0,
gps_lon=gps_lon or 0.0,
gps_alt=gps_alt,
confidence=confidence,
proximity_score=ocr.proximity_score,
id_method=id_method,
source_image=str(dest),
needs_review=needs_review,
review_note=review_note,
)
logger.info("Passering logget: bib=%s station=%s", ocr.digits, station_name)
# Skriv alle funne bibs til EXIF-metadata i filen
found_bibs = [ocr.digits for ocr in bibs if ocr.digits]
if found_bibs:
write_bib_tags(dest, found_bibs, station=station_name, race_id=race_id)
async def process_existing() -> None: