Støtte for flere bibs per bilde, EXIF-metadata og zoom i gjennomgang
Build & Deploy / build-and-deploy (push) Successful in 46s
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:
+103
-2
@@ -68,6 +68,7 @@ class RaceRequest(BaseModel):
|
||||
name: str
|
||||
date: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
bib_filter_enabled: bool = False
|
||||
|
||||
|
||||
@app.get("/api/races")
|
||||
@@ -83,12 +84,12 @@ async def active_race(db=Depends(get_connection)):
|
||||
|
||||
@app.post("/api/races")
|
||||
async def create_race_endpoint(body: RaceRequest, db=Depends(get_connection)):
|
||||
return await create_race(db, body.name, body.date, body.description)
|
||||
return await create_race(db, body.name, body.date, body.description, body.bib_filter_enabled)
|
||||
|
||||
|
||||
@app.put("/api/races/{race_id}")
|
||||
async def update_race_endpoint(race_id: str, body: RaceRequest, db=Depends(get_connection)):
|
||||
ok = await update_race(db, race_id, body.name, body.date, body.description)
|
||||
ok = await update_race(db, race_id, body.name, body.date, body.description, body.bib_filter_enabled)
|
||||
if not ok:
|
||||
raise HTTPException(404, "Løp ikke funnet")
|
||||
return await get_race(db, race_id)
|
||||
@@ -223,6 +224,30 @@ async def resolve(passage_id: str, body: ResolveRequest, db=Depends(get_connecti
|
||||
review_note=body.review_note)
|
||||
if not ok:
|
||||
raise HTTPException(404, "Passering ikke funnet")
|
||||
|
||||
# Oppdater EXIF-metadata med bekreftet startnummer
|
||||
if body.bib_number:
|
||||
from image_tagger import write_bib_tags, read_bib_tags
|
||||
async with db.execute(
|
||||
"SELECT source_image, station, race_id FROM passages WHERE passage_id = ?",
|
||||
(passage_id,),
|
||||
) as cur:
|
||||
row = await cur.fetchone()
|
||||
if row:
|
||||
img_path = Path(row["source_image"])
|
||||
if img_path.exists():
|
||||
# Behold allerede taggede bibs, legg til bekreftet
|
||||
existing = read_bib_tags(img_path)
|
||||
all_bibs = list(dict.fromkeys(
|
||||
(existing or {}).get("bibs", []) + [body.bib_number]
|
||||
))
|
||||
write_bib_tags(
|
||||
img_path, all_bibs,
|
||||
station=row["station"],
|
||||
race_id=row["race_id"],
|
||||
confirmed=True,
|
||||
)
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -239,6 +264,82 @@ async def remove_passage(passage_id: str, db=Depends(get_connection)):
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/passages/{passage_id}/reanalyze")
|
||||
async def reanalyze_passage(passage_id: str, db=Depends(get_connection)):
|
||||
"""
|
||||
Kjør OCR på nytt på passeringens kildebilde.
|
||||
Oppretter nye passeringer for eventuelle startnumre som ikke allerede er logget fra dette bildet.
|
||||
Returnerer alle funne startnumre og eventuelle nye passage_id-er.
|
||||
"""
|
||||
from datetime import timezone as _tz
|
||||
from ocr import read_all_bibs
|
||||
from ingest import MIN_AUTO_CONFIDENCE
|
||||
from profile_db import get_athlete_by_bib
|
||||
|
||||
async with db.execute("SELECT * FROM passages WHERE passage_id = ?", (passage_id,)) as cur:
|
||||
row = await cur.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Passering ikke funnet")
|
||||
|
||||
passage = dict(row)
|
||||
image_path = Path(passage["source_image"])
|
||||
if not image_path.exists():
|
||||
raise HTTPException(404, "Kildebilde ikke funnet på disk")
|
||||
|
||||
bibs = read_all_bibs(image_path)
|
||||
|
||||
# Finn bib-numre som allerede er logget fra dette bildet
|
||||
async with db.execute(
|
||||
"SELECT bib_number FROM passages WHERE source_image = ?", (passage["source_image"],)
|
||||
) as cur:
|
||||
existing_bibs = {r["bib_number"] for r in await cur.fetchall()}
|
||||
|
||||
new_passages = []
|
||||
ts_str = passage["timestamp_utc"]
|
||||
ts = datetime.fromisoformat(ts_str)
|
||||
if ts.tzinfo is None:
|
||||
ts = ts.replace(tzinfo=_tz.utc)
|
||||
|
||||
for ocr in bibs:
|
||||
if ocr.digits in existing_bibs:
|
||||
continue
|
||||
|
||||
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:
|
||||
athlete = await get_athlete_by_bib(db, ocr.digits)
|
||||
if athlete:
|
||||
profile_id = athlete["profile_id"]
|
||||
|
||||
new_id = await log_passage(
|
||||
db,
|
||||
race_id=passage["race_id"],
|
||||
profile_id=profile_id,
|
||||
bib_number=ocr.digits,
|
||||
station=passage["station"],
|
||||
timestamp_utc=ts,
|
||||
gps_lat=passage["gps_lat"],
|
||||
gps_lon=passage["gps_lon"],
|
||||
gps_alt=passage["gps_alt"],
|
||||
confidence=confidence,
|
||||
proximity_score=ocr.proximity_score,
|
||||
id_method=id_method,
|
||||
source_image=passage["source_image"],
|
||||
needs_review=needs_review,
|
||||
review_note=review_note,
|
||||
)
|
||||
new_passages.append({"passage_id": new_id, "bib_number": ocr.digits, "confidence": confidence})
|
||||
|
||||
return {
|
||||
"found_bibs": [ocr.digits for ocr in bibs],
|
||||
"new_passages": new_passages,
|
||||
}
|
||||
|
||||
|
||||
# =====================
|
||||
# Resultater
|
||||
# =====================
|
||||
|
||||
Reference in New Issue
Block a user