Integrating asr/tts models w/ new security config

This commit is contained in:
Huaaudio 2026-03-28 02:38:32 +01:00
parent 1e8cabb063
commit 33b7729252
6 changed files with 38 additions and 23 deletions

View file

@ -46,20 +46,23 @@ func supportsAudioTranscription(model string) bool {
func DetectTranscriber(cfg *config.Config) Transcriber { func DetectTranscriber(cfg *config.Config) Transcriber {
if modelName := strings.TrimSpace(cfg.Voice.ModelName); modelName != "" { if modelName := strings.TrimSpace(cfg.Voice.ModelName); modelName != "" {
modelCfg, err := cfg.GetModelConfig(modelName) modelCfg, err := cfg.GetModelConfig(modelName)
if err != nil { if err == nil {
return nil protocol, _ := providers.ExtractProtocol(modelCfg.Model)
if protocol == "elevenlabs" && modelCfg.APIKey() != "" {
return NewElevenLabsTranscriber(modelCfg.APIKey(), modelCfg.APIBase)
} }
if supportsAudioTranscription(modelCfg.Model) { if supportsAudioTranscription(modelCfg.Model) {
return NewAudioModelTranscriber(modelCfg) return NewAudioModelTranscriber(modelCfg)
} }
} }
// ElevenLabs voice config (supports Scribe STT).
if key := strings.TrimSpace(cfg.Voice.ElevenLabsAPIKey); key != "" {
return NewElevenLabsTranscriber(key)
} }
// Fall back to any model-list entry that uses the groq/ protocol.
// Fall back to scanning ModelList for suitable ASR providers
for _, mc := range cfg.ModelList { for _, mc := range cfg.ModelList {
protocol, _ := providers.ExtractProtocol(mc.Model)
if protocol == "elevenlabs" && mc.APIKey() != "" {
return NewElevenLabsTranscriber(mc.APIKey(), mc.APIBase)
}
if (strings.HasPrefix(mc.Model, "groq/") || mc.ModelName == "groq" || mc.Model == "whisper-large-v3-turbo") && if (strings.HasPrefix(mc.Model, "groq/") || mc.ModelName == "groq" || mc.Model == "whisper-large-v3-turbo") &&
mc.APIKey() != "" { mc.APIKey() != "" {
return NewGroqTranscriber(mc.APIKey()) return NewGroqTranscriber(mc.APIKey())

View file

@ -127,15 +127,17 @@ func TestDetectTranscriber(t *testing.T) {
{ {
name: "elevenlabs voice config key", name: "elevenlabs voice config key",
cfg: &config.Config{ cfg: &config.Config{
Voice: config.VoiceConfig{ElevenLabsAPIKey: "sk_elevenlabs_test"}, ModelList: []*config.ModelConfig{
{Model: "elevenlabs/scribe_v1", APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test")},
},
}, },
wantName: "elevenlabs", wantName: "elevenlabs",
}, },
{ {
name: "elevenlabs takes priority over groq model list", name: "elevenlabs takes priority over groq model list",
cfg: &config.Config{ cfg: &config.Config{
Voice: config.VoiceConfig{ElevenLabsAPIKey: "sk_elevenlabs_test"},
ModelList: []*config.ModelConfig{ ModelList: []*config.ModelConfig{
{Model: "elevenlabs/scribe_v1", APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test")},
{ {
ModelName: "groq", ModelName: "groq",
Model: "groq/llama-3.3-70b", Model: "groq/llama-3.3-70b",
@ -150,9 +152,9 @@ func TestDetectTranscriber(t *testing.T) {
cfg: &config.Config{ cfg: &config.Config{
Voice: config.VoiceConfig{ Voice: config.VoiceConfig{
ModelName: "voice-gemini", ModelName: "voice-gemini",
ElevenLabsAPIKey: "sk_elevenlabs_test",
}, },
ModelList: []*config.ModelConfig{ ModelList: []*config.ModelConfig{
{Model: "elevenlabs", APIKeys: config.SimpleSecureStrings("sk_elevenlabs_test")},
{ {
ModelName: "voice-gemini", ModelName: "voice-gemini",
Model: "gemini/gemini-2.5-flash", Model: "gemini/gemini-2.5-flash",

View file

@ -23,12 +23,16 @@ type ElevenLabsTranscriber struct {
httpClient *http.Client httpClient *http.Client
} }
func NewElevenLabsTranscriber(apiKey string) *ElevenLabsTranscriber { func NewElevenLabsTranscriber(apiKey, apiBase string) *ElevenLabsTranscriber {
logger.DebugCF("voice", "Creating ElevenLabs transcriber", map[string]any{"has_api_key": apiKey != ""}) logger.DebugCF("voice", "Creating ElevenLabs transcriber", map[string]any{"has_api_key": apiKey != ""})
if apiBase == "" {
apiBase = "https://api.elevenlabs.io"
}
return &ElevenLabsTranscriber{ return &ElevenLabsTranscriber{
apiKey: apiKey, apiKey: apiKey,
apiBase: "https://api.elevenlabs.io", apiBase: apiBase,
httpClient: &http.Client{ httpClient: &http.Client{
Timeout: 120 * time.Second, Timeout: 120 * time.Second,
}, },

View file

@ -14,7 +14,7 @@ import (
var _ Transcriber = (*ElevenLabsTranscriber)(nil) var _ Transcriber = (*ElevenLabsTranscriber)(nil)
func TestElevenLabsTranscriberName(t *testing.T) { func TestElevenLabsTranscriberName(t *testing.T) {
tr := NewElevenLabsTranscriber("sk_test") tr := NewElevenLabsTranscriber("sk_test", "")
if got := tr.Name(); got != "elevenlabs" { if got := tr.Name(); got != "elevenlabs" {
t.Errorf("Name() = %q, want %q", got, "elevenlabs") t.Errorf("Name() = %q, want %q", got, "elevenlabs")
} }
@ -43,7 +43,7 @@ func TestElevenLabsTranscribe(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
tr := NewElevenLabsTranscriber("sk_test") tr := NewElevenLabsTranscriber("sk_test", "")
tr.apiBase = srv.URL tr.apiBase = srv.URL
resp, err := tr.Transcribe(context.Background(), audioPath) resp, err := tr.Transcribe(context.Background(), audioPath)
@ -64,7 +64,7 @@ func TestElevenLabsTranscribe(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
tr := NewElevenLabsTranscriber("sk_bad") tr := NewElevenLabsTranscriber("sk_bad", "")
tr.apiBase = srv.URL tr.apiBase = srv.URL
_, err := tr.Transcribe(context.Background(), audioPath) _, err := tr.Transcribe(context.Background(), audioPath)
@ -74,7 +74,7 @@ func TestElevenLabsTranscribe(t *testing.T) {
}) })
t.Run("missing file", func(t *testing.T) { t.Run("missing file", func(t *testing.T) {
tr := NewElevenLabsTranscriber("sk_test") tr := NewElevenLabsTranscriber("sk_test", "")
_, err := tr.Transcribe(context.Background(), filepath.Join(tmpDir, "nonexistent.ogg")) _, err := tr.Transcribe(context.Background(), filepath.Join(tmpDir, "nonexistent.ogg"))
if err == nil { if err == nil {
t.Fatal("expected error for missing file, got nil") t.Fatal("expected error for missing file, got nil")

View file

@ -19,6 +19,12 @@ type TTSProvider interface {
} }
func DetectTTS(cfg *config.Config) TTSProvider { func DetectTTS(cfg *config.Config) TTSProvider {
if modelName := strings.TrimSpace(cfg.Voice.TTSModelName); modelName != "" {
if mc, err := cfg.GetModelConfig(modelName); err == nil && mc.APIKey() != "" {
return NewOpenAITTSProvider(mc.APIKey(), mc.APIBase, mc.Proxy)
}
}
for _, mc := range cfg.ModelList { for _, mc := range cfg.ModelList {
if strings.Contains(strings.ToLower(mc.Model), "tts") && mc.APIKey() != "" { if strings.Contains(strings.ToLower(mc.Model), "tts") && mc.APIKey() != "" {
return NewOpenAITTSProvider(mc.APIKey(), mc.APIBase, mc.Proxy) return NewOpenAITTSProvider(mc.APIKey(), mc.APIBase, mc.Proxy)

View file

@ -640,8 +640,8 @@ type DevicesConfig struct {
type VoiceConfig struct { type VoiceConfig struct {
ModelName string `json:"model_name,omitempty" env:"PICOCLAW_VOICE_MODEL_NAME"` ModelName string `json:"model_name,omitempty" env:"PICOCLAW_VOICE_MODEL_NAME"`
TTSModelName string `json:"tts_model_name,omitempty" env:"PICOCLAW_VOICE_TTS_MODEL_NAME"`
EchoTranscription bool `json:"echo_transcription" env:"PICOCLAW_VOICE_ECHO_TRANSCRIPTION"` EchoTranscription bool `json:"echo_transcription" env:"PICOCLAW_VOICE_ECHO_TRANSCRIPTION"`
ElevenLabsAPIKey string `json:"elevenlabs_api_key,omitempty" env:"PICOCLAW_VOICE_ELEVENLABS_API_KEY"`
} }
// ModelConfig represents a model-centric provider configuration. // ModelConfig represents a model-centric provider configuration.