PageSourceSearch

https://www.samaterials.es/js/speech.js?v=1789002534

js samaterials.es collected 2026-10-02 20:45:22 UTC 12,840 bytes, 360 lines download raw bytes

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.