shelf-control / src /shelf /validation.py
fgeeha's picture
fix: gradio 6 compat, remove gradio from requirements
b0414d1 verified
Raw History Blame Contribute Delete
4.04 kB
"""Validation helpers for barcodes, SKU values and normalized numeric fields.
The challenge scoring is very sensitive to false positives: a wrong barcode or
SKU is worse than an empty value. This module centralizes the strict validators
used by OCR, QR parsing, multi-frame voting and catalog lookup.
"""
from __future__ import annotations
import re
from typing import Any
SKU_RE = re.compile(r"^2\d{11}$")
def normalize_digits(raw: Any) -> str:
"""Return only digits from ``raw`` while handling pandas float strings."""
if raw is None:
return ""
try:
if raw != raw: # NaN
return ""
except Exception:
pass
text = str(raw).strip().replace("\u00a0", " ")
if not text or text.lower() in {"nan", "none", "нет"}:
return ""
if re.fullmatch(r"\d+\.0", text):
text = text[:-2]
return re.sub(r"\D", "", text)
def ean13_checksum_valid(code: str) -> bool:
"""Return True only for a valid 13-digit EAN-13 code."""
if len(code) != 13 or not code.isdigit():
return False
digits = [int(c) for c in code]
total = sum(d * (3 if idx % 2 else 1) for idx, d in enumerate(digits[:12]))
return (10 - total % 10) % 10 == digits[12]
def ean13_check_digit(prefix12: str) -> str:
"""Compute the EAN-13 check digit for a 12-digit prefix."""
if len(prefix12) != 12 or not prefix12.isdigit():
raise ValueError("EAN-13 prefix must contain exactly 12 digits")
digits = [int(c) for c in prefix12]
total = sum(d * (3 if idx % 2 else 1) for idx, d in enumerate(digits))
return str((10 - total % 10) % 10)
def repair_ean13(
raw: Any,
*,
allow_append_12: bool = True,
allow_drop_14: bool = True,
allow_one_digit_repair: bool = False,
) -> str:
"""Conservatively repair a trusted barcode candidate into valid EAN-13.
OCR text is not a trusted source for 12-digit values because Lenta SKU values
have exactly the same length. Callers should set ``allow_append_12`` only for
QR/pyzbar/barcode-ROI sources, never for arbitrary OCR text.
"""
digits = normalize_digits(raw)
if ean13_checksum_valid(digits):
return digits
if len(digits) == 12 and allow_append_12:
candidate = digits + ean13_check_digit(digits)
return candidate if ean13_checksum_valid(candidate) else ""
if len(digits) == 14 and allow_drop_14:
for candidate in (digits[:-1], digits[1:]):
if ean13_checksum_valid(candidate):
return candidate
if len(digits) == 13 and allow_one_digit_repair:
for idx, current in enumerate(digits):
for repl in "0123456789":
if repl == current:
continue
candidate = digits[:idx] + repl + digits[idx + 1 :]
if ean13_checksum_valid(candidate):
return candidate
return ""
def normalize_ean13(
raw: Any,
*,
allow_repair: bool = False,
allow_append_12: bool = False,
allow_drop_14: bool = True,
allow_one_digit_repair: bool = False,
) -> str:
"""Normalize and validate an EAN-13 candidate.
By default this function is strict: invalid 13-digit values and 12-digit SKU
lookalikes are rejected. Repair is opt-in and should be enabled only for
trusted scanner/ROI sources.
"""
digits = normalize_digits(raw)
if ean13_checksum_valid(digits):
return digits
if not allow_repair:
return ""
return repair_ean13(
digits,
allow_append_12=allow_append_12,
allow_drop_14=allow_drop_14,
allow_one_digit_repair=allow_one_digit_repair,
)
def is_valid_sku(raw: Any) -> bool:
"""Return True for Lenta SKU values: exactly 12 digits starting with 2."""
return bool(SKU_RE.fullmatch(normalize_digits(raw)))
def normalize_sku(raw: Any) -> str:
"""Return a strict SKU string or an empty value."""
digits = normalize_digits(raw)
return digits if SKU_RE.fullmatch(digits) else ""