From 1353d455e419c3ed1a52ad3891d1ab57b392762c Mon Sep 17 00:00:00 2001 From: posimai Date: Sun, 22 Mar 2026 18:23:39 +0900 Subject: [PATCH] fix: retry TTS_BUSY up to 3 times with backoff Auto-retries on TTS_BUSY (2s/4s/6s backoff) so pre-warm race at startup is transparent to the user. Co-Authored-By: Claude Sonnet 4.6 --- index.html | 53 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index ac33af0..7c02dd0 100644 --- a/index.html +++ b/index.html @@ -671,29 +671,38 @@ async function tryVoicevox(text) { if (!apiKey) return false; const ctx = getAudioCtx(); - const res = await fetch(TTS_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, - body: JSON.stringify({ text: preprocessText(text), speaker: ttsSpeaker }), - }); - if (!res.ok) { - const msg = res.status === 401 ? 'APIキーが無効です' : res.status === 503 ? 'VOICEVOXが起動していません' : `サーバーエラー (${res.status})`; - if (currentIdx === 0 && typeof showToast === 'function') showToast('VOICEVOX: ' + msg); - return false; + for (let attempt = 0; attempt < 4; attempt++) { + if (attempt > 0) await new Promise(r => setTimeout(r, 2000 * attempt)); + const res = await fetch(TTS_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, + body: JSON.stringify({ text: preprocessText(text), speaker: ttsSpeaker }), + }); + if (res.status === 503) { + const body = await res.json().catch(() => ({})); + if (body.error === 'TTS_BUSY' && attempt < 3) continue; // 自動リトライ + const msg = body.error === 'TTS_BUSY' ? 'サーバーが混雑しています' : 'VOICEVOXが起動していません'; + if (currentIdx === 0 && typeof showToast === 'function') showToast('VOICEVOX: ' + msg); + return false; + } + if (!res.ok) { + const msg = res.status === 401 ? 'APIキーが無効です' : `サーバーエラー (${res.status})`; + if (currentIdx === 0 && typeof showToast === 'function') showToast('VOICEVOX: ' + msg); + return false; + } + const buf = await ctx.decodeAudioData(await res.arrayBuffer()); + if (!isPlaying) return true; + return new Promise(resolve => { + currentSource = ctx.createBufferSource(); + currentSource.buffer = buf; + currentSource.playbackRate.value = speechRate; + currentSource.connect(ctx.destination); + currentSource.onended = () => { currentSource = null; resolve(true); }; + currentSource.start(0); + updateVoiceBadge(true); + }); } - - const buf = await ctx.decodeAudioData(await res.arrayBuffer()); - if (!isPlaying) return true; - - return new Promise(resolve => { - currentSource = ctx.createBufferSource(); - currentSource.buffer = buf; - currentSource.playbackRate.value = speechRate; - currentSource.connect(ctx.destination); - currentSource.onended = () => { currentSource = null; resolve(true); }; - currentSource.start(0); - updateVoiceBadge(true); - }); + return false; } function speakBrowser(text, onEnd) {