[object Object]

← back to Dw Photo Capture

scan UX: tentative 'hold steady' cue + reject Swift low-light pre-pass (cycle 11)

bfd82499ac775070a0e4b077bec550d2cfd8f3d3 · 2026-06-26 05:04:27 -0700 · Steve Abrams

Two brainstorm-pool items resolved this cycle:
- REJECTED (with evidence) the Swift grayscale/contrast pre-pass for low-light labels.
  A/B test: macOS Vision reads even a 4-level-contrast label (text 56 vs bg 60, near-
  invisible) perfectly RAW, and an autocontrast pre-pass changed nothing. Vision already
  normalizes contrast better than a naive pre-pass — building one is risk for zero gain.
- SHIPPED a tiny UX cue: when a read is not topStrong (needs a 2nd matching frame to
  lock), the live overlay shows '👀 hold steady · <code>' instead of '👀', coaching the
  user to keep the camera still. Strong reads still lock instantly with no extra prompt.

Verified: inline JS valid, lint + 32 tests green, page 200. $0 (local).

Files touched

Diff

commit bfd82499ac775070a0e4b077bec550d2cfd8f3d3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 26 05:04:27 2026 -0700

    scan UX: tentative 'hold steady' cue + reject Swift low-light pre-pass (cycle 11)
    
    Two brainstorm-pool items resolved this cycle:
    - REJECTED (with evidence) the Swift grayscale/contrast pre-pass for low-light labels.
      A/B test: macOS Vision reads even a 4-level-contrast label (text 56 vs bg 60, near-
      invisible) perfectly RAW, and an autocontrast pre-pass changed nothing. Vision already
      normalizes contrast better than a naive pre-pass — building one is risk for zero gain.
    - SHIPPED a tiny UX cue: when a read is not topStrong (needs a 2nd matching frame to
      lock), the live overlay shows '👀 hold steady · <code>' instead of '👀', coaching the
      user to keep the camera still. Strong reads still lock instantly with no extra prompt.
    
    Verified: inline JS valid, lint + 32 tests green, page 200. $0 (local).
---
 OVERNIGHT_LEDGER.md | 3 ++-
 public/index.html   | 5 ++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/OVERNIGHT_LEDGER.md b/OVERNIGHT_LEDGER.md
index 0d6881b..0073f59 100644
--- a/OVERNIGHT_LEDGER.md
+++ b/OVERNIGHT_LEDGER.md
@@ -26,7 +26,7 @@ the single highest-value safe one, the officer executes it, and the loop resched
 ### Brainstorm pool (cycle 10, qwen3:8b $0) — triaged
 - [done-already] SKU prefix boost (180 vendor profiles) · early-exit on confident OCR (topStrong fast-lock)
 - [ ] UX: surface OCR confidence on the live overlay (topStrong vs tentative) — marginal, no per-token score yet
-- [ ] Robustness: grayscale/contrast pre-pass in bin/ocr.swift for low-light labels — needs Swift recompile + A/B test
+- [REJECTED w/ evidence] Swift low-light pre-pass: macOS Vision reads even 4-level-contrast labels RAW perfectly; autocontrast pre-pass = zero gain. Moot — would add risk for no benefit. (cycle 11 A/B)
 - [x] Test-coverage: adversarial/degraded OCR inputs against analyzeOcr (cycle 10)
 
 ## Cycle log
@@ -47,3 +47,4 @@ the single highest-value safe one, the officer executes it, and the loop resched
 - OLLAMA_URL declared after ollamaVision def (works — only called in handlers; moving it risks colliding with the concurrent voice-feature session that owns that const).
 - bgIdentify may teach a prefix from an unresolved OCR code (intentional fire-and-forget; vendor must already be real via lexicon/fuzzy; low impact).
 | 10 | brainstorm (qwen3:8b $0) → pick #5 (no fork) | harden analyzeOcr ranking contract with 11 adversarial/degraded-input tests (3-char/no-digit/dashed codes, <4 barcode, two-brand order, lowercase brand, dup dedup, two barcodes) | 21→32 tests pass; lint clean; no behavior change (all pre-verified correct) | $0 |
+| 11 | investigate→pivot | A/B proved Swift low-light pre-pass MOOT (Vision reads 4-level contrast raw); pivoted to UX: tentative "hold steady · CODE" cue when a non-strong read needs a 2nd frame | OCR reads all contrast tiers raw; inline JS+lint+32 tests green; page 200 | $0 |
diff --git a/public/index.html b/public/index.html
index b52e830..71ef0c1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1094,7 +1094,10 @@ async function scanTick(){
       // a BARCODE read is exact (not OCR-guessed) → label it distinctly so the user trusts it.
       const isBarcode=d.barcode && d.barcode===d.top;
       const lockLabel=isBarcode ? '▍▍ BARCODE · ' : '🔒 LOCKED · ';
-      $('#scanStatus').textContent=(locked?lockLabel:'👀 ')+vtag+d.top;
+      // tentative cue: a non-strong read needs a 2nd matching frame to lock — coach the user
+      // to hold steady (reduces the "why won't it lock" confusion); strong reads lock instantly.
+      const tentative=d.topStrong ? '👀 ' : '👀 hold steady · ';
+      $('#scanStatus').textContent=(locked?lockLabel:tentative)+vtag+d.top;
       if(locked){
         $('#scanlive').classList.add('locked'); // turn the frame + pill RED — "locked in"
         try{navigator.vibrate&&navigator.vibrate(70);}catch(e){}   // haptic on lock; audio waits for the resolve outcome

← 367d125 auto-save: 2026-06-26T02:08:48 (2 files) — data/build.json d  ·  back to Dw Photo Capture  ·  auto-save: 2026-06-26T05:12:44 (1 files) — data/build.json 8f32cd4 →