feat: sync TTS voice with chat bubbles and battle log for zero-gap playback
Adds await-then-play pattern: audio is pre-generated and cached before visuals appear, so log text + speech bubble + mouth animation + voice all fire in the same frame. Prefetches both answers during question playback for instant transitions. Adds hideSpeechBubble() to dismiss bubbles when voice ends instead of fixed 5s timer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e7d1f9b97b
commit
cb8a1d50fe
@@ -205,6 +205,11 @@ export {
|
||||
prefetchQuestion,
|
||||
prefetchAnswer,
|
||||
prefetchNarration,
|
||||
awaitQuestionReady,
|
||||
awaitAnswerReady,
|
||||
playQuestionNow,
|
||||
playAnswerNow,
|
||||
playNarrationNow,
|
||||
} from './voice'
|
||||
|
||||
// music
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { getCtx, getSfxDest, getMasterMuted, getSfxGain } from './context'
|
||||
import { tone, noise } from './primitives'
|
||||
import { initKokoro, isKokoroReady, kokoroSpeak, kokoroSpeakAsync, kokoroStop, kokoroPrefetch, setAudioContext } from '../tts'
|
||||
import { initKokoro, isKokoroReady, kokoroSpeak, kokoroSpeakAsync, kokoroStop, kokoroPrefetch, kokoroAwaitReady, kokoroPlayCached, setAudioContext } from '../tts'
|
||||
|
||||
// === VOICE PROFILES ===
|
||||
|
||||
@@ -756,6 +756,48 @@ export function prefetchNarration(text: string) {
|
||||
kokoroPrefetch(smartTruncate(text, 200), 'sportscaster')
|
||||
}
|
||||
|
||||
// === AWAIT + PLAY-NOW pattern for tight sync ===
|
||||
// Call awaitReady to block until audio is generated, then playNow fires instantly
|
||||
|
||||
export async function awaitQuestionReady(text: string): Promise<void> {
|
||||
if (getMasterMuted()) return
|
||||
const trimmed = smartTruncate(text, 200)
|
||||
if (isKokoroReady()) await kokoroAwaitReady(trimmed, 'question_reader')
|
||||
}
|
||||
|
||||
export async function awaitAnswerReady(botName: string, answer: string): Promise<void> {
|
||||
if (getMasterMuted()) return
|
||||
const trimmed = smartTruncate(answer, 200)
|
||||
if (isKokoroReady()) await kokoroAwaitReady(trimmed, botVoiceKey(botName))
|
||||
}
|
||||
|
||||
export function playQuestionNow(text: string): Promise<void> {
|
||||
const trimmed = smartTruncate(text, 200)
|
||||
return _playNowCore(trimmed, 'question_reader')
|
||||
}
|
||||
|
||||
export function playAnswerNow(botName: string, answer: string): Promise<void> {
|
||||
const trimmed = smartTruncate(answer, 200)
|
||||
return _playNowCore(trimmed, botVoiceKey(botName), 1.15)
|
||||
}
|
||||
|
||||
export function playNarrationNow(text: string): Promise<void> {
|
||||
const trimmed = smartTruncate(text, 200)
|
||||
return _playNowCore(trimmed, 'sportscaster', 1.2)
|
||||
}
|
||||
|
||||
async function _playNowCore(text: string, profileName: string, rateOverride?: number): Promise<void> {
|
||||
if (getMasterMuted()) return
|
||||
const sfxGain = getSfxGain()
|
||||
if (isKokoroReady() && sfxGain) {
|
||||
const profile = voiceProfiles[profileName] || voiceProfiles.announcer
|
||||
await kokoroPlayCached(text, profileName, sfxGain, profile.volume * VOICE_VOLUME_SCALE)
|
||||
return
|
||||
}
|
||||
// Fallback to Web Speech
|
||||
return _speakAsyncCore(text, profileName, rateOverride)
|
||||
}
|
||||
|
||||
// Re-export TTS utilities needed by ensureAudioContext (in index.ts)
|
||||
export { initKokoro, setAudioContext, loadVoices as _loadVoices }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user