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>
171 lines
9.5 KiB
HTML
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 & 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">×</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>
|