fix: mobile menu overlay, speech bubble timing, TTS static file fallback
- Mobile nav menu now overlays content (absolute positioning) instead of pushing it down - Speech bubbles stay visible for minimum 400ms even when TTS resolves instantly or fails - kokoroPlayCached checks audio cache and loads static files even when Kokoro worker hasn't loaded — fixes TTS not playing on production - CORS_ORIGIN env now supports comma-separated origins - Rename "VIDEO REPLAY" to play icon + "REPLAY" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1a7ad74859
commit
e6b5aa4b9b
@@ -788,10 +788,11 @@ export function playNarrationNow(text: string): Promise<void> {
|
||||
async function _playNowCore(text: string, profileName: string, rateOverride?: number): Promise<void> {
|
||||
if (getMasterMuted()) return
|
||||
const sfxGain = getSfxGain()
|
||||
if (isKokoroReady() && sfxGain) {
|
||||
// Try Kokoro (includes pre-generated static files from cache)
|
||||
if (sfxGain) {
|
||||
const profile = voiceProfiles[profileName] || voiceProfiles.announcer
|
||||
await kokoroPlayCached(text, profileName, sfxGain, profile.volume * VOICE_VOLUME_SCALE)
|
||||
return
|
||||
const played = await kokoroPlayCached(text, profileName, sfxGain, profile.volume * VOICE_VOLUME_SCALE)
|
||||
if (played) return
|
||||
}
|
||||
// Fallback to Web Speech
|
||||
return _speakAsyncCore(text, profileName, rateOverride)
|
||||
|
||||
@@ -510,22 +510,37 @@ export async function kokoroAwaitReady(text: string, profileName: string): Promi
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
/** Play already-cached audio immediately. Returns playback promise. If not cached, returns resolved. */
|
||||
export function kokoroPlayCached(
|
||||
/** Play already-cached audio immediately. Returns true if played, false if nothing available. */
|
||||
export async function kokoroPlayCached(
|
||||
text: string,
|
||||
profileName: string,
|
||||
dest: AudioNode,
|
||||
volume: number = 0.7,
|
||||
): Promise<void> {
|
||||
if (!_workerReady) return Promise.resolve()
|
||||
): Promise<boolean> {
|
||||
const key = _cacheKey(text, profileName)
|
||||
// Check in-memory cache first (includes pre-loaded static files)
|
||||
const cached = audioCache.get(key)
|
||||
if (cached) {
|
||||
cached.lastAccess = Date.now()
|
||||
return _playBuffer(cached.buf, dest, volume)
|
||||
await _playBuffer(cached.buf, dest, volume)
|
||||
return true
|
||||
}
|
||||
// Not cached — fall through to generate+play
|
||||
return kokoroSpeakAsync(text, profileName, dest, volume).then(() => {})
|
||||
// Try loading static file on demand if not in cache yet
|
||||
const staticUrl = STATIC_AUDIO[key]
|
||||
if (staticUrl) {
|
||||
const buf = await _loadStaticAudio(staticUrl)
|
||||
if (buf) {
|
||||
audioCache.set(key, { buf, lastAccess: Date.now() })
|
||||
await _playBuffer(buf, dest, volume)
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Try worker generation if available
|
||||
if (_workerReady) {
|
||||
const played = await kokoroSpeakAsync(text, profileName, dest, volume)
|
||||
return played
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Stop all currently playing kokoro audio */
|
||||
|
||||
Reference in New Issue
Block a user