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,57 @@
|
||||
// Package reconcile merges two transcripts of the same speech: it finds where they
|
||||
// disagree word by word, resolves the trivial disagreements itself, and asks a LOCAL
|
||||
// model to choose between the two readings of the rest (ADR-034). It depends on
|
||||
// nothing outside the standard library and the domain; the model, the engines and
|
||||
// the store are wired in from outside.
|
||||
package reconcile
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Token is one word of a transcript. Text is the word as written, with its casing
|
||||
// and punctuation, so the merged transcript reads naturally; Key is what two engines
|
||||
// are compared on: lower case, letters and digits only.
|
||||
type Token struct {
|
||||
Text string
|
||||
Key string
|
||||
}
|
||||
|
||||
// Tokenize splits s on whitespace. A "word" that has no letter or digit (a dash, a
|
||||
// lone full stop) is not something two engines can disagree about, so it is joined
|
||||
// to the neighbouring word instead of becoming a token of its own.
|
||||
func Tokenize(s string) []Token {
|
||||
var out []Token
|
||||
pending := ""
|
||||
for _, w := range strings.Fields(s) {
|
||||
k := key(w)
|
||||
if k == "" {
|
||||
if len(out) > 0 {
|
||||
out[len(out)-1].Text += " " + w
|
||||
} else if pending == "" {
|
||||
pending = w
|
||||
} else {
|
||||
pending += " " + w
|
||||
}
|
||||
continue
|
||||
}
|
||||
text := w
|
||||
if pending != "" {
|
||||
text = pending + " " + w
|
||||
pending = ""
|
||||
}
|
||||
out = append(out, Token{Text: text, Key: k})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func key(w string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range w {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user