Skip to content

Commit 0c26d96

Browse files
feat: slower Mandarin TTS, add 庞立亮 vocabulary
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 70bb1d9 commit 0c26d96

3 files changed

Lines changed: 80 additions & 17 deletions

File tree

index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ <h1>Học Tiếng Trung</h1>
2222
</div>
2323
</div>
2424
<div class="progress-badge">
25-
<span id="learned-count">0</span> / 150 từ
25+
<span id="learned-count">0</span> / <span id="vocab-total">151</span> từ
2626
</div>
2727
</div>
2828
</header>
@@ -51,8 +51,8 @@ <h2>Xin chào! 你好!</h2>
5151
<div class="feature-grid">
5252
<div class="feature-item">
5353
<span>📖</span>
54-
<strong>150 từ HSK1</strong>
55-
<small>Có nghĩa tiếng Việt</small>
54+
<strong>151 từ</strong>
55+
<small>HSK1 + từ đặc biệt</small>
5656
</div>
5757
<div class="feature-item">
5858
<span>🔊</span>
@@ -96,7 +96,7 @@ <h2>🎤 Luyện phát âm</h2>
9696
</div>
9797
<div class="pronounce-card">
9898
<div class="pronounce-progress">
99-
Từ <span id="pronounce-index">1</span> / 150
99+
Từ <span id="pronounce-index">1</span> / <span id="pronounce-total">151</span>
100100
</div>
101101
<div class="pronounce-hanzi" id="pronounce-hanzi">你好</div>
102102
<div class="pronounce-pinyin" id="pronounce-pinyin">nǐ hǎo</div>

js/app.js

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,95 @@
33
'use strict';
44

55
const STORAGE_KEY = 'hsk1-learned-words';
6+
const DEFAULT_SPEECH_RATE = 0.65;
7+
const SLOW_SPEECH_RATE = 0.45;
8+
const MANDARIN_TTS = text =>
9+
`https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(text)}&type=1`;
10+
611
let learnedWords = loadLearned();
712
let pronounceIndex = 0;
813
let quizState = null;
914
let chineseVoice = null;
15+
let currentAudio = null;
1016

1117
// --- Speech ---
18+
function pickMandarinVoice(voices) {
19+
const list = voices.filter(v => {
20+
const lang = (v.lang || '').toLowerCase();
21+
if (lang === 'zh-cn') return true;
22+
if (!lang.startsWith('zh')) return false;
23+
return !/(tw|hk|mo)/.test(lang);
24+
});
25+
26+
const prefer = patterns =>
27+
patterns.reduce((found, pattern) => {
28+
if (found) return found;
29+
return list.find(v => pattern.test(v.name) || pattern.test(v.lang));
30+
}, null);
31+
32+
return (
33+
prefer([
34+
/|mandarin/i,
35+
/huihui|yaoyao|kangkang|tingting|yunxi|xiaoxiao|xiaoyi/i,
36+
/google.*|chinese.*china|zh-cn/i
37+
]) ||
38+
list.find(v => v.lang === 'zh-CN') ||
39+
list[0] ||
40+
null
41+
);
42+
}
43+
1244
function initSpeech() {
1345
if (!('speechSynthesis' in window)) return;
1446

1547
const loadVoices = () => {
16-
const voices = speechSynthesis.getVoices();
17-
chineseVoice =
18-
voices.find(v => v.lang === 'zh-CN') ||
19-
voices.find(v => v.lang.startsWith('zh')) ||
20-
null;
48+
chineseVoice = pickMandarinVoice(speechSynthesis.getVoices());
2149
};
2250

2351
loadVoices();
2452
speechSynthesis.onvoiceschanged = loadVoices;
2553
}
2654

27-
function speak(text, rate = 0.85) {
28-
if (!('speechSynthesis' in window)) {
29-
alert('Trình duyệt không hỗ trợ phát âm. Hãy thử Chrome hoặc Edge.');
30-
return;
31-
}
55+
function rateToPlayback(speechRate) {
56+
return speechRate >= 0.55 ? 0.82 : 0.62;
57+
}
58+
59+
function speakWithSynth(text, rate) {
60+
if (!('speechSynthesis' in window)) return null;
3261

3362
speechSynthesis.cancel();
3463
const utterance = new SpeechSynthesisUtterance(text);
3564
utterance.lang = 'zh-CN';
3665
utterance.rate = rate;
66+
utterance.pitch = 1;
67+
chineseVoice = pickMandarinVoice(speechSynthesis.getVoices()) || chineseVoice;
3768
if (chineseVoice) utterance.voice = chineseVoice;
3869
speechSynthesis.speak(utterance);
3970
return utterance;
4071
}
4172

73+
function speak(text, rate = DEFAULT_SPEECH_RATE) {
74+
speechSynthesis.cancel();
75+
if (currentAudio) {
76+
currentAudio.pause();
77+
currentAudio = null;
78+
}
79+
80+
const audio = new Audio(MANDARIN_TTS(text));
81+
audio.playbackRate = rateToPlayback(rate);
82+
currentAudio = audio;
83+
84+
audio.play().catch(() => {
85+
if (!('speechSynthesis' in window)) {
86+
alert('Trình duyệt không hỗ trợ phát âm. Hãy thử Chrome hoặc Edge.');
87+
return;
88+
}
89+
speakWithSynth(text, rate);
90+
});
91+
92+
return audio;
93+
}
94+
4295
function flashAudioBtn(btn) {
4396
if (!btn) return;
4497
btn.classList.add('playing');
@@ -160,12 +213,12 @@
160213
updatePronounceCard();
161214

162215
document.getElementById('btn-listen')?.addEventListener('click', e => {
163-
speak(HSK1_VOCABULARY[pronounceIndex].hanzi, 0.85);
216+
speak(HSK1_VOCABULARY[pronounceIndex].hanzi, DEFAULT_SPEECH_RATE);
164217
flashAudioBtn(e.currentTarget);
165218
});
166219

167220
document.getElementById('btn-slow')?.addEventListener('click', e => {
168-
speak(HSK1_VOCABULARY[pronounceIndex].hanzi, 0.55);
221+
speak(HSK1_VOCABULARY[pronounceIndex].hanzi, SLOW_SPEECH_RATE);
169222
flashAudioBtn(e.currentTarget);
170223
});
171224

@@ -303,6 +356,14 @@
303356
}
304357

305358
// --- Init ---
359+
function updateVocabTotal() {
360+
const total = HSK1_VOCABULARY.length;
361+
const vocabTotal = document.getElementById('vocab-total');
362+
const pronounceTotal = document.getElementById('pronounce-total');
363+
if (vocabTotal) vocabTotal.textContent = total;
364+
if (pronounceTotal) pronounceTotal.textContent = total;
365+
}
366+
306367
function init() {
307368
initSpeech();
308369
setupNavigation();
@@ -311,6 +372,7 @@
311372
setupPronounce();
312373
setupQuiz();
313374
updateProgressBadge();
375+
updateVocabTotal();
314376
if (window.HSK1Surprise) window.HSK1Surprise.init();
315377
}
316378

js/vocabulary.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,6 @@ const HSK1_VOCABULARY = [
149149
{ hanzi: "字", pinyin: "zì", vietnamese: "chữ" },
150150
{ hanzi: "昨天", pinyin: "zuótiān", vietnamese: "hôm qua" },
151151
{ hanzi: "做", pinyin: "zuò", vietnamese: "làm" },
152-
{ hanzi: "坐", pinyin: "zuò", vietnamese: "ngồi" }
152+
{ hanzi: "坐", pinyin: "zuò", vietnamese: "ngồi" },
153+
{ hanzi: "庞立亮", pinyin: "Páng Lìliàng", vietnamese: "Pang Liliang (người tặng quà)" }
153154
];

0 commit comments

Comments
 (0)