Upload a recording (audio, or video with the camera off), get a private Swedish transcript, and chat about it. - ADR-033: RLS-scoped private transcript store; ffprobe/ffmpeg intake; gateway transcription (iguana kb-whisper, berget fallback); in-process single-replica worker; chat over the transcript (local models only). - ADR-034: optional two-engine mode (pianissimo + kb-whisper) reconciled by a LOCAL model; off unless TAPIR_RECONCILE_BASE_MODEL is set. - ADR-035: real progress (phases, running/quiet/lost), phase logging, every judged spot stored, merged segments. - Hardening from four adversarial reviews: errors logged and stored as kinds never text; per-engine deadlines; converted-length check; bounded queue; client hang-ups are not errors; strict local-alias check. - Dockerfile: static ffmpeg/ffprobe 7.0.2 pinned by sha256 (#34). Built and inspected by hand; CI does not build the image on a PR, so its first CI build is this merge. Uploads stay OFF until TAPIR_UPLOADS_ENABLED is set in the deployment. Closes #33, #34, #42, #43, #44. Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #39.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package transcribe_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.d-ma.be/mathias/tapir/internal/adapters/transcribe"
|
||||
"git.d-ma.be/mathias/tapir/internal/domain"
|
||||
)
|
||||
|
||||
type fakeBackend struct {
|
||||
name string
|
||||
text string
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakeBackend) Name() string { return f.name }
|
||||
func (f *fakeBackend) Transcribe(_ context.Context, _ string) (domain.Transcription, error) {
|
||||
f.calls++
|
||||
if f.err != nil {
|
||||
return domain.Transcription{}, f.err
|
||||
}
|
||||
return domain.Transcription{Text: f.text, Endpoint: f.name}, nil
|
||||
}
|
||||
|
||||
func TestChain_PrimarySuccessNeverTouchesTheFallback(t *testing.T) {
|
||||
primary := &fakeBackend{name: "iguana/kb-whisper", text: "primary text"}
|
||||
fallback := &fakeBackend{name: "berget/kb-whisper", text: "fallback text"}
|
||||
|
||||
got, err := transcribe.NewChain(primary, fallback).Transcribe(context.Background(), "/a.wav")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "primary text", got.Text)
|
||||
assert.Equal(t, "iguana/kb-whisper", got.Endpoint)
|
||||
assert.Zero(t, fallback.calls, "audio must not leave the homelab when the local endpoint worked")
|
||||
}
|
||||
|
||||
func TestChain_AnyPrimaryFailureFallsBackAndRecordsTheFallbackEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{"server error", &transcribe.StatusError{Code: 500}},
|
||||
{"gateway timeout", &transcribe.StatusError{Code: 504}},
|
||||
{"client error", &transcribe.StatusError{Code: 400}},
|
||||
{"transport failure", errors.New("dial tcp: connection refused")},
|
||||
{"empty transcript", transcribe.ErrEmptyTranscript},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
primary := &fakeBackend{name: "iguana/kb-whisper", err: tt.err}
|
||||
fallback := &fakeBackend{name: "berget/kb-whisper", text: "fallback text"}
|
||||
|
||||
got, err := transcribe.NewChain(primary, fallback).Transcribe(context.Background(), "/a.wav")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "fallback text", got.Text)
|
||||
assert.Equal(t, "berget/kb-whisper", got.Endpoint,
|
||||
"the row must say berget served it: that means the audio left the homelab")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChain_BothFailingReportsBothErrors(t *testing.T) {
|
||||
primary := &fakeBackend{name: "iguana/kb-whisper", err: errors.New("iguana down")}
|
||||
fallback := &fakeBackend{name: "berget/kb-whisper", err: errors.New("berget down")}
|
||||
|
||||
_, err := transcribe.NewChain(primary, fallback).Transcribe(context.Background(), "/a.wav")
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "iguana down")
|
||||
assert.Contains(t, err.Error(), "berget down")
|
||||
}
|
||||
|
||||
func TestChain_WithoutAFallbackAPrimaryFailureIsTheResult(t *testing.T) {
|
||||
primary := &fakeBackend{name: "iguana/kb-whisper", err: errors.New("iguana down")}
|
||||
|
||||
_, err := transcribe.NewChain(primary, nil).Transcribe(context.Background(), "/a.wav")
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "iguana down")
|
||||
}
|
||||
|
||||
func TestChain_ACancelledJobDoesNotFallBack(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
primary := &fakeBackend{name: "iguana/kb-whisper", err: context.Canceled}
|
||||
fallback := &fakeBackend{name: "berget/kb-whisper", text: "x"}
|
||||
|
||||
_, err := transcribe.NewChain(primary, fallback).Transcribe(ctx, "/a.wav")
|
||||
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
assert.Zero(t, fallback.calls, "a cancelled job must not ship the audio to a cloud endpoint")
|
||||
}
|
||||
Reference in New Issue
Block a user