feat: upload audio, get a private Swedish transcript, discuss it (#33, ADR-033) #39
+44
-4
@@ -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`,
|
- **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
|
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
|
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
|
lists **local models only** (amended by ADR-035 after the adversarial review found it listed
|
||||||
inline. A per-model budget is not built.
|
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.
|
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`
|
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.
|
ADR-016 window, it was the interesting thing, not the useful one — remove it.
|
||||||
|
|
||||||
**Open items this ADR creates.**
|
**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,
|
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
|
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
|
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
|
(second deployable, ADR-018's reasoning against). *Blocks:* the Dockerfile slice, not the
|
||||||
tests, which use the port.
|
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)
|
#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:*
|
disabled by default, a failed iguana job fails honestly and the user retries. *Blocks:*
|
||||||
the production default only; the adapter ships either way.
|
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
|
~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.
|
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.
|
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
|
**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
|
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.
|
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
|
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".
|
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
|
**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
|
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
|
the page already does. A percentage from the engine calls: there is no data behind it. A separate
|
||||||
|
|||||||
Reference in New Issue
Block a user