From 1e192e53ef4905b9dd941b0eaba77ce6f3ac15c4 Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 1 Oct 2026 18:41:49 +0200 Subject: [PATCH] docs(adr): ADR-033 accepts files with a video track, uses the audio, says so The first draft rejected video and named #28. The maintainer's first real recording -- a Teams meeting with the camera off -- is still an .mp4 with a video track, so that rejected the primary use case. Decision 1 and 4 now: accept, extract audio only, discard the video, record had_video, and tell the user. A file with no audio stream is still rejected. Video features stay #28. In-place amendment: the ADR is still Proposed. Co-Authored-By: Claude Sonnet 5.5 --- DECISIONS.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index a0710cf..3aafc8c 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1363,11 +1363,16 @@ an hour of Swedish *was* an unknown here; it is now measured, see decision 8.) **Decision.** -1. **Audio only; transcript is the only thing kept.** An authenticated user uploads an audio - file; Tapir transcribes it and stores the transcript. The audio itself is deleted after - transcription (temp file, removed in a `defer`, swept at start-up) — no custody problem to - solve, and nothing to hand to #31. Video is rejected with a message naming #28. YouTube - audio-download stays rejected. No public/anonymous upload (ADR-008). +1. **Audio is what is used; the transcript is the only thing kept.** An authenticated user + uploads a recording; Tapir transcribes its audio and stores the transcript. The audio itself + is deleted after transcription (temp file, removed in a `defer`, swept at start-up) — no + custody problem to solve, and nothing to hand to #31. **A file that also carries a video + track is accepted: its audio is extracted, the video is discarded, and the user is told so** + (see 4). *Amended 2026-10-01, the same day this ADR was drafted: the first draft rejected + video and named #28. The maintainer's first real recording — a Teams meeting with the camera + off — is still an `.mp4` with a video track, so rejecting video rejected the primary use + case.* This keeps no video and analyses none: playback, keyframes and what-is-shown analysis + remain #28. YouTube audio-download stays rejected. No public/anonymous upload (ADR-008). 2. **Private store: a separate RLS-scoped table, `uploaded_transcripts`** (resolves #28's decision 1). `user_id NOT NULL REFERENCES users ON DELETE CASCADE`, `ENABLE` + `FORCE ROW LEVEL @@ -1387,14 +1392,20 @@ an hour of Swedish *was* an unknown here; it is now measured, see decision 8.) asserted exactly** — so a new table added without RLS fails CI instead of passing it. (c) is new; the existing test only checks the named tables. -4. **Validate by probing, reject clearly.** Format and duration cannot be read from a filename - or MIME type — a Teams "voice-only" recording is typically an `.mp4` container with no video - stream, which is indistinguishable from video by extension. The upload is probed - (`ffprobe`): accept any container with an audio stream and **no** video stream; reject video - with "video is #28"; reject unreadable files. Limits, all configurable, all rejected +4. **Validate by probing, reject clearly, and say what was ignored.** Format, duration and + what a file contains cannot be read from a filename or MIME type — a Teams recording is an + `.mp4` whether or not the camera was on. The upload is probed (`ffprobe`): **accept any + container with an audio stream, whether or not it also has a video stream** (an mp3's cover + art counts as neither). Whether a real video track was present is recorded on the row + (`had_video`) and the transcript page says so plainly — *this file contained a video track; + only its audio was used and the video was discarded* — so the user is never left to wonder + what happened to the picture. Reject a file with **no** audio stream (including a screen + recording with no microphone) and an unreadable one. Limits, all configurable, all rejected synchronously with a specific message rather than by timeout: **200 MB** upload and **90 minutes** duration. 90 minutes is *the longest length measured*, not a ceiling that was - found; raising it is a measurement, not an edit. + found; raising it is a measurement, not an edit. The size cap now matters more: a video file + is much larger than its audio and is spooled whole before the audio is extracted, so a long + recording with screen sharing can exceed 200 MB while its audio is a few tens of MB. 5. **Transcode server-side, always, to 16 kHz mono PCM WAV** — even when the input already is, rather than assuming. whisper-server's `--convert` is rejected (its `--tmp-dir` is created at @@ -1483,6 +1494,9 @@ deferred. The scope cut — chat first, no rich analysis, no video — is what k whisper-server permanently. Chatting with a transcript can therefore load the model that wedges the transcriber. Not solved here; the berget fallback limits the blast radius. - Tapir's own ingress body limit for a 200 MB upload is **unverified** (it lives in `infra`). + - Accepting video means large files are spooled before their audio is extracted, and ffmpeg + demuxes (but, with `-vn`, does not decode) video it then throws away. A client-side audio + extraction would avoid the upload cost; it is not built. **Validation / resolution triggers** (each observable by someone other than the author): - *Chunking*: build it when the duration cap is raised above 90 min, or when a measured