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>
82 lines
3.8 KiB
Go
82 lines
3.8 KiB
Go
package web_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/domain"
|
|
)
|
|
|
|
// TestRenderUploadProgressStatesForReview writes the upload page in each progress state
|
|
// to $TAPIR_SHOT_DIR as standalone HTML (light and dark), for a screenshot review of the
|
|
// visual claims tests cannot assert: that it looks like tapir and reads like a log of
|
|
// phases (ADR-035, #42). Opt-in: it does nothing unless the variable is set. The text is
|
|
// synthetic. Screenshot with any browser, for example:
|
|
//
|
|
// TAPIR_SHOT_DIR=/tmp/shots go test -run TestRenderUploadProgressStatesForReview ./internal/web
|
|
// chromium --headless --screenshot=/tmp/shots/running.png --window-size=900,900 /tmp/shots/running-light.html
|
|
func TestRenderUploadProgressStatesForReview(t *testing.T) {
|
|
dir := os.Getenv("TAPIR_SHOT_DIR")
|
|
if dir == "" {
|
|
t.Skip("set TAPIR_SHOT_DIR to render the progress states for a screenshot review")
|
|
}
|
|
require.NoError(t, os.MkdirAll(dir, 0o750))
|
|
app, _ := newUploadApp(t, audioOK, 200<<20)
|
|
fixedClock(app)
|
|
|
|
decide := twoEngineRun()
|
|
decide[3] = step("engine_b", "Transcribing with iguana/kb-whisper", domain.StepDone, 9*time.Minute, 4*time.Minute, 0, 0)
|
|
decide[4] = step("compare", "Comparing the two transcripts", domain.StepDone, 5*time.Minute, time.Second, 0, 0)
|
|
decide[5] = step("decide", "Deciding the differences", domain.StepRunning, 4*time.Minute+30*time.Second, 0, 142, 282)
|
|
|
|
failed := twoEngineRun()
|
|
failed[3] = step("engine_b", "Transcribing with iguana/kb-whisper", domain.StepFailed, 3*time.Minute, time.Minute, 0, 0)
|
|
|
|
done := []domain.ProgressStep{
|
|
step("received", "Received", domain.StepDone, 22*time.Minute, 0, 0, 0),
|
|
step("audio", "Preparing the audio", domain.StepDone, 22*time.Minute, 41*time.Second, 0, 0),
|
|
step("engine_a", "Transcribing with iguana/pianissimo", domain.StepDone, 21*time.Minute, 2*time.Minute+10*time.Second, 0, 0),
|
|
step("engine_b", "Transcribing with iguana/kb-whisper", domain.StepDone, 19*time.Minute, 4*time.Minute+5*time.Second, 0, 0),
|
|
step("compare", "Comparing the two transcripts", domain.StepDone, 15*time.Minute, 2*time.Second, 0, 0),
|
|
step("decide", "Deciding the differences", domain.StepDone, 15*time.Minute, 14*time.Minute, 282, 282),
|
|
step("save", "Saving the transcript", domain.StepDone, time.Minute, time.Second, 0, 0),
|
|
}
|
|
|
|
noted := twoEngineRun()
|
|
noted[1].Note = "The audio is 20 min long; the file claimed 1 h 19 min. If the recording was cut off, upload the complete one."
|
|
|
|
cases := []struct {
|
|
name, status, content string
|
|
steps []domain.ProgressStep
|
|
progressAgo, aliveAgo time.Duration
|
|
}{
|
|
{"running", "transcribing", "", twoEngineRun(), 10 * time.Second, 4 * time.Second},
|
|
{"deciding", "transcribing", "", decide, 3 * time.Second, 3 * time.Second},
|
|
{"quiet", "transcribing", "", decide, 6 * time.Minute, 4 * time.Second},
|
|
{"lost", "transcribing", "", twoEngineRun(), 3 * time.Minute, 3 * time.Minute},
|
|
{"failed", "failed", "transcription_failed", failed, 2 * time.Minute, 2 * time.Minute},
|
|
{"done", "done", "Synthetic transcript text for the review.", done, time.Minute, time.Minute},
|
|
{"noted", "transcribing", "", noted, 10 * time.Second, 4 * time.Second},
|
|
{"queued", "queued", "", nil, 0, 0},
|
|
}
|
|
for _, c := range cases {
|
|
id := seedUpload(t, userID, "Project notes (synthetic)", c.status, c.content)
|
|
if c.steps != nil {
|
|
seedProgress(t, id, c.steps, progressNow.Add(-c.progressAgo), progressNow.Add(-c.aliveAgo))
|
|
}
|
|
page := body(t, do(t, app, get("/uploads/"+id)))
|
|
for _, theme := range []string{"light", "dark"} {
|
|
html := page
|
|
if theme == "dark" {
|
|
html = strings.Replace(html, "<html", `<html data-theme="dark"`, 1)
|
|
}
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, c.name+"-"+theme+".html"), []byte(html), 0o600))
|
|
}
|
|
}
|
|
}
|