Library
🎬 Video
B2
Bruce Lee Interview
Bruce Lee Interview
李小龍訪問
Playing
Transcript
JP
EN
/** * Cantonese.hk — Audio Helper * Uses POST to /api/audio.php to bypass Cloudflare CDN cache. * Converts response to Blob URL for playback. * * Exposes: * window.cantoAudio — the shared Audio element (set once created) * window.cantoSpeak(text, btn, rate) * window.cantoSpeakWord(wordId, fallbackText, btn) * window.cantoStop() * * Dispatches on document: * 'cantoplaystart' — when audio.play() is called ({ detail: { text } }) * 'cantoplayend' — when audio ends or errors */ (function() { 'use strict'; var audioEl = null; var currentBtn = null; var currentBlob = null; function getAudio() { if (!audioEl) { audioEl = new Audio(); window.cantoAudio = audioEl; // expose for external karaoke hooks } return audioEl; } function revokeBlob() { if (currentBlob) { URL.revokeObjectURL(currentBlob); currentBlob = null; } } function clearPlaying() { if (currentBtn) { currentBtn.classList.remove('playing'); currentBtn = null; } } function dispatchEnd() { document.dispatchEvent(new CustomEvent('cantoplayend')); } window.cantoStop = function() { clearPlaying(); if (audioEl) { audioEl.pause(); audioEl.src = ''; } revokeBlob(); if ('speechSynthesis' in window) window.speechSynthesis.cancel(); dispatchEnd(); }; // POST text to audio.php, play response as blob window.cantoSpeak = function(text, btn, rate) { if (!text) return; clearPlaying(); if (audioEl) { audioEl.pause(); audioEl.src = ''; } revokeBlob(); if ('speechSynthesis' in window) window.speechSynthesis.cancel(); if (btn) { currentBtn = btn; btn.classList.add('playing'); } var fd = new FormData(); fd.append('action', 'speak'); fd.append('text', text); fetch('/api/audio.php', {method: 'POST', body: fd}) .then(function(r) { if (r.status === 204 || !r.ok) throw new Error('no audio'); return r.blob(); }) .then(function(blob) { var url = URL.createObjectURL(blob); currentBlob = url; var audio = getAudio(); audio.onended = function() { clearPlaying(); revokeBlob(); dispatchEnd(); }; audio.onerror = function() { clearPlaying(); revokeBlob(); dispatchEnd(); }; audio.src = url; // Fire cantoplaystart so readers can attach timeupdate karaoke document.dispatchEvent(new CustomEvent('cantoplaystart', { detail: { text: text, audio: audio } })); return audio.play(); }) .catch(function() { fallbackWebSpeech(text, rate); }); }; function fallbackWebSpeech(text, rate) { if (!('speechSynthesis' in window)) { clearPlaying(); return; } var u = new SpeechSynthesisUtterance(text); u.lang = 'zh-HK'; u.rate = rate || 0.8; var voices = window.speechSynthesis.getVoices(); for (var i = 0; i < voices.length; i++) { var vl = voices[i].lang.toLowerCase(); if (vl === 'zh-hk' || vl === 'yue-hk' || vl.indexOf('yue') !== -1 || vl === 'zh-tw') { u.voice = voices[i]; break; } } u.onend = function() { clearPlaying(); dispatchEnd(); }; u.onerror = function() { clearPlaying(); dispatchEnd(); }; window.speechSynthesis.speak(u); } if ('speechSynthesis' in window) { window.speechSynthesis.getVoices(); window.speechSynthesis.onvoiceschanged = function() { window.speechSynthesis.getVoices(); }; } window.cantoSpeakWord = function(wordId, fallbackText, btn) { if (!wordId) { cantoSpeak(fallbackText, btn); return; } var xhr = new XMLHttpRequest(); xhr.open('GET', '/api/audio.php?action=word&id=' + encodeURIComponent(wordId), true); xhr.timeout = 2000; xhr.onload = function() { if (xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); cantoSpeak(data.text || fallbackText, btn); } catch(e) { cantoSpeak(fallbackText, btn); } } else { cantoSpeak(fallbackText, btn); } }; xhr.onerror = function() { cantoSpeak(fallbackText, btn); }; xhr.ontimeout = function() { cantoSpeak(fallbackText, btn); }; xhr.send(); }; document.addEventListener('DOMContentLoaded', function() { document.addEventListener('click', function(e) { var btn = e.target.closest('.audio-play'); if (!btn) return; e.preventDefault(); e.stopPropagation(); if (typeof window.stopPlayAll === 'function') window.stopPlayAll(); var text = btn.getAttribute('data-text'); var wordId = btn.getAttribute('data-word-id'); if (wordId) { cantoSpeakWord(wordId, text, btn); } else if (text) { cantoSpeak(text, btn); } }); }); })();
📚 Learning Tools
✕
🔍 Lookup
📚 Saved
🃏 Cards
→
✍️
Search any word above.
While reading, click a word to look it up here.
Learning
Tools