Add async audio generation: job tracking in PocketBase + UI polling
Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 1s
CI / UI / Build (pull_request) Failing after 7s
CI / Scraper / Lint (pull_request) Failing after 8s
CI / Scraper / Test (pull_request) Failing after 9s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped

Replace blocking POST /api/audio with a non-blocking 202 flow: the Go
handler immediately enqueues a job in a new `audio_jobs` PocketBase
collection and returns {job_id, status}. A background goroutine runs
the actual Kokoro TTS work and updates job status (pending → generating
→ done/failed). A new GET /api/audio/status/{slug}/{n} endpoint lets
clients poll progress. The SvelteKit proxy and AudioPlayer.svelte are
updated to POST, then poll the status route every 2s until done.
This commit is contained in:
Admin
2026-03-07 20:12:08 +05:00
parent 88644341d8
commit 89f0dfb113
9 changed files with 540 additions and 83 deletions

View File

@@ -11,7 +11,8 @@
// GET /api/presign/chapter/{slug}/{n} — presigned MinIO URL for chapter markdown
// GET /api/presign/audio/{slug}/{n} — presigned MinIO URL for chapter audio
// GET /api/chapter-text/{slug}/{n} — plain text of chapter (markdown stripped)
// POST /api/audio/{slug}/{n} — trigger Kokoro audio generation
// POST /api/audio/{slug}/{n} — trigger Kokoro audio generation (async, returns 202)
// GET /api/audio/status/{slug}/{n} — poll audio generation job status
// GET /api/audio-proxy/{slug}/{n} — proxy generated audio from Kokoro
package server
@@ -47,11 +48,11 @@ type Server struct {
voiceMu sync.RWMutex
cachedVoices []string // populated on first request from Kokoro /v1/audio/voices
// audioMu guards audioInFlight only.
// audioMu guards audioJobIDs only.
// Completed audio filenames are persisted to the Store (PocketBase).
// audioInFlight deduplicates concurrent generation requests for the same key.
audioMu sync.Mutex
audioInFlight map[string]chan struct{} // cacheKey → closed when done
// audioJobIDs deduplicates concurrent generation requests for the same key.
audioMu sync.Mutex
audioJobIDs map[string]string // cacheKey → PocketBase job ID (empty string if record creation failed)
// browseMu guards browseInFlight — keys currently being refreshed
// in the background.
@@ -82,7 +83,7 @@ func New(addr string, oCfg orchestrator.Config, novel scraper.NovelScraper, log
store: store,
kokoroURL: kokoroURL,
kokoroVoice: kokoroVoice,
audioInFlight: make(map[string]chan struct{}),
audioJobIDs: make(map[string]string),
browseInFlight: make(map[string]struct{}),
browseMemCache: make(map[string]browseCacheEntry),
}
@@ -177,13 +178,11 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
)
mux.Handle("POST /api/audio/voice-samples", voiceSampleHandler)
// Server-side audio generation via Kokoro /v1/audio/speech.
// Generation can take several minutes, so wrap in its own timeout handler.
audioGenHandler := http.TimeoutHandler(
http.HandlerFunc(s.handleAudioGenerate),
10*time.Minute,
`{"error":"audio generation timed out"}`,
)
mux.Handle("POST /api/audio/{slug}/{n}", audioGenHandler)
// POST returns 202 immediately and starts a background goroutine;
// poll GET /api/audio/status/{slug}/{n} to track progress.
mux.HandleFunc("POST /api/audio/{slug}/{n}", s.handleAudioGenerate)
// Audio job status polling endpoint.
mux.HandleFunc("GET /api/audio/status/{slug}/{n}", s.handleAudioStatus)
// Proxy route: fetches the generated file from Kokoro /v1/download/{filename}.
mux.HandleFunc("GET /api/audio-proxy/{slug}/{n}", s.handleAudioProxy)