feat: play cached TTS audio immediately on auto-start if already generated
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled

This commit is contained in:
Admin
2026-03-03 13:46:37 +05:00
parent f28de2195d
commit 09e90d2ab0
2 changed files with 69 additions and 12 deletions

View File

@@ -134,6 +134,8 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
`{"error":"audio generation timed out"}`,
)
mux.Handle("POST /ui/audio/{slug}/{n}", audioGenHandler)
// Status route: returns the proxy URL if audio was already generated, 404 otherwise.
mux.HandleFunc("GET /ui/audio/{slug}/{n}", s.handleAudioStatus)
// Proxy route: fetches the generated file from Kokoro /v1/download/{filename}.
mux.HandleFunc("GET /ui/audio-proxy/{slug}/{n}", s.handleAudioProxy)
@@ -355,6 +357,39 @@ func (s *Server) writeAudioResponse(w http.ResponseWriter, slug string, n int, v
})
}
// handleAudioStatus handles GET /ui/audio/{slug}/{n}.
// Returns the proxy URL if audio was already generated this session, 404 otherwise.
func (s *Server) handleAudioStatus(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
n, err := strconv.Atoi(r.PathValue("n"))
if err != nil || n < 1 {
http.Error(w, `{"error":"invalid chapter"}`, http.StatusBadRequest)
return
}
voice := r.URL.Query().Get("voice")
if voice == "" {
voice = s.kokoroVoice
}
speedStr := r.URL.Query().Get("speed")
speed := 1.0
if speedStr != "" {
if v, err := strconv.ParseFloat(speedStr, 64); err == nil && v > 0 {
speed = v
}
}
cacheKey := fmt.Sprintf("%s/%d/%s/%.2f", slug, n, voice, speed)
s.audioMu.Lock()
filename, ok := s.audioCache[cacheKey]
s.audioMu.Unlock()
if !ok {
http.Error(w, `{"error":"not generated"}`, http.StatusNotFound)
return
}
s.writeAudioResponse(w, slug, n, voice, speed, filename)
}
// handleAudioProxy handles GET /ui/audio-proxy/{slug}/{n}.
// It looks up the Kokoro download filename for this chapter (voice/speed) and
// proxies GET /v1/download/{filename} from the Kokoro server back to the browser.

View File

@@ -2922,21 +2922,43 @@ const chapterTmpl = `
navigator.mediaSession.setActionHandler('seekto', function(d) { if (isFinite(d.seekTime)) { audio.currentTime = d.seekTime; updateSeek(); } });
}
function loadAndPlay(url) {
// Always set autoplay=true before load(): the browser needs this hint set
// synchronously before load() regardless of whether we got here via a user
// gesture or a programmatic trigger (e.g. chapter transition). The canplay
// handler will call play() as well, but autoplay on the element is the only
// reliable path after an async fetch where the gesture stack is gone.
audio.autoplay = true;
audio.src = url; audio.load();
audio.playbackRate = getSpeed();
registerMediaSession();
}
function startAudio(autoplay) {
setGenerating();
highlightPara(0);
generateAudio(CHAPTER_N, function (url) {
if (stale()) return;
// Always set autoplay=true before load(): the browser needs this hint set
// synchronously before load() regardless of whether we got here via a user
// gesture or a programmatic trigger (e.g. chapter transition). The canplay
// handler will call play() as well, but autoplay on the element is the only
// reliable path after an async fetch where the gesture stack is gone.
audio.autoplay = true;
audio.src = url; audio.load();
audio.playbackRate = getSpeed();
registerMediaSession();
});
// Check if audio was already generated for this chapter this session.
// If so, play it directly without re-generating.
fetch('/ui/audio/' + SLUG + '/' + CHAPTER_N + '?voice=' + encodeURIComponent(voiceSel.value) + '&speed=1')
.then(function(res) {
if (stale()) return;
if (res.ok) {
return res.json().then(function(data) {
if (stale()) return;
if (data && data.url) { loadAndPlay(data.url); return; }
throw new Error('no url in response');
});
}
// Not cached yet — fall through to generation.
generateAudio(CHAPTER_N, function(url) {
if (stale()) return;
loadAndPlay(url);
});
})
.catch(function(e) {
if (stale()) return;
setError(e.message);
});
}
window.ttsToggle = function () {