refactor elevenlabs adapter to use EndpointConfig

This commit is contained in:
leonardotrapani
2026-02-01 00:57:31 +01:00
parent 38a1aa18c1
commit edc7a5f26b
5 changed files with 106 additions and 35 deletions
+38 -13
View File
@@ -10,12 +10,18 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"time" "time"
"github.com/leonardotrapani/hyprvoice/internal/language"
"github.com/leonardotrapani/hyprvoice/internal/provider"
) )
// ElevenLabsAdapter implements BatchAdapter for ElevenLabs Scribe API // ElevenLabsAdapter implements BatchAdapter for ElevenLabs Scribe API
type ElevenLabsAdapter struct { type ElevenLabsAdapter struct {
client *http.Client client *http.Client
config Config endpoint *provider.EndpointConfig
apiKey string
model string
language string
} }
// ElevenLabsResponse represents the API response // ElevenLabsResponse represents the API response
@@ -23,14 +29,32 @@ type ElevenLabsResponse struct {
Text string `json:"text"` Text string `json:"text"`
} }
// NewElevenLabsAdapter creates a new ElevenLabs adapter // NewElevenLabsAdapter creates an adapter for ElevenLabs Scribe API
func NewElevenLabsAdapter(config Config) *ElevenLabsAdapter { // endpoint: the endpoint config (BaseURL + Path)
// apiKey: ElevenLabs API key
// model: model ID (e.g., "scribe_v1")
// lang: canonical language code (will be converted to provider format)
func NewElevenLabsAdapter(endpoint *provider.EndpointConfig, apiKey, model, lang string) *ElevenLabsAdapter {
return &ElevenLabsAdapter{ return &ElevenLabsAdapter{
client: &http.Client{Timeout: 30 * time.Second}, client: &http.Client{Timeout: 30 * time.Second},
config: config, endpoint: endpoint,
apiKey: apiKey,
model: model,
language: lang,
} }
} }
// NewElevenLabsAdapterFromConfig creates an adapter using the legacy Config struct
// for backwards compatibility during migration
func NewElevenLabsAdapterFromConfig(config Config) *ElevenLabsAdapter {
return NewElevenLabsAdapter(
&provider.EndpointConfig{BaseURL: "https://api.elevenlabs.io", Path: "/v1/speech-to-text"},
config.APIKey,
config.Model,
config.Language,
)
}
// Transcribe sends audio to ElevenLabs API for transcription // Transcribe sends audio to ElevenLabs API for transcription
func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) { func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
if len(audioData) == 0 { if len(audioData) == 0 {
@@ -57,13 +81,14 @@ func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (s
} }
// Add model_id // Add model_id
if err := writer.WriteField("model_id", a.config.Model); err != nil { if err := writer.WriteField("model_id", a.model); err != nil {
return "", fmt.Errorf("write model_id: %w", err) return "", fmt.Errorf("write model_id: %w", err)
} }
// Add language_code if specified // Add language_code if specified (convert to provider format)
if a.config.Language != "" { providerLang := language.ToProviderFormat(a.language, "elevenlabs")
if err := writer.WriteField("language_code", a.config.Language); err != nil { if providerLang != "" {
if err := writer.WriteField("language_code", providerLang); err != nil {
return "", fmt.Errorf("write language_code: %w", err) return "", fmt.Errorf("write language_code: %w", err)
} }
} }
@@ -72,15 +97,15 @@ func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (s
return "", fmt.Errorf("close writer: %w", err) return "", fmt.Errorf("close writer: %w", err)
} }
// Create HTTP request // Create HTTP request using endpoint config
url := "https://api.elevenlabs.io/v1/speech-to-text" url := a.endpoint.BaseURL + a.endpoint.Path
req, err := http.NewRequestWithContext(ctx, "POST", url, &body) req, err := http.NewRequestWithContext(ctx, "POST", url, &body)
if err != nil { if err != nil {
return "", fmt.Errorf("create request: %w", err) return "", fmt.Errorf("create request: %w", err)
} }
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("xi-api-key", a.config.APIKey) req.Header.Set("xi-api-key", a.apiKey)
start := time.Now() start := time.Now()
resp, err := a.client.Do(req) resp, err := a.client.Do(req)
+55 -20
View File
@@ -3,9 +3,44 @@ package transcriber
import ( import (
"context" "context"
"testing" "testing"
"github.com/leonardotrapani/hyprvoice/internal/provider"
) )
func TestNewElevenLabsAdapter(t *testing.T) { func TestNewElevenLabsAdapter(t *testing.T) {
endpoint := &provider.EndpointConfig{
BaseURL: "https://api.elevenlabs.io",
Path: "/v1/speech-to-text",
}
adapter := NewElevenLabsAdapter(endpoint, "test-api-key", "scribe_v1", "en")
if adapter == nil {
t.Fatalf("NewElevenLabsAdapter() returned nil")
}
if adapter.apiKey != "test-api-key" {
t.Errorf("APIKey not set correctly, got: %s", adapter.apiKey)
}
if adapter.model != "scribe_v1" {
t.Errorf("Model not set correctly, got: %s", adapter.model)
}
if adapter.language != "en" {
t.Errorf("Language not set correctly, got: %s", adapter.language)
}
if adapter.endpoint.BaseURL != "https://api.elevenlabs.io" {
t.Errorf("Endpoint BaseURL not set correctly, got: %s", adapter.endpoint.BaseURL)
}
if adapter.endpoint.Path != "/v1/speech-to-text" {
t.Errorf("Endpoint Path not set correctly, got: %s", adapter.endpoint.Path)
}
}
func TestNewElevenLabsAdapterFromConfig(t *testing.T) {
config := Config{ config := Config{
Provider: "elevenlabs", Provider: "elevenlabs",
APIKey: "test-api-key", APIKey: "test-api-key",
@@ -13,29 +48,33 @@ func TestNewElevenLabsAdapter(t *testing.T) {
Model: "scribe_v1", Model: "scribe_v1",
} }
adapter := NewElevenLabsAdapter(config) adapter := NewElevenLabsAdapterFromConfig(config)
if adapter == nil { if adapter == nil {
t.Fatalf("NewElevenLabsAdapter() returned nil") t.Fatalf("NewElevenLabsAdapterFromConfig() returned nil")
} }
if adapter.config.APIKey != "test-api-key" { if adapter.apiKey != "test-api-key" {
t.Errorf("APIKey not set correctly, got: %s", adapter.config.APIKey) t.Errorf("APIKey not set correctly, got: %s", adapter.apiKey)
} }
if adapter.config.Model != "scribe_v1" { if adapter.model != "scribe_v1" {
t.Errorf("Model not set correctly, got: %s", adapter.config.Model) t.Errorf("Model not set correctly, got: %s", adapter.model)
}
// should use default endpoint
if adapter.endpoint.BaseURL != "https://api.elevenlabs.io" {
t.Errorf("Default endpoint BaseURL not set correctly, got: %s", adapter.endpoint.BaseURL)
} }
} }
func TestElevenLabsAdapter_Transcribe_EmptyAudio(t *testing.T) { func TestElevenLabsAdapter_Transcribe_EmptyAudio(t *testing.T) {
config := Config{ endpoint := &provider.EndpointConfig{
Provider: "elevenlabs", BaseURL: "https://api.elevenlabs.io",
APIKey: "test-key", Path: "/v1/speech-to-text",
Model: "scribe_v1",
} }
adapter := NewElevenLabsAdapter(config) adapter := NewElevenLabsAdapter(endpoint, "test-key", "scribe_v1", "")
ctx := context.Background() ctx := context.Background()
result, err := adapter.Transcribe(ctx, []byte{}) result, err := adapter.Transcribe(ctx, []byte{})
@@ -50,22 +89,18 @@ func TestElevenLabsAdapter_Transcribe_EmptyAudio(t *testing.T) {
} }
func TestElevenLabsAdapter_Transcribe_ValidAudio(t *testing.T) { func TestElevenLabsAdapter_Transcribe_ValidAudio(t *testing.T) {
// This test will require mocking the HTTP client endpoint := &provider.EndpointConfig{
// For now, we test the structure exists BaseURL: "https://api.elevenlabs.io",
config := Config{ Path: "/v1/speech-to-text",
Provider: "elevenlabs",
APIKey: "test-key",
Language: "en",
Model: "scribe_v1",
} }
adapter := NewElevenLabsAdapter(config) adapter := NewElevenLabsAdapter(endpoint, "test-key", "scribe_v1", "en")
if adapter == nil { if adapter == nil {
t.Fatal("NewElevenLabsAdapter() returned nil") t.Fatal("NewElevenLabsAdapter() returned nil")
} }
// Test that adapter has a client // test that adapter has a client
if adapter.client == nil { if adapter.client == nil {
t.Error("adapter.client is nil") t.Error("adapter.client is nil")
} }
+1 -1
View File
@@ -67,7 +67,7 @@ func NewTranscriber(config Config) (Transcriber, error) {
if config.APIKey == "" { if config.APIKey == "" {
return nil, fmt.Errorf("ElevenLabs API key required") return nil, fmt.Errorf("ElevenLabs API key required")
} }
adapter = NewElevenLabsAdapter(config) adapter = NewElevenLabsAdapterFromConfig(config)
default: default:
return nil, fmt.Errorf("unsupported provider: %s", config.Provider) return nil, fmt.Errorf("unsupported provider: %s", config.Provider)
+11
View File
@@ -123,3 +123,14 @@ Started: Sun Feb 1 12:22:47 AM CET 2026
- Updated `transcriber.go` factory to use consolidated OpenAI adapter for groq-transcription and mistral-transcription - Updated `transcriber.go` factory to use consolidated OpenAI adapter for groq-transcription and mistral-transcription
- Both now use `NewOpenAIAdapter` with their respective endpoints - Both now use `NewOpenAIAdapter` with their respective endpoints
- All tests passing, typecheck passes - All tests passing, typecheck passes
### Task 14: Update ElevenLabs BatchAdapter to use EndpointConfig
- Refactored `internal/transcriber/adapter_elevenlabs.go` to use EndpointConfig
- New constructor: `NewElevenLabsAdapter(endpoint *EndpointConfig, apiKey, model, lang string)`
- Uses `endpoint.BaseURL + endpoint.Path` for URL (no hardcoded URL)
- Language converted via `language.ToProviderFormat(a.language, "elevenlabs")`
- Kept `xi-api-key` header for ElevenLabs-specific auth
- Added `NewElevenLabsAdapterFromConfig` for backward compatibility
- Updated factory to use `NewElevenLabsAdapterFromConfig`
- Updated tests for new constructor signature
- All tests passing, typecheck passes
+1 -1
View File
@@ -349,7 +349,7 @@
"Still uses xi-api-key header (ElevenLabs-specific)", "Still uses xi-api-key header (ElevenLabs-specific)",
"Typecheck passes" "Typecheck passes"
], ],
"passes": false "passes": true
}, },
{ {
"title": "Update transcriber factory to use Model metadata", "title": "Update transcriber factory to use Model metadata",