diff --git a/frontend/src/components/korea/OpsGuideModal.tsx b/frontend/src/components/korea/OpsGuideModal.tsx index 559a763..fd0fd58 100644 --- a/frontend/src/components/korea/OpsGuideModal.tsx +++ b/frontend/src/components/korea/OpsGuideModal.tsx @@ -148,15 +148,26 @@ export function OpsGuideModal({ ships, onClose, onFlyTo, onRouteSelect }: Props) window.speechSynthesis.cancel(); const utter = new SpeechSynthesisUtterance(text); utter.lang = 'zh-CN'; - utter.rate = 0.85; + utter.rate = 0.8; + utter.pitch = 1.0; utter.volume = 1; - // Try to find a Chinese voice const voices = window.speechSynthesis.getVoices(); - const zhVoice = voices.find(v => v.lang.startsWith('zh')) || voices.find(v => v.lang.includes('CN')); + const zhVoice = voices.find(v => v.lang === 'zh-CN') || voices.find(v => v.lang.startsWith('zh')); if (zhVoice) utter.voice = zhVoice; - utter.onstart = () => setSpeakingIdx(idx); - utter.onend = () => setSpeakingIdx(null); - utter.onerror = () => setSpeakingIdx(null); + // Chrome bug workaround: pause/resume keepalive to prevent cutoff + let keepAlive: ReturnType | null = null; + utter.onstart = () => { + setSpeakingIdx(idx); + keepAlive = setInterval(() => { + if (window.speechSynthesis.speaking && !window.speechSynthesis.paused) { + window.speechSynthesis.pause(); + window.speechSynthesis.resume(); + } + }, 5000); + }; + const cleanup = () => { setSpeakingIdx(null); if (keepAlive) clearInterval(keepAlive); }; + utter.onend = cleanup; + utter.onerror = cleanup; window.speechSynthesis.speak(utter); }, []);