1087 lines
25 KiB
Go
1087 lines
25 KiB
Go
package transcriber
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/leonardotrapani/hyprvoice/internal/provider"
|
|
"github.com/leonardotrapani/hyprvoice/internal/recording"
|
|
)
|
|
|
|
func TestNewTranscriber(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config Config
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid openai config",
|
|
config: Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "openai config without api key",
|
|
config: Config{
|
|
Provider: "openai",
|
|
APIKey: "",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid groq-transcription config",
|
|
config: Config{
|
|
Provider: "groq-transcription",
|
|
APIKey: "gsk-test-key",
|
|
Language: "en",
|
|
Model: "whisper-large-v3",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "groq-transcription config without api key",
|
|
config: Config{
|
|
Provider: "groq-transcription",
|
|
APIKey: "",
|
|
Language: "en",
|
|
Model: "whisper-large-v3",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid groq-translation config",
|
|
config: Config{
|
|
Provider: "groq-translation",
|
|
APIKey: "gsk-test-key",
|
|
Language: "es",
|
|
Model: "whisper-large-v3-turbo",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "groq-translation config without api key",
|
|
config: Config{
|
|
Provider: "groq-translation",
|
|
APIKey: "",
|
|
Language: "es",
|
|
Model: "whisper-large-v3-turbo",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid mistral-transcription config",
|
|
config: Config{
|
|
Provider: "mistral-transcription",
|
|
APIKey: "test-key",
|
|
Language: "de",
|
|
Model: "voxtral-mini-latest",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mistral-transcription config without api key",
|
|
config: Config{
|
|
Provider: "mistral-transcription",
|
|
APIKey: "",
|
|
Language: "de",
|
|
Model: "voxtral-mini-latest",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid elevenlabs config with scribe_v1",
|
|
config: Config{
|
|
Provider: "elevenlabs",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "scribe_v1",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid elevenlabs config with scribe_v2",
|
|
config: Config{
|
|
Provider: "elevenlabs",
|
|
APIKey: "test-key",
|
|
Language: "pt",
|
|
Model: "scribe_v2",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "elevenlabs config without api key",
|
|
config: Config{
|
|
Provider: "elevenlabs",
|
|
APIKey: "",
|
|
Language: "en",
|
|
Model: "scribe_v1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unsupported provider",
|
|
config: Config{
|
|
Provider: "unsupported",
|
|
APIKey: "test-key",
|
|
Model: "whisper-1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty provider",
|
|
config: Config{
|
|
Provider: "",
|
|
APIKey: "test-key",
|
|
Model: "whisper-1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty model uses default",
|
|
config: Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "",
|
|
},
|
|
wantErr: false, // uses default model when empty
|
|
},
|
|
{
|
|
name: "elevenlabs streaming model creates StreamingTranscriber",
|
|
config: Config{
|
|
Provider: "elevenlabs",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "scribe_v2_realtime",
|
|
Streaming: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "deepgram streaming model creates StreamingTranscriber",
|
|
config: Config{
|
|
Provider: "deepgram",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "nova-3",
|
|
Streaming: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "openai streaming model creates StreamingTranscriber",
|
|
config: Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "gpt-4o-transcribe",
|
|
Streaming: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "unknown model returns error",
|
|
config: Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "nonexistent-model",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid whisper-cpp config creates adapter",
|
|
config: Config{
|
|
Provider: "whisper-cpp",
|
|
Language: "en",
|
|
Model: "base.en",
|
|
Threads: 4,
|
|
},
|
|
wantErr: false, // creates adapter even if model file doesn't exist (runtime check)
|
|
},
|
|
{
|
|
name: "whisper-cpp without api key is valid",
|
|
config: Config{
|
|
Provider: "whisper-cpp",
|
|
APIKey: "", // no api key required
|
|
Language: "en",
|
|
Model: "tiny.en",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "whisper-cpp with unknown model returns error",
|
|
config: Config{
|
|
Provider: "whisper-cpp",
|
|
Language: "en",
|
|
Model: "nonexistent-whisper-model",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
transcriber, err := NewTranscriber(tt.config)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("NewTranscriber() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
if !tt.wantErr && transcriber == nil {
|
|
t.Errorf("NewTranscriber() returned nil transcriber")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfig(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
if config.Provider != "openai" {
|
|
t.Errorf("Provider mismatch: got %s, want %s", config.Provider, "openai")
|
|
}
|
|
|
|
if config.APIKey != "test-key" {
|
|
t.Errorf("APIKey mismatch: got %s, want %s", config.APIKey, "test-key")
|
|
}
|
|
|
|
if config.Language != "en" {
|
|
t.Errorf("Language mismatch: got %s, want %s", config.Language, "en")
|
|
}
|
|
|
|
if config.Model != "whisper-1" {
|
|
t.Errorf("Model mismatch: got %s, want %s", config.Model, "whisper-1")
|
|
}
|
|
}
|
|
|
|
// MockBatchAdapter implements BatchAdapter for testing
|
|
type MockBatchAdapter struct {
|
|
TranscribeFunc func(ctx context.Context, audioData []byte) (string, error)
|
|
}
|
|
|
|
func (m *MockBatchAdapter) Transcribe(ctx context.Context, audioData []byte) (string, error) {
|
|
if m.TranscribeFunc != nil {
|
|
return m.TranscribeFunc(ctx, audioData)
|
|
}
|
|
return "mock transcription", nil
|
|
}
|
|
|
|
func TestSimpleTranscriber_Start(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
adapter := &MockBatchAdapter{}
|
|
transcriber := NewSimpleTranscriber(config, adapter)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
// Test starting transcriber
|
|
errCh, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if errCh == nil {
|
|
t.Errorf("Start() returned nil error channel")
|
|
}
|
|
|
|
// Test starting again should fail
|
|
_, err = transcriber.Start(ctx, frameCh)
|
|
if err == nil {
|
|
t.Errorf("Start() should fail when already running")
|
|
}
|
|
|
|
// Stop the transcriber
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSimpleTranscriber_Stop(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
adapter := &MockBatchAdapter{}
|
|
transcriber := NewSimpleTranscriber(config, adapter)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
// Stop should be safe when not running
|
|
err := transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error when not running = %v", err)
|
|
}
|
|
|
|
// Start and then stop
|
|
_, err = transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// Close the frame channel to signal completion
|
|
close(frameCh)
|
|
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
|
|
// Stop again should be safe
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error after already stopped = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSimpleTranscriber_GetFinalTranscription(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
adapter := &MockBatchAdapter{
|
|
TranscribeFunc: func(ctx context.Context, audioData []byte) (string, error) {
|
|
return "test transcription", nil
|
|
},
|
|
}
|
|
transcriber := NewSimpleTranscriber(config, adapter)
|
|
|
|
// Test getting transcription before any processing
|
|
transcription, err := transcriber.GetFinalTranscription()
|
|
if err != nil {
|
|
t.Errorf("GetFinalTranscription() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// Should return empty string initially
|
|
if transcription != "" {
|
|
t.Errorf("GetFinalTranscription() = %q, want empty string", transcription)
|
|
}
|
|
}
|
|
|
|
func TestSimpleTranscriber_CollectAudio(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
adapter := &MockBatchAdapter{}
|
|
transcriber := NewSimpleTranscriber(config, adapter)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
errCh := make(chan error, 1)
|
|
|
|
// Start collecting audio in background
|
|
transcriber.wg.Add(1)
|
|
go transcriber.collectAudio(ctx, frameCh, errCh)
|
|
|
|
// Send some test audio frames
|
|
testData1 := []byte{1, 2, 3, 4}
|
|
testData2 := []byte{5, 6, 7, 8}
|
|
|
|
frame1 := recording.AudioFrame{
|
|
Data: testData1,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
frame2 := recording.AudioFrame{
|
|
Data: testData2,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
frameCh <- frame1
|
|
frameCh <- frame2
|
|
close(frameCh)
|
|
|
|
// Wait for processing to complete
|
|
transcriber.wg.Wait()
|
|
|
|
// Check that audio was collected
|
|
if len(transcriber.audioBuffer) != len(testData1)+len(testData2) {
|
|
t.Errorf("Audio buffer length = %d, want %d", len(transcriber.audioBuffer), len(testData1)+len(testData2))
|
|
}
|
|
}
|
|
|
|
func TestSimpleTranscriber_TranscribeAll(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
audioData []byte
|
|
mockResult string
|
|
mockError error
|
|
expectError bool
|
|
expectedResult string
|
|
}{
|
|
{
|
|
name: "successful transcription",
|
|
audioData: []byte{1, 2, 3, 4},
|
|
mockResult: "hello world",
|
|
mockError: nil,
|
|
expectError: false,
|
|
expectedResult: "hello world",
|
|
},
|
|
{
|
|
name: "empty audio data",
|
|
audioData: []byte{},
|
|
mockResult: "",
|
|
mockError: nil,
|
|
expectError: false,
|
|
expectedResult: "",
|
|
},
|
|
{
|
|
name: "transcription error",
|
|
audioData: []byte{1, 2, 3, 4},
|
|
mockResult: "",
|
|
mockError: fmt.Errorf("api error"),
|
|
expectError: true,
|
|
expectedResult: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
adapter := &MockBatchAdapter{
|
|
TranscribeFunc: func(ctx context.Context, audioData []byte) (string, error) {
|
|
return tt.mockResult, tt.mockError
|
|
},
|
|
}
|
|
transcriber := NewSimpleTranscriber(config, adapter)
|
|
|
|
// Set up audio buffer
|
|
transcriber.audioBuffer = tt.audioData
|
|
|
|
ctx := context.Background()
|
|
err := transcriber.transcribeAll(ctx)
|
|
|
|
if (err != nil) != tt.expectError {
|
|
t.Errorf("transcribeAll() error = %v, expectError %v", err, tt.expectError)
|
|
return
|
|
}
|
|
|
|
if !tt.expectError {
|
|
result, err := transcriber.GetFinalTranscription()
|
|
if err != nil {
|
|
t.Errorf("GetFinalTranscription() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if result != tt.expectedResult {
|
|
t.Errorf("GetFinalTranscription() = %q, want %q", result, tt.expectedResult)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSimpleTranscriber(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "test-key",
|
|
Language: "en",
|
|
Model: "whisper-1",
|
|
}
|
|
|
|
adapter := &MockBatchAdapter{}
|
|
transcriber := NewSimpleTranscriber(config, adapter)
|
|
|
|
if transcriber == nil {
|
|
t.Errorf("NewSimpleTranscriber() returned nil")
|
|
return
|
|
}
|
|
|
|
if transcriber.adapter != adapter {
|
|
t.Errorf("Adapter not set correctly")
|
|
}
|
|
|
|
if transcriber.config.Provider != config.Provider {
|
|
t.Errorf("Config not set correctly")
|
|
}
|
|
|
|
if transcriber.running {
|
|
t.Errorf("Transcriber should not be running initially")
|
|
}
|
|
|
|
if len(transcriber.audioBuffer) != 0 {
|
|
t.Errorf("Audio buffer should be empty initially")
|
|
}
|
|
}
|
|
|
|
func TestTranscriptionAdapter(t *testing.T) {
|
|
adapter := &MockBatchAdapter{
|
|
TranscribeFunc: func(ctx context.Context, audioData []byte) (string, error) {
|
|
return "test result", nil
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
audioData := []byte{1, 2, 3, 4}
|
|
|
|
result, err := adapter.Transcribe(ctx, audioData)
|
|
if err != nil {
|
|
t.Errorf("Transcribe() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if result != "test result" {
|
|
t.Errorf("Transcribe() = %q, want %q", result, "test result")
|
|
}
|
|
}
|
|
|
|
func TestOpenAIAdapter_Creation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint *provider.EndpointConfig
|
|
apiKey string
|
|
model string
|
|
language string
|
|
keywords []string
|
|
providerName string
|
|
}{
|
|
{
|
|
name: "openai with nil endpoint uses default",
|
|
endpoint: nil,
|
|
apiKey: "sk-test-key",
|
|
model: "whisper-1",
|
|
language: "en",
|
|
keywords: []string{"hello", "world"},
|
|
providerName: "openai",
|
|
},
|
|
{
|
|
name: "openai with explicit endpoint",
|
|
endpoint: &provider.EndpointConfig{BaseURL: "https://api.openai.com", Path: "/v1/audio/transcriptions"},
|
|
apiKey: "sk-test-key",
|
|
model: "whisper-1",
|
|
language: "es",
|
|
keywords: nil,
|
|
providerName: "openai",
|
|
},
|
|
{
|
|
name: "groq with custom endpoint",
|
|
endpoint: &provider.EndpointConfig{BaseURL: "https://api.groq.com/openai", Path: "/v1/audio/transcriptions"},
|
|
apiKey: "gsk-test-key",
|
|
model: "whisper-large-v3",
|
|
language: "fr",
|
|
keywords: []string{"bonjour"},
|
|
providerName: "groq",
|
|
},
|
|
{
|
|
name: "mistral with custom endpoint",
|
|
endpoint: &provider.EndpointConfig{BaseURL: "https://api.mistral.ai", Path: "/v1/audio/transcriptions"},
|
|
apiKey: "mistral-test-key",
|
|
model: "voxtral-mini-latest",
|
|
language: "de",
|
|
keywords: nil,
|
|
providerName: "mistral",
|
|
},
|
|
{
|
|
name: "auto language",
|
|
endpoint: nil,
|
|
apiKey: "sk-test-key",
|
|
model: "whisper-1",
|
|
language: "", // auto
|
|
keywords: nil,
|
|
providerName: "openai",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
adapter := NewOpenAIAdapter(tt.endpoint, tt.apiKey, tt.model, tt.language, tt.keywords, tt.providerName)
|
|
if adapter == nil {
|
|
t.Errorf("NewOpenAIAdapter() returned nil")
|
|
return
|
|
}
|
|
|
|
if adapter.model != tt.model {
|
|
t.Errorf("model = %q, want %q", adapter.model, tt.model)
|
|
}
|
|
|
|
if adapter.language != tt.language {
|
|
t.Errorf("language = %q, want %q", adapter.language, tt.language)
|
|
}
|
|
|
|
if adapter.providerName != tt.providerName {
|
|
t.Errorf("providerName = %q, want %q", adapter.providerName, tt.providerName)
|
|
}
|
|
|
|
if len(adapter.keywords) != len(tt.keywords) {
|
|
t.Errorf("keywords len = %d, want %d", len(adapter.keywords), len(tt.keywords))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOpenAIAdapterFromConfig(t *testing.T) {
|
|
config := Config{
|
|
Provider: "openai",
|
|
APIKey: "sk-test-key",
|
|
Model: "whisper-1",
|
|
Language: "en",
|
|
Keywords: []string{"test"},
|
|
}
|
|
|
|
adapter := NewOpenAIAdapterFromConfig(config)
|
|
if adapter == nil {
|
|
t.Errorf("NewOpenAIAdapterFromConfig() returned nil")
|
|
return
|
|
}
|
|
|
|
if adapter.model != config.Model {
|
|
t.Errorf("model = %q, want %q", adapter.model, config.Model)
|
|
}
|
|
|
|
if adapter.language != config.Language {
|
|
t.Errorf("language = %q, want %q", adapter.language, config.Language)
|
|
}
|
|
|
|
if adapter.providerName != "openai" {
|
|
t.Errorf("providerName = %q, want %q", adapter.providerName, "openai")
|
|
}
|
|
}
|
|
|
|
// MockStreamingAdapter implements StreamingAdapter for testing
|
|
type MockStreamingAdapter struct {
|
|
StartFunc func(ctx context.Context, language string) error
|
|
SendChunkFunc func(audio []byte) error
|
|
ResultsFunc func() <-chan TranscriptionResult
|
|
FinalizeFunc func(ctx context.Context) error
|
|
CloseFunc func() error
|
|
|
|
resultsCh chan TranscriptionResult
|
|
}
|
|
|
|
func NewMockStreamingAdapter() *MockStreamingAdapter {
|
|
return &MockStreamingAdapter{
|
|
resultsCh: make(chan TranscriptionResult, 10),
|
|
}
|
|
}
|
|
|
|
func (m *MockStreamingAdapter) Start(ctx context.Context, language string) error {
|
|
if m.StartFunc != nil {
|
|
return m.StartFunc(ctx, language)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStreamingAdapter) SendChunk(audio []byte) error {
|
|
if m.SendChunkFunc != nil {
|
|
return m.SendChunkFunc(audio)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStreamingAdapter) Results() <-chan TranscriptionResult {
|
|
if m.ResultsFunc != nil {
|
|
return m.ResultsFunc()
|
|
}
|
|
return m.resultsCh
|
|
}
|
|
|
|
func (m *MockStreamingAdapter) Finalize(ctx context.Context) error {
|
|
if m.FinalizeFunc != nil {
|
|
return m.FinalizeFunc(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStreamingAdapter) Close() error {
|
|
if m.CloseFunc != nil {
|
|
return m.CloseFunc()
|
|
}
|
|
close(m.resultsCh)
|
|
return nil
|
|
}
|
|
|
|
func (m *MockStreamingAdapter) SendResult(result TranscriptionResult) {
|
|
m.resultsCh <- result
|
|
}
|
|
|
|
func TestStreamingTranscriber_Start(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
errCh, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if errCh == nil {
|
|
t.Errorf("Start() returned nil error channel")
|
|
}
|
|
|
|
close(frameCh)
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStreamingTranscriber_StartError(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
adapter.StartFunc = func(ctx context.Context, language string) error {
|
|
return fmt.Errorf("connection failed")
|
|
}
|
|
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx := context.Background()
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
_, err := transcriber.Start(ctx, frameCh)
|
|
if err == nil {
|
|
t.Errorf("Start() should fail when adapter.Start fails")
|
|
}
|
|
}
|
|
|
|
func TestStreamingTranscriber_AccumulatesResults(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
_, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// send some final results
|
|
adapter.SendResult(TranscriptionResult{Text: "hello", IsFinal: true})
|
|
adapter.SendResult(TranscriptionResult{Text: "world", IsFinal: true})
|
|
|
|
// give time for results to be processed
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
close(frameCh)
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
|
|
result, err := transcriber.GetFinalTranscription()
|
|
if err != nil {
|
|
t.Errorf("GetFinalTranscription() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if result != "hello world" {
|
|
t.Errorf("GetFinalTranscription() = %q, want %q", result, "hello world")
|
|
}
|
|
}
|
|
|
|
func TestStreamingTranscriber_IgnoresPartialResults(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
_, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// partial results should be ignored
|
|
adapter.SendResult(TranscriptionResult{Text: "hel", IsFinal: false})
|
|
adapter.SendResult(TranscriptionResult{Text: "hello", IsFinal: true})
|
|
adapter.SendResult(TranscriptionResult{Text: "hello wor", IsFinal: false})
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
close(frameCh)
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
|
|
result, err := transcriber.GetFinalTranscription()
|
|
if err != nil {
|
|
t.Errorf("GetFinalTranscription() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if result != "hello" {
|
|
t.Errorf("GetFinalTranscription() = %q, want %q", result, "hello")
|
|
}
|
|
}
|
|
|
|
func TestStreamingTranscriber_HandlesErrors(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
errCh, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// send an error result
|
|
adapter.SendResult(TranscriptionResult{Error: fmt.Errorf("transcription error")})
|
|
|
|
// error should be received on errCh
|
|
select {
|
|
case e := <-errCh:
|
|
if e == nil {
|
|
t.Errorf("expected error on errCh")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Errorf("timeout waiting for error on errCh")
|
|
}
|
|
|
|
close(frameCh)
|
|
_ = transcriber.Stop(ctx)
|
|
}
|
|
|
|
func TestStreamingTranscriber_SendsAudioChunks(t *testing.T) {
|
|
var receivedChunks [][]byte
|
|
adapter := NewMockStreamingAdapter()
|
|
adapter.SendChunkFunc = func(audio []byte) error {
|
|
chunk := make([]byte, len(audio))
|
|
copy(chunk, audio)
|
|
receivedChunks = append(receivedChunks, chunk)
|
|
return nil
|
|
}
|
|
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
_, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// send audio frames
|
|
frameCh <- recording.AudioFrame{Data: []byte{1, 2, 3, 4}}
|
|
frameCh <- recording.AudioFrame{Data: []byte{5, 6, 7, 8}}
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
close(frameCh)
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
|
|
if len(receivedChunks) != 2 {
|
|
t.Errorf("expected 2 chunks, got %d", len(receivedChunks))
|
|
}
|
|
}
|
|
|
|
func TestStreamingTranscriber_ContextCancellation(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
_, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// cancel context
|
|
cancel()
|
|
|
|
// stop should complete without hanging
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = transcriber.Stop(context.Background())
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// success
|
|
case <-time.After(2 * time.Second):
|
|
t.Errorf("Stop() timed out after context cancellation")
|
|
}
|
|
}
|
|
|
|
func TestStreamingTranscriber_GetFinalTranscriptionSafe(t *testing.T) {
|
|
adapter := NewMockStreamingAdapter()
|
|
transcriber := NewStreamingTranscriber(adapter, "en")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
frameCh := make(chan recording.AudioFrame, 10)
|
|
|
|
_, err := transcriber.Start(ctx, frameCh)
|
|
if err != nil {
|
|
t.Errorf("Start() error = %v", err)
|
|
return
|
|
}
|
|
|
|
// call GetFinalTranscription concurrently while results are being added
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for i := 0; i < 100; i++ {
|
|
_, _ = transcriber.GetFinalTranscription()
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
// send results concurrently
|
|
for i := 0; i < 10; i++ {
|
|
adapter.SendResult(TranscriptionResult{Text: "word", IsFinal: true})
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
|
|
<-done
|
|
|
|
close(frameCh)
|
|
err = transcriber.Stop(ctx)
|
|
if err != nil {
|
|
t.Errorf("Stop() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewTranscriber_LanguageFallback(t *testing.T) {
|
|
// test that incompatible language falls back to auto-detect (no error)
|
|
// base.en only supports English
|
|
config := Config{
|
|
Provider: "whisper-cpp",
|
|
Language: "es", // Spanish not supported by English-only model
|
|
Model: "base.en",
|
|
}
|
|
|
|
// should succeed (fallback to auto), not error
|
|
transcriber, err := NewTranscriber(config)
|
|
if err != nil {
|
|
t.Errorf("NewTranscriber() should fall back to auto, got error: %v", err)
|
|
return
|
|
}
|
|
|
|
if transcriber == nil {
|
|
t.Errorf("NewTranscriber() returned nil transcriber")
|
|
}
|
|
}
|
|
|
|
func TestNewTranscriber_AutoLanguageNoFallback(t *testing.T) {
|
|
// test that auto language never triggers warning/fallback
|
|
config := Config{
|
|
Provider: "whisper-cpp",
|
|
Language: "", // auto
|
|
Model: "base.en",
|
|
}
|
|
|
|
transcriber, err := NewTranscriber(config)
|
|
if err != nil {
|
|
t.Errorf("NewTranscriber() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if transcriber == nil {
|
|
t.Errorf("NewTranscriber() returned nil transcriber")
|
|
}
|
|
}
|
|
|
|
func TestNewTranscriber_CompatibleLanguageNoFallback(t *testing.T) {
|
|
// test that compatible language works normally
|
|
config := Config{
|
|
Provider: "whisper-cpp",
|
|
Language: "en", // English supported by English-only model
|
|
Model: "base.en",
|
|
}
|
|
|
|
transcriber, err := NewTranscriber(config)
|
|
if err != nil {
|
|
t.Errorf("NewTranscriber() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if transcriber == nil {
|
|
t.Errorf("NewTranscriber() returned nil transcriber")
|
|
}
|
|
}
|
|
|
|
func TestNewTranscriber_MultilingualModelAllLanguages(t *testing.T) {
|
|
// test that multilingual model accepts any language without fallback
|
|
config := Config{
|
|
Provider: "groq-transcription",
|
|
APIKey: "test-key",
|
|
Language: "es", // Spanish
|
|
Model: "whisper-large-v3", // multilingual
|
|
}
|
|
|
|
transcriber, err := NewTranscriber(config)
|
|
if err != nil {
|
|
t.Errorf("NewTranscriber() error = %v", err)
|
|
return
|
|
}
|
|
|
|
if transcriber == nil {
|
|
t.Errorf("NewTranscriber() returned nil transcriber")
|
|
}
|
|
}
|