Upload a recording (audio, or video with the camera off), get a private Swedish transcript, and chat about it. - ADR-033: RLS-scoped private transcript store; ffprobe/ffmpeg intake; gateway transcription (iguana kb-whisper, berget fallback); in-process single-replica worker; chat over the transcript (local models only). - ADR-034: optional two-engine mode (pianissimo + kb-whisper) reconciled by a LOCAL model; off unless TAPIR_RECONCILE_BASE_MODEL is set. - ADR-035: real progress (phases, running/quiet/lost), phase logging, every judged spot stored, merged segments. - Hardening from four adversarial reviews: errors logged and stored as kinds never text; per-engine deadlines; converted-length check; bounded queue; client hang-ups are not errors; strict local-alias check. - Dockerfile: static ffmpeg/ffprobe 7.0.2 pinned by sha256 (#34). Built and inspected by hand; CI does not build the image on a PR, so its first CI build is this merge. Uploads stay OFF until TAPIR_UPLOADS_ENABLED is set in the deployment. Closes #33, #34, #42, #43, #44. Co-authored-by: Mathias <mthbqv@gmail.com>
This commit was merged in pull request #39.
This commit is contained in:
@@ -101,6 +101,61 @@ var scenarioCoverage = map[string]string{
|
||||
"A multi-turn conversation is ephemeral": "TestChatMultiTurnHistoryIsEphemeral",
|
||||
"Chat is reachable only from my own summary view": "TestChatOnlyReachableForOwnVideo",
|
||||
|
||||
// audio_upload.feature (ADR-033, #33)
|
||||
"The upload form states the limits up front": "TestUploadsPage_ShowsAMultipartFormWithAFileField",
|
||||
"An accepted recording is queued and I am taken to its page": "TestUploadPost_AcceptedAudioQueuesARowAndRedirectsToIt",
|
||||
"A recording with no title is named after its file": "TestUploadPost_WithoutATitleUsesTheFilenameMinusItsExtension",
|
||||
"A file with a video track is accepted and I am told the video was ignored": "TestUploadStatus_TellsTheUserTheVideoWasIgnoredInEveryStateAndOnlyWhenItWas",
|
||||
"A video file with no audio is rejected for having no audio": "TestUploadPost_RejectionsAreSynchronousWithAMessageAndNoRow",
|
||||
"A recording over the length limit is rejected with both lengths": "TestUploadPost_RejectionsAreSynchronousWithAMessageAndNoRow",
|
||||
"A file over the size limit is rejected as too large": "TestUploadPost_RejectionsAreSynchronousWithAMessageAndNoRow",
|
||||
"Cover art on an audio file is not a video track": "TestParseProbe",
|
||||
"A file that is not media is rejected as unreadable": "TestAccept_UnreadableFileIsARejectionButAProberFaultIsAnError",
|
||||
"A broken ffprobe is reported as our fault, not as a bad file": "TestAccept_UnreadableFileIsARejectionButAProberFaultIsAnError",
|
||||
"A recording becomes a transcript and the audio is deleted": "TestWorkerProcess_SuccessConvertsTranscribesStoresAndDeletesTheAudio",
|
||||
"The endpoint that served a transcript is recorded and shown": "TestUploadStatus_DoneNamesTheEndpointThatServedIt",
|
||||
"The local endpoint is tried first and the fallback only on failure": "TestChain_AnyPrimaryFailureFallsBackAndRecordsTheFallbackEndpoint",
|
||||
"A failed transcription ends in a message, never a spinner": "TestUploadStatus_UnfinishedStatesKeepPollingTerminalStatesStop",
|
||||
"A restart fails the jobs it interrupted": "TestFailInterruptedUploads_FailsOnlyUnfinishedRowsForEveryUser",
|
||||
"Only one recording is transcribed at a time": "TestWorkerRun_ProcessesOneJobAtATime",
|
||||
"My uploads are invisible to other users": "TestUploadChat_AnotherUsersUploadIs404AndNoModelIsCalled",
|
||||
"I can ask about my recording and get a grounded answer": "TestUploadChatPost_SendsTheOwnersTranscriptWithTheUploadBudget",
|
||||
"A long recording is not cut to its first third": "TestAnswerMaxCharsOverridesTheServiceCap",
|
||||
|
||||
// audio_upload.feature, ADR-034
|
||||
"Two engines transcribe the audio and their disagreements are reconciled": "TestEnsemble_BothEnginesRunInOrderAndTheirDisagreementIsReconciled",
|
||||
"Trivial disagreements never reach the model": "TestReconcile_TrivialDisputesAreResolvedWithoutAskingTheModel",
|
||||
"Differences close together are one decision": "TestFindDisputes_TwoDifferencesSeparatedByOneSharedWordAreOneDispute",
|
||||
"An unsure or changed reading is flagged for me": "TestUploadStatus_ListsTheContestedSpotsWithAMinuteMarkBothReadingsAndWhichWasUsed",
|
||||
"The model cannot be a cloud model": "TestLoad_ReconcileRefusesAnUnsafeOrIncompleteSetup",
|
||||
"One engine failing does not fail the upload": "TestEnsemble_WhenOneEngineFailsTheOtherIsUsedAndItIsSaidSo",
|
||||
"Only when both local engines fail does the cloud fallback run": "TestEnsemble_OnlyWhenBothLocalEnginesFailDoesTheCloudFallbackRun",
|
||||
"A model that is down gives an unreconciled transcript, not a failed job": "TestEnsemble_AModelThatIsDownGivesTheBaseTextUnreconciledNotAFailedJob",
|
||||
"A cancelled job never reaches the cloud": "TestEnsemble_ACancelledJobNeverReachesTheCloud",
|
||||
"A single-engine transcript claims no second opinion": "TestUploadStatus_ASingleEngineTranscriptDoesNotClaimASecondOpinion",
|
||||
|
||||
// audio_upload.feature, ADR-035
|
||||
"I can see which phase my upload is in": "TestUploadProgress_ARunningJobShowsEveryPhaseInOrderWithTheRunningOneMarked",
|
||||
"A bar appears only where the count is real": "TestUploadProgress_OnlyACountedPhaseGetsABarAndItShowsRealCounts",
|
||||
"A slow engine call is not mistaken for a dead service": "TestUploadProgress_ALongEngineCallWithAFreshHeartbeatIsStillJustRunning",
|
||||
"A service that stopped reporting is called lost": "TestUploadProgress_AHeartbeatThatStoppedMeansTheServiceIsLostAndTheJobIsNotCalledRunning",
|
||||
"A model that stops advancing is called quiet": "TestUploadProgress_ACountedPhaseThatStopsAdvancingGoesQuietNotLost",
|
||||
"A failed job shows where it stopped": "TestUploadProgress_AFailedJobShowsWhichPhaseFailedAndWhatNeverStarted",
|
||||
"A finished job keeps its record of what was done": "TestUploadProgress_ADoneJobKeepsItsRecordOfWhatWasDoneAndHowLongEachTook",
|
||||
"The heartbeat runs on its own while an engine call is stuck": "TestWorkerProgress_TheHeartbeatBeatsWhileAnEngineCallIsStuckAndStopsWhenTheJobEnds",
|
||||
"The log names every phase and never the transcript": "TestWorkerProgress_OnASuccessfulJobTheLogNamesEveryPhaseInOrderAndNeverTheTranscriptOrThePath",
|
||||
"Every judged spot is stored but only the ones to check are listed": "TestUploadStatus_OnlyChangedAndUncertainSpotsAreListedNotTheKeptOnes",
|
||||
"Chat about a recording is offered with local models only": "TestUploadChatPage_DoesNotOfferACloudModelEvenThoughTheServiceKnowsOne",
|
||||
"A forged cloud model is never sent the transcript": "TestUploadChatPost_AForgedCloudModelIsReplacedByALocalOneBeforeTheTranscriptIsSent",
|
||||
"A wedged engine does not use up the whole job": "TestChain_AHungPrimaryReachesTheFallbackWhenItHasItsOwnDeadline",
|
||||
"A file that lies about its length is refused": "TestWorkerDuration_AConvertedLengthOverTheLimitFailsTheJobBeforeAnyEngineSeesIt",
|
||||
"A recording much shorter than its file claimed is transcribed and noted": "TestWorkerDuration_AFileThatEndsFarEarlierThanItClaimedIsStillTranscribedAndSaysSo",
|
||||
"A full queue is a retry, not an error": "TestAccept_AFullQueueIsABusyRejectionNotAServerErrorAndLeavesNothingBehind",
|
||||
"One user cannot fill the queue for everyone": "TestWorkerSubmit_OneUserCannotFillTheQueueForEveryoneElse",
|
||||
"Errors are shown as kinds, never their text": "TestReconcile_TheBreakerReasonCarriesAKindNotTheModelsText",
|
||||
"Input that cannot be right is a client error": "TestUploadRoutes_ANonUUIDIdIs404NotA500",
|
||||
"The saved segments carry the merged words": "TestReconcileSegments_AChangedWordReplacesTheBaseWordInItsOwnSegmentOnly",
|
||||
|
||||
// summarize_new_video.feature
|
||||
"A subscribed channel posts a video that has captions": "TestSubscribedVideoWithCaptionsIsSummarizedAndDelivered",
|
||||
"A subscribed channel posts a video with no usable transcript": "TestVideoWithNoTranscriptIsSkipped",
|
||||
|
||||
Reference in New Issue
Block a user