← back to Dw Photo Capture
Voice search: mic button → speak vendor/SKU/what-to-look-for → search
3948d6ac3852178f00d234b9d1f5335864162de5 · 2026-06-25 18:04:58 -0700 · Steve
Web Speech API (webkitSpeechRecognition) mic in the header; transcribes a spoken
query, strips lead-ins (show me/find/look for), and runs it through all-Shopify
search (resolves vendors, product types, titles, SKUs + vendor codes via the mfr
index). Graceful fallback to the keyboard mic where SpeechRecognition is absent.
Files touched
M data/build.jsonM public/index.html
Diff
commit 3948d6ac3852178f00d234b9d1f5335864162de5
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jun 25 18:04:58 2026 -0700
Voice search: mic button → speak vendor/SKU/what-to-look-for → search
Web Speech API (webkitSpeechRecognition) mic in the header; transcribes a spoken
query, strips lead-ins (show me/find/look for), and runs it through all-Shopify
search (resolves vendors, product types, titles, SKUs + vendor codes via the mfr
index). Graceful fallback to the keyboard mic where SpeechRecognition is absent.
---
data/build.json | 5 +++--
public/index.html | 28 ++++++++++++++++++++++++++++
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/data/build.json b/data/build.json
index 97d26f4..30f1227 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
{
- "next": 44,
+ "next": 45,
"map": {
"63863152": 27,
"578af86f": 2,
@@ -42,6 +42,7 @@
"2e98c767": 40,
"7d42b31f": 41,
"260c91c6": 42,
- "3b47a090": 43
+ "3b47a090": 43,
+ "798e88ce": 44
}
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 3fd0f90..69550b8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -161,6 +161,7 @@
<div class="controls">
<input type="search" id="q" placeholder="🔎 Search name or model #…" autocapitalize="characters">
<button class="scanbtn" id="scanBtn" title="Scan a printed SKU/model #">🔢 Scan</button>
+ <button class="scanbtn" id="micBtn" title="Voice search — say a vendor, SKU, or what to look for">🎤</button>
<button class="scanbtn" id="camBtn" title="Choose camera (back / front)">📷 Back</button>
<input type="file" accept="image/*" capture="environment" id="scanInput" hidden>
<select id="sort">
@@ -867,6 +868,32 @@ function tone(seq,type,vol){ // seq = [[freq,startOffset,dur],…]
}
function playYoohoo(){ tone([[784,0.00,0.17],[659,0.15,0.34]],'triangle',0.30); } // ✓ FOUND: bright falling "yoo-HOO"
function playUhoh(){ tone([[311,0.00,0.20],[233,0.20,0.36]],'square',0.22); } // ✗ NOT RIGHT: low descending "uh-oh"
+
+// ── Voice search: speak a vendor / SKU / "what to look for" → transcribe → run the search ──
+let _recog=null, _listening=false;
+function syncMicBtn(){ const b=$('#micBtn'); if(b){ b.textContent='🎤'; b.classList.remove('on'); } }
+function voiceSearch(){
+ const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
+ if(!SR){ toast('Voice not supported here — tap the search box and use the keyboard mic 🎙️'); $('#q').focus(); return; }
+ if(_listening){ try{ _recog&&_recog.stop(); }catch(e){} return; } // tap again = stop
+ unlockAudio();
+ _recog=new SR(); _recog.lang='en-US'; _recog.interimResults=false; _recog.maxAlternatives=1;
+ _listening=true; const b=$('#micBtn'); if(b){ b.textContent='🔴'; b.classList.add('on'); }
+ toast('🎤 Listening — say a vendor, SKU, or what to look for…');
+ _recog.onresult=e=>{ const t=((e.results[0][0]||{}).transcript||'').trim(); if(t) runVoiceQuery(t); };
+ _recog.onerror=e=>{ toast('Mic: '+(e.error||'error')); };
+ _recog.onend=()=>{ _listening=false; syncMicBtn(); };
+ try{ _recog.start(); }catch(e){ _listening=false; syncMicBtn(); toast('Mic busy — try again'); }
+}
+function runVoiceQuery(t){
+ // strip spoken lead-ins ("show me…", "find…", "look for…") so the query is just the subject
+ const q=t.replace(/^\s*(hey |ok |okay )?(show me|find( me)?|search( for)?|look( for)?|pull up|get me|i want|do you have|any)\s+/i,'').trim() || t.trim();
+ $('#q').value=q;
+ toast('🔎 “'+q+'”');
+ // All-Shopify is the broadest lane — it resolves vendor names, product types, titles, SKUs,
+ // and (via the mfr# index) printed vendor codes. Exactly what "look for a vendor" needs.
+ chipTo('shop'); doShopSearch();
+}
async function openLiveScan(){
unlockAudio(); // we're inside the user's tap → unlock audio for the lock-in cue
try{ await startScanStream(); }
@@ -954,6 +981,7 @@ $('#scanCancel').addEventListener('click',closeLiveScan);
$('#scanFlip').addEventListener('click',flipCam);
function syncCamBtn(){ $('#camBtn').textContent=(camFacing==='user'?'🤳 Front':'📷 Back'); }
$('#camBtn').addEventListener('click',flipCam);
+$('#micBtn').addEventListener('click',voiceSearch);
$('#scanBtn').addEventListener('click',()=>{
if(window.isSecureContext && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) openLiveScan();
else $('#scanInput').click(); // HTTP (no secure context) → fall back to single-snap
← 610fc30 Scan audio: outcome-based — yoo-hoo on resolve, 'uh-oh' on n
·
back to Dw Photo Capture
·
auto-save: 2026-06-25T18:36:37 (1 files) — data/recents.json fc5196a →