product-recognition-demo/recognization/code/web/index.html
kiyo9w 6902067322 feat: web client with live and photo modes, served by the server
The client is a pure API consumer: live frames go out over /v1/stream, stills
over /v1/recognize, and it can be pointed at another server with
?server=&token=. It keeps the team's visual identity (green palette, logo, EN/JA
toggle) and adds the live surface: state-coloured boxes with name/price chips,
interpolated track boxes, a results/s + latency HUD, quality hints, and an
in-view list that highlights a SKU's boxes.

Photo mode recognises the whole shelf at once instead of one click per product:
boxes paint immediately, identities fill in, tapping a product inspects it
(crop + feature-match visualization), the algorithm selector re-classifies, and
dispense updates stock.

Fixes found by browser verification with a synthetic camera feed:
- The WebSocket handler referenced its processing loop before definition, so the
  socket died with NameError and live mode never received a result. Helpers are
  now defined inside the handler.
- The client never kept a reference to the video element, so frame capture
  returned early and nothing was ever sent.
- /v1/images/{id}/classify passed detection dicts where the engine expected box
  dicts (KeyError: 'width').
- Placeholder chrome (thumb, dashes, hidden groups) leaked on first paint.

Verified in Chrome with a fake camera: live 8-12 results/s, ~60 products in
view, product card and feature matching populated, photo 105/170 recognised on
image.jpg and 59/111 on add/image6.jpg, 0 console errors.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-24 12:42:02 +09:00

171 lines
9.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dorayaki - Smart Supermarket Shelf AI</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><ellipse cx='50' cy='55' rx='44' ry='24' fill='%23f59e0b'/><ellipse cx='50' cy='45' rx='42' ry='20' fill='%23b45309'/></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<div class="brand">
<svg class="brand-logo-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="pancakeTop" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#b45309"/>
</linearGradient>
<linearGradient id="ankoFill" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#450a0a"/>
<stop offset="100%" stop-color="#2d0606"/>
</linearGradient>
<linearGradient id="pancakeBottom" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#d97706"/>
<stop offset="100%" stop-color="#92400e"/>
</linearGradient>
</defs>
<ellipse cx="50" cy="62" rx="42" ry="18" fill="url(#pancakeBottom)" />
<ellipse cx="50" cy="53" rx="43" ry="14" fill="url(#ankoFill)" />
<ellipse cx="50" cy="42" rx="44" ry="22" fill="url(#pancakeTop)" />
<ellipse cx="44" cy="35" rx="20" ry="8" fill="#fde68a" opacity="0.4" />
<path d="M 52,18 C 65,12 72,25 65,30 C 58,28 54,23 52,18 Z" fill="#22c55e" />
</svg>
<div>
<div class="brand-title">Dorayaki <span class="brand-jp">どら焼き</span></div>
<div class="brand-sub" id="t-subtitle">Smart Supermarket Shelf AI &amp; Inventory Recognition</div>
</div>
</div>
<div class="header-actions">
<span class="conn-pill" id="connPill" data-state="idle"><span class="dot"></span><span id="connText">Offline</span></span>
<button class="lang-switch-btn" id="langBtn" type="button">
<span>🌐</span> <span id="langText">日本語 / JA</span>
</button>
</div>
</header>
<main>
<section class="stage-panel">
<div class="toolbar">
<div class="toolbar-group">
<div class="segmented" id="modeControl">
<button class="seg-btn active" data-mode="live" type="button" id="t-mode-live">Live</button>
<button class="seg-btn" data-mode="photo" type="button" id="t-mode-photo">Photo</button>
</div>
<div class="segmented" id="methodControl">
<button class="seg-btn active" data-method="hybrid" type="button">Hybrid</button>
<button class="seg-btn" data-method="surf" type="button">SURF</button>
<button class="seg-btn" data-method="orb" type="button">ORB</button>
<button class="seg-btn" data-method="brisk" type="button">BRISK</button>
</div>
</div>
<div class="toolbar-group" id="liveControls">
<select id="sourceSelect" class="select" title="Camera or video file"></select>
<button class="tool-btn primary" id="startBtn" type="button" id-label="start">▶ <span id="t-start">Start</span></button>
<button class="tool-btn" id="stopBtn" type="button" disabled>■ <span id="t-stop">Stop</span></button>
<div class="hud" id="hud" title="Click for detail">--</div>
</div>
<div class="toolbar-group" id="photoControls" hidden>
<select id="sampleSelect" class="select"></select>
<button class="tool-btn" id="captureBtn" type="button">📷 <span id="t-capture">Take photo</span></button>
<button class="tool-btn" id="uploadBtn" type="button">📁 <span id="t-upload">Upload image</span></button>
<input type="file" id="fileInput" accept="image/*" hidden>
</div>
<div class="toolbar-group">
<button class="tool-btn danger" id="emptyBtn" type="button">🔴 <span id="t-empty">Empty spaces</span></button>
</div>
</div>
<div class="stage" id="stage">
<video id="video" playsinline muted></video>
<img id="photo" alt="" hidden>
<canvas id="overlay"></canvas>
<div class="stage-empty" id="stageEmpty">
<div class="stage-empty-icon">📷</div>
<div id="t-stage-empty">Start the live scan, or open Photo mode to inspect a shelf image.</div>
</div>
<div class="hint-banner" id="hintBanner" hidden></div>
</div>
</section>
<aside class="side-panel">
<div class="card">
<div class="card-header">
<span id="t-card-title">Product details</span>
<span class="status-badge badge-neutral" id="statusBadge"></span>
</div>
<div class="placeholder" id="cardPlaceholder">
<div class="placeholder-icon">🏷️</div>
<span id="t-card-placeholder">Tap a product on the shelf to see its details.</span>
</div>
<div class="card-body" id="cardBody" hidden>
<div class="product-hero">
<img id="productThumb" class="product-thumb" alt="" hidden>
<div class="product-details">
<span class="product-sku" id="productSku" hidden>—</span>
<div class="product-name" id="productName"></div>
<div class="product-price" id="productPrice"></div>
</div>
</div>
<div class="candidates" id="candidates" hidden></div>
<div class="stock-grid">
<div class="stock-metric">
<div class="metric-title" id="t-metric-on">On shelf</div>
<div class="metric-val" id="metricOn"></div>
</div>
<div class="stock-metric">
<div class="metric-title" id="t-metric-out">Sold / dispatched</div>
<div class="metric-val" id="metricOut"></div>
</div>
<div class="stock-metric">
<div class="metric-title" id="t-metric-receipt">Received</div>
<div class="metric-val small" id="metricReceipt"></div>
</div>
<div class="stock-metric">
<div class="metric-title" id="t-metric-expiry">Expires</div>
<div class="metric-val small warn" id="metricExpiry"></div>
</div>
</div>
<button class="action-btn" id="dispenseBtn" type="button">🛒 <span id="t-dispense">Dispense 1 unit</span></button>
</div>
</div>
<div class="card">
<div class="card-header">
<span id="t-match-title">Feature matching</span>
<span class="score-label" id="scoreLabel"></span>
</div>
<div class="match-box" id="matchBox">
<span class="match-placeholder" id="t-match-placeholder">Tap a product, then “Show matching”.</span>
</div>
<button class="tool-btn wide" id="showMatchBtn" type="button" disabled>🔍 <span id="t-show-match">Show matching</span></button>
</div>
<div class="card">
<div class="card-header">
<span id="t-inview-title">On this photo</span>
<span class="score-label" id="inViewCount">0</span>
</div>
<div class="inview-list" id="inViewList">
<div class="placeholder small" id="t-inview-placeholder">Recognized products appear here.</div>
</div>
</div>
</aside>
</main>
<div class="modal-backdrop" id="cameraModal">
<div class="modal-window">
<div class="modal-header">
<div class="modal-title">📷 <span id="t-camera-title">Take shelf photo</span></div>
<button class="close-btn" id="cameraClose" type="button">&times;</button>
</div>
<div class="video-box"><video id="cameraVideo" autoplay playsinline></video></div>
<button class="action-btn" id="snapshotBtn" type="button">📸 <span id="t-snapshot">Capture</span></button>
</div>
</div>
<script type="module" src="app.js"></script>
</body>
</html>