Add TTS and pronunciation display to vocabulary games
Implement click-to-speak functionality with visual pronunciation feedback in QuizGame and FlashcardLearning. When users click on vocabulary options or answers, the system plays native language audio (e.g., Chinese) and highlights the pronunciation (pinyin) with animation. Features: - TTS uses chapter language (zh-CN, en-US, etc.) for correct pronunciation - Pronunciation text displayed under each quiz option - Click on answer triggers TTS + 2s highlight animation - Hover effects on clickable elements - Auto-detect and use matching voice from speechSynthesis API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
838c8289b8
commit
7a18e27a44
@ -1364,19 +1364,22 @@ class FlashcardLearning extends Module {
|
||||
">
|
||||
${this._currentCard.displayFront}
|
||||
</div>
|
||||
<div style="
|
||||
<div id="answer-tts" style="
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||
">
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
" title="Click to hear pronunciation">
|
||||
${this._currentCard.displayBack}
|
||||
</div>
|
||||
<div style="
|
||||
<div id="pronunciation-display" style="
|
||||
font-size: 18px;
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
transition: all 0.3s ease;
|
||||
">
|
||||
${this._currentCard.pronunciation || ''}
|
||||
</div>
|
||||
@ -1388,6 +1391,23 @@ class FlashcardLearning extends Module {
|
||||
|
||||
this._isRevealed = true;
|
||||
|
||||
// Add click listener on answer for TTS
|
||||
const answerTTS = document.getElementById('answer-tts');
|
||||
if (answerTTS) {
|
||||
answerTTS.addEventListener('click', () => {
|
||||
this._playAudio(this._currentCard.front);
|
||||
this._highlightPronunciation();
|
||||
});
|
||||
answerTTS.addEventListener('mouseenter', () => {
|
||||
answerTTS.style.transform = 'scale(1.05)';
|
||||
answerTTS.style.color = '#6dd5fa';
|
||||
});
|
||||
answerTTS.addEventListener('mouseleave', () => {
|
||||
answerTTS.style.transform = 'scale(1)';
|
||||
answerTTS.style.color = 'white';
|
||||
});
|
||||
}
|
||||
|
||||
// Add difficulty buttons in game-controls section
|
||||
const gameControls = document.querySelector('.game-controls');
|
||||
if (gameControls) {
|
||||
@ -1512,6 +1532,7 @@ class FlashcardLearning extends Module {
|
||||
// Always play audio for pronunciation, regardless of mode
|
||||
setTimeout(() => {
|
||||
this._playAudio(this._currentCard.front);
|
||||
this._highlightPronunciation();
|
||||
}, 200);
|
||||
}, 150);
|
||||
}
|
||||
@ -1712,24 +1733,58 @@ class FlashcardLearning extends Module {
|
||||
speechSynthesis.cancel();
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
|
||||
// Get language from chapter content, fallback to en-US
|
||||
const chapterLanguage = this._content?.language || 'en-US';
|
||||
utterance.lang = chapterLanguage;
|
||||
utterance.rate = 0.8;
|
||||
utterance.pitch = 1.0;
|
||||
utterance.volume = 1.0;
|
||||
|
||||
// Try to use a good English voice
|
||||
// Try to find a suitable voice for the language
|
||||
const voices = speechSynthesis.getVoices();
|
||||
const englishVoice = voices.find(voice =>
|
||||
voice.lang.startsWith('en') && voice.name.includes('Neural')
|
||||
) || voices.find(voice => voice.lang.startsWith('en'));
|
||||
if (voices.length > 0) {
|
||||
// Find voice matching the chapter language
|
||||
const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN"
|
||||
const matchingVoice = voices.find(voice =>
|
||||
voice.lang.startsWith(langPrefix) && (voice.name.includes('Neural') || voice.default)
|
||||
) || voices.find(voice => voice.lang.startsWith(langPrefix));
|
||||
|
||||
if (englishVoice) {
|
||||
utterance.voice = englishVoice;
|
||||
if (matchingVoice) {
|
||||
utterance.voice = matchingVoice;
|
||||
console.log('🔊 Using voice:', matchingVoice.name, matchingVoice.lang);
|
||||
}
|
||||
}
|
||||
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
|
||||
_highlightPronunciation() {
|
||||
// Highlight pronunciation when TTS is played
|
||||
const pronunciation = document.getElementById('pronunciation-display');
|
||||
|
||||
if (pronunciation) {
|
||||
// Store original styles
|
||||
const originalColor = pronunciation.style.color;
|
||||
const originalFontSize = pronunciation.style.fontSize;
|
||||
|
||||
// Add highlight
|
||||
pronunciation.style.color = '#6dd5fa';
|
||||
pronunciation.style.fontWeight = 'bold';
|
||||
pronunciation.style.fontSize = '22px';
|
||||
pronunciation.style.transform = 'scale(1.1)';
|
||||
|
||||
// Remove highlight after animation
|
||||
setTimeout(() => {
|
||||
pronunciation.style.color = originalColor;
|
||||
pronunciation.style.fontWeight = 'normal';
|
||||
pronunciation.style.fontSize = originalFontSize;
|
||||
pronunciation.style.transform = 'scale(1)';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
_generatePronunciation(word) {
|
||||
if (!word || typeof word !== 'string') return '';
|
||||
|
||||
|
||||
@ -190,7 +190,8 @@ class QuizGame extends Module {
|
||||
vocabulary.push({
|
||||
english: word,
|
||||
translation: data.user_language,
|
||||
type: data.type || 'unknown'
|
||||
type: data.type || 'unknown',
|
||||
pronunciation: data.pronunciation || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -396,6 +397,14 @@ class QuizGame extends Module {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.option-pronunciation {
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
color: #6c757d;
|
||||
margin-top: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.quiz-feedback {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
@ -692,9 +701,19 @@ class QuizGame extends Module {
|
||||
? vocab.translation
|
||||
: vocab.english;
|
||||
|
||||
// Store the Chinese word and pronunciation for TTS
|
||||
const chineseWord = vocab.english; // In our data structure, english is the Chinese word
|
||||
const pronunciation = vocab.pronunciation || '';
|
||||
|
||||
return `
|
||||
<div class="quiz-option" data-option="${index}" data-value="${optionText}">
|
||||
<div class="quiz-option"
|
||||
data-option="${index}"
|
||||
data-value="${optionText}"
|
||||
data-word="${chineseWord}"
|
||||
data-pronunciation="${pronunciation}"
|
||||
title="Click to hear pronunciation">
|
||||
${optionText}
|
||||
${pronunciation ? `<div class="option-pronunciation">[${pronunciation}]</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@ -765,6 +784,16 @@ class QuizGame extends Module {
|
||||
return;
|
||||
}
|
||||
|
||||
// Play TTS when clicking on an option
|
||||
const word = optionElement.dataset.word;
|
||||
const pronunciation = optionElement.dataset.pronunciation;
|
||||
if (word) {
|
||||
this._playAudio(word);
|
||||
if (pronunciation) {
|
||||
this._highlightPronunciation(optionElement);
|
||||
}
|
||||
}
|
||||
|
||||
this._isAnswering = true;
|
||||
const question = this._questions[this._currentQuestion];
|
||||
const selectedAnswer = optionElement.dataset.value;
|
||||
@ -962,6 +991,62 @@ class QuizGame extends Module {
|
||||
this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name);
|
||||
}
|
||||
|
||||
_playAudio(text) {
|
||||
if ('speechSynthesis' in window) {
|
||||
// Cancel any ongoing speech
|
||||
speechSynthesis.cancel();
|
||||
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
|
||||
// Get language from chapter content, fallback to en-US
|
||||
const chapterLanguage = this._content?.language || 'en-US';
|
||||
utterance.lang = chapterLanguage;
|
||||
utterance.rate = 0.8;
|
||||
utterance.pitch = 1.0;
|
||||
utterance.volume = 1.0;
|
||||
|
||||
// Try to find a suitable voice for the language
|
||||
const voices = speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
// Find voice matching the chapter language
|
||||
const langPrefix = chapterLanguage.split('-')[0]; // e.g., "zh" from "zh-CN"
|
||||
const matchingVoice = voices.find(voice =>
|
||||
voice.lang.startsWith(langPrefix) && (voice.name.includes('Neural') || voice.default)
|
||||
) || voices.find(voice => voice.lang.startsWith(langPrefix));
|
||||
|
||||
if (matchingVoice) {
|
||||
utterance.voice = matchingVoice;
|
||||
console.log('🔊 Using voice:', matchingVoice.name, matchingVoice.lang);
|
||||
}
|
||||
}
|
||||
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
|
||||
_highlightPronunciation(optionElement) {
|
||||
const pronunciation = optionElement.querySelector('.option-pronunciation');
|
||||
|
||||
if (pronunciation) {
|
||||
// Store original styles
|
||||
const originalColor = pronunciation.style.color;
|
||||
const originalFontWeight = pronunciation.style.fontWeight;
|
||||
|
||||
// Add highlight
|
||||
pronunciation.style.color = '#007bff';
|
||||
pronunciation.style.fontWeight = 'bold';
|
||||
pronunciation.style.transform = 'scale(1.2)';
|
||||
pronunciation.style.transition = 'all 0.3s ease';
|
||||
|
||||
// Remove highlight after animation
|
||||
setTimeout(() => {
|
||||
pronunciation.style.color = originalColor;
|
||||
pronunciation.style.fontWeight = originalFontWeight;
|
||||
pronunciation.style.transform = 'scale(1)';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
_showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) {
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'victory-popup';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user