feat: add Eleven Labs Scribe adapter
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ElevenLabsAdapter implements TranscriptionAdapter for ElevenLabs Scribe API
|
||||||
|
type ElevenLabsAdapter struct {
|
||||||
|
client *http.Client
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// ElevenLabsResponse represents the API response
|
||||||
|
type ElevenLabsResponse struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewElevenLabsAdapter creates a new ElevenLabs adapter
|
||||||
|
func NewElevenLabsAdapter(config Config) *ElevenLabsAdapter {
|
||||||
|
return &ElevenLabsAdapter{
|
||||||
|
client: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transcribe sends audio to ElevenLabs API for transcription
|
||||||
|
func (a *ElevenLabsAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
|
||||||
|
if len(audioData) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert raw PCM to WAV format
|
||||||
|
wavData, err := convertToWAV(audioData)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("convert to WAV: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multipart form body
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
|
||||||
|
// Add audio file
|
||||||
|
part, err := writer.CreateFormFile("file", "audio.wav")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create form file: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(part, bytes.NewReader(wavData)); err != nil {
|
||||||
|
return "", fmt.Errorf("copy audio data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add model_id
|
||||||
|
if err := writer.WriteField("model_id", a.config.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 {
|
||||||
|
return "", fmt.Errorf("write language_code: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return "", fmt.Errorf("close writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP request
|
||||||
|
url := "https://api.elevenlabs.io/v1/speech-to-text"
|
||||||
|
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)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("elevenlabs-adapter: API call failed after %v: %v", duration, err)
|
||||||
|
return "", fmt.Errorf("elevenlabs request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Printf("elevenlabs-adapter: API returned status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
return "", fmt.Errorf("elevenlabs API status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result ElevenLabsResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return "", fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("elevenlabs-adapter: transcribed %d bytes in %v: %q", len(audioData), duration, result.Text)
|
||||||
|
return result.Text, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package transcriber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewElevenLabsAdapter(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "elevenlabs",
|
||||||
|
APIKey: "test-api-key",
|
||||||
|
Language: "en",
|
||||||
|
Model: "scribe_v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := NewElevenLabsAdapter(config)
|
||||||
|
|
||||||
|
if adapter == nil {
|
||||||
|
t.Fatalf("NewElevenLabsAdapter() returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if adapter.config.APIKey != "test-api-key" {
|
||||||
|
t.Errorf("APIKey not set correctly, got: %s", adapter.config.APIKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if adapter.config.Model != "scribe_v1" {
|
||||||
|
t.Errorf("Model not set correctly, got: %s", adapter.config.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElevenLabsAdapter_Transcribe_EmptyAudio(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Provider: "elevenlabs",
|
||||||
|
APIKey: "test-key",
|
||||||
|
Model: "scribe_v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := NewElevenLabsAdapter(config)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
result, err := adapter.Transcribe(ctx, []byte{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Transcribe() with empty audio should not error, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("Transcribe() with empty audio should return empty string, got: %s", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := NewElevenLabsAdapter(config)
|
||||||
|
|
||||||
|
if adapter == nil {
|
||||||
|
t.Fatal("NewElevenLabsAdapter() returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that adapter has a client
|
||||||
|
if adapter.client == nil {
|
||||||
|
t.Error("adapter.client is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user