1// ç½é¡µå è½½å®æåæ§è¡ 2$(document).ready(function () { 3 const speechDom = $('#speech-component'); 4 const speechSearchIcon = $('#speech-search-icon'); 5 const mobileSpeechBox = $("#speech-search-icon-mobile"); 6 speechDom.hide(); 7 // å½é³ç¸å ³åé 8 let mediaRecorder; 9 let audioChunks = []; 10 let stream; 11 let currentInputDom; 12 let audioContext; 13 let analyser; 14 let dataArray; 15 let silentTimer; 16 let initialSilentTimer; // åå§éé³è®¡æ¶å¨ 17 let isSilent = true; 18 let hasSpeech = false; 19 const SPEECH_THRESHOLD = 0.05; // è¯é³éå¼ 20 const SILENT_TIMEOUT = 3000; // éé³è¶ æ¶æ¶é´ï¼æ¯«ç§ï¼ 21 const INITIAL_SILENT_TIMEOUT = 3000; // åå§3ç§æ è¯é³èªå¨å ³é 22 const speechText = { 23 listening: 'Escuchando...', 24 stopListening: 'Detener escucha', 25 serverNotAllow: 'La grabación de voz no es compatible en el entorno actual. Utilice Chrome o Edge con HTTPS.', 26 roleDenied: 'Se denegó el acceso al micrófono o el dispositivo no está disponible', 27 pendingRecognizion: 'Esperando reconocimiento...', 28 noSpeech: 'No se detectó sonido. Inténtelo de nuevo.', 29 microphoneError: 'Error del micrófono. Verifique la configuración de su navegador.', 30 }; 31 const website = 'sam_es'; 32 33 // æé®å ç´ 34 const listenBtn = document.getElementById('listenBtn'); 35 const resultDiv = document.getElementById('result'); 36 37 38 // ç§»å¨ç«¯æå¼è¯é³DOM 39 mobileSpeechBox.click(function () { 40 currentInputDom = 'mobile-search-box'; 41 openSearchDom(); 42 showSpeechDom(); 43 }); 44 45 // PC 端æå¼å½é³DOM 46 speechSearchIcon.click(function () { 47 currentInputDom = 'pc-search-box'; 48 showSpeechDom(); 49 }); 50 51 // è°ç¨header ä¸æå¼æç´¢æ¡ 52 function openSearchDom() { 53 $(".oc-main-sam-header-mobile .btn-wrapper .search-icon-wrapper").trigger("click"); 54 } 55 56 // å±ç¤ºè¯é³DOM 57 function showSpeechDom() { 58 speechDom.show(); 59 setTimeout(() => { 60 listenBtn.classList.add('recording'); 61 startListen(); 62 63 // å¯å¨åå§éé³è®¡æ¶å¨ï¼å¦æ1.5ç§å 没æè¯é³è¾å ¥åå ³é 64 initialSilentTimer = setTimeout(() => { 65 if (!hasSpeech) { 66 resultDiv.innerHTML = `<p>${speechText.noSpeech}</p>`; 67 setTimeout(() => { 68 closeSpeechDom(); 69 }, 1000); 70 } 71 }, INITIAL_SILENT_TIMEOUT); 72 }, 300); 73 } 74 75 // å¼å§å½é³ 76 async function startListen() { 77 // iOS/Safari å ¼å®¹å¤ç 78 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { 79 // æ§ç Safari æ¯æ 80 navigator.mediaDevices = navigator.mediaDevices || { 81 getUserMedia: function(constraints) { 82 const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 83 if (!getUserMedia) { 84 return Promise.reject(new Error('getUserMedia is not implemented in this browser')); 85 } 86 return new Promise(function(resolve, reject) { 87 getUserMedia.call(navigator, constraints, resolve, reject); 88 }); 89 } 90 }; 91 } 92 93 try { 94 stream = await navigator.mediaDevices.getUserMedia({ 95 audio: { 96 // iOS/Safari å ¼å®¹æ§è®¾ç½® 97 echoCancellation: true, 98 noiseSuppression: true, 99 autoGainControl: true 100 } 101 }); 102 103 // é ç½® MediaRecorder 104 const options = { 105 mimeType: 'audio/webm;codecs=opus' 106 }; 107 108 // iOS/Safari å¯è½ä¸æ¯æ webm æ ¼å¼
109 if (!MediaRecorder.isTypeSupported(options.mimeType)) { 110 const otherTypes = ['audio/ogg;codecs=opus', 'audio/webm', 'audio/ogg', 'audio/wav']; 111 for (const type of otherTypes) { 112 if (MediaRecorder.isTypeSupported(type)) { 113 options.mimeType = type; 114 break; 115 } 116 } 117 } 118 119 mediaRecorder = new MediaRecorder(stream, options); 120 audioChunks = []; 121 hasSpeech = false; 122 123 mediaRecorder.addEventListener('dataavailable', e => { 124 audioChunks.push(e.data); 125 }); 126 127 mediaRecorder.addEventListener('stop', () => { 128 const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType }); 129 sendAudioToServer(audioBlob); 130 }); 131 132 mediaRecorder.start(100); // 100ms çæ®µ 133 resultDiv.textContent = speechText.listening; 134 135 // åå§åé³é¢åæå¨ 136 initAudioAnalyzer(stream); 137 } catch (err) { 138 console.error('麦å é£è·å失败', err); 139 // iOS/Safari æéé误å¤ç 140 if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { 141 alert(speechText.roleDenied); 142 } else if (err.name === 'NotReadableError' || err.name === 'DeviceInUseError') { 143 alert(speechText.microphoneError); 144 } else { 145 alert(speechText.serverNotAllow); 146 } 147 } 148 } 149 150 // åå§åé³é¢åæå¨ 151 function initAudioAnalyzer(stream) { 152 try { 153 // å建é³é¢ä¸ä¸æï¼å ¼å®¹æ§çæµè§å¨ 154 audioContext = new (window.AudioContext || window.webkitAudioContext)(); 155 analyser = audioContext.createAnalyser(); 156 const source = audioContext.createMediaStreamSource(stream); 157 158 // é ç½®åæå¨ 159 analyser.fftSize = 256; 160 const bufferLength = analyser.frequencyBinCount; 161 dataArray = new Uint8Array(bufferLength); 162 163 // è¿æ¥é³é¢å¤çé¾ 164 source.connect(analyser); 165 166 // å¼å§åæé³é¢ 167 analyzeAudio(); 168 } catch (err) { 169 console.error('é³é¢åæå¨åå§å失败', err); 170 // 妿åæå¨åå§å失败ï¼ä»å 许å½é³ï¼ä½ä¸èªå¨åæ¢ 171 } 172 } 173 174 // åæé³é¢ï¼æ£æµéé³åè¯é³ 175 function analyzeAudio() { 176 if (!analyser) return; 177 178 analyser.getByteTimeDomainData(dataArray); 179 180 // 计ç®é³é¢è½é 181 let sum = 0; 182 for (let i = 0; i < dataArray.length; i++) { 183 const value = (dataArray[i] - 128) / 128; 184 sum += value * value; 185 } 186 const rms = Math.sqrt(sum / dataArray.length); 187 188 // 夿æ¯å¦ä¸ºè¯é³ 189 if (rms > SPEECH_THRESHOLD) { 190 isSilent = false; 191 hasSpeech = true; 192 clearTimeout(silentTimer); 193 // æ£æµå°è¯é³ï¼æ¸ é¤åå§éé³è®¡æ¶å¨ 194 clearTimeout(initialSilentTimer); 195 } else { 196 if (!isSilent) { 197 isSilent = true; 198 // å¯å¨éé³è®¡æ¶å¨ 199 silentTimer = setTimeout(() => { 200 if (mediaRecorder && mediaRecorder.state !== 'inactive' && hasSpeech) { 201 mediaRecorder.stop(); 202 } 203 }, SILENT_TIMEOUT); 204 } 205 } 206 207 // ç»§ç»åæ 208 requestAnimationFrame(analyzeAudio); 209 } 210 211 212 213 // 忢å½é³ 214 listenBtn.addEventListener('click', () => { 215 mediaRecorder.stop(); 216 stream.getTracks().forEach(track => track.stop()); 217 // æ¸ é¤ææè®¡æ¶å¨ 218 clearTimeout(silentTimer); 219 clearTimeout(initialSilentTimer); 220 resultDiv.textContent = speechText.pendingRecognizion; 221 });
222 223 // åéé³é¢å°æå¡å¨ 224 async function sendAudioToServer(audioBlob) { 225 // æ£æ¥æ¯å¦æè¯é³ 226 if (!hasSpeech || audioBlob.size < 1024) { // æä»¶å¤ªå°å¯è½æ¯éé³ 227 resultDiv.innerHTML = `<p>${speechText.noSpeech}</p>`; 228 setTimeout(() => { 229 closeSpeechDom(); 230 }, 3000); 231 return; 232 } 233 234 const formData = new FormData(); 235 const fileName = `recording_${Date.now()}.${getExtensionFromMimeType(mediaRecorder.mimeType)}`; 236 formData.append('audio', audioBlob, fileName); 237 formData.append('website', website); 238 239 try { 240 const response = await fetch(Apis.getSpeechText || 'https://sam.datasortingcenter.com/speech/recognize', { 241 method: 'POST', 242 body: formData 243 }); 244 245 const result = await response.json(); 246 if (response.ok) { 247 if (result.text) { 248 resultDiv.innerHTML = `<p>${result.text}</p>`; 249 setSearchValue(result.text, document.getElementById(currentInputDom)); 250 } else { 251 resultDiv.innerHTML = `<p>${speechText.noSpeech}</p>`; 252 } 253 } else { 254 resultDiv.innerHTML = ` 255 <h3>Error de reconocimiento:</h3> 256 <p>${result.message || 'Error desconocido'}</p> 257 `; 258 } 259 } catch (error) { 260 console.error('è¯é³è¯å«è¯·æ±å¤±è´¥', error); 261 resultDiv.innerHTML = `<p>Network error: ${error.message}</p>`; 262 } finally { 263 // æ¸ é¤è®¡æ¶å¨ 264 clearTimeout(silentTimer); 265 clearTimeout(initialSilentTimer); 266 silentTimer = null; 267 initialSilentTimer = null; 268 // å ³éè¯é³DOM 269 setTimeout(() => { 270 closeSpeechDom(); 271 }, 2000); 272 } 273 } 274 275 // æ ¹æ®MIMEç±»åè·åæä»¶æ©å±å 276 function getExtensionFromMimeType(mimeType) { 277 const mimeMap = { 278 'audio/webm': 'webm', 279 'audio/webm;codecs=opus': 'webm', 280 'audio/ogg': 'ogg', 281 'audio/ogg;codecs=opus': 'ogg', 282 'audio/wav': 'wav' 283 }; 284 return mimeMap[mimeType] || 'webm'; 285 } 286 287 // å ³éè¯é³DOM并忢å½é³ 288 function closeSpeechDom() { 289 speechDom.hide(); 290 291 // 忢åªä½å½å¶å¨ 292 if (mediaRecorder && mediaRecorder.state !== 'inactive') { 293 mediaRecorder.stop(); 294 } 295 296 // 忢é³é¢æµ 297 if (stream) { 298 stream.getTracks().forEach(track => track.stop()); 299 stream = null; 300 } 301 302 // æ¸ çé³é¢åæå¨ 303 if (analyser) { 304 analyser.disconnect(); 305 analyser = null; 306 } 307 308 // å ³éé³é¢ä¸ä¸æ 309 if (audioContext) { 310 try { 311 audioContext.close(); 312 } catch (err) { 313 console.error('é³é¢ä¸ä¸æå ³é失败', err); 314 } 315 audioContext = null; 316 } 317 318 // æ¸ é¤è®¡æ¶å¨å¹¶éç½®åé 319 clearTimeout(silentTimer); 320 clearTimeout(initialSilentTimer); 321 silentTimer = null; 322 initialSilentTimer = null; 323 324 // 夿æ¯å¦å·²ç»æ¶å¬å°äºè¯é³ï¼å¦ææ¶å°äº å°±ä¸å¨å ³éæç´¢æ ï¼å¦ææ²¡æå°±å ³éæç´¢æ 325 if (currentInputDom === 'mobile-search-box' && !hasSpeech) { 326 $(".oc-main-sam-header-mobile .oc-main-sam-header-input .close-icon-wrapper").trigger("click"); 327 } 328 329 // éç½®ç¶æ 330 audioChunks = []; 331 hasSpeech = false; 332 isSilent = true; 333 listenBtn.classList.remove('recording'); 334 resultDiv.textContent = ''; 335 } 336 337 // çå¬é®ç ESC é®ï¼å ³éè¯é³DOM 338 $(document).on('keydown', function (e) { 339 if (e.key === 'Escape') { 340 closeSpeechDom(); 341 } 342 });
343 344 345 // æ°æ®ååinput ï¼å¹¶è§¦ååçäºä»¶ 346 function setSearchValue(inputValue,inputDom) { 347 // 设置 DOM å¼ 348 inputDom.value = inputValue; 349 350 // 主å¨è§¦å input äºä»¶ï¼å ³é®ï¼ 351 inputDom.dispatchEvent( 352 new Event('input', { 353 bubbles: true, 354 cancelable: true 355 }) 356 ); 357 // å æ ç§»å ¥è¾å ¥æ¡ 358 inputDom.focus(); 359 } 360});
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.