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 <noreply@anthropic.com>
This commit is contained in:
+25
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user