refactor elevenlabs adapter to use EndpointConfig
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user