Files
goclaw/internal/http/tts_capabilities.go
viettranx 613b6e38d7 feat(tts): provider capabilities schema + Gemini TTS + dynamic param forms
Baseline groundwork for TTS expansion plan. Introduces a capabilities
system that the follow-up plan (phase-01..04) builds on.

Backend:
- `audio.ParamSchema` / `ProviderCapabilities` types + per-provider
  capabilities.go for edge, elevenlabs, minimax, openai, gemini.
- Gemini TTS provider (client, models, voices, wav encoder,
  multi-speaker via SpeakerVoice, audio-tag aware prompts).
- TTSOptions gains `Params map[string]any` (read-only) + `Speakers`.
- `VoiceListProvider` interface decouples HTTP voice handler from
  provider-specific impls.
- `nested_keys.go` resolves dot-separated param paths for nested
  provider bodies (voice_settings.stability etc.).
- Characterization + defaults-invariant tests per provider lock
  nil-params byte-equivalence before new params land.
- `/v1/tts/capabilities` HTTP endpoint + integration coverage.
- Dual-read tests (PG + SQLite) for tts_config.

Frontend (web + desktop):
- `DynamicParamForm` with depends-on evaluation, split into
  fields/logic modules. Slider primitive added.
- `AudioTagPicker` + `MultiSpeakerEditor` for Gemini.
- `voice-picker` refactored toward portal UX; combobox tightened.
- tts-capabilities API client + typed hooks.
- i18n catalogs (en/vi/zh) expanded; parity tests guard key drift.
- TTS page reorganised (voice-model-section removed; playground +
  credentials + provider-setup split cleanly).

Docs: codebase-summary, project-changelog, tts-provider-capabilities.
2026-04-20 00:05:19 +07:00

71 lines
2.5 KiB
Go

package http
import (
"net/http"
"github.com/nextlevelbuilder/goclaw/internal/audio"
"github.com/nextlevelbuilder/goclaw/internal/audio/edge"
"github.com/nextlevelbuilder/goclaw/internal/audio/elevenlabs"
"github.com/nextlevelbuilder/goclaw/internal/audio/gemini"
"github.com/nextlevelbuilder/goclaw/internal/audio/minimax"
"github.com/nextlevelbuilder/goclaw/internal/audio/openai"
"github.com/nextlevelbuilder/goclaw/internal/permissions"
)
// ttsCapabilitiesResponse is the JSON envelope for GET /v1/tts/capabilities.
type ttsCapabilitiesResponse struct {
Providers []audio.ProviderCapabilities `json:"providers"`
}
// builtinTTSCapabilities returns the static capability catalog for every known
// TTS provider — independent of which providers happen to be registered in the
// manager at runtime. Frontend needs this so users can browse + configure a
// provider before saving credentials. Each provider's Capabilities() reads no
// receiver state, so calling on a zero-value Provider is safe.
func builtinTTSCapabilities() []audio.ProviderCapabilities {
return []audio.ProviderCapabilities{
(&openai.Provider{}).Capabilities(),
(&elevenlabs.TTSProvider{}).Capabilities(),
(&edge.Provider{}).Capabilities(),
(&minimax.Provider{}).Capabilities(),
(&gemini.Provider{}).Capabilities(),
}
}
// handleCapabilities serves GET /v1/tts/capabilities.
// Returns catalog-level metadata for all known TTS providers — registered
// providers in the manager take precedence; unregistered ones fall back to
// the static builtin catalog so users can configure a fresh provider.
// Response is tenant-agnostic.
func (h *TTSHandler) handleCapabilities(w http.ResponseWriter, r *http.Request) {
h.mu.RLock()
mgr := h.manager
h.mu.RUnlock()
var registered []audio.ProviderCapabilities
if mgr != nil {
registered = mgr.ListCapabilities()
}
seen := make(map[string]bool, len(registered))
caps := make([]audio.ProviderCapabilities, 0, 5)
for _, c := range registered {
caps = append(caps, c)
seen[c.Provider] = true
}
for _, c := range builtinTTSCapabilities() {
if !seen[c.Provider] {
caps = append(caps, c)
}
}
writeJSON(w, http.StatusOK, ttsCapabilitiesResponse{Providers: caps})
}
// registerCapabilitiesRoute wires GET /v1/tts/capabilities onto mux.
// Called from TTSHandler.RegisterRoutes to keep all TTS routes co-located.
func (h *TTSHandler) registerCapabilitiesRoute(mux *http.ServeMux) {
mux.HandleFunc("GET /v1/tts/capabilities",
requireAuth(permissions.RoleOperator, h.handleCapabilities))
}