Implement WhisperX HTTP adapter
This commit is contained in:
271
internal/adapters/whisperx/http_test.go
Normal file
271
internal/adapters/whisperx/http_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package whisperx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestHTTPClientTranscribeSuccess(t *testing.T) {
|
||||
var gotLanguage string
|
||||
var gotFileField string
|
||||
var gotFileSize int
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Fatalf("method = %s, want POST", r.Method)
|
||||
}
|
||||
if err := r.ParseMultipartForm(4 << 20); err != nil {
|
||||
t.Fatalf("ParseMultipartForm() error = %v", err)
|
||||
}
|
||||
|
||||
gotLanguage = r.FormValue("language")
|
||||
file, fh, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
t.Fatalf("FormFile(file) error = %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
gotFileField = fh.Filename
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll(file) error = %v", err)
|
||||
}
|
||||
gotFileSize = len(data)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"schema":"speaker_transcript.v1","segments":[]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, err := NewHTTPClientFromConfigValues(srv.URL, "en", "2s", "10ms", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClientFromConfigValues() error = %v", err)
|
||||
}
|
||||
|
||||
audioPath := writeWhisperXTestFile(t, "audio.flac", "audio-data")
|
||||
outPath := filepath.Join(t.TempDir(), "raw.json")
|
||||
|
||||
res, err := client.Transcribe(context.Background(), TranscribeRequest{
|
||||
SpeakerID: "alice",
|
||||
AudioPath: audioPath,
|
||||
OutputRawTranscriptPath: outPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Transcribe() error = %v", err)
|
||||
}
|
||||
if res.Attempts != 1 {
|
||||
t.Fatalf("Attempts = %d, want 1", res.Attempts)
|
||||
}
|
||||
if res.HTTPStatus != http.StatusOK {
|
||||
t.Fatalf("HTTPStatus = %d, want %d", res.HTTPStatus, http.StatusOK)
|
||||
}
|
||||
if gotLanguage != "en" {
|
||||
t.Fatalf("language = %q, want en", gotLanguage)
|
||||
}
|
||||
if gotFileField != "audio.flac" {
|
||||
t.Fatalf("file field filename = %q, want audio.flac", gotFileField)
|
||||
}
|
||||
if gotFileSize == 0 {
|
||||
t.Fatal("file size = 0, want >0")
|
||||
}
|
||||
verifyJSONFile(t, outPath)
|
||||
}
|
||||
|
||||
func TestHTTPClientRetriesOnTransientAndSucceeds(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n := calls.Add(1)
|
||||
if n == 1 {
|
||||
http.Error(w, "temporary", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, err := NewHTTPClientFromConfigValues(srv.URL, "en", "2s", "1ms", 2)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClientFromConfigValues() error = %v", err)
|
||||
}
|
||||
|
||||
audioPath := writeWhisperXTestFile(t, "audio.flac", "audio-data")
|
||||
outPath := filepath.Join(t.TempDir(), "raw.json")
|
||||
res, err := client.Transcribe(context.Background(), TranscribeRequest{
|
||||
AudioPath: audioPath,
|
||||
OutputRawTranscriptPath: outPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Transcribe() error = %v", err)
|
||||
}
|
||||
if res.Attempts != 2 {
|
||||
t.Fatalf("Attempts = %d, want 2", res.Attempts)
|
||||
}
|
||||
if calls.Load() != 2 {
|
||||
t.Fatalf("calls = %d, want 2", calls.Load())
|
||||
}
|
||||
verifyJSONFile(t, outPath)
|
||||
}
|
||||
|
||||
func TestHTTPClientDoesNotRetryOnNonRetryableStatus(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, err := NewHTTPClientFromConfigValues(srv.URL, "en", "2s", "1ms", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClientFromConfigValues() error = %v", err)
|
||||
}
|
||||
|
||||
audioPath := writeWhisperXTestFile(t, "audio.flac", "audio-data")
|
||||
outPath := filepath.Join(t.TempDir(), "raw.json")
|
||||
_, err = client.Transcribe(context.Background(), TranscribeRequest{
|
||||
AudioPath: audioPath,
|
||||
OutputRawTranscriptPath: outPath,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Transcribe() error = nil, want non-nil")
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("calls = %d, want 1", calls.Load())
|
||||
}
|
||||
if _, statErr := os.Stat(outPath); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("output should not exist, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientInvalidJSONFailsAndDoesNotPromote(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`not-json`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, err := NewHTTPClientFromConfigValues(srv.URL, "en", "2s", "1ms", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClientFromConfigValues() error = %v", err)
|
||||
}
|
||||
|
||||
audioPath := writeWhisperXTestFile(t, "audio.flac", "audio-data")
|
||||
outPath := filepath.Join(t.TempDir(), "raw.json")
|
||||
_, err = client.Transcribe(context.Background(), TranscribeRequest{
|
||||
AudioPath: audioPath,
|
||||
OutputRawTranscriptPath: outPath,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Transcribe() error = nil, want non-nil")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "json") {
|
||||
t.Fatalf("error = %q, want json context", err.Error())
|
||||
}
|
||||
if _, statErr := os.Stat(outPath); !errors.Is(statErr, os.ErrNotExist) {
|
||||
t.Fatalf("output should not exist, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientTimeoutRetryDeterministic(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, err := NewHTTPClientFromConfigValues(srv.URL, "en", "50ms", "1ms", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClientFromConfigValues() error = %v", err)
|
||||
}
|
||||
|
||||
audioPath := writeWhisperXTestFile(t, "audio.flac", "audio-data")
|
||||
outPath := filepath.Join(t.TempDir(), "raw.json")
|
||||
res, err := client.Transcribe(context.Background(), TranscribeRequest{
|
||||
AudioPath: audioPath,
|
||||
OutputRawTranscriptPath: outPath,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Transcribe() error = nil, want timeout error")
|
||||
}
|
||||
if calls.Load() != 2 {
|
||||
t.Fatalf("calls = %d, want 2 attempts", calls.Load())
|
||||
}
|
||||
if res.Attempts != 2 {
|
||||
t.Fatalf("Attempts = %d, want 2", res.Attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientCancelDoesNotRetry(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls.Add(1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
_, _ = w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, err := NewHTTPClientFromConfigValues(srv.URL, "en", "2s", "1ms", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPClientFromConfigValues() error = %v", err)
|
||||
}
|
||||
|
||||
audioPath := writeWhisperXTestFile(t, "audio.flac", "audio-data")
|
||||
outPath := filepath.Join(t.TempDir(), "raw.json")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err = client.Transcribe(ctx, TranscribeRequest{
|
||||
AudioPath: audioPath,
|
||||
OutputRawTranscriptPath: outPath,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Transcribe() error = nil, want cancellation error")
|
||||
}
|
||||
if calls.Load() > 1 {
|
||||
t.Fatalf("calls = %d, want <= 1 when already canceled", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientConstructorValidation(t *testing.T) {
|
||||
_, err := NewHTTPClientFromConfigValues("", "en", "30m", "2s", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing URL error")
|
||||
}
|
||||
_, err = NewHTTPClientFromConfigValues("https://example.com", "en", "bad", "2s", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected bad timeout error")
|
||||
}
|
||||
_, err = NewHTTPClientFromConfigValues("https://example.com", "en", "30m", "bad", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected bad retry_delay error")
|
||||
}
|
||||
}
|
||||
|
||||
func writeWhisperXTestFile(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(%q) error = %v", path, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func verifyJSONFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%q) error = %v", path, err)
|
||||
}
|
||||
var v any
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
t.Fatalf("json unmarshal output %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user