-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaudio.js
More file actions
164 lines (144 loc) · 5.15 KB
/
Copy pathaudio.js
File metadata and controls
164 lines (144 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// audio.js - Multi-tier natural neural voice pronunciation manager
let currentAudioInstance = null;
// Ensure browser voices are pre-loaded into cache upon extension initialization
if (typeof window !== 'undefined' && window.speechSynthesis) {
window.speechSynthesis.getVoices();
if (window.speechSynthesis.onvoiceschanged !== undefined) {
window.speechSynthesis.onvoiceschanged = () => {
window.speechSynthesis.getVoices();
};
}
}
/**
* Immediately stop all active audio playback (both HTML5 Audio and Web Speech API)
*/
function stopAudio() {
if (currentAudioInstance) {
try {
currentAudioInstance.pause();
currentAudioInstance.currentTime = 0;
} catch (e) {}
currentAudioInstance = null;
}
if (typeof window !== 'undefined' && window.speechSynthesis) {
try {
window.speechSynthesis.cancel();
} catch (e) {}
}
}
/**
* Main entrance to play human-like natural voice pronunciation
*/
function playPronunciation(word, lang, dictAudioUrl) {
stopAudio();
const cleanText = (word || '').replace(/\s+/g, ' ').trim();
if (!cleanText) return;
// 1. For paragraph/long text (>180 chars), use local Web Speech API (handles paragraphs natively)
if (cleanText.length > 180) {
speakWordLocal(cleanText, lang);
return;
}
// 2. Try playing Free Dictionary API direct human recording (MP3) if available
if (lang === 'en' && dictAudioUrl) {
currentAudioInstance = new Audio(dictAudioUrl);
currentAudioInstance.play().then(() => {
console.log('Played Free Dictionary human audio');
}).catch(err => {
console.warn('Free Dictionary audio playback failed, falling back to Neural TTS:', err);
playGoogleNeuralTts(cleanText, lang);
});
return;
}
// 3. Play Google Translate HD Neural TTS (high-definition natural voice)
playGoogleNeuralTts(cleanText, lang);
}
/**
* Play Google Neural Wavenet TTS (24kHz HD Natural Voice)
*/
function playGoogleNeuralTts(word, lang) {
const ttsLang = lang === 'en' ? 'en' : 'fr';
// Use client=gtx endpoint for high-definition Wavenet neural voice
const ttsUrl = `https://translate.google.com/translate_tts?ie=UTF-8&tl=${ttsLang}&client=gtx&q=${encodeURIComponent(word)}`;
currentAudioInstance = new Audio(ttsUrl);
currentAudioInstance.play().then(() => {
console.log('Played Google HD Neural TTS audio');
}).catch(err => {
console.warn('Google Neural TTS playback failed, falling back to Web Speech API:', err);
speakWordLocal(word, lang);
});
}
/**
* Maps language codes (en, fr, es, de, ja, ko, it, pt, zh-TW, zh-CN) to SpeechVoice locales
*/
function getSpeechVoiceLocale(lang) {
const map = {
'en': 'en-US',
'fr': 'fr-FR',
'es': 'es-ES',
'de': 'de-DE',
'ja': 'ja-JP',
'ko': 'ko-KR',
'it': 'it-IT',
'pt': 'pt-PT',
'zh-TW': 'zh-TW',
'zh-CN': 'zh-CN',
'zh': 'zh-TW'
};
return map[lang] || `${lang}-${lang.toUpperCase()}`;
}
/**
* Maps language codes to UI display segment labels
*/
function getLanguageLabel(code) {
const labels = {
'en': 'EN',
'fr': 'FR',
'es': 'ES',
'de': 'DE',
'ja': 'JA',
'ko': 'KO',
'it': 'IT',
'pt': 'PT',
'zh-TW': '繁中',
'zh-CN': '簡中',
'zh': '繁中'
};
return labels[code] || (code ? code.toUpperCase() : 'EN');
}
/**
* Native SpeechSynthesis Local Voice Fallback
*/
function speakWordLocal(text, lang) {
if (typeof window === 'undefined' || !window.speechSynthesis) return;
const utterance = new SpeechSynthesisUtterance(text);
const langPrefix = lang.split('-')[0];
utterance.lang = getSpeechVoiceLocale(lang);
const voices = window.speechSynthesis.getVoices();
const matchingVoices = voices.filter(v => v.lang.startsWith(langPrefix));
if (matchingVoices.length > 0) {
// Score voices by quality tier to pick the most natural human-like voice
const scoredVoices = matchingVoices.map(voice => {
const name = voice.name.toLowerCase();
let score = 0;
// Tier 1: Modern Neural / Wavenet / Premium / Enhanced / Natural online voices
if (name.includes('google') || name.includes('natural') || name.includes('neural') || name.includes('enhanced') || name.includes('premium') || name.includes('online') || name.includes('wavenet')) {
score += 100;
}
// Tier 2: Siri & Premium Human Voice Models
if (name.includes('siri') || name.includes('samantha') || name.includes('thomas') || name.includes('amélie') || name.includes('amelie') || name.includes('audrey') || name.includes('daniel') || name.includes('karen')) {
score += 50;
}
// Penalty: Known legacy robotic synthesizer voices
if (name.includes('alex') || name.includes('fred') || name.includes('zarvox') || name.includes('albert') || name.includes('boing') || name.includes('cellos') || name.includes('pipe organ')) {
score -= 200;
}
return { voice, score };
});
// Sort descending by score
scoredVoices.sort((a, b) => b.score - a.score);
utterance.voice = scoredVoices[0].voice;
}
utterance.rate = 0.92; // Natural human speech tempo (slightly relaxed)
utterance.pitch = 1.0; // Natural pitch
window.speechSynthesis.speak(utterance);
}