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
|
||||
# =====================
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Skriv og les bib-metadata i bildefiler via EXIF UserComment (JSON).
|
||||
|
||||
Bruker piexif + Pillow som allerede er i requirements.txt.
|
||||
Metadata lagres i Exif.UserComment som:
|
||||
b"ASCII\x00\x00\x00" + JSON-bytes
|
||||
|
||||
Eksempel-innhold:
|
||||
{"bibs": ["42", "87"], "station": "finish", "confidence": 0.93, "confirmed": true}
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import piexif
|
||||
from PIL import Image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ASCII_PREFIX = b"ASCII\x00\x00\x00"
|
||||
|
||||
|
||||
def write_bib_tags(
|
||||
image_path: Path,
|
||||
bibs: list[str],
|
||||
*,
|
||||
station: Optional[str] = None,
|
||||
race_id: Optional[str] = None,
|
||||
confidence: Optional[float] = None,
|
||||
confirmed: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Skriv startnummer-metadata til bildefil som EXIF UserComment (JSON).
|
||||
Bevarer all eksisterende EXIF. Feiler stille — krasjer ikke ingest-prosessen.
|
||||
"""
|
||||
data: dict = {"bibs": bibs}
|
||||
if station:
|
||||
data["station"] = station
|
||||
if race_id:
|
||||
data["race_id"] = race_id
|
||||
if confidence is not None:
|
||||
data["confidence"] = round(confidence, 4)
|
||||
if confirmed:
|
||||
data["confirmed"] = True
|
||||
|
||||
json_bytes = json.dumps(data, ensure_ascii=True, separators=(",", ":")).encode("ascii")
|
||||
comment_bytes = _ASCII_PREFIX + json_bytes
|
||||
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
raw_exif = img.info.get("exif")
|
||||
if raw_exif:
|
||||
exif_dict = piexif.load(raw_exif)
|
||||
else:
|
||||
exif_dict = {"0th": {}, "Exif": {}, "GPS": {}, "Interop": {}, "1st": {}}
|
||||
|
||||
exif_dict.setdefault("Exif", {})[piexif.ExifIFD.UserComment] = comment_bytes
|
||||
new_exif = piexif.dump(exif_dict)
|
||||
|
||||
# Lagre til midlertidig fil, rename for atomisk skriving
|
||||
tmp = image_path.with_suffix(".tmp" + image_path.suffix)
|
||||
img.save(tmp, exif=new_exif)
|
||||
tmp.replace(image_path)
|
||||
|
||||
logger.debug("EXIF-tags skrevet til %s: bibs=%s", image_path.name, bibs)
|
||||
except Exception as e:
|
||||
logger.warning("Kunne ikke skrive EXIF-tags til %s: %s", image_path, e)
|
||||
|
||||
|
||||
def read_bib_tags(image_path: Path) -> Optional[dict]:
|
||||
"""
|
||||
Les startnummer-metadata fra EXIF UserComment.
|
||||
Returnerer dict (med nøkkel 'bibs') eller None hvis ikke satt.
|
||||
"""
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
raw_exif = img.info.get("exif")
|
||||
if not raw_exif:
|
||||
return None
|
||||
exif_dict = piexif.load(raw_exif)
|
||||
comment = exif_dict.get("Exif", {}).get(piexif.ExifIFD.UserComment)
|
||||
if not comment or not comment.startswith(_ASCII_PREFIX):
|
||||
return None
|
||||
json_str = comment[len(_ASCII_PREFIX):].decode("ascii", errors="replace")
|
||||
return json.loads(json_str)
|
||||
except Exception:
|
||||
return None
|
||||
+110
-68
@@ -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:
|
||||
|
||||
+58
-40
@@ -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)
|
||||
|
||||
+21
-10
@@ -11,12 +11,13 @@ import aiosqlite
|
||||
async def init_race_tables(db: aiosqlite.Connection) -> None:
|
||||
await db.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS races (
|
||||
race_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
date TEXT,
|
||||
description TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
race_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
date TEXT,
|
||||
description TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
bib_filter_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stations (
|
||||
@@ -34,6 +35,14 @@ async def init_race_tables(db: aiosqlite.Connection) -> None:
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_stations_race ON stations(race_id, station_order);
|
||||
""")
|
||||
# Migrering: legg til kolonne for eksisterende databaser
|
||||
try:
|
||||
await db.execute(
|
||||
"ALTER TABLE races ADD COLUMN bib_filter_enabled INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass # Kolonnen finnes allerede
|
||||
await db.commit()
|
||||
|
||||
|
||||
@@ -44,11 +53,12 @@ async def create_race(
|
||||
name: str,
|
||||
date: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
bib_filter_enabled: bool = False,
|
||||
) -> dict:
|
||||
race_id = str(uuid.uuid4())
|
||||
await db.execute(
|
||||
"INSERT INTO races (race_id, name, date, description) VALUES (?, ?, ?, ?)",
|
||||
(race_id, name, date, description),
|
||||
"INSERT INTO races (race_id, name, date, description, bib_filter_enabled) VALUES (?, ?, ?, ?, ?)",
|
||||
(race_id, name, date, description, int(bib_filter_enabled)),
|
||||
)
|
||||
# Sett som aktivt hvis det er første løpet
|
||||
async with db.execute("SELECT COUNT(*) FROM races") as cur:
|
||||
@@ -86,10 +96,11 @@ async def update_race(
|
||||
name: str,
|
||||
date: Optional[str],
|
||||
description: Optional[str],
|
||||
bib_filter_enabled: bool = False,
|
||||
) -> bool:
|
||||
cur = await db.execute(
|
||||
"UPDATE races SET name = ?, date = ?, description = ? WHERE race_id = ?",
|
||||
(name, date, description, race_id),
|
||||
"UPDATE races SET name = ?, date = ?, description = ?, bib_filter_enabled = ? WHERE race_id = ?",
|
||||
(name, date, description, int(bib_filter_enabled), race_id),
|
||||
)
|
||||
await db.commit()
|
||||
return cur.rowcount > 0
|
||||
|
||||
Reference in New Issue
Block a user