c7f59e48f4
mavsttd is whisper.cpp linked into a Go daemon and reached over a unix socket. CrisperWhisper 2.0 cannot be reached that way. whisper.cpp derives its language count from the vocabulary size, and CW2's 51897 tokens shift seven special token ids, so it never loads at all. So it runs under transformers on workpc and this is the client. Same stt.Transcriber interface and one method, a second transport rather than a second seam. The body is the PCM itself, because a minute of 16kHz mono is under 2MB raw and the format is fixed by audio.PCM16kMono. Audio is the most sensitive thing that crosses this seam, so the client carries a bearer token. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package stt
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/audio"
|
|
)
|
|
|
|
func TestHTTPTranscriberSendsRawPCM(t *testing.T) {
|
|
t.Parallel()
|
|
var gotBody []byte
|
|
var gotHeader http.Header
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotBody, _ = io.ReadAll(r.Body)
|
|
gotHeader = r.Header.Clone()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"text":"привет","confidence":0.82}`)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
a := audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("pcm-bytes")}
|
|
tr := NewHTTPTranscriber(srv.URL, "s3cret", "ru", 2*time.Second)
|
|
text, conf, err := tr.Transcribe(context.Background(), a)
|
|
if err != nil {
|
|
t.Fatalf("Transcribe: %v", err)
|
|
}
|
|
if text != "привет" || conf != 0.82 {
|
|
t.Fatalf("got %q %v", text, conf)
|
|
}
|
|
if string(gotBody) != "pcm-bytes" {
|
|
t.Fatalf("body should be the PCM itself, got %q", gotBody)
|
|
}
|
|
if got := gotHeader.Get("X-Sample-Rate"); got != strconv.Itoa(audio.PCM16kMono.SampleRate) {
|
|
t.Fatalf("X-Sample-Rate = %q", got)
|
|
}
|
|
if got := gotHeader.Get("X-Language"); got != "ru" {
|
|
t.Fatalf("X-Language = %q", got)
|
|
}
|
|
// Audio is the most sensitive thing crossing this seam.
|
|
if got := gotHeader.Get("Authorization"); got != "Bearer s3cret" {
|
|
t.Fatalf("Authorization = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestHTTPTranscriberOmitsEmptyToken(t *testing.T) {
|
|
t.Parallel()
|
|
var auth string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
auth = r.Header.Get("Authorization")
|
|
_, _ = io.WriteString(w, `{"text":"x"}`)
|
|
}))
|
|
defer srv.Close()
|
|
a := audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("x")}
|
|
if _, _, err := NewHTTPTranscriber(srv.URL, "", "ru", time.Second).Transcribe(context.Background(), a); err != nil {
|
|
t.Fatalf("Transcribe: %v", err)
|
|
}
|
|
if auth != "" {
|
|
t.Fatalf("Authorization should be absent, got %q", auth)
|
|
}
|
|
}
|
|
|
|
func TestHTTPTranscriberRefusesWrongFormat(t *testing.T) {
|
|
t.Parallel()
|
|
a := audio.Audio{Format: audio.Format{SampleRate: 44100, Channels: 2, SampleBits: 16, Encoding: "pcm_s16le"}}
|
|
_, _, err := NewHTTPTranscriber("http://example.invalid", "", "ru", time.Second).Transcribe(context.Background(), a)
|
|
if !errors.Is(err, ErrFormat) {
|
|
t.Fatalf("want ErrFormat, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPTranscriberErrorsOnBadStatus(t *testing.T) {
|
|
t.Parallel()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
a := audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("x")}
|
|
_, _, err := NewHTTPTranscriber(srv.URL, "", "ru", time.Second).Transcribe(context.Background(), a)
|
|
if err == nil {
|
|
t.Fatal("a 401 must be an error, so the Pair falls back")
|
|
}
|
|
}
|