Feature: Upload an audio recording, get a private Swedish transcript, discuss it As someone with a recorded meeting on my computer I want to upload it and then ask questions about what was said So that I can find out what was decided, what is open and who owes what # ADR-033, tapir#33. The load-bearing constraint: an uploaded transcript is # PRIVATE content, so it lives in its own RLS-scoped store and never in the # shared public-caption `transcripts` table (ADR-021). Rejection is synchronous, # the audio is never retained, and a failed job ends in a terminal state with a # message — never a spinner (ADR-025). A file with a video track (a Teams meeting # with the camera off is one) is accepted: only its audio is used and the user is # told. Video playback, keyframes and analysis of what is shown remain #28. Background: Given I am a registered user with audio upload enabled Scenario: The upload form states the limits up front When I open the upload page Then I see the size limit and the length limit before I choose a file Scenario: An accepted recording is queued and I am taken to its page When I upload an audio file within the limits Then it is recorded as queued for me And I am redirected to its page Scenario: A recording with no title is named after its file When I upload an audio file and give no title Then its title is the filename without its extension Scenario: A file with a video track is accepted and I am told the video was ignored When I upload a recording that also has a video track Then it is accepted and queued like any other And its page says only its audio was used and the video was discarded And it says so while queued, while transcribing, when done and when failed Scenario: A video file with no audio is rejected for having no audio When I upload a video that has no audio track Then it is rejected with a message that it has no audio And nothing is recorded Scenario: A recording over the length limit is rejected with both lengths When I upload a recording longer than the limit Then it is rejected stating the recording's length and the limit And nothing is recorded Scenario: A file over the size limit is rejected as too large When I upload a file larger than the size limit Then it is rejected with 413 and a message about the limit And nothing is recorded Scenario: Cover art on an audio file is not a video track When I upload an mp3 whose only video stream is attached cover art Then it is treated as audio and no video notice is shown Scenario: A file that is not media is rejected as unreadable When I upload a file ffprobe cannot read Then it is rejected as the file's fault Scenario: A broken ffprobe is reported as our fault, not as a bad file When the prober itself fails Then the upload is an error rather than a rejection of my file Scenario: A recording becomes a transcript and the audio is deleted Given I have uploaded a recording When it has been transcribed Then the transcript is stored privately for me And the audio is deleted Scenario: The endpoint that served a transcript is recorded and shown Given a recording was transcribed by the fallback endpoint When I view its page Then it names the fallback endpoint, because that means the audio left the homelab Scenario: The local endpoint is tried first and the fallback only on failure When the primary transcription endpoint fails for any reason Then the fallback endpoint transcribes it But when the primary succeeds the fallback is never contacted Scenario: A failed transcription ends in a message, never a spinner When every transcription endpoint fails Then the upload is failed with a typed reason And its page shows that reason and stops polling Scenario: A restart fails the jobs it interrupted Given the service restarted while a recording was queued or transcribing When it starts again Then those uploads are failed as interrupted And any audio left in the spool is deleted Scenario: Only one recording is transcribed at a time When several recordings are queued Then they are transcribed one after another Scenario: My uploads are invisible to other users Given another user has uploaded a recording When I ask for it by its id Then I get 404 and no part of its title or text And the chat for it calls no model Scenario: I can ask about my recording and get a grounded answer Given my recording has been transcribed When I ask a question about it Then the answer comes from my transcript through the same chat as a video's Scenario: A long recording is not cut to its first third Given my recording's transcript is longer than the caption default When I ask a question about it Then the transcript is sent with the upload budget, not the 18,000-character caption default # ADR-034, two engines and a local reconciler (off unless TAPIR_RECONCILE_BASE_MODEL is set) Scenario: Two engines transcribe the audio and their disagreements are reconciled Given two-engine transcription is enabled When a recording is transcribed Then both engines run on the same audio, the fast one first And a local model chooses between their disagreements And the endpoint names every engine and the model that took part Scenario: Trivial disagreements never reach the model When the engines differ only by a compound written as one word or two, or by a filler word Then the base reading is kept without a model call Scenario: Differences close together are one decision When two differences are separated by a single shared word Then they are asked about as one dispute Scenario: An unsure or changed reading is flagged for me When the model changes a reading or cannot decide Then the page lists the spot with a minute mark, both readings and which was used And an unsure spot keeps the base reading and is marked uncertain Scenario: The model cannot be a cloud model When the reconciliation model or base engine is not a koala/ or iguana/ alias Then the service refuses to start And a cloud alias that merely contains a local host name is refused too Scenario: One engine failing does not fail the upload When one of the two engines fails Then the other engine's transcript is used And the page says only one engine produced it and why And the cloud fallback is not used Scenario: Only when both local engines fail does the cloud fallback run When both local engines fail and a fallback is configured Then the fallback transcribes it and the row records that it did Scenario: A model that is down gives an unreconciled transcript, not a failed job When the reconciliation model fails three times in a row Then the base transcript is kept unreconciled with the reason And the page says it could not be reconciled Scenario: A cancelled job never reaches the cloud When the job is cancelled during either engine Then neither the second engine nor the cloud fallback is called Scenario: A single-engine transcript claims no second opinion When only one engine was configured Then the page does not mention reconciliation # ADR-035: progress, the log, and the judged spots Scenario: I can see which phase my upload is in Given a recording is being transcribed When I open its page Then it lists every phase in order, the finished ones with how long each took And the running phase is marked, with how long it has run And the page updates in place every two seconds until the job ends Scenario: A bar appears only where the count is real When the deciding phase has decided 142 of 282 differences Then the page shows a bar with "142 of 282" And an engine call, which is one request with nothing to count, never gets a bar or a percentage Scenario: A slow engine call is not mistaken for a dead service When an engine call has run for 25 minutes and the service is still reporting Then the page shows the elapsed time and says nothing is wrong Scenario: A service that stopped reporting is called lost When nothing has been heard from the service for longer than the heartbeat allows Then the page says the service stopped reporting and may have restarted or crashed And the running phase carries a warning mark and no spinner And the page keeps checking Scenario: A model that stops advancing is called quiet When the deciding phase has not advanced for five minutes and the service is still reporting Then the page says nothing new has happened for that long and that the service is still running And it does not say the service is lost Scenario: A failed job shows where it stopped When a job fails Then the failed phase is marked and what never started stays pending And the page stops checking Scenario: A finished job keeps its record of what was done When a job is done Then the page keeps a collapsed list of the phases with how long each took Scenario: The heartbeat runs on its own while an engine call is stuck When an engine call does not return Then the row is still stamped alive every few seconds And no heartbeat outlives its job Scenario: The log names every phase and never the transcript When a job runs Then the log has a line per phase in the order it ran, each naming the upload And no line contains transcript text or the spool path Scenario: Every judged spot is stored but only the ones to check are listed When the model has changed, kept and been unsure about spots Then every one is stored with its verdict and the words before it And the page lists only the changed and uncertain ones, with the words before each Scenario: The saved segments carry the merged words When the model changes a word Then the segment containing it carries the reconciled word And the segment's timing is unchanged # ADR-035 decision 8: hardening from the adversarial review Scenario: Chat about a recording is offered with local models only When I open the chat for my recording Then the model list holds only models that run on the homelab And a cloud model the shared chat service knows is not in it Scenario: A forged cloud model is never sent the transcript When a chat request names a cloud model Then a local model answers instead, and nothing was sent to the cloud one Scenario: A wedged engine does not use up the whole job When the primary engine never answers Then it fails at its own deadline while the job is still alive And the fallback runs, unless the job itself was cancelled or timed out Scenario: A file that lies about its length is refused When a file claims to be short but converts to more than the limit Then the job fails with a plain explanation and no engine receives the audio Scenario: A recording much shorter than its file claimed is transcribed and noted When a file converts to under 80 percent of the length it claimed and more than two minutes short Then it is transcribed anyway and its audio step says how long the audio really is And a complete recording whose header over-estimated its length is not refused Scenario: A transcript made by the cloud fallback says the audio left the homelab When the transcript was produced by an endpoint that is not local Then its page says in plain words that the audio left the homelab and names the provider And a locally transcribed recording carries no such note Scenario: I can delete a finished recording When I confirm deleting a done or failed recording Then the recording and its transcript are gone and I am back on my list And another user's recording is a 404 and is untouched Scenario: A recording still being transcribed cannot be deleted When I try to delete a recording that is queued or transcribing Then I am told it is still being transcribed and nothing is removed Scenario: The home list links to my recordings when uploads are on Given audio upload is enabled Then the home list links to the recordings page Scenario: A full queue is a retry, not an error When too many recordings are already waiting Then the upload is answered as busy, its row is failed honestly and its audio is deleted Scenario: One user cannot fill the queue for everyone When one user has several recordings waiting Then further uploads from that user are refused and other users are not affected Scenario: Errors are shown as kinds, never their text When a model or an engine fails Then the log, the stored reason and the page carry a kind such as timeout or http_502 And never the provider's response, a file path or a word from the transcript Scenario: Input that cannot be right is a client error When the id is not an id, or a header or title holds control bytes Then the answer is a 4xx or the title is cleaned, never a server error Scenario: I choose the language of a recording When I upload a recording and choose Swedish or Other on the form Then the recording is transcribed as that language and its page says which Scenario: Swedish stays the default When an upload names no language Then it is transcribed as Swedish, as before the choice existed Scenario: A language outside the list is refused When an upload names a language the form does not offer Then it is answered 400 before anything is stored or sent to an engine Scenario: A recording in another language never reaches the Swedish-only engine When I choose Other for a recording Then the language is left for the engine to detect and no Swedish-only engine or reconcile is used