diff --git a/internal/transcriber/adapter_elevenlabs.go b/internal/transcriber/adapter_elevenlabs.go index 2207805..994417b 100644 --- a/internal/transcriber/adapter_elevenlabs.go +++ b/internal/transcriber/adapter_elevenlabs.go @@ -10,12 +10,18 @@ import ( "mime/multipart" "net/http" "time" + + "github.com/leonardotrapani/hyprvoice/internal/language" + "github.com/leonardotrapani/hyprvoice/internal/provider" ) // ElevenLabsAdapter implements BatchAdapter for ElevenLabs Scribe API type ElevenLabsAdapter struct { - client *http.Client - config Config + client *http.Client + endpoint *provider.EndpointConfig + apiKey string + model string + language string } // ElevenLabsResponse represents the API response @@ -23,14 +29,32 @@ type ElevenLabsResponse struct { Text string `json:"text"` } -// NewElevenLabsAdapter creates a new ElevenLabs adapter -func NewElevenLabsAdapter(config Config) *ElevenLabsAdapter { +// NewElevenLabsAdapter creates an adapter for ElevenLabs Scribe API +// 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{ - client: &http.Client{Timeout: 30 * time.Second}, - config: config, + client: &http.Client{Timeout: 30 * time.Second}, + 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 func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) { if len(audioData) == 0 { @@ -57,13 +81,14 @@ func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (s } // 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) } - // Add language_code if specified - if a.config.Language != "" { - if err := writer.WriteField("language_code", a.config.Language); err != nil { + // Add language_code if specified (convert to provider format) + providerLang := language.ToProviderFormat(a.language, "elevenlabs") + if providerLang != "" { + if err := writer.WriteField("language_code", providerLang); err != nil { 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) } - // Create HTTP request - url := "https://api.elevenlabs.io/v1/speech-to-text" + // Create HTTP request using endpoint config + url := a.endpoint.BaseURL + a.endpoint.Path req, err := http.NewRequestWithContext(ctx, "POST", url, &body) if err != nil { return "", fmt.Errorf("create request: %w", err) } 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() resp, err := a.client.Do(req) diff --git a/internal/transcriber/adapter_elevenlabs_test.go b/internal/transcriber/adapter_elevenlabs_test.go index 74bf94e..1d50b6d 100644 --- a/internal/transcriber/adapter_elevenlabs_test.go +++ b/internal/transcriber/adapter_elevenlabs_test.go @@ -3,9 +3,44 @@ package transcriber import ( "context" "testing" + + "github.com/leonardotrapani/hyprvoice/internal/provider" ) 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{ Provider: "elevenlabs", APIKey: "test-api-key", @@ -13,29 +48,33 @@ func TestNewElevenLabsAdapter(t *testing.T) { Model: "scribe_v1", } - adapter := NewElevenLabsAdapter(config) + adapter := NewElevenLabsAdapterFromConfig(config) if adapter == nil { - t.Fatalf("NewElevenLabsAdapter() returned nil") + t.Fatalf("NewElevenLabsAdapterFromConfig() returned nil") } - if adapter.config.APIKey != "test-api-key" { - t.Errorf("APIKey not set correctly, got: %s", adapter.config.APIKey) + if adapter.apiKey != "test-api-key" { + t.Errorf("APIKey not set correctly, got: %s", adapter.apiKey) } - if adapter.config.Model != "scribe_v1" { - t.Errorf("Model not set correctly, got: %s", adapter.config.Model) + if adapter.model != "scribe_v1" { + 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) { - config := Config{ - Provider: "elevenlabs", - APIKey: "test-key", - Model: "scribe_v1", + endpoint := &provider.EndpointConfig{ + BaseURL: "https://api.elevenlabs.io", + Path: "/v1/speech-to-text", } - adapter := NewElevenLabsAdapter(config) + adapter := NewElevenLabsAdapter(endpoint, "test-key", "scribe_v1", "") ctx := context.Background() result, err := adapter.Transcribe(ctx, []byte{}) @@ -50,22 +89,18 @@ func TestElevenLabsAdapter_Transcribe_EmptyAudio(t *testing.T) { } func TestElevenLabsAdapter_Transcribe_ValidAudio(t *testing.T) { - // This test will require mocking the HTTP client - // For now, we test the structure exists - config := Config{ - Provider: "elevenlabs", - APIKey: "test-key", - Language: "en", - Model: "scribe_v1", + endpoint := &provider.EndpointConfig{ + BaseURL: "https://api.elevenlabs.io", + Path: "/v1/speech-to-text", } - adapter := NewElevenLabsAdapter(config) + adapter := NewElevenLabsAdapter(endpoint, "test-key", "scribe_v1", "en") if adapter == nil { t.Fatal("NewElevenLabsAdapter() returned nil") } - // Test that adapter has a client + // test that adapter has a client if adapter.client == nil { t.Error("adapter.client is nil") } diff --git a/internal/transcriber/transcriber.go b/internal/transcriber/transcriber.go index 51c204e..aec7c14 100644 --- a/internal/transcriber/transcriber.go +++ b/internal/transcriber/transcriber.go @@ -67,7 +67,7 @@ func NewTranscriber(config Config) (Transcriber, error) { if config.APIKey == "" { return nil, fmt.Errorf("ElevenLabs API key required") } - adapter = NewElevenLabsAdapter(config) + adapter = NewElevenLabsAdapterFromConfig(config) default: return nil, fmt.Errorf("unsupported provider: %s", config.Provider) diff --git a/progress.txt b/progress.txt index 2d326d3..2224e12 100644 --- a/progress.txt +++ b/progress.txt @@ -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 - Both now use `NewOpenAIAdapter` with their respective endpoints - 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 diff --git a/tasks/prd.jsonc b/tasks/prd.jsonc index 21fe956..c328d78 100644 --- a/tasks/prd.jsonc +++ b/tasks/prd.jsonc @@ -349,7 +349,7 @@ "Still uses xi-api-key header (ElevenLabs-specific)", "Typecheck passes" ], - "passes": false + "passes": true }, { "title": "Update transcriber factory to use Model metadata",