feat: server-side TTS audio generation, audio queue panel, and home screen fix
- Add POST /ui/audio/{slug}/{n} endpoint: calls Kokoro-FastAPI server-side,
writes MP3 atomically to disk, deduplicates concurrent requests via
in-flight channel map, wraps route with 10-min TimeoutHandler
- Add GET /ui/audio-file/{slug}/{n} endpoint: serves cached MP3 with 1-day
cache headers
- Add AudioDir/AudioPath helpers to writer.go
- Rewrite JS TTS: remove all MSE/blob/stream code; use plain fetch to
generate endpoint then set audio.src to returned URL
- Add AbortController to generateAudio; abort on stop() and navigation
- Keep voice/speed controls disabled until setPlaying() fires
- Reset prefetchFired on voice/speed change
- Upgrade prefetch from fire-and-forget to promise chain updating queue badge
- Add sticky bottom audio queue panel: always-visible scrubber with timestamps,
expandable rows for Now/Next/Dbg with color-coded state badges
- Fix iOS Reader Mode: header->nav aria-hidden, audio outside nav, role=main,
visible h1 in content area, hover-only paragraph highlight
- Fix home screen Available books hidden: stop hiding cards in #books-grid
when they appear in Continue Reading; Available always shows all books
This commit is contained in:
@@ -408,6 +408,26 @@ func (w *Writer) bookDir(slug string) string {
|
||||
return filepath.Join(w.root, slug)
|
||||
}
|
||||
|
||||
// AudioDir returns the directory used to cache generated MP3 files for a book.
|
||||
func (w *Writer) AudioDir(slug string) string {
|
||||
return filepath.Join(w.bookDir(slug), "audio")
|
||||
}
|
||||
|
||||
// AudioPath returns the full path for a cached chapter audio file.
|
||||
// The filename is keyed by chapter number, voice, and speed so that different
|
||||
// settings never collide. Speed is formatted to one decimal place (e.g. "1.0").
|
||||
func (w *Writer) AudioPath(slug string, n int, voice string, speed float64) string {
|
||||
// Sanitise voice so it is safe as a filename component.
|
||||
safeVoice := strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, voice)
|
||||
filename := fmt.Sprintf("ch%d-%s-%.1f.mp3", n, safeVoice, speed)
|
||||
return filepath.Join(w.AudioDir(slug), filename)
|
||||
}
|
||||
|
||||
// chapterPath computes the full file path for a chapter.
|
||||
//
|
||||
// vol-{volume}/{folderRange}/chapter-{number}.md
|
||||
|
||||
Reference in New Issue
Block a user