// Package voice provides speech-to-text and text-to-speech capabilities. // TTS providers form a fallback chain: cloud (Bailian/OpenAI-compatible), // local self-hosted HTTP server, edge-tts CLI, espeak CLI, silent. // STT uses a Whisper-compatible /audio/transcriptions endpoint // (cloud or self-hosted, e.g. faster-whisper). package voice import ( "context" "net/http" "time" ) // STTProvider converts speech audio to text. type STTProvider interface { // Name returns the provider identifier. Name() string // "whisper-api", "silent" // Recognize converts audio data to text. Recognize(ctx context.Context, audioData []byte) (string, error) } // TTSProvider converts text to speech audio. type TTSProvider interface { // Name returns the provider identifier. Name() string // "cloud", "local", "edge-tts", "espeak", "silent" // Synthesize converts text to audio bytes. Synthesize(ctx context.Context, text string, voice string) ([]byte, error) } // Service manages STT and TTS providers with fallback chains. type Service struct { sttProvider STTProvider ttsProviders []TTSProvider // Ordered by priority (first = preferred) } // Config holds voice service configuration. type Config struct { // STT (Whisper-compatible endpoint; works with Bailian compatible mode // or a self-hosted faster-whisper server) STTApiURL string // e.g. "https://dashscope.aliyuncs.com/compatible-mode/v1" STTApiKey string STTModel string // e.g. "whisper-large-v3" // TTS fallback chain, e.g. ["cloud", "local", "edge-tts", "silent"] TTSProviders []string TTSCloudURL string // OpenAI-compatible base URL for cloud TTS TTSCloudKey string TTSCloudModel string // e.g. "cosyvoice-v1" (Bailian) / "tts-1" (OpenAI) TTSCloudVoice string // e.g. "longxiaochun" TTSLocalURL string // self-hosted TTS POST endpoint } // New creates a new voice service. func New(cfg Config) *Service { svc := &Service{} // STT if cfg.STTApiURL != "" && cfg.STTApiKey != "" { model := cfg.STTModel if model == "" { model = "whisper-large-v3" } svc.sttProvider = &whisperSTT{ baseURL: cfg.STTApiURL, apiKey: cfg.STTApiKey, model: model, client: &http.Client{Timeout: 60 * time.Second}, } } else { svc.sttProvider = &silentSTT{} } // TTS: build fallback chain for _, name := range cfg.TTSProviders { switch name { case "cloud": if cfg.TTSCloudURL != "" && cfg.TTSCloudKey != "" { svc.ttsProviders = append(svc.ttsProviders, &cloudTTS{ baseURL: cfg.TTSCloudURL, apiKey: cfg.TTSCloudKey, model: cfg.TTSCloudModel, defaultVoice: cfg.TTSCloudVoice, client: &http.Client{Timeout: 60 * time.Second}, }) } case "local": if cfg.TTSLocalURL != "" { svc.ttsProviders = append(svc.ttsProviders, &localTTS{ url: cfg.TTSLocalURL, client: &http.Client{Timeout: 60 * time.Second}, }) } case "edge-tts": if e := newEdgeTTS(); e.available() { svc.ttsProviders = append(svc.ttsProviders, e) } case "espeak": if e := newEspeakTTS(); e.bin != "" { svc.ttsProviders = append(svc.ttsProviders, e) } case "silent": svc.ttsProviders = append(svc.ttsProviders, &silentTTS{}) } } if len(svc.ttsProviders) == 0 { svc.ttsProviders = append(svc.ttsProviders, &silentTTS{}) } return svc } // TTSProviderNames returns the active TTS fallback chain. func (s *Service) TTSProviderNames() []string { names := make([]string, len(s.ttsProviders)) for i, p := range s.ttsProviders { names[i] = p.Name() } return names } // Recognize performs speech recognition with the configured STT provider. func (s *Service) Recognize(ctx context.Context, audioData []byte) (string, error) { return s.sttProvider.Recognize(ctx, audioData) } // Synthesize performs TTS with automatic fallback through the provider chain. func (s *Service) Synthesize(ctx context.Context, text string, voice string) ([]byte, error) { var lastErr error for _, p := range s.ttsProviders { audio, err := p.Synthesize(ctx, text, voice) if err == nil { return audio, nil } lastErr = err } return nil, lastErr } // --- Silent fallbacks --- type silentSTT struct{} func (p *silentSTT) Name() string { return "silent" } func (p *silentSTT) Recognize(_ context.Context, _ []byte) (string, error) { return "", nil } type silentTTS struct{} func (p *silentTTS) Name() string { return "silent" } func (p *silentTTS) Synthesize(_ context.Context, _ string, _ string) ([]byte, error) { return make([]byte, 0), nil }