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,130 @@
|
||||
package reconcile_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.d-ma.be/mathias/tapir/internal/domain"
|
||||
"git.d-ma.be/mathias/tapir/internal/reconcile"
|
||||
)
|
||||
|
||||
// Every judged spot is stored with a few words of left context, and the saved segments
|
||||
// carry the merged words while keeping the base's timing (ADR-035 decisions 6 and 7).
|
||||
|
||||
func TestReconcileSpots_AKeptSpotAndAChangedSpotAreBothStoredWithTheirVerdict(t *testing.T) {
|
||||
adj := &scripted{fn: func(i int, _ reconcile.Request) (reconcile.Verdict, error) {
|
||||
if i == 0 {
|
||||
return reconcile.VerdictBase, nil
|
||||
}
|
||||
return reconcile.VerdictAlt, nil
|
||||
}}
|
||||
|
||||
out := merge(t, tr(progBase), tr(progAlt), adj)
|
||||
|
||||
require.Len(t, out.Reconciliation.Contested, 2)
|
||||
assert.Equal(t, "base", out.Reconciliation.Contested[0].Used)
|
||||
assert.Equal(t, "1015", out.Reconciliation.Contested[0].Base)
|
||||
assert.Equal(t, "alt", out.Reconciliation.Contested[1].Used)
|
||||
assert.Equal(t, 1, out.Reconciliation.Changed, "the exact counts are unchanged by storing more")
|
||||
}
|
||||
|
||||
func TestReconcileSpots_AutoResolvedDisputesAreNotStored(t *testing.T) {
|
||||
out := merge(t, tr("i takrenns delen ska vi ju betala"), tr("i takrennsdelen ska vi betala"), always(reconcile.VerdictAlt))
|
||||
|
||||
assert.Empty(t, out.Reconciliation.Contested, "compounds and fillers were never judged; they are counted, not listed")
|
||||
}
|
||||
|
||||
func TestReconcileSpots_ASpotCarriesTheWordsJustBeforeIt(t *testing.T) {
|
||||
out := merge(t, tr("grannen målar ett vanligt staket mot gatan"), tr("grannen målar ett KORT staket mot gatan"), always(reconcile.VerdictAlt))
|
||||
|
||||
require.Len(t, out.Reconciliation.Contested, 1)
|
||||
assert.Equal(t, "grannen målar ett", out.Reconciliation.Contested[0].Before)
|
||||
}
|
||||
|
||||
func TestReconcileSpots_TheContextIsAFewWordsNotTheWholeMeeting(t *testing.T) {
|
||||
long := "ett två tre fyra fem sex sju åtta nio tio elva tolv"
|
||||
out := merge(t, tr(long+" X slut"), tr(long+" Y slut"), always(reconcile.VerdictAlt))
|
||||
|
||||
require.Len(t, out.Reconciliation.Contested, 1)
|
||||
assert.Equal(t, "fem sex sju åtta nio tio elva tolv", out.Reconciliation.Contested[0].Before,
|
||||
"eight words: enough to find the place, small enough to stay out of the way")
|
||||
}
|
||||
|
||||
func segmented(text string, segs ...domain.Segment) domain.Transcription {
|
||||
t := tr(text)
|
||||
t.Segments = segs
|
||||
return t
|
||||
}
|
||||
|
||||
func TestReconcileSegments_AChangedWordReplacesTheBaseWordInItsOwnSegmentOnly(t *testing.T) {
|
||||
base := segmented("ett två tre. fyra fem sex. sju åtta nio.",
|
||||
domain.Segment{Start: 0, End: 10, Text: "ett två tre."},
|
||||
domain.Segment{Start: 10, End: 20, Text: "fyra fem sex."},
|
||||
domain.Segment{Start: 20, End: 30, Text: "sju åtta nio."})
|
||||
|
||||
out := merge(t, base, tr("ett två tre. fyra femm sex. sju åtta nio."), always(reconcile.VerdictAlt))
|
||||
|
||||
require.Len(t, out.Segments, 3)
|
||||
assert.Equal(t, "ett två tre.", out.Segments[0].Text, "an untouched segment keeps its exact text")
|
||||
assert.Equal(t, "fyra femm sex.", out.Segments[1].Text)
|
||||
assert.Equal(t, "sju åtta nio.", out.Segments[2].Text)
|
||||
assert.InDelta(t, 10.0, out.Segments[1].Start, 1e-9, "timing stays the base's")
|
||||
assert.InDelta(t, 20.0, out.Segments[1].End, 1e-9)
|
||||
}
|
||||
|
||||
func TestReconcileSegments_AWordOnlyTheAlternativeHeardLandsInTheSegmentItBelongsTo(t *testing.T) {
|
||||
base := segmented("a b c. d e f.", domain.Segment{Start: 0, End: 5, Text: "a b c."}, domain.Segment{Start: 5, End: 10, Text: "d e f."})
|
||||
|
||||
out := merge(t, base, tr("a b c. d NY e f."), always(reconcile.VerdictAlt))
|
||||
|
||||
assert.Equal(t, "a b c.", out.Segments[0].Text)
|
||||
assert.Equal(t, "d NY e f.", out.Segments[1].Text)
|
||||
}
|
||||
|
||||
func TestReconcileSegments_AWordOnlyTheBaseHeardIsRemovedFromItsSegment(t *testing.T) {
|
||||
base := segmented("a b c. d X e f.", domain.Segment{Start: 0, End: 5, Text: "a b c."}, domain.Segment{Start: 5, End: 10, Text: "d X e f."})
|
||||
|
||||
out := merge(t, base, tr("a b c. d e f."), always(reconcile.VerdictAlt))
|
||||
|
||||
assert.Equal(t, "d e f.", out.Segments[1].Text)
|
||||
}
|
||||
|
||||
func TestReconcileSegments_AKeptBaseLeavesEverySegmentAsItWas(t *testing.T) {
|
||||
base := segmented("a x b", domain.Segment{Start: 0, End: 4, Text: "a x b"})
|
||||
|
||||
out := merge(t, base, tr("a y b"), always(reconcile.VerdictBase))
|
||||
|
||||
assert.Equal(t, base.Segments, out.Segments)
|
||||
}
|
||||
|
||||
func TestReconcileSegments_TheCallersSegmentsAreNeverChangedInPlace(t *testing.T) {
|
||||
segs := []domain.Segment{{Start: 0, End: 4, Text: "a x b"}}
|
||||
base := segmented("a x b", segs...)
|
||||
|
||||
_ = merge(t, base, tr("a y b"), always(reconcile.VerdictAlt))
|
||||
|
||||
assert.Equal(t, "a x b", segs[0].Text, "Reconcile builds new segments; it does not edit the base transcription's")
|
||||
}
|
||||
|
||||
func TestReconcileSegments_SegmentsThatDoNotLineUpWithTheTextAreLeftAsTheBases(t *testing.T) {
|
||||
// A word wrapped across a segment edge makes the segments' word count differ from
|
||||
// the joined text's. Guessing would put words in the wrong place, so leave them.
|
||||
base := segmented("a x b c", domain.Segment{Start: 0, End: 4, Text: "a x"}, domain.Segment{Start: 4, End: 8, Text: "b c d"})
|
||||
|
||||
out := merge(t, base, tr("a y b c"), always(reconcile.VerdictAlt))
|
||||
|
||||
assert.Equal(t, base.Segments, out.Segments)
|
||||
assert.Equal(t, "a y b c", out.Text, "the merged text is unaffected")
|
||||
}
|
||||
|
||||
func TestReconcileSegments_ASpanThatCrossesASegmentEdgeIsRemovedFromBothSegments(t *testing.T) {
|
||||
base := segmented("a b c d", domain.Segment{Start: 0, End: 5, Text: "a b"}, domain.Segment{Start: 5, End: 10, Text: "c d"})
|
||||
|
||||
out := merge(t, base, tr("a X d"), always(reconcile.VerdictAlt))
|
||||
|
||||
require.Equal(t, "a X d", out.Text)
|
||||
assert.Equal(t, "a X", out.Segments[0].Text)
|
||||
assert.Equal(t, "d", out.Segments[1].Text, "'c' was replaced too: it must not linger in the second segment")
|
||||
}
|
||||
Reference in New Issue
Block a user