diff --git a/internal/uploads/worker.go b/internal/uploads/worker.go index f6914bd..591cab8 100644 --- a/internal/uploads/worker.go +++ b/internal/uploads/worker.go @@ -275,11 +275,18 @@ func (w *Worker) Process(ctx context.Context, job Job) { abort(domain.UploadErrTranscriptionFailed) return } - if kind, bad := w.audioLengthProblem(wav, job.Duration); bad { - abort(kind) - log.Error("upload: converted audio length rejected", "kind", string(kind)) + got, tooLong := w.audioLengthProblem(wav) + if tooLong { + abort(domain.UploadErrTooLong) + log.Error("upload: converted audio is over the limit", "audio_s", int(got.Seconds()), "limit_s", int(w.maxDuration.Seconds())) return } + if w.maxDuration > 0 && job.Duration > 0 { + if note := shortfallNote(got, job.Duration); note != "" { + log.Warn("upload: audio is much shorter than the file claimed", "audio_s", int(got.Seconds()), "claimed_s", int(job.Duration.Seconds())) + tr.Annotate("audio", note) + } + } tr.Done("audio") if !planned { @@ -343,21 +350,29 @@ const ( ) // audioLengthProblem measures the converted WAV (16 kHz mono 16-bit PCM is 32,000 bytes a -// second after a 44-byte header) against the limit and against what the file claimed. -func (w *Worker) audioLengthProblem(wav string, claimed time.Duration) (domain.UploadErrorKind, bool) { +// second after a 44-byte header) against the limit. It returns the real length and whether +// that is over the limit by more than the tolerance. +func (w *Worker) audioLengthProblem(wav string) (got time.Duration, tooLong bool) { if w.maxDuration <= 0 { - return "", false + return 0, false } info, err := os.Stat(wav) if err != nil { - return "", false + return 0, false } - got := time.Duration(float64(max(info.Size()-44, 0)) / 32000 * float64(time.Second)) - switch { - case got > w.maxDuration+tooLongSlack: - return domain.UploadErrTooLong, true - case float64(got) < incompleteBelow*float64(claimed) && claimed-got > incompleteShortfall: - return domain.UploadErrIncomplete, true + got = time.Duration(float64(max(info.Size()-44, 0)) / 32000 * float64(time.Second)) + return got, got > w.maxDuration+tooLongSlack +} + +// shortfallNote is a remark for the audio step when the converted audio is much shorter +// than the file claimed, or "" when it is not. It is a NOTE and never a failure: a +// container header can over-estimate a complete file (a variable-bitrate MP3 without a Xing +// header claimed 79 minutes for 20 real ones), so refusing would turn away real +// recordings, while saying nothing about a truncated one would hide a partial transcript. +func shortfallNote(got, claimed time.Duration) string { + if float64(got) >= incompleteBelow*float64(claimed) || claimed-got <= incompleteShortfall { + return "" } - return "", false + return fmt.Sprintf("The audio is %s long; the file claimed %s. If the recording was cut off, upload the complete one.", + FormatDuration(got), FormatDuration(claimed)) }