Commit graph

22 commits

Author SHA1 Message Date
kiyo9w
3fa82eec37 docs(packshots): record why sku_l08 could not be sourced
The DECISION says l08 has no retailer listing and that the only corroborated
ペリペリミックス is a different brand at 50g. The Rakuten search that shows this
was sitting unreferenced in the repo root as rk2.html; it is now the evidence
file behind the claim, with a manifest in the same shape as l04/l07/l10.

The search returns 141 results and none is the product: every ペリペリ hit is
SPICEDREAM at 50g or 195ml. l08 is the one catalog row with no corroborating
source, so a wrong label there would not be detectable from the catalog alone.
2026-09-29 04:34:58 +09:00
kiyo9w
a716f5aab1 test(server): final smoke with the calibrated gates live
Still endpoint: 111 detections, 44 named, 42 correct, 95.5% precision, 2 wrong
(both unknown facings the local lane matches geometrically).

Stream, 20 s pan 12: 62 tracks labelled, 0 label changes, 0 unexplained, 17/27
facings named at 84% precision, 0 wrong, 0 unenrolled false accepts.

Both recordings committed under server-smoke/ and stream-gated/.
2026-09-29 04:32:38 +09:00
kiyo9w
81a2ab10e7 docs(decision): measure G1 against Van on the same stream and catalog
The G1 row compared against a van run whose server had loaded the legacy
stock-lopia catalog, so its ids were template-level (sku_005) and every answer
scored wrong. Re-run with both servers on stock-lopia-clean, 20 s pan 12:

  van    17/27 (63%) coverage   53% precision   7 wrong   6 unenrolled accepts
  local  19-20/27 (70-74%)      77-85%          0 wrong   0 unenrolled accepts

G1 asks the new system to be no worse than Van on coverage; it is better on
both axes, so G1 now reads as passing on the stream rather than unmeasured.
Both recordings are committed under stream-gated/.
2026-09-29 04:28:20 +09:00
kiyo9w
01d9a5e614 docs(decision): re-measure the stream and live rows after the calibration
The stream row was recorded before the accept lines moved. Three fresh 20 s pan
replays through the shipped lane: 70-74% coverage at 77-85% precision, 0 wrong
labels in all three (0-1 before), 8-14 unknown false accepts - the near-unknown
case, which the size gate and catalog completeness own rather than the accept
line.

Live resolution on the shipped evaluator: 56% end-to-end at 97% precision with
1 false accept, so the calibration is not a full-resolution artifact.

The recordings are committed under stream-gated/ so the row is reproducible.
2026-09-29 04:22:46 +09:00
kiyo9w
5eb4bdf6c8 chore(eval): reconcile the distractor sets and refresh the sweep numbers
The Wikimedia collector truncated the whole basename at 60 characters, so 13
files lost their extension and cv2 could not read them by name - the distractor
sets were quietly scoring 13 images fewer than they held. It now truncates the
stem and keeps the extension, and the files on disk are renamed to match.

The search terms also pull in .djvu volumes whose page 1 Commons serves as a
JPEG thumbnail. Twelve book scans were in the sets; they contributed 0 false
accepts, and they are not shelves, so the collector filters them and the
manifests no longer list them.

With all 2650 crops readable the sweep is re-measured end to end: 3.09% at the
old lines, 0.30% at the shipped 4.0/4, image6 44 shown / 42 correct / 95.5%.
The accept sweep table and the verdict's far-unknown sentence now carry those
numbers instead of the pre-calibration ones.

Also: the fetched images are gitignored (manifests and results are not), the
l10/l11 shelf-tag crops that back the catalog corrections are committed, and a
stale pre-gate eval.json and a duplicate of eval/labels/image6.json are gone.
2026-09-29 04:17:54 +09:00
kiyo9w
4bdad66bbe docs(decision): record the far-unknown calibration and the gate status
The far-unknown claim in the verdict said 0.2-0.7% on 602 crops. That was the
audit's variant matcher on the add/ photos; the shipped lane at the shipped
lines was 3.61% on the Wikimedia sets. Both are now stated, with the sweep that
chose 4.0/4 and the live-endpoint result: 44 named, 42 correct, 95.5% precision
against 53/48/90.6% before.

Gate status is recorded per gate rather than as a blanket claim: G2 and G3's
far half pass offline, G8 passes, and G1/G4/G5/G6/G7 are not yet measured on
held-out data because Tier 2 does not exist.

The Wikimedia manifests and results are committed; the 59 MB of images are not,
because collect_shelf_images.py fetches them from the source URLs the manifests
record.
2026-09-29 04:11:00 +09:00
kiyo9w
d13ce94123 test(server): final smoke with every catalog correction live
/v1/recognize: 111 detections, 53 named in 2.38 s, no view-suffixed ids, no
missing product info. l06 now names all 4 of its facings - it was 1/4 at live
resolution before the current package was enrolled, which is the F-12 fix
visible in the product.

/v1/stream, 20 s pan: 82 tracks labelled, 0 label changes, 0 unexplained. The
label-memory fixes hold under a pan.
2026-09-29 03:41:05 +09:00
kiyo9w
962cdaab5f test(tier3): run the production pipeline over both reusable clips
The two clips were staged but never exercised. One frame per second through the
production detector and the shipped local lane:

- The 1080p supermarket pan: 227 crops, 0 accepted. Correct open-set behaviour -
  the frame is German pickle jars, not catalog products.
- The 360p mayonnaise bay: 1623 crops, 24 accepted (1.5%), across seven SKUs.
  The clip visibly contains Kewpie products, so those are not automatically
  false accepts, but at 640x360 the labels are too soft for per-SKU ground
  truth.

Recorded as an accept-rate probe with false_accepts and accuracy left null
rather than estimated. Tier 3 still needs an operator-recorded walk.
2026-09-29 03:39:33 +09:00
kiyo9w
11826cc43c feat(eval): report the external benchmark per SKU
A set total hides which products the lane can and cannot name. Every SKU in
these sets has images from two independent source domains, so a SKU that scores
the same on both is not one lucky photograph.

The breakdown is what makes the two interesting results visible: l01, l02, l03,
l07 and l09 are 2/2 correct on both their domains, l07 with no enrolled image
at all, while l11 abstains on both and l04 abstains on all four of its images.
2026-09-29 03:37:04 +09:00
kiyo9w
8db280d220 feat(enroll): report views with no recorded source
Six enrolled views predate ATTRIBUTION.json and their source URLs were never
recorded. They are not wrong - each is independent of the evaluation photo
(highest template match 0.166 against add/image6.jpg) and each is above the
enrollment floor - but the provenance is gone, and provenance is what lets a
later audit tell a re-sourced packshot from a leaked shelf crop.

rebuild_catalog now names them instead of leaving the gap to be rediscovered,
and ATTRIBUTION.json records both the l06 replacement and the gap itself, with
what is verifiable about those six.

Reported, not refused: refusing would drop six working references to punish a
missing field.
2026-09-29 03:35:41 +09:00
kiyo9w
56cff09292 docs(decision): report the external benchmark on the corrected basis
19 scored rather than 20: sku_l06_1.jpg is now the enrolled l06 view, so it is
reported as leaked instead of as a held-out correct accept. 12 correct,
7 abstain, 0 false accepts.

Both leak checks are named in the record and in the external README, since the
enrollment that closed F-12 is exactly the kind of change that silently
converts a held-out row into training data.
2026-09-29 03:30:16 +09:00
kiyo9w
e25fcda67d fix(eval): confirm a leak signature before calling it one, and score external sets in code
Enrolling a packshot from an external set is normal - it is how F-12 was
closed - and it silently turns one of those rows into training data. The
external sets were scored by an ad-hoc script that never checked, so
sku_l06_1.jpg was being reported as a held-out correct accept while being
byte-identical to the enrolled view.

code/eval_external.py now scores those sets with the same leaked() check the
shelf evaluator runs, and refuses a row that has since been enrolled.

That check needed fixing first. Its 64x64 signature fires on two different
photographs of the same product: the l09 rear label from a retailer page sits
19.5 from the enrolled front packshot, under the 20.0 line, while being a
different photograph (resized MAD 26.3). A signature hit is now confirmed by
correlation - a genuine re-cut, re-encode or rescale correlates at 0.99+, that
pair at 0.28 - so the two signals together are decisive where either alone is
not. Verified against both directions: identical, q55 re-encode and 700px
rescale are caught; the different-photo pair is not.

Also adds the van backend (always-argmax, no gate) so the stream comparison in
DECISION.md is reproducible rather than asserted, with two tests pinning that
it answers where the shipped gate abstains and still refuses an empty score set.

External result on the corrected basis: 19 scored, 12 correct, 7 abstain,
0 false accepts, 1 leak excluded. Suite 307 -> 309.
2026-09-29 03:29:56 +09:00
kiyo9w
6a45fb990d fix(catalog): enroll the current l06 package, closing F-12
The enrolled sku_l06.jpg showed the superseded 内臓脂肪ケア wrapper. The shelf
and the current retail package both read 内臓脂肪を減らす キューピーフィッテ
210g, so the reference image was of a design the store no longer sells - the
catalog was pointing at the wrong package.

F-12 was recorded as blocked on a capture because Kewpie's own site serves the
old design for that JAN. An independent retailer listing carries the current
one, and it matches the shelf facings exactly (verified by eye against a crop
of add/image6.jpg before enrolling).

Measured effect on the corrected labels: live decision 60% -> 64%, end-to-end
59% -> 62%, full-resolution decision 81% -> 83%. Suite still 307.

Also lands 12 more external-proxy images (six SKUs, two source domains each)
and their scored result. Across both external sets - 20 images - the lane is
13 correct, 7 abstain, 0 false accepts.
2026-09-29 03:22:03 +09:00
5fe3258614 test(external): add 12 kewpie external-proxy images with provenance
Two visually verified non-enrollment images per SKU for sku_l01, l02, l03,
l05, l06 and l11, sourced from Amazon retailer galleries and Yahoo
retailer/customer listings. sku_l06 uses the current 内臓脂肪を減らす package
(F-12); three customer listings showing the superseded wrapper were rejected.
Collection scratch files are removed.
2026-09-29 03:19:23 +09:00
kiyo9w
8754738dc1 docs(decision): refresh every number the corrected labels moved
Five blocks still printed pre-correction figures. Each was re-run through the
evaluator and replaced with what it actually outputs now:

- The lane comparison: local 75% at 91% against Van 64% at 38% (full), and
  59% at 93% against 66% at 39% (live). At live resolution Van now answers more
  facings correctly and is wrong on 67 of them to the local lane's 3 - stated
  plainly rather than left as a win on both axes.
- The accept sweep: 75/67/67/62% end-to-end at 91/91/93/95% precision, with the
  evaluator's own column definitions spelled out, since my first re-derivation
  used the wrong denominator.
- The G1 stream claim: four pan replays give 63-70% coverage at 76-84%
  precision, not 75% at 100%.
- The upscale paragraph: the guard is intact, but sku_l09.jpg is now a
  1000x1000 packshot, so the current run no longer exercises it.
- catalog_hash and the pyramid floor: the hash covers view bytes, not just
  names, and every view is now above the 800 px floor.

Also refreshes the server smoke against the renamed catalog: 53 named in 2.58 s,
no view-suffixed ids, no missing product info. l12 is not among them - it has
no enrolled view, which is the documented gap.
2026-09-29 03:18:30 +09:00
kiyo9w
306ea9b3e8 test(external): score 8 independent-source images, 0 false accepts
Eight images for sku_l04, sku_l07 and sku_l09 from six source domains, scored
through the shipped LocalLaneAdapter and DecisionPolicy against
stock-lopia-clean: 4 correct accepts, 4 abstains, no false accepts.

Leak-tested first: the highest template match of any image against
add/image6.jpg is 0.675, so none is a crop of the evaluation photo.

The result that matters is sku_l07. It has no enrolled image, and both its
images accept through the text lane alone - one reads ラッキー フヨネーズ, the
other the rear label's 製造者:丸和油脂株式会社 with 内容量:200g, which
together name the row 丸和油脂 ラッキーマヨネーズ 200g. The text lane's
coverage claim holds on data that was not used to build it.

The four l04 abstentions are correct behaviour: l04 is also unenrolled and its
reads are weak (one reads only コと旨 タ 味, another scores 0.379 against a
0.45 accept line).

Also records the l12 reconciliation, the Kewpie 450 g research, and two
reusable pan clips with 11 rejected candidates for a future Tier-3 replay.
Nothing here is Tier 2 or Tier 3 and every manifest says so.
2026-09-29 03:13:16 +09:00
kiyo9w
22db40eb93 fix(labels): the tare facings are sku_l12, not unknown
Facings #46-48 carried the note 'possibly what stock.csv calls l12'. The l12
reconciliation settled it: the shelf tag under them reads ユーラス 野菜を
おいしく食べる旨タレ (380g) at ¥299, the price the l12 row carries, and the row
has been renamed to match. Leaving them unknown understated the catalog and
overstated precision.

Counts move from 61 in-catalog / 48 unknown to 64 / 45. The enrollment ceiling
falls 87% -> 83% because l12's three facings now count as in-catalog with no
enrolled view - an honest catalog gap, not a recognition regression.

Also records the Tier-3 source search: two reusable clips (CC BY 3.0 Wikimedia
1920x1080 supermarket pan; CC BY YouTube 360p mayonnaise-bay pan), 11 rejected
candidates, and the LOPIA walk that carries the standard YouTube licence and
was therefore not downloaded. Both clips are labelled tier3_proxy_candidate,
never Tier 3: that still needs the operator's own recorded walks.
2026-09-29 03:09:15 +09:00
kiyo9w
a9f0f61611 fix(catalog): sku_l12 is a vegetable tare, not 焼肉のたれ 中辛
The row named a product that is not on the shelf. The tag under facings #46-48
reads ユーラス 野菜をおいしく食べる旨タレ (380g) at ¥299 - the exact price the
l12 row already carried - and the package reads お子さまOK 甘めテイスト, the
opposite of 中辛. Category, heat level and brand were all wrong.

This one is worse than a wrong size: the text lane scores a crop against the
row's printed name, so a correct read of the real bottle would have been
vetoed by a row describing a different product.

The 2023 green-label predecessor (380ml, JAN 4571233473887, checksum verified)
is recorded in the manifest as predecessor-only evidence; no current JAN is
claimed, because the current white package's rear label has not been
photographed. One in-store rear-label photo would close that.
2026-09-29 03:07:39 +09:00
kiyo9w
74e9afed7d test(server): record the real-endpoint smoke after the size gate
/v1/recognize on add/image6.jpg against stock-lopia-clean: 111 detections,
53 named, every one carrying product_name and price, no view-suffixed ids.
sku_l10 answers 9 facings, which it could not do before it was enrolled.

The live WebSocket path was exercised too: 217 frames, 77 tracks labelled,
0 unexplained label changes, l09 and l10 in the final summary.
2026-09-29 03:06:28 +09:00
kiyo9w
aa838f7b2b docs(packshots): record l04/l07/l10 identity provenance
Three enrollment views were refused by the name/OCR check at 0.70, and the
research says all three refusals are false negatives:

- sku_l04: the packshot is byte-identical (MD5 510db1965bc483899c0cce17c72ba10e)
  to Amazon media 61yWDtDjDuL for ASIN B0197VWHRG, やまや めんたいマヨネーズタイプ
  200g, JAN 4903085062863. 明太子屋さんがつくった is a package slogan, not a
  competing brand. Caveat recorded: 200g is superseded by the current 300g.
- sku_l07: 丸和油脂 ラッキーマヨネーズ 200g, JAN 4978931401615, corroborated by
  Amazon, Rakuten, Yahoo Shopping and Kakaku. The front prints the product name
  but never the maker or size, so full-name overlap cannot reach 0.70.
- sku_l10: exact 400g front, JAN 4901001114030, with the 450g rejection.

Also adds the missing sku_l10 OCR read, generated with the current engine, and
l10's attribution entry. Manifests are tracked; the retailer images they cite
are restricted-research and stay untracked, matching the existing precedent.
2026-09-29 02:51:55 +09:00
kiyo9w
601d72296c fix(catalog): correct sku_l10 to 400g and enroll its packshot
Ajinomoto ピュアセレクト マヨネーズ has no 450g size: the official lineup is
250g/400g/600g/1kg, the shelf tag in add/image6.jpg reads 400g, and DECISION.md
already recorded the mismatch. Both CSVs carried 450g, so every l10 answer
shipped a wrong size.

The packshot is the exact 400g front (JAN 4901001114030, 1000x1000) with its
source URL and the 450g rejection recorded in
data/packshots/research/sku_l10/manifest.json.

Enrolling it moves the catalog ceiling from 69% to 87% of in-catalog facings
answerable by image (l10 was 11 unenrolled facings). Measured on the corrected
image6 labels: local lane 92% identity at 84% precision full, 85% at 91% live.

Also points external-eval defaults at the repository-local data path; the
licensed Wikimedia corpus remains uncommitted pending manifest review.
2026-09-29 02:45:21 +09:00
68061635d9 Enroll from a repo data dir; read packshots in every orientation
Two enrollment defects, both found by trying to enroll real packshots.

`rebuild_catalog.py` crashed on every successful run: the report referenced
`small` but the call to `below_pyramid_floor` was missing, so the pyramid-floor
check the enrollment protocol asks for never ran and the tool died with a
NameError after writing the catalog.

The name/OCR check read one orientation. A packshot whose identifying text runs
vertically - the やまや めんたいマヨネーズ image reads 明太手屋さんが only when
rotated - was refused as a mismatch, which is the worst failure this check has:
it rejects good data while claiming to validate it. `best_orientation_text`
tries all four right-angle rotations and picks the one that explains the most of
the catalog name, not the one that reads the most characters, because the
upright pass returns stray kana that concatenate longer than the real read.

The regression test goes through `audit`, not the helper: the bug was that
`audit` called a single-orientation read, and a helper unit test would have
passed on the broken code. Verified to fail before the fix and pass after.

Data now lives in the repo, not /tmp:

  data/packshots/source/   packshots as downloaded, with ATTRIBUTION.json
  data/evaluations/        per-session run artifacts, so a number in
                           DECISION.md traces to the run that produced it

sku_l09 is re-sourced at 1000x1000 from Matsukiyo's Rakuten image, replacing
the 305px view that the README documents as the reason l09 wins facings it
should not.
2026-09-29 02:33:47 +09:00