Files
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

174 lines
6.2 KiB
Go

// Package audio unifies TTS, STT, Music, and SFX generation under a single
// Manager. Replaces internal/tts (surface preserved via internal/tts/alias.go
// backward-compat layer).
//
// Phase 1 delivers TTS parity; STT/Music/SFX interfaces ship as stubs —
// implementations land in Phase 3 (Music/SFX) and Phase 4 (STT).
package audio
import (
"context"
"errors"
"io"
)
// ErrStreamingNotSupported is returned when a caller requests streaming TTS
// on a provider that does not implement StreamingTTSProvider.
var ErrStreamingNotSupported = errors.New("streaming not supported")
// ---- TTS (implemented Phase 1) ----
// TTSProvider synthesizes text into audio bytes.
type TTSProvider interface {
Name() string
Synthesize(ctx context.Context, text string, opts TTSOptions) (*SynthResult, error)
}
// StreamingTTSProvider is the optional streaming extension of TTSProvider.
// Providers that support HTTP chunked streaming implement this to return
// bytes as they arrive, reducing time-to-first-byte for long texts.
type StreamingTTSProvider interface {
TTSProvider
SynthesizeStream(ctx context.Context, text string, opts TTSOptions) (*StreamResult, error)
}
// StreamResult is the output of a streaming TTS synthesis call. Callers MUST
// Close the Audio reader when finished to release the underlying HTTP body.
type StreamResult struct {
Audio io.ReadCloser // chunked audio stream — caller must Close
Extension string // file extension without dot: "mp3", "opus", "ogg"
MimeType string // e.g. "audio/mpeg", "audio/ogg"
}
// TTSOptions carries per-request synthesis parameters.
//
// Params is a read-only map view owned by the caller.
// Implementations MUST NOT mutate it. If a provider needs to merge
// defaults with caller values, it MUST copy before writing.
// READ-ONLY constraint is documented here and enforced by convention.
type TTSOptions struct {
Voice string // provider-specific voice ID
Model string // provider-specific model ID
Format string // output format: "mp3", "opus", etc.
Speakers []SpeakerVoice // multi-speaker TTS (Gemini); len>0 switches to multi mode
// Params is a READ-ONLY map owned by the caller. Provider implementations
// MUST NOT mutate it; copy before merging with defaults.
Params map[string]any // provider-specific knobs (nested keys like "voice_settings.stability" allowed)
}
// SpeakerVoice binds a speaker label to a voice ID for multi-speaker TTS.
// JSON tags match the frontend MultiSpeakerEditor shape so the persisted
// tts.gemini.speakers blob round-trips without transformation.
type SpeakerVoice struct {
Speaker string `json:"speaker"` // e.g. "host", "guest"
VoiceID string `json:"voiceId"` // provider-specific voice ID
}
// SynthResult is the output of a TTS synthesis call.
type SynthResult struct {
Audio []byte // raw audio bytes
Extension string // file extension without dot: "mp3", "opus", "ogg"
MimeType string // e.g. "audio/mpeg", "audio/ogg"
}
// AutoMode controls when TTS is automatically applied to replies.
type AutoMode string
const (
AutoOff AutoMode = "off" // Disabled
AutoAlways AutoMode = "always" // Apply to all eligible replies
AutoInbound AutoMode = "inbound" // Only if user sent audio/voice
AutoTagged AutoMode = "tagged" // Only if reply contains [[tts]] directive
)
// Mode controls which reply kinds get TTS.
type Mode string
const (
ModeFinal Mode = "final" // Only final replies (default)
ModeAll Mode = "all" // All replies including tool/block
)
// VoiceListProvider is implemented by TTS providers that support dynamic
// voice listing. The HTTP voices handler uses this interface so it is not
// coupled to a specific provider implementation.
type VoiceListProvider interface {
// ListVoices returns the provider's available voices.
// Implementations may cache results internally.
ListVoices(ctx context.Context) ([]Voice, error)
}
// ---- STT (Phase 4) ----
// STTProvider transcribes audio bytes to text.
type STTProvider interface {
Name() string
Transcribe(ctx context.Context, in STTInput, opts STTOptions) (*TranscriptResult, error)
}
// STTInput is the audio to transcribe. Bytes or FilePath may be set; FilePath
// preferred for large files to avoid memory pressure.
type STTInput struct {
Bytes []byte // raw audio bytes (in-memory)
FilePath string // path on disk (preferred for >1MB files)
MimeType string // e.g. "audio/ogg"
Filename string // original filename (used for multipart form)
}
// STTOptions tunes transcription.
type STTOptions struct {
Language string // BCP-47/ISO-639-1 hint, empty = auto-detect
ModelID string // provider-specific model ID (default "scribe_v1")
Diarize bool // enable speaker diarization
TimeoutMs int // per-call timeout override; 0 = provider default
}
// TranscriptResult is the output of transcription.
type TranscriptResult struct {
Text string // transcribed text
Language string // detected or hinted language
Duration float64 // audio duration in seconds (if returned by provider)
Provider string // provider name that produced the transcript
}
// ---- Music (stubs — implementations land in Phase 3) ----
// MusicProvider generates music from prompt + optional lyrics.
type MusicProvider interface {
Name() string
GenerateMusic(ctx context.Context, opts MusicOptions) (*AudioResult, error)
}
// MusicOptions controls music generation.
type MusicOptions struct {
Prompt string
Lyrics string
Instrumental bool
Duration int // seconds (ElevenLabs: converts to music_length_ms)
Model string // provider-specific model override
TimeoutSec int // 0 = provider default
}
// ---- SFX (implementations land in Phase 3) ----
// SFXProvider generates short sound effects from a prompt.
type SFXProvider interface {
Name() string
GenerateSFX(ctx context.Context, opts SFXOptions) (*AudioResult, error)
}
// SFXOptions controls SFX generation.
type SFXOptions struct {
Prompt string
Duration int // seconds (provider may cap)
}
// AudioResult is the shared output of music/SFX generation.
type AudioResult struct {
Audio []byte
Extension string
MimeType string
Model string // actual model used (optional, for observability)
Provider string // provider name that produced the audio (optional, for observability)
}