feature/backend-rewrite #2
@@ -188,6 +188,10 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
|
||||
s.log.Info("HTTP server listening", "addr", s.addr)
|
||||
|
||||
// Pre-populate voice samples in the background so the UI voice selector
|
||||
// has playable previews without requiring a manual trigger.
|
||||
go s.warmVoiceSamples(ctx)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
@@ -1486,6 +1490,78 @@ func voiceSampleKey(voice string) string {
|
||||
return fmt.Sprintf("_voice-samples/%s.mp3", safe)
|
||||
}
|
||||
|
||||
// warmVoiceSamples runs at startup in a background goroutine.
|
||||
// It generates a short audio sample for every available Kokoro voice that
|
||||
// doesn't already have one in MinIO, so the UI voice selector has playable
|
||||
// previews without requiring a manual trigger.
|
||||
// It respects ctx cancellation and waits up to 30 s for Kokoro to become
|
||||
// reachable before giving up.
|
||||
func (s *Server) warmVoiceSamples(ctx context.Context) {
|
||||
if s.kokoroURL == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for Kokoro to be reachable (it may still be starting up).
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, s.kokoroURL+"/v1/audio/voices", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
voices := s.voices()
|
||||
s.log.Info("warming voice samples", "voices", len(voices))
|
||||
|
||||
generated, skipped, failed := 0, 0, 0
|
||||
for _, voice := range voices {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
key := voiceSampleKey(voice)
|
||||
if s.store.AudioExists(ctx, key) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
filename, err := s.generateSpeech(ctx, voiceSampleText, voice, 1.0)
|
||||
if err != nil {
|
||||
s.log.Warn("voice sample warmup: generation failed", "voice", voice, "err", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
audioData, err := s.downloadFromKokoro(ctx, filename)
|
||||
if err != nil {
|
||||
s.log.Warn("voice sample warmup: download failed", "voice", voice, "err", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.store.PutAudio(ctx, key, audioData); err != nil {
|
||||
s.log.Warn("voice sample warmup: upload failed", "voice", voice, "key", key, "err", err)
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
s.log.Debug("voice sample warmed", "voice", voice)
|
||||
generated++
|
||||
}
|
||||
|
||||
s.log.Info("voice sample warmup complete",
|
||||
"generated", generated, "skipped", skipped, "failed", failed)
|
||||
}
|
||||
|
||||
// handleGenerateVoiceSamples handles POST /api/audio/voice-samples.
|
||||
// It generates short audio samples for each available voice and stores them
|
||||
// in the audio MinIO bucket so the UI can play them during voice selection.
|
||||
|
||||
Reference in New Issue
Block a user