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:
@@ -111,3 +111,161 @@ type Summary struct {
|
||||
FallbackUsed bool
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Segment is one timed span of a transcription. Times are seconds from the start
|
||||
// of the audio.
|
||||
type Segment struct {
|
||||
Start float64 `json:"start"`
|
||||
End float64 `json:"end"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// Transcription is the result of transcribing user-supplied audio (ADR-033):
|
||||
// the full text, its timed segments, which backend served it, and how long the
|
||||
// audio was. Endpoint is recorded because the backends differ in timestamp
|
||||
// granularity — which one served a transcript is a quality fact.
|
||||
type Transcription struct {
|
||||
Text string
|
||||
Segments []Segment
|
||||
Endpoint string
|
||||
AudioSeconds float64
|
||||
// Reconciliation is set when two engines transcribed the same audio and a local
|
||||
// model chose between their disagreements (ADR-034). Nil for a single engine.
|
||||
Reconciliation *Reconciliation
|
||||
}
|
||||
|
||||
// ReconcileStatus says what became of an attempt to merge two transcripts.
|
||||
type ReconcileStatus string
|
||||
|
||||
const (
|
||||
// ReconcileDone: both engines produced text and the disagreements were resolved.
|
||||
ReconcileDone ReconcileStatus = "reconciled"
|
||||
// ReconcileSingle: only one engine produced text, so there was nothing to merge.
|
||||
ReconcileSingle ReconcileStatus = "single_engine"
|
||||
// ReconcileSkipped: both engines produced text but the model could not be used;
|
||||
// the base text stands, unreconciled.
|
||||
ReconcileSkipped ReconcileStatus = "skipped"
|
||||
)
|
||||
|
||||
// ContestedSpot is a place the engines disagreed that the model judged: it changed it,
|
||||
// kept the base's reading, or could not decide. Every judged spot is stored, not only
|
||||
// the ones a reader should check, because the audio is not retained and the kept ones
|
||||
// are what a later evaluation needs (ADR-035). The page lists only the changed and
|
||||
// uncertain ones.
|
||||
type ContestedSpot struct {
|
||||
Seconds float64 `json:"seconds"` // roughly where in the recording
|
||||
Base string `json:"base"` // what the base engine heard
|
||||
Alt string `json:"alt"` // what the other engine heard
|
||||
Used string `json:"used"` // "base" or "alt": the reading now in the text
|
||||
Unsure bool `json:"unsure"` // the model could not decide; the base was kept
|
||||
Before string `json:"before,omitempty"` // the few words just before the spot, to find it by
|
||||
}
|
||||
|
||||
// Reconciliation records how a merged transcript came about, so the page can say so
|
||||
// plainly and the user can check the uncertain spots. The counts are exact; Contested
|
||||
// is bounded.
|
||||
type Reconciliation struct {
|
||||
Status ReconcileStatus `json:"status"`
|
||||
Reason string `json:"reason,omitempty"` // why single_engine or skipped
|
||||
Base string `json:"base"` // engine whose text is the base
|
||||
Alt string `json:"alt"`
|
||||
Reconciler string `json:"reconciler"`
|
||||
Disputes int `json:"disputes"` // places the engines disagreed
|
||||
AutoResolved int `json:"auto_resolved"` // trivial: compounds, filler words
|
||||
Adjudicated int `json:"adjudicated"` // decided by the model
|
||||
Changed int `json:"changed"` // the alternative's reading was used
|
||||
Unsure int `json:"unsure"` // base kept, flagged
|
||||
Errors int `json:"errors"` // model calls that failed
|
||||
Contested []ContestedSpot `json:"contested,omitempty"`
|
||||
}
|
||||
|
||||
// StepState is where one phase of an upload job stands (ADR-035).
|
||||
type StepState string
|
||||
|
||||
const (
|
||||
StepPending StepState = "pending"
|
||||
StepRunning StepState = "running"
|
||||
StepDone StepState = "done"
|
||||
StepFailed StepState = "failed"
|
||||
// StepSkipped is a step that never ran because the job finished without it, for
|
||||
// example the second engine's step when the first engine's text was used alone.
|
||||
StepSkipped StepState = "skipped"
|
||||
)
|
||||
|
||||
// ProgressStep is one named phase of an upload job. Total is zero for a phase with no
|
||||
// count; only a phase whose work is really countable carries one (ADR-035: no
|
||||
// percentage that does not come from real counts).
|
||||
type ProgressStep struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
State StepState `json:"state"`
|
||||
StartedAt time.Time `json:"started_at,omitzero"`
|
||||
FinishedAt time.Time `json:"finished_at,omitzero"`
|
||||
Done int `json:"done,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
// Note is a short plain remark about the phase that the reader should see even though
|
||||
// it did not fail, for example that the audio is much shorter than the file claimed.
|
||||
// Durations and counts only: never transcript text, a name or a path.
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// UploadStatus is the state of an uploaded-audio transcription job.
|
||||
type UploadStatus string
|
||||
|
||||
const (
|
||||
UploadQueued UploadStatus = "queued"
|
||||
UploadTranscribing UploadStatus = "transcribing"
|
||||
UploadDone UploadStatus = "done"
|
||||
UploadFailed UploadStatus = "failed"
|
||||
)
|
||||
|
||||
// UploadErrorKind is the typed reason an upload failed. It is deliberately NOT a
|
||||
// provider's error string: the whisper server's "failed to process audio" has had
|
||||
// at least five distinct causes, so nothing may branch on it (ADR-033).
|
||||
type UploadErrorKind string
|
||||
|
||||
const (
|
||||
// UploadErrInterrupted: the process restarted mid-job. The audio is not
|
||||
// retained, so the job cannot resume; the user re-uploads.
|
||||
UploadErrInterrupted UploadErrorKind = "interrupted"
|
||||
// UploadErrTranscriptionFailed: every configured transcription endpoint failed.
|
||||
UploadErrTranscriptionFailed UploadErrorKind = "transcription_failed"
|
||||
// UploadErrTooLong: the audio, once converted, is longer than the limit. The limit is
|
||||
// first checked against the file's header, which a hostile or damaged file can lie
|
||||
// about (ADR-035 review).
|
||||
UploadErrTooLong UploadErrorKind = "audio_too_long"
|
||||
// UploadErrBusy: too many recordings were already waiting, so this one was not
|
||||
// queued. The user's to retry later (ADR-035 review).
|
||||
UploadErrBusy UploadErrorKind = "queue_full"
|
||||
)
|
||||
|
||||
// UploadedTranscript is a user's PRIVATE transcript of audio they uploaded. It is
|
||||
// never stored in, or merged with, the shared public-caption Transcript (ADR-021).
|
||||
// Content and Segments are empty until Status is UploadDone.
|
||||
type UploadedTranscript struct {
|
||||
ID string
|
||||
UserID string
|
||||
Title string
|
||||
Status UploadStatus
|
||||
Content string
|
||||
Segments []Segment
|
||||
Endpoint string
|
||||
AudioSeconds float64
|
||||
ErrorKind UploadErrorKind
|
||||
// HadVideo records that the uploaded file carried a video track. Only its audio
|
||||
// was used and the video was discarded; the page says so. It is set at upload
|
||||
// and never cleared (ADR-033).
|
||||
HadVideo bool
|
||||
// Reconciliation is how a merged transcript came about, or nil for a single
|
||||
// engine (ADR-034). Set by Get, not by List.
|
||||
Reconciliation *Reconciliation
|
||||
// Progress is the job's phases, in order. Set by Get, not by List (ADR-035).
|
||||
Progress []ProgressStep
|
||||
// ProgressAt is when a phase last really changed or advanced; AliveAt is the
|
||||
// heartbeat. They differ on purpose: alive says the service is still reporting,
|
||||
// progress says work is moving (ADR-035). Both are zero until the job starts.
|
||||
ProgressAt time.Time
|
||||
AliveAt time.Time
|
||||
CreatedAt time.Time
|
||||
FinishedAt time.Time // zero until the job reaches done or failed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user