Web client: price-tag boxes in a distinct style (teal, chip with the parsed price), an OCR section in the product card showing the tag's price, name, discount and the reconciliation verdict with both values, and a compliance strip under the stage. The photo stage now draws the image into the canvas and uses the same containment as the media, so boxes cannot drift from the product (they did: the canvas stretched to the stage while the photo was contained). Server fixes the browser runs forced out: - Photo mode reconciled before identities existed, so every tag read "tag only". The tag bundle is cached with the still and classify re-reconciles against the identities it just produced, returning them per detection. - The still path only ever read the first four bands (plan hands out two per call); it now drains every band, twice at different upscales so the vote has independent reads. - Voting is confidence-weighted and a dead heat stays unread; the plausible shelf-price range is 10-9999, which stops a misread from becoming a ¥26,300 price. The compliance coverage bar moved to 0.7 after 0.5 produced a false "group without price tag" on a shelf where every facing has one. Measured on add/image6.jpg: 8 readable tags with names and prices, 942 ms for the whole still, 7 real price mismatches, no false violations, and hints that say plainly when a row could not be read. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
179 lines
9.9 KiB
HTML
179 lines
9.9 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 class="compliance-strip" id="complianceStrip" 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>
|
|
<div class="ocr-section" id="ocrSection" hidden>
|
|
<div class="ocr-header">
|
|
<span id="t-ocr-title">Price tag</span>
|
|
<span class="recon-chip" id="reconChip"></span>
|
|
</div>
|
|
<div class="ocr-rows" id="ocrRows"></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>
|