diff --git a/DECISIONS.md b/DECISIONS.md index 553ceeb..6013702 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1461,8 +1461,9 @@ an hour of Swedish *was* an unknown here; it is now measured, see decision 8.) - **The opening model.** The offered list leads with the summarizer default `koala/phi4-mini`, whose 8,192-token context returns HTTP 400 on an hour of speech (~13,000 tokens). Chat over an upload opens on `TAPIR_UPLOAD_CHAT_MODEL` (default `iguana/gemma4-26b`); the switcher - still lists every model, and picking `phi4-mini` on a long recording fails loudly and - inline. A per-model budget is not built. + lists **local models only** (amended by ADR-035 after the adversarial review found it listed + the cloud model too: the whole private transcript goes to the one chosen), and picking + `phi4-mini` on a long recording fails loudly and inline. A per-model budget is not built. Past ~90,000 chars (≈100 minutes) no offered model can answer by stuffing; see triggers. 9. **Not decided here.** Rich `AVSNITT`/`ALTERNATIV` analysis (gated on #30 — `analyze_srt.py` @@ -1513,13 +1514,20 @@ deferred. The scope cut — chat first, no rich analysis, no video — is what k ADR-016 window, it was the interesting thing, not the useful one — remove it. **Open items this ADR creates.** -- **DECISION NEEDED — ffmpeg in the runtime image.** *Options:* (A) static ffmpeg/ffprobe copied +- **DECIDED (Mathias, 2026-10-02): option A.** Static ffmpeg and ffprobe 7.0.2 are copied into the + distroless image, pinned by version and sha256 (Dockerfile in this PR; #34). Measured at that time: + **about 160 MB** added, not the 70 MB first estimated (80 MB per binary); the build is **GPLv3**; + the checksum proves the bytes reviewed, not the publisher; upstream's `releases/` URL moves on, so + vendoring the verified tarball is the durable fix (not done). Uploads stay off until the deployment + sets `TAPIR_UPLOADS_ENABLED`. *The question as it was put:* *Options:* (A) static ffmpeg/ffprobe copied into distroless **(recommended — the only option that satisfies "accept m4a/mp3/opus/mp4, validate duration, reject video"; pure-Go decoders cover wav/mp3 only)**; (B) accept WAV only, convert client-side (fails "I just want to upload it"); (C) a transcode sidecar (second deployable, ADR-018's reasoning against). *Blocks:* the Dockerfile slice, not the tests, which use the port. -- **DECISION NEEDED — berget fallback for private audio.** *Options:* (A) enabled by default as +- **DECIDED (Mathias, 2026-10-02): option A, enabled by default (#35).** Scope: the audio + transcription fallback only. Reconciliation text and chat over a recording never go to a cloud + model (ADR-034, ADR-035). *The question as it was put:* *Options:* (A) enabled by default as #33 specifies, with the serving endpoint shown **(recommended, matches the issue)**; (B) disabled by default, a failed iguana job fails honestly and the user retries. *Blocks:* the production default only; the adapter ships either way. @@ -1642,6 +1650,12 @@ recording. ~30 disputes where the reconciled text differs from pianissimo alone. Keep the feature only if the reconciled reading is right at least 85% of the time **and** no worse than the better single engine. If not, switch it off (config) and keep the single-engine path. + **Decision (Mathias, 2026-10-02), on the first result below: keep it as an opt-in feature**, reading + the bar as 95% of decided items, with the second condition to be answered on the next real run. + For deployment the reconciler is `iguana/gemma4-31b` (97% against the speaker's answers, about 22 + minutes for the 63.6-minute recording with Low Power Mode off). A cloud reconciler was raised and is + **not adopted**: it would send private transcript text to a cloud API on every upload; recorded as + an evaluation question in #45. **First result, 2026-10-02, before merge** (`docs/research/reconcile-blind-check-2026-10-02.md`): 30 random replacement spots on the real 63.6-minute recording, judged blind by Opus from text (not by the speaker, who delegated it): 21 right, 1 worse than pianissimo, 3 neither reading right, 5 undecidable. @@ -1711,6 +1725,32 @@ wording while only `content` is merged. a segment edge), they stay the base's and the result says so in the log. This replaces ADR-034 decision 6's "segments stay untouched". +8. **Hardening from the adversarial review** (`skills:pr-gatekeeper`, head `7029dd1`: BLOCK; recorded on + PR #39). Each finding was reproduced by a failing test first. + - **Chat over a recording offers and accepts local models only** (it listed `berget/mistral-small`); + `TAPIR_UPLOAD_CHAT_MODEL` must be local at start-up; with no local model, nothing is sent. + - **A model alias counts as local only if it is one plain name after `koala/` or `iguana/`** + (`koala/../berget/x`, `koala//x`, `koala/x?host=…` were accepted by a bare prefix check). + - **An error reaches a log line, the row or the page as a kind** (`timeout`, `canceled`, `http_502`, + `network`, `error`), never its text, which can hold a provider's response body, the spool path or + a word from the transcript (`internal/errkind`). The stored reconcile reason and an engine's + failure reason are kinds; the page no longer shows provider text. + - **Every local engine call has its own deadline** (`TAPIR_TRANSCRIBE_ENGINE_TIMEOUT`, default 25 + minutes), so a wedged engine fails while the job lives and the fallback can run; the job's own + deadline or a cancel still never reaches the cloud. + - **The duration limit is checked against what is converted**, not the file's header: ffmpeg is + capped at the limit plus a minute, a longer result fails the job (`audio_too_long`), and a result + under 80% of the claimed length and more than two minutes short fails it + (`audio_incomplete`) instead of returning a partial transcript with no warning. + - **The queue is bounded**, 20 waiting in total and 3 per user; a full queue is a 429 with a typed + reason, never a stuck row or a server error. + - **Bad input is a 4xx**: a non-UUID id is a 404, a malformed multipart header is a 400 whose text is + not logged, and a title or file name with control bytes or invalid UTF-8 is cleaned, not a 500. + Not done, filed as follow-ups: a vendored ffmpeg tarball, CSRF cookie review, ffprobe CPU and + stream-count limits, mov external references, `alive_at` clock skew, a hung database blocking the + tracker, an unbounded gateway response body, a user-facing notice for a recording that is partial but + inside the tolerance. + **Alternatives rejected.** Server-sent events or a websocket: a long-lived connection through the ingress, with buffering to get wrong, for a page that changes every few seconds at best; polling is what the page already does. A percentage from the engine calls: there is no data behind it. A separate