feat(uploads): choose the language of a recording; record it, show it, carry it to the engines (#61)
Form field (Swedish preselected, Other = auto) read before the file part and refused before anything is spooled if outside the closed set; stored on the row (migration 021, CHECK constraint, default sv for history); carried on the Job and the worker's context; shown on the status page. Feature scenarios and the coverage map are updated, and ADR-033/034 amended. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+31
-2
@@ -28,7 +28,7 @@ import (
|
||||
// *uploads.Intake satisfies it. It is the whole of what the handlers know about
|
||||
// validation, spooling and queueing.
|
||||
type Uploader interface {
|
||||
Accept(ctx context.Context, userID, title string, src io.Reader) (id string, err error)
|
||||
Accept(ctx context.Context, userID, title string, lang domain.Language, src io.Reader) (id string, err error)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -36,6 +36,8 @@ const (
|
||||
// and the title field; the cap itself is enforced on the file part by Intake.
|
||||
uploadOverhead = 1 << 20
|
||||
maxTitleRunes = 200
|
||||
// maxLanguageBytes bounds the language field: the longest value is four bytes.
|
||||
maxLanguageBytes = 16
|
||||
// uploadPollEvery is how often an unfinished upload re-asks for its status.
|
||||
uploadPollEvery = "2s"
|
||||
// serviceSilentAfter is how long a job may go without any signal (heartbeat or
|
||||
@@ -89,6 +91,7 @@ func (a *App) handleUploadPost(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
title := ""
|
||||
lang := domain.LangSwedish
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if errors.Is(err, io.EOF) {
|
||||
@@ -121,11 +124,29 @@ func (a *App) handleUploadPost(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
title = cleanTitle(string(b))
|
||||
case "language":
|
||||
// A closed set read before the file: anything else is refused before the file
|
||||
// is spooled, so a crafted value never reaches an engine (tapir#61).
|
||||
b, err := io.ReadAll(io.LimitReader(part, maxLanguageBytes+1))
|
||||
if err != nil {
|
||||
if a.abandoned(w, r) {
|
||||
return
|
||||
}
|
||||
a.renderUploadsPage(w, r, userID, http.StatusBadRequest, "That upload could not be read. Please try again.")
|
||||
return
|
||||
}
|
||||
l, ok := domain.ParseLanguage(string(b))
|
||||
if !ok || len(b) > maxLanguageBytes {
|
||||
a.logger().Info("upload rejected", "reason", "language")
|
||||
a.renderUploadsPage(w, r, userID, http.StatusBadRequest, "Choose a language from the list.")
|
||||
return
|
||||
}
|
||||
lang = l
|
||||
case "file":
|
||||
if title == "" {
|
||||
title = titleFromFilename(part.FileName())
|
||||
}
|
||||
id, err := a.Uploads.Accept(r.Context(), userID, title, part)
|
||||
id, err := a.Uploads.Accept(r.Context(), userID, title, lang, part)
|
||||
if err != nil {
|
||||
a.uploadFailure(w, r, userID, err)
|
||||
return
|
||||
@@ -300,6 +321,14 @@ func cloudProvider(endpoint string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// languageLabel is how the page names the language a recording was transcribed as.
|
||||
func languageLabel(l domain.Language) string {
|
||||
if l == domain.LangAuto {
|
||||
return "Other (detected by the engine)"
|
||||
}
|
||||
return "Swedish"
|
||||
}
|
||||
|
||||
// hasNote reports whether any phase carries a note. A note is the only warning for a
|
||||
// recording that is shorter than its file claimed, so a record that has one is shown open.
|
||||
func hasNote(steps []domain.ProgressStep) bool {
|
||||
|
||||
Reference in New Issue
Block a user