feat: refactor

This commit is contained in:
leonardotrapani
2026-02-01 17:24:43 +01:00
parent 13b1de4e04
commit 8df3021a9d
33 changed files with 1290 additions and 1577 deletions
+12 -32
View File
@@ -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,
},
}
+27 -8
View File
@@ -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)
}
}
}
+14 -25
View File
@@ -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
View File
@@ -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"},
},
+9 -5
View File
@@ -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,
+10 -2
View File
@@ -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.
+53 -7
View File
@@ -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")
+73
View File
@@ -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
View File
@@ -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"},
},
+62 -60
View File
@@ -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)
+4 -3
View File
@@ -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{