PageSourceSearch

https://himachal.gov.in/MyAssets/js/voice-search.js

js himachal.gov.in collected 2026-09-25 07:53:21 UTC 14,656 bytes, 398 lines download raw bytes

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.