diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue
index 37ccc3d..9e06981 100644
--- a/frontend/src/components/FightViewer.vue
+++ b/frontend/src/components/FightViewer.vue
@@ -10,6 +10,8 @@ import {
setMasterMute, isMasterMuted, ensureAudioContext,
speakQuestion, speakAnswer, speakNarration,
prefetchQuestion, prefetchAnswer, prefetchNarration,
+ awaitQuestionReady, awaitAnswerReady,
+ playQuestionNow, playAnswerNow, playNarrationNow,
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
} from '../game/audio'
import { isKokoroLoading, getKokoroProgress } from '../game/tts'
@@ -400,60 +402,61 @@ async function _doReplay() {
scene.startShowboating('b')
}
- // 1. Show question in log AND speak it (audio should be pre-generated by now)
+ // 1. Ensure question audio is ready BEFORE showing anything (zero-gap)
+ if (doTTS && questionText) {
+ // Kick off answer prefetches early so they generate in parallel
+ if (round.botAResponse) prefetchAnswer(props.fight.botA!.name, round.botAResponse)
+ if (round.botBResponse) prefetchAnswer(props.fight.botB!.name, round.botBResponse)
+ await awaitQuestionReady(questionText)
+ }
+ // Show question text + play voice at the exact same moment
logItems.value.push(
{ type: 'header', round: round.roundNumber, text: `ROUND ${round.roundNumber}: ${challengeLabel(round.challengeType)}`, color: 'neon-purple' },
)
- scrollLog(); await sleep(insanityMode.value ? 30 : 100)
logItems.value.push(
{ type: 'prompt', round: round.roundNumber, text: questionText, color: 'text-muted' },
)
scrollLog()
if (doTTS && questionText) {
- // Prefetch bot A answer while question plays
- if (round.botAResponse) prefetchAnswer(props.fight.botA!.name, round.botAResponse)
- await speakQuestion(questionText)
- await sleep(150)
+ await playQuestionNow(questionText)
} else {
- await sleep(insanityMode.value ? 50 : 600)
+ await sleep(insanityMode.value ? 50 : 300)
}
- // 2. Bot A: log + bubble + mouth + TTS (all synced)
- // Stop A's showboat when it's their turn to talk, B keeps showboating
+ // 2. Bot A: ensure audio ready, then show log + bubble + mouth + voice all at once
if (round.botAResponse) {
+ if (doTTS) await awaitAnswerReady(props.fight.botA!.name, round.botAResponse)
scene?.stopShowboating('a')
logItems.value.push({ type: 'responseA', round: round.roundNumber, text: `${props.fight.botA?.name}: ${round.botAResponse.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-cyan' })
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botATimeMs}ms | Score: ${round.botAScore}`, color: 'text-muted' })
scrollLog()
if (scene && !insanityMode.value) {
- scene.showSpeechBubble('a', round.botAResponse.slice(0, 60), 5)
+ scene.showSpeechBubble('a', round.botAResponse.slice(0, 60), 8)
scene.startTalking('a')
}
- // Prefetch bot B answer while bot A talks
- if (doTTS && round.botBResponse) prefetchAnswer(props.fight.botB!.name, round.botBResponse)
- if (doTTS) await speakAnswer(props.fight.botA!.name, round.botAResponse)
- else await sleep(insanityMode.value ? 30 : 800)
+ if (doTTS) await playAnswerNow(props.fight.botA!.name, round.botAResponse)
+ else await sleep(insanityMode.value ? 30 : 400)
scene?.stopTalking('a')
- if (!insanityMode.value) await sleep(150)
+ scene?.hideSpeechBubble('a')
}
- // 3. Bot B: log + bubble + mouth + TTS
- // Stop B's showboat when it's their turn to talk
+ // 3. Bot B: ensure audio ready, then show log + bubble + mouth + voice all at once
if (round.botBResponse) {
+ if (doTTS) await awaitAnswerReady(props.fight.botB!.name, round.botBResponse)
scene?.stopShowboating('b')
logItems.value.push({ type: 'responseB', round: round.roundNumber, text: `${props.fight.botB?.name}: ${round.botBResponse.slice(0, 120) || '[NO RESPONSE]'}`, color: 'neon-pink' })
logItems.value.push({ type: 'time', round: round.roundNumber, text: ` ${round.botBTimeMs}ms | Score: ${round.botBScore}`, color: 'text-muted' })
scrollLog()
- if (scene && !insanityMode.value) {
- scene.showSpeechBubble('b', round.botBResponse.slice(0, 60), 5)
- scene.startTalking('b')
- }
// Prefetch narration while bot B talks
if (doTTS && round.narration) prefetchNarration(round.narration)
- if (doTTS) await speakAnswer(props.fight.botB!.name, round.botBResponse)
- else await sleep(insanityMode.value ? 30 : 800)
+ if (scene && !insanityMode.value) {
+ scene.showSpeechBubble('b', round.botBResponse.slice(0, 60), 8)
+ scene.startTalking('b')
+ }
+ if (doTTS) await playAnswerNow(props.fight.botB!.name, round.botBResponse)
+ else await sleep(insanityMode.value ? 30 : 400)
scene?.stopTalking('b')
- if (!insanityMode.value) await sleep(150)
+ scene?.hideSpeechBubble('b')
}
// Stop all showboating before fight animation
@@ -815,14 +818,11 @@ async function _doReplay() {