1async function initVoiceSearch(voiceSearchOptions) { 2 const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; 3 const inputBox = document.querySelectorAll('.search-input'); 4 const voiceStatus = document.getElementById("voiceStatus"); 5 const voiceText = document.getElementById("voiceText"); 6 7 let activeRecognition = null; 8 let activeMediaRecorder = null; 9 let activeStream = null; 10 let activeAudioContext = null; 11 let activeFetchController = null; 12 let progressTimer = null; 13 let isCancelled = false; 14 15 if (voiceSearchOptions && voiceSearchOptions.isEnabled === "True") { 16 $(document).on("click", ".btn-voice-search", async function () { 17 isCancelled = false; 18 cleanupResources({ keepModal: false }); 19 20 const micExists = await hasMicrophone(); 21 if (!micExists) { 22 Swal.fire({ icon: "error", title: "â No microphone detected", text: "Please connect a microphone to use voice search.", confirmButtonText: "OK" }); 23 return; 24 } 25 26 const micPermission = await checkMicPermission(); 27 if (micPermission === "denied") { 28 Swal.fire({ icon: "error", title: "â Microphone access blocked", text: "â ï¸ Microphone access is blocked in your browser settings. Please enable it to continue.", confirmButtonText: "OK" }); 29 return; 30 } 31 32 if (!voiceSearchOptions || !voiceSearchOptions.searchEngine) { 33 showNotSupportedModal(); 34 } else { 35 const engine = voiceSearchOptions.searchEngine; 36 37 switch (engine) { 38 case "C": 39 if (SpeechRecognition) { 40 runNativeRecognition(); 41 } else { 42 showNotSupportedModal(); 43 } 44 break; 45 46 case "CW": 47 if (SpeechRecognition) { 48 runNativeRecognition(); 49 } else { 50 await fallbackWhisper(); 51 } 52 break; 53 54 case "W": 55 await fallbackWhisper(); 56 break; 57 58 default: 59 showNotSupportedModal(); 60 break; 61 } 62 } 63 }); 64 } 65 66 function showNotSupportedModal() { 67 $(".mic-container").hide(); 68 $("#modal_voice_search").modal('show'); 69 voiceStatus.innerText = "â ï¸ Voice search not supported by this browser."; 70 voiceText.innerText = ""; 71 } 72 73 function runNativeRecognition() { 74 $(".mic-container").show(); 75 $("#modal_voice_search").modal('show'); 76 77 const recognition = new SpeechRecognition(); 78 activeRecognition = recognition; 79 80 recognition.lang = "en-US"; 81 recognition.interimResults = true; 82 recognition.continuous = false; 83 recognition.maxAlternatives = 1; 84 85 try { recognition.start(); } catch (_) { } 86 87 recognition.onstart = () => { 88 if (isCancelled) return; 89 voiceStatus.innerText = "ð¤ Listening..."; 90 voiceText.innerText = "Speak clearly into your microphone."; 91 }; 92 93 recognition.onspeechend = () => { 94 if (isCancelled) return; 95 try { recognition.stop(); } catch (_) { } 96 voiceStatus.innerText = "â³ Processing for voice search..."; 97 }; 98 99 recognition.onend = () => { 100 if (isCancelled) return; 101 activeRecognition = null; 102 }; 103 104 recognition.onerror = (event) => { 105 if (isCancelled) return; 106 console.log("Recognition error:", event.error); 107 if (event.error === "not-allowed") { 108 voiceStatus.innerText = "â Microphone permission denied."; 109 voiceText.innerText = "Enable microphone access in browser settings."; 110 } else if (event.error === "no-speech" || event.error === "aborted") { 111 voiceStatus.innerText = "â ï¸ No speech detected."; 112 voiceText.innerText = "Please try speaking again."; 113 } else { 114 voiceStatus.innerText = "â ï¸ Error: " + event.error; 115 voiceText.innerText = "Something went wrong."; 116 } 117 setTimeout(() => $("#modal_voice_search").modal('hide'), 2500); 118 }; 119 120 recognition.onresult = (event) => { 121 if (isCancelled) return; 122 123 let transcript = ""; 124 for (let i = event.resultIndex; i < event.results.length; ++i) { 125 transcript += event.results[i][0].transcript; 126 } 127 128 voiceText.innerText = "ð You said: " + transcript; 129 130 const last = event.results[event.results.length - 1]; 131 if (last && last.isFinal) { 132 let finalText = (transcript || "").trim().replace(/[^\w\s]+/g, "");
133 inputBox.forEach(box => { box.value = finalText; }); 134 setTimeout(() => { 135 const form = $("#frm_SiteSearch"); 136 PerformSiteSearch(form); 137 }, 300); 138 } 139 }; 140 } 141 142 async function fallbackWhisper() { 143 $(".mic-container").show(); 144 $("#modal_voice_search").modal('show'); 145 voiceStatus.innerText = "ð¤ Recording... Please speak"; 146 147 activeStream = await navigator.mediaDevices.getUserMedia({ audio: true }); 148 const mediaRecorder = new MediaRecorder(activeStream); 149 activeMediaRecorder = mediaRecorder; 150 const chunks = []; 151 152 mediaRecorder.ondataavailable = e => { if (!isCancelled) chunks.push(e.data); }; 153 154 mediaRecorder.onstop = async () => { 155 if (isCancelled) return; 156 if (!chunks.length) return; 157 158 voiceStatus.innerText = "â³ Processing for voice search..."; 159 voiceText.innerText = ""; 160 161 const blob = new Blob(chunks, { type: "audio/webm" }); 162 const arrayBuffer = await blob.arrayBuffer(); 163 164 activeAudioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 }); 165 const audioBuffer = await activeAudioContext.decodeAudioData(arrayBuffer); 166 const wavBlob = encodeWAV(audioBuffer); 167 168 activeFetchController = new AbortController(); 169 let text; 170 try { 171 text = await recognizeWithWhisper(wavBlob, activeFetchController.signal); 172 } catch (err) { 173 if (isCancelled) return; 174 console.error("Whisper error:", err); 175 text = "[Whisper failed]"; 176 } finally { 177 activeFetchController = null; 178 } 179 180 if (isCancelled) return; 181 182 let finalText = (text || "").trim().replace(/[^\w\s]+/g, ""); 183 voiceText.innerText = "ð Recognized Speech: " + finalText; 184 185 const isNoSpeech = (finalText === "[No speech recognized]"); 186 if (!isNoSpeech) inputBox.forEach(box => { box.value = finalText; }); 187 188 setTimeout(() => { 189 if (!isNoSpeech) { 190 const form = $("#frm_SiteSearch"); 191 PerformSiteSearch(form); 192 } else { 193 $("#modal_voice_search").modal('hide'); 194 } 195 }, 500); 196 }; 197 198 const duration = 5000; 199 startRecordingProgress(duration); 200 mediaRecorder.start(); 201 202 setTimeout(() => { 203 if (!isCancelled && mediaRecorder.state !== "inactive") { 204 mediaRecorder.stop(); 205 } 206 if (activeStream) { 207 activeStream.getTracks().forEach(t => t.stop()); 208 activeStream = null; 209 } 210 }, duration); 211 } 212 213 async function recognizeWithWhisper(audioBlob, signal) { 214 const formData = new FormData(); 215 formData.append("file", audioBlob, "speech.wav"); 216 217 const response = await fetch("https://hprera.nic.in/SpeechRecognition/API/SpeechAPI/RecognizeWhisper", { 218 method: "POST", 219 body: formData, 220 signal 221 }); 222 223 if (!response.ok) throw new Error("Server error: " + response.statusText); 224 225 const data = await response.json(); 226 return data.text || "[No speech recognized]"; 227 } 228 229 $(document).on("click", ".btn-cancel-voice", function () { 230 cancelVoiceSearch(); 231 }); 232 233 $(document).on("hide.bs.modal", "#modal_voice_search", function () { 234 cancelVoiceSearch(); 235 }); 236 237 function cancelVoiceSearch() { 238 if (isCancelled) return; 239 isCancelled = true; 240 241 if (activeRecognition) { 242 try { 243 activeRecognition.onresult = null; 244 activeRecognition.onerror = null; 245 activeRecognition.onend = null; 246 activeRecognition.onspeechend = null; 247 activeRecognition.stop(); 248 } catch (_) { } 249 activeRecognition = null; 250 } 251 252 if (activeMediaRecorder) { 253 try { 254 if (activeMediaRecorder.state !== "inactive") { 255 activeMediaRecorder.stop(); 256 } 257 } catch (_) { } 258 activeMediaRecorder = null; 259 } 260 261 if (activeStream) {
262 try { activeStream.getTracks().forEach(track => track.stop()); } catch (_) { } 263 activeStream = null; 264 } 265 266 if (activeFetchController) { 267 try { activeFetchController.abort(); } catch (_) { } 268 activeFetchController = null; 269 } 270 271 if (activeAudioContext) { 272 try { activeAudioContext.close(); } catch (_) { } 273 activeAudioContext = null; 274 } 275 276 if (progressTimer) { 277 clearInterval(progressTimer); 278 progressTimer = null; 279 } 280 281 voiceStatus.innerText = "â Cancelled"; 282 voiceText.innerText = "Voice search was cancelled."; 283 $("#modal_voice_search").modal('hide'); 284 } 285 286 function cleanupResources({ keepModal = false } = {}) { 287 if (activeRecognition) { 288 try { activeRecognition.stop(); } catch (_) { } 289 activeRecognition = null; 290 } 291 if (activeMediaRecorder) { 292 try { 293 if (activeMediaRecorder.state !== "inactive") activeMediaRecorder.stop(); 294 } catch (_) { } 295 activeMediaRecorder = null; 296 } 297 if (activeStream) { 298 try { activeStream.getTracks().forEach(t => t.stop()); } catch (_) { } 299 activeStream = null; 300 } 301 if (activeFetchController) { 302 try { activeFetchController.abort(); } catch (_) { } 303 activeFetchController = null; 304 } 305 if (activeAudioContext) { 306 try { activeAudioContext.close(); } catch (_) { } 307 activeAudioContext = null; 308 } 309 if (progressTimer) { 310 clearInterval(progressTimer); 311 progressTimer = null; 312 } 313 if (!keepModal) { 314 try { $("#modal_voice_search").modal('hide'); } catch (_) { } 315 } 316 } 317 318 async function hasMicrophone() { 319 if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return false; 320 try { 321 const devices = await navigator.mediaDevices.enumerateDevices(); 322 return devices.some(device => device.kind === 'audioinput'); 323 } catch (err) { 324 console.error("Device check error:", err); 325 return false; 326 } 327 } 328 329 async function checkMicPermission() { 330 if (!navigator.permissions || typeof navigator.permissions.query !== "function") { 331 return "unknown"; 332 } 333 try { 334 const status = await navigator.permissions.query({ name: "microphone" }); 335 return status.state; 336 } catch (e) { 337 return "unknown"; 338 } 339 } 340 341 function encodeWAV(audioBuffer) { 342 const sampleRate = 16000; 343 const numChannels = 1; 344 345 const floats = audioBuffer.getChannelData(0); 346 const buffer = new ArrayBuffer(44 + floats.length * 2); 347 const view = new DataView(buffer); 348 349 writeString(view, 0, "RIFF"); 350 view.setUint32(4, 36 + floats.length * 2, true); 351 writeString(view, 8, "WAVE"); 352 writeString(view, 12, "fmt "); 353 view.setUint32(16, 16, true); 354 view.setUint16(20, 1, true); 355 view.setUint16(22, numChannels, true); 356 view.setUint32(24, sampleRate, true); 357 view.setUint32(28, sampleRate * numChannels * 2, true); 358 view.setUint16(32, numChannels * 2, true); 359 view.setUint16(34, 16, true); 360 writeString(view, 36, "data"); 361 view.setUint32(40, floats.length * 2, true); 362 363 let offset = 44; 364 for (let i = 0; i < floats.length; i++, offset += 2) { 365 let s = Math.max(-1, Math.min(1, floats[i])); 366 view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); 367 } 368 369 return new Blob([buffer], { type: "audio/wav" }); 370 371 function writeString(dv, off, str) { 372 for (let i = 0; i < str.length; i++) dv.setUint8(off + i, str.charCodeAt(i)); 373 } 374 } 375 376 function startRecordingProgress(total = 5000) { 377 voiceText.innerHTML = ` 378 <div class="mt-4" style="width:100%; background:#ddd; border-radius:10px; overflow:hidden; height:8px;"> 379 <div id="bar" style="height:100%; width:0%; background:#56CA3D;"></div> 380 </div>`; 381 const bar = document.getElementById("bar"); 382 const start = Date.now(); 383 384 if (progressTimer) clearInterval(progressTimer); 385 386 progressTimer = setInterval(() => { 387 if (!bar) return;
388 const elapsed = Date.now() - start; 389 const width = Math.min((elapsed / total) * 100, 100); 390 bar.style.width = width + "%"; 391 392 if (elapsed >= total) { 393 clearInterval(progressTimer); 394 progressTimer = null; 395 } 396 }, 10); 397 } 398}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.