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,101 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// What counts as a LOCAL model alias, and where that is demanded (ADR-034, ADR-035 review).
|
||||
// A private transcript may be sent to a local model and to nothing else; the check is a
|
||||
// guard against a misconfiguration or a crafted alias, so it must be strict about shape,
|
||||
// not just about a prefix.
|
||||
|
||||
func TestIsLocalModel_AcceptsRealLocalAliasesAndNothingShapedLikeAnEscape(t *testing.T) {
|
||||
tests := []struct {
|
||||
alias string
|
||||
want bool
|
||||
}{
|
||||
{"koala/phi4-mini", true},
|
||||
{"iguana/gemma4-26b", true},
|
||||
{"iguana/gemma4-31b-think", true},
|
||||
{"koala/qwen36-35b-a3b", true},
|
||||
{"iguana/kb-whisper", true},
|
||||
{"iguana/hf.co-model_v1:q4", true},
|
||||
{"", false},
|
||||
{"koala/", false},
|
||||
{"iguana/", false},
|
||||
{"gemma4-26b", false},
|
||||
{"berget/mistral-small", false},
|
||||
{"berget/koala/x", false},
|
||||
{"xkoala/x", false},
|
||||
{"Koala/x", false},
|
||||
{" koala/x", false},
|
||||
{"koala/x ", false},
|
||||
{"koala/x y", false},
|
||||
{"koala/x\n", false},
|
||||
{"koala/../berget/kb", false},
|
||||
{"koala//berget/mistral-small", false},
|
||||
{"koala/x/y", false},
|
||||
{"koala/x?host=berget", false},
|
||||
{"koala/x#frag", false},
|
||||
{"koala/x@evil", false},
|
||||
{"koalа/x", false}, // Cyrillic а
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.alias, func(t *testing.T) {
|
||||
if got := IsLocalModel(tt.alias); got != tt.want {
|
||||
t.Errorf("IsLocalModel(%q) = %v, want %v", tt.alias, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_UploadChatModelMustBeLocalBecauseTheWholeTranscriptIsSentToIt(t *testing.T) {
|
||||
for _, alias := range []string{"berget/mistral-small", "gemini/gemini-2.5-pro", "gemma4-26b", "koala/../berget/kb"} {
|
||||
t.Run(alias, func(t *testing.T) {
|
||||
clearUploadEnv(t)
|
||||
setEnv(t, map[string]string{"TAPIR_UPLOAD_CHAT_MODEL": alias})
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Errorf("Load accepted TAPIR_UPLOAD_CHAT_MODEL=%q: a private transcript of up to 90,000 characters would be sent to it", alias)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_ALocalUploadChatModelIsAccepted(t *testing.T) {
|
||||
clearUploadEnv(t)
|
||||
setEnv(t, map[string]string{"TAPIR_UPLOAD_CHAT_MODEL": "iguana/gemma4-31b"})
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.UploadChatModel != "iguana/gemma4-31b" {
|
||||
t.Errorf("UploadChatModel = %q", cfg.UploadChatModel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_TheEngineTimeoutDefaultsToTwentyMinutesAndMustBePositive(t *testing.T) {
|
||||
clearUploadEnv(t)
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.TranscribeEngineTimeout != 20*time.Minute {
|
||||
t.Errorf("TranscribeEngineTimeout = %v, want 20m: a 90-minute recording takes about 8 minutes on kb-whisper, and two wedged engines in sequence must leave the job time to reach a fallback", cfg.TranscribeEngineTimeout)
|
||||
}
|
||||
|
||||
setEnv(t, map[string]string{"TAPIR_TRANSCRIBE_ENGINE_TIMEOUT": "7m"})
|
||||
cfg, err = Load()
|
||||
if err != nil || cfg.TranscribeEngineTimeout != 7*time.Minute {
|
||||
t.Errorf("override: %v, %v", cfg.TranscribeEngineTimeout, err)
|
||||
}
|
||||
|
||||
for _, bad := range []string{"0", "-1m", "soon"} {
|
||||
setEnv(t, map[string]string{"TAPIR_TRANSCRIBE_ENGINE_TIMEOUT": bad})
|
||||
if _, err := Load(); err == nil {
|
||||
t.Errorf("Load accepted TAPIR_TRANSCRIBE_ENGINE_TIMEOUT=%q", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user