|
10 | 10 | let pronounceIndex = 0; |
11 | 11 | let quizState = null; |
12 | 12 | let chineseVoice = null; |
13 | | - let currentSpeechAudio = null; |
14 | 13 |
|
15 | | - function stopSpeechAudio() { |
16 | | - if (currentSpeechAudio) { |
17 | | - currentSpeechAudio.pause(); |
18 | | - currentSpeechAudio = null; |
19 | | - } |
20 | | - } |
21 | | - |
22 | | - function speechPlaybackRate(rate) { |
23 | | - return rate >= 0.55 ? 0.85 : 0.65; |
24 | | - } |
25 | | - |
26 | | - function getGoogleTtsUrls(text) { |
27 | | - const q = encodeURIComponent(text); |
28 | | - return [ |
29 | | - `https://translate.google.com/translate_tts?ie=UTF-8&client=gtx&sl=zh-CN&tl=zh-CN&q=${q}`, |
30 | | - `https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&sl=zh-CN&tl=zh-CN&q=${q}`, |
31 | | - `https://translate.google.com/translate_tts?ie=UTF-8&client=gtx&tl=zh-CN&q=${q}` |
32 | | - ]; |
33 | | - } |
34 | | - |
35 | | - function playAudioUrl(url, rate) { |
36 | | - return new Promise((resolve, reject) => { |
37 | | - const audio = new Audio(url); |
38 | | - audio.playbackRate = speechPlaybackRate(rate); |
39 | | - currentSpeechAudio = audio; |
40 | | - audio.onended = resolve; |
41 | | - audio.onerror = reject; |
42 | | - audio.play().catch(reject); |
43 | | - }); |
44 | | - } |
| 14 | + function speak(text, rate = DEFAULT_SPEECH_RATE) { |
| 15 | + if (!('speechSynthesis' in window)) return; |
45 | 16 |
|
46 | | - async function playGoogleTts(text, rate = DEFAULT_SPEECH_RATE) { |
47 | 17 | speechSynthesis.cancel(); |
48 | | - stopSpeechAudio(); |
49 | | - |
50 | | - const urls = getGoogleTtsUrls(text); |
51 | | - for (const url of urls) { |
52 | | - try { |
53 | | - await playAudioUrl(url, rate); |
54 | | - return; |
55 | | - } catch { |
56 | | - stopSpeechAudio(); |
57 | | - } |
58 | | - } |
59 | 18 |
|
60 | | - throw new Error('Google TTS failed'); |
61 | | - } |
62 | | - |
63 | | - function speakWithSynth(text, rate = DEFAULT_SPEECH_RATE) { |
64 | | - return new Promise((resolve, reject) => { |
65 | | - if (!('speechSynthesis' in window)) { |
66 | | - reject(new Error('No speech synthesis')); |
67 | | - return; |
68 | | - } |
69 | | - |
70 | | - stopSpeechAudio(); |
71 | | - speechSynthesis.cancel(); |
72 | | - |
73 | | - const utterance = new SpeechSynthesisUtterance(text); |
74 | | - utterance.lang = 'zh-CN'; |
75 | | - utterance.rate = rate; |
76 | | - const voice = getChineseVoice(); |
77 | | - if (voice) utterance.voice = voice; |
78 | | - utterance.onend = () => resolve(); |
79 | | - utterance.onerror = () => reject(new Error('Speech synthesis failed')); |
80 | | - speechSynthesis.speak(utterance); |
81 | | - }); |
82 | | - } |
83 | | - |
84 | | - async function speak(text, rate = DEFAULT_SPEECH_RATE) { |
85 | | - try { |
86 | | - await speakWithSynth(text, rate); |
87 | | - } catch { |
88 | | - try { |
89 | | - await playGoogleTts(text, rate); |
90 | | - } catch { |
91 | | - alert('Không phát được âm thanh. Kiểm tra mạng và thử lại.'); |
92 | | - } |
93 | | - } |
| 19 | + const utterance = new SpeechSynthesisUtterance(text); |
| 20 | + utterance.lang = 'zh-CN'; |
| 21 | + utterance.rate = rate; |
| 22 | + const voice = getChineseVoice(); |
| 23 | + if (voice) utterance.voice = voice; |
| 24 | + speechSynthesis.speak(utterance); |
94 | 25 | } |
95 | 26 |
|
96 | 27 | function initSpeech() { |
|
0 commit comments