feat: upload audio, get a private Swedish transcript, discuss it (#33, ADR-033) (#39)
CI / Deploy via GitOps (push) Successful in 7s
CI / Lint / Test / Vet (push) Successful in 33s
CI / Build & Import (push) Successful in 23s

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:
2026-10-03 14:13:37 +00:00
committed by mathias
parent 4bafd5f082
commit 4a2067063d
104 changed files with 14214 additions and 311 deletions
+66
View File
@@ -0,0 +1,66 @@
package reconcile_test
import (
"testing"
"github.com/stretchr/testify/assert"
"git.d-ma.be/mathias/tapir/internal/reconcile"
)
func texts(ts []reconcile.Token) []string {
out := make([]string, len(ts))
for i, t := range ts {
out[i] = t.Text
}
return out
}
func keys(ts []reconcile.Token) []string {
out := make([]string, len(ts))
for i, t := range ts {
out[i] = t.Key
}
return out
}
func TestTokenize_KeepsTheWordAsWrittenAndKeysItWithoutCaseOrPunctuation(t *testing.T) {
got := reconcile.Tokenize("Hej, allihopa! Vi går igenom ÅÄÖ.")
assert.Equal(t, []string{"Hej,", "allihopa!", "Vi", "går", "igenom", "ÅÄÖ."}, texts(got),
"the original spelling and punctuation are kept for the merged text")
assert.Equal(t, []string{"hej", "allihopa", "vi", "går", "igenom", "åäö"}, keys(got),
"the key is lower case with punctuation removed, Swedish letters lowered correctly")
}
func TestTokenize_KeysIgnoreHyphensAndPlusSoOnlyWordsDecideEquality(t *testing.T) {
got := reconcile.Tokenize("end-to-end A+2 5 000 kr")
assert.Equal(t, []string{"endtoend", "a2", "5", "000", "kr"}, keys(got))
}
func TestTokenize_APunctuationOnlyWordJoinsThePreviousWordInsteadOfBecomingOne(t *testing.T) {
got := reconcile.Tokenize("Vi går – sen tillbaka . Slut")
assert.Equal(t, []string{"Vi", "går –", "sen", "tillbaka .", "Slut"}, texts(got),
"a dash or a lone full stop is not a word the engines can disagree about")
assert.Equal(t, []string{"vi", "går", "sen", "tillbaka", "slut"}, keys(got))
}
func TestTokenize_LeadingPunctuationOnlyWordJoinsTheNextWord(t *testing.T) {
got := reconcile.Tokenize("— Hej då")
assert.Equal(t, []string{"— Hej", "då"}, texts(got))
}
func TestTokenize_NewlinesAndRunsOfSpaceAreJustSeparators(t *testing.T) {
got := reconcile.Tokenize(" Hej\n allihopa \t Vi ")
assert.Equal(t, []string{"Hej", "allihopa", "Vi"}, texts(got))
}
func TestTokenize_EmptyAndPunctuationOnlyInputGiveNoTokens(t *testing.T) {
assert.Empty(t, reconcile.Tokenize(""))
assert.Empty(t, reconcile.Tokenize(" \n "))
assert.Empty(t, reconcile.Tokenize("— . !"))
}