feat: refactor
This commit is contained in:
@@ -4,7 +4,7 @@ package provider
|
||||
type DeepgramProvider struct{}
|
||||
|
||||
func (p *DeepgramProvider) Name() string {
|
||||
return "deepgram"
|
||||
return ProviderDeepgram
|
||||
}
|
||||
|
||||
func (p *DeepgramProvider) RequiresAPIKey() bool {
|
||||
@@ -43,25 +43,15 @@ func (p *DeepgramProvider) Models() []Model {
|
||||
{
|
||||
ID: "nova-3",
|
||||
Name: "Nova-3",
|
||||
Description: "Best accuracy, 40+ languages, real-time",
|
||||
Description: "Best accuracy, 40+ languages",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "deepgram",
|
||||
AdapterType: AdapterDeepgram,
|
||||
SupportedLanguages: nova3Langs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.deepgram.com", Path: "/v1/listen"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
ID: "nova-3-general",
|
||||
Name: "Nova-3 General",
|
||||
Description: "General purpose, same as nova-3",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
Local: false,
|
||||
AdapterType: "deepgram",
|
||||
SupportedLanguages: nova3Langs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.deepgram.com", Path: "/v1/listen"},
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.deepgram.com", Path: "/v1/listen"},
|
||||
StreamingEndpoint: &EndpointConfig{BaseURL: "wss://api.deepgram.com", Path: "/v1/listen"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
@@ -69,23 +59,13 @@ func (p *DeepgramProvider) Models() []Model {
|
||||
Name: "Nova-2",
|
||||
Description: "Fast, 30+ languages, filler words",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "deepgram",
|
||||
AdapterType: AdapterDeepgram,
|
||||
SupportedLanguages: nova2Langs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.deepgram.com", Path: "/v1/listen"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
ID: "nova-2-general",
|
||||
Name: "Nova-2 General",
|
||||
Description: "General purpose, same as nova-2",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
Local: false,
|
||||
AdapterType: "deepgram",
|
||||
SupportedLanguages: nova2Langs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.deepgram.com", Path: "/v1/listen"},
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.deepgram.com", Path: "/v1/listen"},
|
||||
StreamingEndpoint: &EndpointConfig{BaseURL: "wss://api.deepgram.com", Path: "/v1/listen"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,14 +25,20 @@ func TestDeepgramProvider_Models(t *testing.T) {
|
||||
p := &DeepgramProvider{}
|
||||
models := p.Models()
|
||||
|
||||
if len(models) != 4 {
|
||||
t.Errorf("Models() returned %d models, want 4", len(models))
|
||||
if len(models) != 2 {
|
||||
t.Errorf("Models() returned %d models, want 2", len(models))
|
||||
}
|
||||
|
||||
// all models should be streaming
|
||||
// all models should support both batch and streaming
|
||||
for _, m := range models {
|
||||
if !m.Streaming {
|
||||
t.Errorf("model %s should be streaming", m.ID)
|
||||
if !m.SupportsBatch {
|
||||
t.Errorf("model %s should support batch", m.ID)
|
||||
}
|
||||
if !m.SupportsStreaming {
|
||||
t.Errorf("model %s should support streaming", m.ID)
|
||||
}
|
||||
if !m.SupportsBothModes() {
|
||||
t.Errorf("model %s should support both modes", m.ID)
|
||||
}
|
||||
if m.AdapterType != "deepgram" {
|
||||
t.Errorf("model %s has AdapterType %q, want 'deepgram'", m.ID, m.AdapterType)
|
||||
@@ -96,15 +102,28 @@ func TestDeepgramProvider_Endpoint(t *testing.T) {
|
||||
models := p.Models()
|
||||
|
||||
for _, m := range models {
|
||||
// batch endpoint (HTTP)
|
||||
if m.Endpoint == nil {
|
||||
t.Errorf("model %s has nil Endpoint", m.ID)
|
||||
continue
|
||||
}
|
||||
if m.Endpoint.BaseURL != "wss://api.deepgram.com" {
|
||||
t.Errorf("model %s has BaseURL %q, want 'wss://api.deepgram.com'", m.ID, m.Endpoint.BaseURL)
|
||||
if m.Endpoint.BaseURL != "https://api.deepgram.com" {
|
||||
t.Errorf("model %s has Endpoint.BaseURL %q, want 'https://api.deepgram.com'", m.ID, m.Endpoint.BaseURL)
|
||||
}
|
||||
if m.Endpoint.Path != "/v1/listen" {
|
||||
t.Errorf("model %s has Path %q, want '/v1/listen'", m.ID, m.Endpoint.Path)
|
||||
t.Errorf("model %s has Endpoint.Path %q, want '/v1/listen'", m.ID, m.Endpoint.Path)
|
||||
}
|
||||
|
||||
// streaming endpoint (WebSocket)
|
||||
if m.StreamingEndpoint == nil {
|
||||
t.Errorf("model %s has nil StreamingEndpoint", m.ID)
|
||||
continue
|
||||
}
|
||||
if m.StreamingEndpoint.BaseURL != "wss://api.deepgram.com" {
|
||||
t.Errorf("model %s has StreamingEndpoint.BaseURL %q, want 'wss://api.deepgram.com'", m.ID, m.StreamingEndpoint.BaseURL)
|
||||
}
|
||||
if m.StreamingEndpoint.Path != "/v1/listen" {
|
||||
t.Errorf("model %s has StreamingEndpoint.Path %q, want '/v1/listen'", m.ID, m.StreamingEndpoint.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "github.com/leonardotrapani/hyprvoice/internal/language"
|
||||
type ElevenLabsProvider struct{}
|
||||
|
||||
func (p *ElevenLabsProvider) Name() string {
|
||||
return "elevenlabs"
|
||||
return ProviderElevenLabs
|
||||
}
|
||||
|
||||
func (p *ElevenLabsProvider) RequiresAPIKey() bool {
|
||||
@@ -29,15 +29,15 @@ func (p *ElevenLabsProvider) Models() []Model {
|
||||
docsURL := "https://elevenlabs.io/docs/capabilities/speech-to-text#supported-languages"
|
||||
|
||||
return []Model{
|
||||
// batch models
|
||||
{
|
||||
ID: "scribe_v1",
|
||||
Name: "Scribe v1",
|
||||
Description: "90+ languages, best accuracy",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "elevenlabs",
|
||||
AdapterType: AdapterElevenLabs,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.elevenlabs.io", Path: "/v1/speech-to-text"},
|
||||
DocsURL: docsURL,
|
||||
@@ -45,36 +45,25 @@ func (p *ElevenLabsProvider) Models() []Model {
|
||||
{
|
||||
ID: "scribe_v2",
|
||||
Name: "Scribe v2",
|
||||
Description: "Lower latency, real-time optimized",
|
||||
Description: "Lower latency batch transcription",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "elevenlabs",
|
||||
AdapterType: AdapterElevenLabs,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.elevenlabs.io", Path: "/v1/speech-to-text"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
// streaming models
|
||||
{
|
||||
ID: "scribe_v1-streaming",
|
||||
Name: "Scribe v1 Streaming",
|
||||
Description: "Real-time transcription, 90+ languages",
|
||||
ID: "scribe_v2_realtime",
|
||||
Name: "Scribe v2 Realtime",
|
||||
Description: "Real-time streaming, <150ms latency",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
SupportsBatch: false,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "elevenlabs-streaming",
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.elevenlabs.io", Path: "/v1/speech-to-text/realtime"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
ID: "scribe_v2-streaming",
|
||||
Name: "Scribe v2 Streaming",
|
||||
Description: "Real-time with <150ms latency",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
Local: false,
|
||||
AdapterType: "elevenlabs-streaming",
|
||||
AdapterType: AdapterElevenLabsStream,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.elevenlabs.io", Path: "/v1/speech-to-text/realtime"},
|
||||
DocsURL: docsURL,
|
||||
|
||||
+16
-23
@@ -10,7 +10,7 @@ import (
|
||||
type GroqProvider struct{}
|
||||
|
||||
func (p *GroqProvider) Name() string {
|
||||
return "groq"
|
||||
return ProviderGroq
|
||||
}
|
||||
|
||||
func (p *GroqProvider) RequiresAPIKey() bool {
|
||||
@@ -36,9 +36,10 @@ func (p *GroqProvider) Models() []Model {
|
||||
Name: "Whisper Large v3",
|
||||
Description: "Full Whisper v3 model, best accuracy",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
@@ -48,34 +49,24 @@ func (p *GroqProvider) Models() []Model {
|
||||
Name: "Whisper Large v3 Turbo",
|
||||
Description: "Faster Whisper v3 with slightly lower accuracy",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
ID: "distil-whisper-large-v3-en",
|
||||
Name: "Distil Whisper Large v3 EN",
|
||||
Description: "English-only, fastest option",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
SupportedLanguages: []string{"en"},
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
// LLM models
|
||||
{
|
||||
ID: "llama-3.3-70b-versatile",
|
||||
Name: "Llama 3.3 70B Versatile",
|
||||
Description: "Most capable Llama model",
|
||||
Type: LLM,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/chat/completions"},
|
||||
},
|
||||
@@ -84,9 +75,10 @@ func (p *GroqProvider) Models() []Model {
|
||||
Name: "Llama 3.1 8B Instant",
|
||||
Description: "Fast and efficient",
|
||||
Type: LLM,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/chat/completions"},
|
||||
},
|
||||
@@ -95,9 +87,10 @@ func (p *GroqProvider) Models() []Model {
|
||||
Name: "Mixtral 8x7B",
|
||||
Description: "Mixture of experts model",
|
||||
Type: LLM,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/chat/completions"},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import "github.com/leonardotrapani/hyprvoice/internal/language"
|
||||
type MistralProvider struct{}
|
||||
|
||||
func (p *MistralProvider) Name() string {
|
||||
return "mistral"
|
||||
return ProviderMistral
|
||||
}
|
||||
|
||||
func (p *MistralProvider) RequiresAPIKey() bool {
|
||||
@@ -32,9 +32,11 @@ func (p *MistralProvider) Models() []Model {
|
||||
Name: "Voxtral Mini Latest",
|
||||
Description: "Latest Voxtral model, best for most uses",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
StreamingAdapter: "mistral-streaming", // not yet implemented
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.mistral.ai", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
@@ -44,9 +46,11 @@ func (p *MistralProvider) Models() []Model {
|
||||
Name: "Voxtral Mini 2507",
|
||||
Description: "Stable Voxtral version from July 2025",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
StreamingAdapter: "mistral-streaming", // not yet implemented
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.mistral.ai", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
|
||||
@@ -16,9 +16,12 @@ type Model struct {
|
||||
Name string // display name (e.g., "Whisper 1", "GPT-4o Mini")
|
||||
Description string // short description
|
||||
Type ModelType // transcription or LLM
|
||||
Streaming bool // supports streaming
|
||||
SupportsBatch bool // can do batch/non-streaming transcription
|
||||
SupportsStreaming bool // can do real-time streaming transcription
|
||||
Local bool // runs locally (no API call)
|
||||
AdapterType string // which adapter to use (e.g., "openai", "elevenlabs", "whisper-cpp")
|
||||
StreamingAdapter string // adapter for streaming mode (if different from AdapterType)
|
||||
StreamingEndpoint *EndpointConfig // endpoint for streaming mode (if different from Endpoint)
|
||||
SupportedLanguages []string // explicit list of supported language codes
|
||||
Endpoint *EndpointConfig // nil for local models
|
||||
LocalInfo *LocalModelInfo // nil for cloud models
|
||||
@@ -45,7 +48,12 @@ func (m *Model) NeedsDownload() bool {
|
||||
|
||||
// IsStreaming returns true if this model supports streaming
|
||||
func (m *Model) IsStreaming() bool {
|
||||
return m.Streaming
|
||||
return m.SupportsStreaming
|
||||
}
|
||||
|
||||
// SupportsBothModes returns true if this model supports both batch and streaming
|
||||
func (m *Model) SupportsBothModes() bool {
|
||||
return m.SupportsBatch && m.SupportsStreaming
|
||||
}
|
||||
|
||||
// SupportsLanguage returns true if the model supports the given language code.
|
||||
|
||||
@@ -60,15 +60,20 @@ func TestModel_IsStreaming(t *testing.T) {
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "streaming model",
|
||||
model: Model{ID: "scribe_v1-streaming", Streaming: true},
|
||||
name: "streaming-only model",
|
||||
model: Model{ID: "scribe_v2_realtime", SupportsBatch: false, SupportsStreaming: true},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "batch model",
|
||||
model: Model{ID: "whisper-1", Streaming: false},
|
||||
name: "batch-only model",
|
||||
model: Model{ID: "whisper-1", SupportsBatch: true, SupportsStreaming: false},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "both modes model",
|
||||
model: Model{ID: "nova-3", SupportsBatch: true, SupportsStreaming: true},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -80,6 +85,38 @@ func TestModel_IsStreaming(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_SupportsBothModes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model Model
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "streaming-only model",
|
||||
model: Model{ID: "scribe_v2_realtime", SupportsBatch: false, SupportsStreaming: true},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "batch-only model",
|
||||
model: Model{ID: "whisper-1", SupportsBatch: true, SupportsStreaming: false},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "both modes model",
|
||||
model: Model{ID: "nova-3", SupportsBatch: true, SupportsStreaming: true},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.model.SupportsBothModes(); got != tc.expected {
|
||||
t.Errorf("SupportsBothModes() = %v, want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModel_SupportsLanguage(t *testing.T) {
|
||||
allCodes := language.AllLanguageCodes()
|
||||
|
||||
@@ -283,14 +320,20 @@ func TestModel_AllFields(t *testing.T) {
|
||||
Name: "Test Model",
|
||||
Description: "A test model for verification",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: true,
|
||||
AdapterType: "test-adapter",
|
||||
StreamingAdapter: "test-streaming-adapter",
|
||||
SupportedLanguages: []string{"en", "es"},
|
||||
Endpoint: &EndpointConfig{
|
||||
BaseURL: "https://api.test.com",
|
||||
Path: "/v1/test",
|
||||
},
|
||||
StreamingEndpoint: &EndpointConfig{
|
||||
BaseURL: "wss://api.test.com",
|
||||
Path: "/v1/stream",
|
||||
},
|
||||
LocalInfo: &LocalModelInfo{
|
||||
Filename: "test.bin",
|
||||
Size: "100MB",
|
||||
@@ -311,8 +354,11 @@ func TestModel_AllFields(t *testing.T) {
|
||||
if model.Type != Transcription {
|
||||
t.Errorf("Type = %v, want Transcription", model.Type)
|
||||
}
|
||||
if !model.Streaming {
|
||||
t.Error("Streaming should be true")
|
||||
if !model.SupportsBatch {
|
||||
t.Error("SupportsBatch should be true")
|
||||
}
|
||||
if !model.SupportsStreaming {
|
||||
t.Error("SupportsStreaming should be true")
|
||||
}
|
||||
if !model.Local {
|
||||
t.Error("Local should be true")
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package provider
|
||||
|
||||
// Provider name constants for config and registry
|
||||
const (
|
||||
ProviderOpenAI = "openai"
|
||||
ProviderGroq = "groq"
|
||||
ProviderMistral = "mistral"
|
||||
ProviderElevenLabs = "elevenlabs"
|
||||
ProviderDeepgram = "deepgram"
|
||||
ProviderWhisperCpp = "whisper-cpp"
|
||||
)
|
||||
|
||||
// Config provider names (used in config file transcription.provider)
|
||||
const (
|
||||
ConfigProviderOpenAI = "openai"
|
||||
ConfigProviderGroqTranscription = "groq-transcription"
|
||||
ConfigProviderGroqTranslation = "groq-translation"
|
||||
ConfigProviderMistralTranscription = "mistral-transcription"
|
||||
ConfigProviderElevenLabs = "elevenlabs"
|
||||
ConfigProviderDeepgram = "deepgram"
|
||||
ConfigProviderWhisperCpp = "whisper-cpp"
|
||||
)
|
||||
|
||||
// Environment variable names for API keys
|
||||
const (
|
||||
EnvOpenAIKey = "OPENAI_API_KEY"
|
||||
EnvGroqKey = "GROQ_API_KEY"
|
||||
EnvMistralKey = "MISTRAL_API_KEY"
|
||||
EnvElevenLabsKey = "ELEVENLABS_API_KEY"
|
||||
EnvDeepgramKey = "DEEPGRAM_API_KEY"
|
||||
)
|
||||
|
||||
// Adapter type constants for transcription backends
|
||||
const (
|
||||
AdapterOpenAI = "openai"
|
||||
AdapterElevenLabs = "elevenlabs"
|
||||
AdapterElevenLabsStream = "elevenlabs-streaming"
|
||||
AdapterDeepgram = "deepgram"
|
||||
AdapterWhisperCpp = "whisper-cpp"
|
||||
AdapterOpenAIRealtime = "openai-realtime"
|
||||
)
|
||||
|
||||
// BaseProviderName maps config provider names to registry provider names
|
||||
// e.g. "groq-transcription" -> "groq", "mistral-transcription" -> "mistral"
|
||||
func BaseProviderName(configProvider string) string {
|
||||
switch configProvider {
|
||||
case ConfigProviderGroqTranscription, ConfigProviderGroqTranslation:
|
||||
return ProviderGroq
|
||||
case ConfigProviderMistralTranscription:
|
||||
return ProviderMistral
|
||||
default:
|
||||
return configProvider
|
||||
}
|
||||
}
|
||||
|
||||
// EnvVarForProvider returns the environment variable name for a provider's API key
|
||||
func EnvVarForProvider(provider string) string {
|
||||
base := BaseProviderName(provider)
|
||||
switch base {
|
||||
case ProviderOpenAI:
|
||||
return EnvOpenAIKey
|
||||
case ProviderGroq:
|
||||
return EnvGroqKey
|
||||
case ProviderMistral:
|
||||
return EnvMistralKey
|
||||
case ProviderElevenLabs:
|
||||
return EnvElevenLabsKey
|
||||
case ProviderDeepgram:
|
||||
return EnvDeepgramKey
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
+20
-23
@@ -10,7 +10,7 @@ import (
|
||||
type OpenAIProvider struct{}
|
||||
|
||||
func (p *OpenAIProvider) Name() string {
|
||||
return "openai"
|
||||
return ProviderOpenAI
|
||||
}
|
||||
|
||||
func (p *OpenAIProvider) RequiresAPIKey() bool {
|
||||
@@ -37,9 +37,10 @@ func (p *OpenAIProvider) Models() []Model {
|
||||
Name: "Whisper 1",
|
||||
Description: "OpenAI's production speech-to-text model",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
@@ -49,11 +50,14 @@ func (p *OpenAIProvider) Models() []Model {
|
||||
Name: "GPT-4o Transcribe",
|
||||
Description: "High quality transcription with GPT-4o",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
StreamingAdapter: AdapterOpenAIRealtime,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"},
|
||||
StreamingEndpoint: &EndpointConfig{BaseURL: "wss://api.openai.com", Path: "/v1/realtime"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
@@ -61,23 +65,14 @@ func (p *OpenAIProvider) Models() []Model {
|
||||
Name: "GPT-4o Mini Transcribe",
|
||||
Description: "Fast transcription with GPT-4o Mini",
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: true,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
StreamingAdapter: AdapterOpenAIRealtime,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
{
|
||||
ID: "gpt-4o-realtime-preview",
|
||||
Name: "GPT-4o Realtime",
|
||||
Description: "Real-time streaming transcription with GPT-4o",
|
||||
Type: Transcription,
|
||||
Streaming: true,
|
||||
Local: false,
|
||||
AdapterType: "openai-realtime",
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "wss://api.openai.com", Path: "/v1/realtime"},
|
||||
StreamingEndpoint: &EndpointConfig{BaseURL: "wss://api.openai.com", Path: "/v1/realtime"},
|
||||
DocsURL: docsURL,
|
||||
},
|
||||
// LLM models
|
||||
@@ -86,9 +81,10 @@ func (p *OpenAIProvider) Models() []Model {
|
||||
Name: "GPT-4o Mini",
|
||||
Description: "Fast and affordable GPT-4 variant",
|
||||
Type: LLM,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/chat/completions"},
|
||||
},
|
||||
@@ -97,9 +93,10 @@ func (p *OpenAIProvider) Models() []Model {
|
||||
Name: "GPT-4o",
|
||||
Description: "Most capable GPT-4 model",
|
||||
Type: LLM,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: false,
|
||||
AdapterType: "openai",
|
||||
AdapterType: AdapterOpenAI,
|
||||
SupportedLanguages: allLangs,
|
||||
Endpoint: &EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/chat/completions"},
|
||||
},
|
||||
|
||||
@@ -177,9 +177,9 @@ func TestModelsOfType(t *testing.T) {
|
||||
trans := ModelsOfType(p, Transcription)
|
||||
llm := ModelsOfType(p, LLM)
|
||||
|
||||
// OpenAI has 4 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe, gpt-4o-realtime-preview
|
||||
if len(trans) != 4 {
|
||||
t.Errorf("ModelsOfType(Transcription) = %d, want 4", len(trans))
|
||||
// OpenAI has 3 transcription models: whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe
|
||||
if len(trans) != 3 {
|
||||
t.Errorf("ModelsOfType(Transcription) = %d, want 3", len(trans))
|
||||
}
|
||||
// OpenAI has 2 LLM models: gpt-4o-mini, gpt-4o
|
||||
if len(llm) != 2 {
|
||||
@@ -213,22 +213,22 @@ func TestFindModelByID(t *testing.T) {
|
||||
func TestModelsForLanguage(t *testing.T) {
|
||||
groq := GetProvider("groq")
|
||||
|
||||
// en should include all models (distil supports en)
|
||||
// en should include all models
|
||||
enModels := ModelsForLanguage(groq, Transcription, "en")
|
||||
if len(enModels) != 3 {
|
||||
t.Errorf("ModelsForLanguage('en') = %d, want 3", len(enModels))
|
||||
if len(enModels) != 2 {
|
||||
t.Errorf("ModelsForLanguage('en') = %d, want 2", len(enModels))
|
||||
}
|
||||
|
||||
// es should exclude distil-whisper-large-v3-en
|
||||
// es should include all models (both are multilingual)
|
||||
esModels := ModelsForLanguage(groq, Transcription, "es")
|
||||
if len(esModels) != 2 {
|
||||
t.Errorf("ModelsForLanguage('es') = %d, want 2 (distil excluded)", len(esModels))
|
||||
t.Errorf("ModelsForLanguage('es') = %d, want 2", len(esModels))
|
||||
}
|
||||
|
||||
// auto ("") should include all models
|
||||
autoModels := ModelsForLanguage(groq, Transcription, "")
|
||||
if len(autoModels) != 3 {
|
||||
t.Errorf("ModelsForLanguage('') = %d, want 3 (auto returns all)", len(autoModels))
|
||||
if len(autoModels) != 2 {
|
||||
t.Errorf("ModelsForLanguage('') = %d, want 2 (auto returns all)", len(autoModels))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,16 +239,16 @@ func TestValidateModelLanguage(t *testing.T) {
|
||||
t.Errorf("ValidateModelLanguage(whisper-large-v3, 'es') unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// invalid language for English-only model
|
||||
err = ValidateModelLanguage("groq", "distil-whisper-large-v3-en", "es")
|
||||
if err == nil {
|
||||
t.Error("ValidateModelLanguage(distil-whisper, 'es') should return error")
|
||||
// valid language for another multilingual model
|
||||
err = ValidateModelLanguage("groq", "whisper-large-v3-turbo", "de")
|
||||
if err != nil {
|
||||
t.Errorf("ValidateModelLanguage(whisper-large-v3-turbo, 'de') unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// auto always passes
|
||||
err = ValidateModelLanguage("groq", "distil-whisper-large-v3-en", "")
|
||||
err = ValidateModelLanguage("groq", "whisper-large-v3", "")
|
||||
if err != nil {
|
||||
t.Errorf("ValidateModelLanguage(distil-whisper, '') should pass (auto): %v", err)
|
||||
t.Errorf("ValidateModelLanguage(whisper-large-v3, '') should pass (auto): %v", err)
|
||||
}
|
||||
|
||||
// unknown provider
|
||||
@@ -266,19 +266,20 @@ func TestValidateModelLanguage(t *testing.T) {
|
||||
|
||||
func TestValidateModelLanguage_ErrorFormat(t *testing.T) {
|
||||
// verify error includes model name, not ID
|
||||
err := ValidateModelLanguage("groq", "distil-whisper-large-v3-en", "es")
|
||||
// use whisper-cpp base.en model which is English-only
|
||||
err := ValidateModelLanguage("whisper-cpp", "base.en", "es")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unsupported language")
|
||||
}
|
||||
errMsg := err.Error()
|
||||
|
||||
// should contain model name (from Model.Name)
|
||||
if !strings.Contains(errMsg, "Distil Whisper Large v3 EN") {
|
||||
if !strings.Contains(errMsg, "Base English") {
|
||||
t.Errorf("error should contain model name, got: %s", errMsg)
|
||||
}
|
||||
|
||||
// should contain docs URL
|
||||
if !strings.Contains(errMsg, "https://console.groq.com/docs/speech-to-text#supported-languages") {
|
||||
if !strings.Contains(errMsg, "https://github.com/openai/whisper") {
|
||||
t.Errorf("error should contain docs URL, got: %s", errMsg)
|
||||
}
|
||||
|
||||
@@ -287,36 +288,39 @@ func TestValidateModelLanguage_ErrorFormat(t *testing.T) {
|
||||
t.Errorf("error should contain language code, got: %s", errMsg)
|
||||
}
|
||||
|
||||
// should have truncated language list (only 5 supported langs, English-only has 1)
|
||||
// should contain supported languages (English-only has just 'en')
|
||||
if !strings.Contains(errMsg, "en") {
|
||||
t.Errorf("error should contain supported languages, got: %s", errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIRealtimeModel(t *testing.T) {
|
||||
m, err := GetModel("openai", "gpt-4o-realtime-preview")
|
||||
func TestOpenAIStreamingModels(t *testing.T) {
|
||||
// gpt-4o-transcribe supports both batch and streaming
|
||||
m, err := GetModel("openai", "gpt-4o-transcribe")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModel('openai', 'gpt-4o-realtime-preview') error: %v", err)
|
||||
t.Fatalf("GetModel('openai', 'gpt-4o-transcribe') error: %v", err)
|
||||
}
|
||||
|
||||
if !m.Streaming {
|
||||
t.Error("gpt-4o-realtime-preview should have Streaming=true")
|
||||
if !m.SupportsBatch {
|
||||
t.Error("gpt-4o-transcribe should have SupportsBatch=true")
|
||||
}
|
||||
if !m.SupportsStreaming {
|
||||
t.Error("gpt-4o-transcribe should have SupportsStreaming=true")
|
||||
}
|
||||
if !m.SupportsBothModes() {
|
||||
t.Error("gpt-4o-transcribe should support both modes")
|
||||
}
|
||||
|
||||
if m.AdapterType != "openai-realtime" {
|
||||
t.Errorf("gpt-4o-realtime-preview AdapterType=%q, want 'openai-realtime'", m.AdapterType)
|
||||
if m.StreamingAdapter != "openai-realtime" {
|
||||
t.Errorf("gpt-4o-transcribe StreamingAdapter=%q, want 'openai-realtime'", m.StreamingAdapter)
|
||||
}
|
||||
|
||||
if m.Endpoint == nil {
|
||||
t.Fatal("gpt-4o-realtime-preview should have Endpoint set")
|
||||
if m.StreamingEndpoint == nil {
|
||||
t.Fatal("gpt-4o-transcribe should have StreamingEndpoint set")
|
||||
}
|
||||
|
||||
if m.Endpoint.BaseURL != "wss://api.openai.com" {
|
||||
t.Errorf("gpt-4o-realtime-preview Endpoint.BaseURL=%q, want 'wss://api.openai.com'", m.Endpoint.BaseURL)
|
||||
}
|
||||
|
||||
if len(m.SupportedLanguages) != 57 {
|
||||
t.Errorf("gpt-4o-realtime-preview has %d languages, want 57", len(m.SupportedLanguages))
|
||||
if m.StreamingEndpoint.BaseURL != "wss://api.openai.com" {
|
||||
t.Errorf("gpt-4o-transcribe StreamingEndpoint.BaseURL=%q, want 'wss://api.openai.com'", m.StreamingEndpoint.BaseURL)
|
||||
}
|
||||
|
||||
// default model should still be whisper-1
|
||||
@@ -334,18 +338,21 @@ func TestElevenLabsProvider(t *testing.T) {
|
||||
|
||||
models := p.Models()
|
||||
|
||||
// ElevenLabsProvider.Models() returns 4 models
|
||||
if len(models) != 4 {
|
||||
t.Errorf("ElevenLabsProvider.Models() = %d models, want 4", len(models))
|
||||
// ElevenLabsProvider.Models() returns 3 models
|
||||
if len(models) != 3 {
|
||||
t.Errorf("ElevenLabsProvider.Models() = %d models, want 3", len(models))
|
||||
}
|
||||
|
||||
// Check batch models
|
||||
// Check batch-only models
|
||||
scribeV1, err := GetModel("elevenlabs", "scribe_v1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModel('elevenlabs', 'scribe_v1') error: %v", err)
|
||||
}
|
||||
if scribeV1.Streaming {
|
||||
t.Error("scribe_v1 should have Streaming=false")
|
||||
if !scribeV1.SupportsBatch {
|
||||
t.Error("scribe_v1 should have SupportsBatch=true")
|
||||
}
|
||||
if scribeV1.SupportsStreaming {
|
||||
t.Error("scribe_v1 should have SupportsStreaming=false")
|
||||
}
|
||||
if scribeV1.AdapterType != "elevenlabs" {
|
||||
t.Errorf("scribe_v1 AdapterType=%q, want 'elevenlabs'", scribeV1.AdapterType)
|
||||
@@ -355,34 +362,29 @@ func TestElevenLabsProvider(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetModel('elevenlabs', 'scribe_v2') error: %v", err)
|
||||
}
|
||||
if scribeV2.Streaming {
|
||||
t.Error("scribe_v2 should have Streaming=false")
|
||||
if !scribeV2.SupportsBatch {
|
||||
t.Error("scribe_v2 should have SupportsBatch=true")
|
||||
}
|
||||
if scribeV2.SupportsStreaming {
|
||||
t.Error("scribe_v2 should have SupportsStreaming=false")
|
||||
}
|
||||
if scribeV2.AdapterType != "elevenlabs" {
|
||||
t.Errorf("scribe_v2 AdapterType=%q, want 'elevenlabs'", scribeV2.AdapterType)
|
||||
}
|
||||
|
||||
// Check streaming models
|
||||
scribeV1S, err := GetModel("elevenlabs", "scribe_v1-streaming")
|
||||
// Check streaming-only model
|
||||
scribeV2Realtime, err := GetModel("elevenlabs", "scribe_v2_realtime")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModel('elevenlabs', 'scribe_v1-streaming') error: %v", err)
|
||||
t.Fatalf("GetModel('elevenlabs', 'scribe_v2_realtime') error: %v", err)
|
||||
}
|
||||
if !scribeV1S.Streaming {
|
||||
t.Error("scribe_v1-streaming should have Streaming=true")
|
||||
if scribeV2Realtime.SupportsBatch {
|
||||
t.Error("scribe_v2_realtime should have SupportsBatch=false")
|
||||
}
|
||||
if scribeV1S.AdapterType != "elevenlabs-streaming" {
|
||||
t.Errorf("scribe_v1-streaming AdapterType=%q, want 'elevenlabs-streaming'", scribeV1S.AdapterType)
|
||||
if !scribeV2Realtime.SupportsStreaming {
|
||||
t.Error("scribe_v2_realtime should have SupportsStreaming=true")
|
||||
}
|
||||
|
||||
scribeV2S, err := GetModel("elevenlabs", "scribe_v2-streaming")
|
||||
if err != nil {
|
||||
t.Fatalf("GetModel('elevenlabs', 'scribe_v2-streaming') error: %v", err)
|
||||
}
|
||||
if !scribeV2S.Streaming {
|
||||
t.Error("scribe_v2-streaming should have Streaming=true")
|
||||
}
|
||||
if scribeV2S.AdapterType != "elevenlabs-streaming" {
|
||||
t.Errorf("scribe_v2-streaming AdapterType=%q, want 'elevenlabs-streaming'", scribeV2S.AdapterType)
|
||||
if scribeV2Realtime.AdapterType != "elevenlabs-streaming" {
|
||||
t.Errorf("scribe_v2_realtime AdapterType=%q, want 'elevenlabs-streaming'", scribeV2Realtime.AdapterType)
|
||||
}
|
||||
|
||||
// All models have explicit SupportedLanguages from docs (subset of our 57)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
type WhisperCppProvider struct{}
|
||||
|
||||
func (p *WhisperCppProvider) Name() string {
|
||||
return "whisper-cpp"
|
||||
return ProviderWhisperCpp
|
||||
}
|
||||
|
||||
func (p *WhisperCppProvider) RequiresAPIKey() bool {
|
||||
@@ -45,9 +45,10 @@ func (p *WhisperCppProvider) Models() []Model {
|
||||
Name: wm.Name,
|
||||
Description: modelDescription(wm),
|
||||
Type: Transcription,
|
||||
Streaming: false,
|
||||
SupportsBatch: true,
|
||||
SupportsStreaming: false,
|
||||
Local: true,
|
||||
AdapterType: "whisper-cpp",
|
||||
AdapterType: AdapterWhisperCpp,
|
||||
SupportedLanguages: langs,
|
||||
Endpoint: nil, // local CLI, no HTTP endpoint
|
||||
LocalInfo: &LocalModelInfo{
|
||||
|
||||
Reference in New Issue
Block a user