feat: finish the enrollment protocol's reporting checks
Three items from DECISION §8's enrollment protocol were unimplemented and none of them needed Tier-2 data: - Eval reports now record provenance: catalog hash, label-file hash, image hash and the code revision, so a number can be traced to the inputs that produced it. The catalog hash covers the CSV bytes and the view names, not image bytes: a re-encoded view of the same product is the same catalog. - rebuild_catalog flags cross-SKU near-duplicates as text-discriminated siblings. These are the near-unknown case no threshold rejects - the geometry is real because the sibling shares the brand's artwork - so they are reported rather than refused. - --shelf-tags turns the completeness rule into a live-ready verdict. Without the shelf's SKU list the verdict is 'unknown', not a guess. Adds test_rebuild_catalog.py (11 tests; the tool had no test file). Suite 261 -> 272. Also corrects score_crop_lanes' docstring, which still described the live path as averaging local scores when the local scale decides on the newest crop.
This commit is contained in:
parent
7fff0cf724
commit
e53c1ef59b
4 changed files with 301 additions and 3 deletions
|
|
@ -35,8 +35,10 @@ from __future__ import annotations
|
|||
|
||||
import argparse
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
|
|
@ -86,6 +88,55 @@ def load_labels(path: str) -> Dict[str, Any]:
|
|||
return payload
|
||||
|
||||
|
||||
def file_hash(path: str) -> Optional[str]:
|
||||
"""SHA-256 of a file's bytes, or None when it cannot be read."""
|
||||
try:
|
||||
with open(path, "rb") as handle:
|
||||
return hashlib.sha256(handle.read()).hexdigest()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def catalog_hash(stock_dir: str) -> Optional[str]:
|
||||
"""SHA-256 over the catalog's CSV bytes and its image names.
|
||||
|
||||
Image bytes are not hashed: a re-encoded view of the same product is the
|
||||
same catalog, and hashing every packshot would make the number move for
|
||||
reasons that do not change a single decision. A changed row or a different
|
||||
set of views does change it.
|
||||
"""
|
||||
digest = hashlib.sha256()
|
||||
csv_path = os.path.join(stock_dir, "stock.csv")
|
||||
try:
|
||||
with open(csv_path, "rb") as handle:
|
||||
digest.update(handle.read())
|
||||
except OSError:
|
||||
digest.update(b"missing-csv")
|
||||
try:
|
||||
names = sorted(
|
||||
name for name in os.listdir(stock_dir)
|
||||
if os.path.splitext(name)[1].lower() in (".jpg", ".jpeg", ".png")
|
||||
)
|
||||
except OSError:
|
||||
names = []
|
||||
digest.update(b"\n")
|
||||
digest.update("\n".join(names).encode("utf-8"))
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def code_revision() -> Optional[str]:
|
||||
"""The checked-out revision, or None outside a git worktree."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
return result.stdout.strip() or None if result.returncode == 0 else None
|
||||
|
||||
|
||||
def crop_of(image: Any, box: Sequence[int]) -> Any:
|
||||
x1, y1, x2, y2 = [int(v) for v in box]
|
||||
return image[max(0, y1):y2, max(0, x1):x2]
|
||||
|
|
@ -539,6 +590,15 @@ def main() -> int:
|
|||
"image": image_path,
|
||||
"stock_dir": args.stock_dir,
|
||||
"labels": args.labels,
|
||||
# The enrollment protocol's versioning rule: every report records the
|
||||
# catalog hash, the label-file hash and the code revision, so a number
|
||||
# can be traced to the exact inputs that produced it.
|
||||
"provenance": {
|
||||
"catalog_hash": catalog_hash(args.stock_dir),
|
||||
"labels_hash": file_hash(args.labels),
|
||||
"image_hash": file_hash(image_path),
|
||||
"code_revision": code_revision(),
|
||||
},
|
||||
"facings": len(facings),
|
||||
"leakage": {"excluded": len(refused), "labels": refused},
|
||||
"hub_share_limit": HUB_SHARE,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue