← 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
M OVERNIGHT_LEDGER.mdM public/index.html
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 →