[object Object]

← 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

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 →