fix: restore reliable authenticated live recognition

This commit is contained in:
kiyo9w 2026-09-28 12:38:15 +09:00
parent 0e23425ef0
commit 934615b593
40 changed files with 2556 additions and 820 deletions

View file

@ -49,6 +49,88 @@ MILK_SHELF_LABELS = {
}
def enrollment_report(stock: str, csv_name: str = "stock.csv") -> dict:
"""Catalog versus enrolled views. Does not load a model or write files.
A SKU with no readable view cannot be recognized. One view is reported, not
treated as enough: the measured knee is two views. Duplicate CSV rows and a
head whose class list disagrees with the catalog are blockers.
"""
import csv
import re
from gallery import ProductGallery
names = []
try:
names = os.listdir(stock)
except OSError:
names = []
images = [name for name in names if os.path.splitext(name)[1].lower() in (".jpeg", ".jpg", ".png")]
rows = []
csv_path = os.path.join(stock, csv_name)
if os.path.exists(csv_path):
with open(csv_path, encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
sku_ids = []
seen = set()
duplicate_rows = []
for row in rows:
sku_id = (row.get("SKU_ID") or "").strip()
if not sku_id:
continue
if sku_id in seen:
duplicate_rows.append(sku_id)
seen.add(sku_id)
sku_ids.append(sku_id)
gallery = ProductGallery(stock, sku_ids or [])
views = {sku_id: gallery._views_for(sku_id) for sku_id in dict.fromkeys(sku_ids)}
missing = [sku_id for sku_id, paths in views.items() if not paths]
single = [sku_id for sku_id, paths in views.items() if len(paths) == 1]
claimed = {os.path.basename(path) for paths in views.values() for path in paths}
orphans = sorted(name for name in images if name not in claimed)
# ponytail: prefix collision check. A view named sku_001 must not also match sku_00.
collisions = []
for sku_id, paths in views.items():
bare = [os.path.basename(path) for path in paths if os.path.splitext(os.path.basename(path))[0] != sku_id
and not re.fullmatch(re.escape(sku_id) + r"[_-]\d+", os.path.splitext(os.path.basename(path))[0])]
if bare:
collisions.append(sku_id)
blockers = []
if duplicate_rows:
blockers.append("duplicate_csv_sku")
if missing:
blockers.append("sku_without_view")
if collisions:
blockers.append("view_prefix_collision")
head_path = os.path.join(stock, "identity_head.pt")
head_note = None
if os.path.exists(head_path):
head_note = "present_not_opened"
# Do not torch.load here: the head can be large, and this report must stay cheap.
# Class-list drift is reported when the caller passes class_ids.
return {
"stock": stock,
"sku_ids": list(dict.fromkeys(sku_ids)),
"images": len(images),
"missing": missing,
"single_view": single,
"orphans": orphans,
"duplicate_rows": duplicate_rows,
"collisions": collisions,
"blockers": blockers,
"head": head_note,
}
def enrollment_report_text(report: dict) -> str:
return (
f"{report['stock']}: {len(report['sku_ids'])} SKUs, {report['images']} images, "
f"missing={len(report['missing'])}, single_view={len(report['single_view'])}, "
f"orphans={len(report['orphans'])}, blockers={report['blockers'] or 'none'}"
)
def build_model(image_size: int = 518):
import timm
import torch
@ -149,13 +231,13 @@ def train_from_stock(args) -> None:
from gallery import ProductGallery
stock = args.stock
gallery = ProductGallery(stock, [])
# the SKUs are whatever the stock directory holds
skus = sorted({os.path.splitext(name)[0].rsplit("_", 1)[0] if os.path.splitext(name)[0].rsplit("_", 1)[-1].isdigit()
else os.path.splitext(name)[0]
for name in os.listdir(stock)
if os.path.splitext(name)[1].lower() in (".jpeg", ".jpg", ".png")})
views = {sku: gallery._views_for(sku) for sku in skus}
report = enrollment_report(stock)
print(enrollment_report_text(report))
if report["blockers"]:
print(f"{stock}: not training; enrollment blockers: {', '.join(report['blockers'])}")
return
gallery = ProductGallery(stock, report["sku_ids"])
views = {sku: gallery._views_for(sku) for sku in report["sku_ids"]}
views = {sku: paths for sku, paths in views.items() if paths}
if len(views) < 2:
print(f"{stock}: needs at least two products with views, found {len(views)}")