1/** 2 * jWriter Text-to-Speechæ©è½ 3 * Chrome, Edge, Safari対å¿ç 4 * 5 * ãããã°ã¢ã¼ã: TTS_DEBUG_MODE = true; ããã©ã¦ã¶ã³ã³ã½ã¼ã«ã§å®è¡ããã¨æå¹å 6 * Debug mode: Run TTS_DEBUG_MODE = true; in browser console to enable 7 */ 8 9// ãããã°ã¢ã¼ãè¨å®ï¼ãããã°ãå¿ è¦ãªå ´åã¯trueã«è¨å®ï¼ 10var TTS_DEBUG_MODE = false; 11 12// ãããã°ãã°é¢æ° 13function ttsLog(message) { 14 if (TTS_DEBUG_MODE) { 15 console.log(message); 16 } 17} 18 19// ã°ãã¼ãã«å¤æ° 20var ifSpeakable = false; 21var isSpeaking = false; 22// ã¦ã¼ã¶ã«ãã忢ãã©ã°ï¼cancelæã®ã¨ã©ã¼ãã³ããªã³ã°æå¶ç¨ï¼ 23var userCancelled = false; 24var ttsVoices = []; 25var selectedVoice = null; 26var currentUtterance = null; 27 28// ãã©ã¦ã¶å¤å® 29var browserInfo = { 30 isChrome: /Chrome/.test(navigator.userAgent) && !/Edge|Edg/.test(navigator.userAgent), 31 isEdge: /Edge|Edg/.test(navigator.userAgent), 32 isSafari: /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent), 33 isFirefox: /Firefox/.test(navigator.userAgent) 34}; 35 36// åæåãã©ã° 37var ttsInitialized = false; 38// ãªã¢ã¼ãé³å£°è¦å表示ãã©ã°
39var remoteVoiceWarningShown = false; 40 41/** 42 * TTSåæå 43 */ 44function initTTS() { 45 ttsLog('[TTS] Initializing TTS system...'); 46 ttsLog('[TTS] Browser: ' + JSON.stringify(browserInfo)); 47 48 // Speech Synthesis API ã®å¯ç¨æ§ãã§ã㯠49 if (!window.speechSynthesis) { 50 console.error('[TTS] Speech Synthesis API not supported'); 51 return false; 52 } 53 54 ifSpeakable = true; 55 56 // é³å£°ãªã¹ãã®èªã¿è¾¼ã¿ 57 loadVoices(); 58 59 // é³å£°ãªã¹ã夿´ã¤ãã³ã 60 if (window.speechSynthesis.onvoiceschanged !== undefined) { 61 window.speechSynthesis.onvoiceschanged = loadVoices; 62 } 63 64 ttsLog('[TTS] TTS system initialized'); 65 return true; 66} 67 68/** 69 * é³å£°ãªã¹ãã®èªã¿è¾¼ã¿ 70 */ 71function loadVoices() { 72 ttsVoices = window.speechSynthesis.getVoices(); 73 ttsLog('[TTS] Total voices loaded: ' + ttsVoices.length); 74 75 if (ttsVoices.length > 0) { 76 selectBestVoice(); 77 ttsInitialized = true; 78 // æ¥æ¬èªé³å£°ãªã¹ããUIã«åæ 79 populateVoiceList(); 80 } 81} 82 83/** 84 * æé©ãªæ¥æ¬èªé³å£°ã鏿 85 */ 86function selectBestVoice() { 87 // æ¥æ¬èªé³å£°ããã£ã«ã¿ãªã³ã° 88 var japaneseVoices = ttsVoices.filter(function(voice) { 89 return voice.lang === 'ja-JP' || voice.lang.startsWith('ja'); 90 }); 91 92 if (japaneseVoices.length === 0) { 93 console.warn('[TTS] No Japanese voices found'); 94 selectedVoice = ttsVoices.length > 0 ? ttsVoices[0] : null; 95 return; 96 } 97 98 // ãã©ã¦ã¶å¥ã®æé©ãªé³å£°é¸æ 99 if (browserInfo.isChrome) { 100 // Chrome: OSãã¼ã«ã«ã®æ¥æ¬èªé³å£°ãåªå 101 var localVoices = japaneseVoices.filter(function(voice) { 102 return voice.localService; 103 }); 104 selectedVoice = localVoices.length > 0 ? localVoices[0] : japaneseVoices[0]; 105 } else if (browserInfo.isEdge) { 106 // Edge: Microsofté³å£°ãåªå 107 selectedVoice = japaneseVoices.find(function(voice) { 108 return voice.name.includes('Nanami') || voice.name.includes('Ayumi'); 109 }) || japaneseVoices[0]; 110 } else if (browserInfo.isSafari) { 111 // Safari: Kyokoé³å£°ãåªå 112 selectedVoice = japaneseVoices.find(function(voice) { 113 return voice.name.includes('Kyoko'); 114 }) || japaneseVoices[0]; 115 } else { 116 // ãã®ä»: æåã®æ¥æ¬èªé³å£° 117 selectedVoice = japaneseVoices[0]; 118 } 119 120 ttsLog('[TTS] Selected voice: ' + (selectedVoice ? selectedVoice.name : 'none')); 121 122 // å©ç¨å¯è½ãªæ¥æ¬èªé³å£°ããã°åºå 123 if (TTS_DEBUG_MODE) { 124 console.log('[TTS] Available Japanese voices:'); 125 japaneseVoices.forEach(function(voice, index) { 126 console.log(' [' + index + '] ' + voice.name + ' (' + voice.lang + ')'); 127 }); 128 } 129} 130/** 131 * å©ç¨å¯è½ãªæ¥æ¬èªé³å£°ã®ä¸è¦§ãUIã«è¿½å 132 */ 133function populateVoiceList() { 134 if (typeof $ === 'undefined') return; 135 var select = $('#voice_select'); 136 select.empty(); 137 ttsVoices.forEach(function(voice, index) { 138 if (voice.lang === 'ja-JP' || voice.lang.startsWith('ja')) { 139 // é³å£°åããä¸è¦ãªè¨èªæ å ±ãåé¤: (ja-JP)/(Japan)/(Japanese/(Japan))/(æ¥æ¬èªï¼æ¥æ¬ï¼) 140 var label = voice.name.replace(/\s*\((?:(?:Japanese(?: \(Japan\))?)|Japan|ja-JP|æ¥æ¬èª(?:ï¼æ¥æ¬ï¼)?)\)\s*/g, ' '); 141 label = label.replace(/\s*-\s*Japanese/g, ''); 142 label = label.replace(/\s{2,}/g, ' ').trim(); 143 // Local/Remoteæ å ±ãä»å 144 label += voice.localService ? ' (Local)' : ' (Remote)'; 145 select.append($('<option>').val(index).text(label)); 146 } 147 }); 148 var selIndex = ttsVoices.indexOf(selectedVoice); 149 if (selIndex >= 0) select.val(selIndex); 150 select.show(); 151 select.off('change').on('change', function() { 152 var idx = parseInt($(this).val(), 10); 153 if (!isNaN(idx) && ttsVoices[idx]) { 154 selectedVoice = ttsVoices[idx]; 155 ttsLog('[TTS] Voice changed by user to ' + selectedVoice.name); 156 // æ¢åã®é³å£°ã忢 157 if (window.speechSynthesis.speaking) { 158 window.speechSynthesis.cancel(); 159 } 160 // Chrome + ãªã¢ã¼ãé³å£°é¸ææã®è¦å表示 161 if (browserInfo.isChrome && selectedVoice && !selectedVoice.localService && !remoteVoiceWarningShown) { 162 showRemoteVoiceWarning();
163 remoteVoiceWarningShown = true; 164 } 165 } 166 }); 167} 168 169/** 170 * é³å£°ãã©ã¡ã¼ã¿ã®åå¾ï¼ãã©ã¦ã¶å¥æé©åï¼ 171 */ 172function getOptimalSpeechParams() { 173 var params = { 174 rate: 1.0, 175 pitch: 1.0, 176 volume: 1.0 177 }; 178 179 if (browserInfo.isChrome) { 180 params.rate = 0.9; 181 params.volume = 0.8; 182 } else if (browserInfo.isEdge) { 183 params.rate = 1.0; 184 params.volume = 0.9; 185 } else if (browserInfo.isSafari) { 186 params.rate = 1.1; 187 params.volume = 1.0; 188 } 189 190 return params; 191} 192 193/** 194 * é³å£°åçã®ã¡ã¤ã³é¢æ° 195 */ 196function playSpeechSound(text) { 197 ttsLog('[TTS] playSpeechSound called'); 198 199 // åºæ¬ãã§ã㯠200 if (!ifSpeakable) { 201 console.warn('[TTS] TTS not available'); 202 return false; 203 } 204 205 if (!text || text.trim() === '') { 206 console.warn('[TTS] No text provided'); 207 return false; 208 } 209 210 // æ¢åã®é³å£°ã忢 211 if (isSpeaking) { 212 ttsLog('[TTS] Stopping current speech'); 213 stopSpeech(); 214 return false; 215 } 216 217 // TTSåæå確èªï¼ååã®ã¿é³å£°ãªã¹ããèªã¿è¾¼ãï¼ 218 if (!ttsInitialized) { 219 loadVoices(); 220 } 221 222 // ãã©ã¦ã¶æé©åãã©ã¡ã¼ã¿ 223 var params = getOptimalSpeechParams(); 224 225 // é·æã®å ´åã¯æãã¨ã«åå²ãã¦é次åçï¼Chunk TTSã®å¶ç´åé¿ï¼ 226 if (text.length > 200) { 227 // ãããããï¼ï¼ãã§æãåå² 228 var segments = text.match(/[^ãï¼ï¼?]+[ãï¼ï¼?]?/g); 229 if (segments && segments.length > 1) { 230 ttsLog('[TTS] Splitting into ' + segments.length + ' segments for playback'); 231 // åçãã¥ã¼ãã¯ãªã¢ 232 window.speechSynthesis.cancel(); 233 isSpeaking = true; 234 updateUIForSpeechStart(); 235 236 // åã»ã°ã¡ã³ãã«å¯¾ãã¦çºè©±ãè¨å® 237 var lastSegmentIndex = segments.length - 1; 238 segments.forEach(function(seg, index) { 239 var u = new SpeechSynthesisUtterance(seg); 240 u.lang = 'ja-JP'; 241 u.voice = selectedVoice; 242 u.rate = params.rate; 243 u.pitch = params.pitch; 244 u.volume = params.volume; 245 246 // æå¾ã®ã»ã°ã¡ã³ãã®çµäºæã«ã¯ãªã¼ã³ã¢ãã 247 if (index === lastSegmentIndex) { 248 u.onend = function() { 249 ttsLog('[TTS] All segments completed'); 250 cleanupAfterSpeech(); 251 }; 252 u.onerror = function(event) { 253 console.error('[TTS] Segment error:', event.error); 254 if (event.error === 'canceled' && userCancelled) { 255 userCancelled = false; 256 } 257 cleanupAfterSpeech(); 258 }; 259 } 260 261 window.speechSynthesis.speak(u); 262 }); 263 return true; 264 } 265 } 266 267 // é常ã®åä¸Utteranceçæ 268 currentUtterance = new SpeechSynthesisUtterance(text); 269 currentUtterance.lang = 'ja-JP'; 270 currentUtterance.rate = params.rate; 271 currentUtterance.pitch = params.pitch; 272 currentUtterance.volume = params.volume; 273 274 // é³å£°è¨å® 275 if (selectedVoice) { 276 currentUtterance.voice = selectedVoice; 277 ttsLog('[TTS] Using voice: ' + selectedVoice.name); 278 } 279 280 // ã¤ãã³ããã³ãã©ã¼è¨å® 281 setupUtteranceEvents(); 282 283 // é³å£°åçå®è¡ 284 try { 285 ttsLog('[TTS] SpeechSynthesis state before play - speaking: ' + window.speechSynthesis.speaking + 286 ', pending: ' + window.speechSynthesis.pending + 287 ', paused: ' + window.speechSynthesis.paused); 288 // ãã¥ã¼ãã¯ãªã¢ 289 window.speechSynthesis.cancel(); 290 ttsLog('[TTS] Previous utterances cleared'); 291 ttsLog('[TTS] Utterance text: ' + text); 292 // åçéå§ 293 ttsLog('[TTS] Starting speech synthesis...'); 294 window.speechSynthesis.speak(currentUtterance); 295 // å³åº§ã«åçä¸ãã©ã°ãã»ãã 296 isSpeaking = true; 297 // ä¸æåæ¢ç¶æ ããã®å¾©å¸° 298 if (window.speechSynthesis.paused) { 299 window.speechSynthesis.resume(); 300 } 301 ttsLog('[TTS] SpeechSynthesis state after play - speaking: ' + window.speechSynthesis.speaking + 302 ', pending: ' + window.speechSynthesis.pending + 303 ', paused: ' + window.speechSynthesis.paused); 304 // UIæ´æ° 305 updateUIForSpeechStart(); 306 return true; 307 } catch (error) { 308 console.error('[TTS] Speech synthesis failed:', error); 309 return false;
310 } 311} 312 313/** 314 * Utteranceã¤ãã³ããã³ãã©ã¼ã®è¨å® 315 */ 316function setupUtteranceEvents() { 317 currentUtterance.onstart = function() { 318 ttsLog('[TTS] Speech started'); 319 isSpeaking = true; 320 updateUIForSpeechStart(); 321 }; 322 323 currentUtterance.onend = function() { 324 ttsLog('[TTS] Speech ended normally'); 325 cleanupAfterSpeech(); 326 }; 327 328 currentUtterance.onerror = function(event) { 329 console.error('[TTS] Speech error:', event.error); 330 console.error('[TTS] Error details:', event); 331 // ã¦ã¼ã¶æä½ã«ãã忢ãã©ãããå¤å® 332 if (event.error === 'canceled' && userCancelled) { 333 ttsLog('[TTS] Speech canceled by user'); 334 userCancelled = false; 335 cleanupAfterSpeech(); 336 return; 337 } 338 // Chrome ã®ä»æ§ã§ canceled ãçºçããå ´åãã¯ãªã¼ã³ã¢ãã 339 cleanupAfterSpeech(); 340 }; 341 342 currentUtterance.onpause = function() { 343 ttsLog('[TTS] Speech paused'); 344 }; 345 346 currentUtterance.onresume = function() { 347 ttsLog('[TTS] Speech resumed'); 348 }; 349} 350 351/** 352 * é³å£°åæ¢ 353 */ 354function stopSpeech() { 355 ttsLog('[TTS] Stopping speech...'); 356 // ã¦ã¼ã¶æä½ã¨ãã¦ãã£ã³ã»ã«ããã¼ã¯ 357 userCancelled = true; 358 if (window.speechSynthesis.speaking || window.speechSynthesis.pending) { 359 window.speechSynthesis.cancel(); 360 } 361 cleanupAfterSpeech(); 362} 363 364/** 365 * é³å£°çµäºå¾ã®ã¯ãªã¼ã³ã¢ãã 366 */ 367function cleanupAfterSpeech() { 368 isSpeaking = false; 369 currentUtterance = null; 370 // 忢ãã©ã°ããªã»ãã 371 userCancelled = false; 372 updateUIForSpeechEnd(); 373} 374 375/** 376 * UIæ´æ° - é³å£°éå§æ 377 */ 378function updateUIForSpeechStart() { 379 $('#speaker_icon').removeClass('glyphicon-headphones').addClass('glyphicon-stop'); 380 $('#speaking').removeClass('btn-danger').addClass('btn-warning'); 381} 382 383/** 384 * UIæ´æ° - é³å£°çµäºæ 385 */ 386function updateUIForSpeechEnd() { 387 $('#speaker_icon').removeClass('glyphicon-stop').addClass('glyphicon-headphones'); 388 $('#speaking').removeClass('btn-warning').addClass('btn-danger'); 389} 390 391/** 392 * ãªã¢ã¼ãé³å£°è¦åãããã¢ãã表示 393 */ 394function showRemoteVoiceWarning() { 395 var lang = $('#current_mode').attr('language') || 'ja'; 396 var title, message, closeText; 397 398 if (lang === 'en') { 399 title = "Chrome Remote Voice Notice"; 400 message = "When using Google's remote voice in Chrome, " + 401 "the first click on the play button may not produce sound.<br><br>" + 402 "If this happens, please click the stop button once, then click play again.<br><br>" + 403 "This is due to Chrome's security restrictions on remote voice synthesis."; 404 closeText = "Close"; 405 } else { 406 title = "Chromeãªã¢ã¼ãé³å£°ã«ã¤ãã¦"; 407 message = "Chromeã§ãGoogle æ¥æ¬èª (Remote)ãã使ç¨ããå ´åã" + 408 "æåã®åçãã¿ã³ã¯ãªãã¯ã§ã¯é³å£°ãåçãããªããã¨ãããã¾ãã<br><br>" + 409 "ãã®å ´åã¯ãä¸åº¦åæ¢ãã¿ã³ãã¯ãªãã¯ãã¦ãããããä¸åº¦åçãã¿ã³ã" + 410 "ã¯ãªãã¯ãã¦ãã ããã<br><br>" + 411 "ããã¯Chromeã®ãªã¢ã¼ãé³å£°åæã«é¢ããã»ãã¥ãªãã£å¶éã«ãããã®ã§ãã"; 412 closeText = "éãã"; 413 } 414 415 // ã«ã¹ã¿ã ã¢ã¼ãã«ä½æ 416 var modalHTML = 417 '<div id="tts-warning-modal" style="' + 418 'position: fixed; ' + 419 'top: 0; left: 0; right: 0; bottom: 0; ' + 420 'background-color: rgba(0,0,0,0.5); ' + 421 'z-index: 9999; ' + 422 'display: flex; ' + 423 'align-items: center; ' + 424 'justify-content: center;">' + 425 '<div style="' + 426 'background-color: white; ' + 427 'padding: 30px; ' + 428 'border-radius: 8px; ' + 429 'max-width: 500px; ' + 430 'box-shadow: 0 4px 6px rgba(0,0,0,0.1);">' + 431 '<h3 style="margin-top: 0; margin-bottom: 20px;">' + title + '</h3>' + 432 '<p style="line-height: 1.6; margin-bottom: 20px;">' + message + '</p>' + 433 '<button id="tts-warning-close" style="' + 434 'background-color: #007bff; ' + 435 'color: white; ' + 436 'border: none; ' + 437 'padding: 10px 20px; ' + 438 'border-radius: 4px; ' + 439 'cursor: pointer; ' + 440 'font-size: 16px;">' + closeText + '</button>' + 441 '</div>' + 442 '</div>'; 443 444 // ã¢ã¼ãã«ã表示 445 $('body').append(modalHTML); 446 447 // éãããã¿ã³ã®ã¤ãã³ãå¦ç 448 $('#tts-warning-close, #tts-warning-modal').on('click', function(e) { 449 if (e.target.id === 'tts-warning-close' || e.target.id === 'tts-warning-modal') { 450 $('#tts-warning-modal').remove(); 451 } 452 }); 453} 454 455/** 456 * ãã¼ã¸èªã¿è¾¼ã¿æã®åæå 457 */ 458$(document).ready(function() { 459 ttsLog('[TTS] Document ready, initializing...'); 460 461 // å°ãé å»¶ãã¦ããTTSåæå 462 setTimeout(function() { 463 initTTS(); 464 465 // åæåç¢ºèª 466 setTimeout(function() { 467 if (TTS_DEBUG_MODE) { 468 console.log('[TTS] Initialization check:'); 469 console.log(' ifSpeakable:', ifSpeakable); 470 console.log(' ttsInitialized:', ttsInitialized); 471 console.log(' selectedVoice:', selectedVoice ? selectedVoice.name : 'none'); 472 console.log(' Total voices:', ttsVoices.length); 473 } 474 475 // åæé¸æããªã¢ã¼ãé³å£°ã®å ´åãè¦å表示 476 if (browserInfo.isChrome && selectedVoice && !selectedVoice.localService && !remoteVoiceWarningShown) { 477 showRemoteVoiceWarning();
478 remoteVoiceWarningShown = true; 479 } 480 }, 1000); 481 }, 100); 482});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.