feat: upload audio, get a private Swedish transcript, discuss it (#33, ADR-033) #39
+25
-11
@@ -1363,11 +1363,16 @@ an hour of Swedish *was* an unknown here; it is now measured, see decision 8.)
|
|||||||
|
|
||||||
**Decision.**
|
**Decision.**
|
||||||
|
|
||||||
1. **Audio only; transcript is the only thing kept.** An authenticated user uploads an audio
|
1. **Audio is what is used; the transcript is the only thing kept.** An authenticated user
|
||||||
file; Tapir transcribes it and stores the transcript. The audio itself is deleted after
|
uploads a recording; Tapir transcribes its audio and stores the transcript. The audio itself
|
||||||
transcription (temp file, removed in a `defer`, swept at start-up) — no custody problem to
|
is deleted after transcription (temp file, removed in a `defer`, swept at start-up) — no
|
||||||
solve, and nothing to hand to #31. Video is rejected with a message naming #28. YouTube
|
custody problem to solve, and nothing to hand to #31. **A file that also carries a video
|
||||||
audio-download stays rejected. No public/anonymous upload (ADR-008).
|
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
|
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
|
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
|
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.
|
new; the existing test only checks the named tables.
|
||||||
|
|
||||||
4. **Validate by probing, reject clearly.** Format and duration cannot be read from a filename
|
4. **Validate by probing, reject clearly, and say what was ignored.** Format, duration and
|
||||||
or MIME type — a Teams "voice-only" recording is typically an `.mp4` container with no video
|
what a file contains cannot be read from a filename or MIME type — a Teams recording is an
|
||||||
stream, which is indistinguishable from video by extension. The upload is probed
|
`.mp4` whether or not the camera was on. The upload is probed (`ffprobe`): **accept any
|
||||||
(`ffprobe`): accept any container with an audio stream and **no** video stream; reject video
|
container with an audio stream, whether or not it also has a video stream** (an mp3's cover
|
||||||
with "video is #28"; reject unreadable files. Limits, all configurable, all rejected
|
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
|
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
|
**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,
|
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
|
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
|
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.
|
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`).
|
- 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):
|
**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
|
- *Chunking*: build it when the duration cap is raised above 90 min, or when a measured
|
||||||
|
|||||||
Reference in New Issue
Block a user