feat: play cached TTS audio immediately on auto-start if already generated
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user