diff --git a/internal/adapters/transcribe/gateway.go b/internal/adapters/transcribe/gateway.go index e885c71..33cf464 100644 --- a/internal/adapters/transcribe/gateway.go +++ b/internal/adapters/transcribe/gateway.go @@ -110,8 +110,9 @@ func (g *Gateway) Transcribe(ctx context.Context, path string) (domain.Transcrip } // verboseJSON is the subset of the verbose_json response used. Both backends -// return it; they differ in segment granularity (iguana: whole seconds, berget: -// sub-second) and in the language token, neither of which matters here. +// return it; they differ in segment granularity (iguana: whole seconds and +// sub-sentence pieces, berget: sub-second sentences), in the language token, and in +// whether a segment's text starts with a space. type verboseJSON struct { Text string `json:"text"` Duration float64 `json:"duration"` @@ -124,13 +125,21 @@ type verboseJSON struct { // parseVerbose normalises a verbose_json body. Whisper's final window can end past // the audio (9.0 s for 4.25 s of audio was observed), so segment ends are clamped -// to the reported duration. +// to the reported duration. Text is rebuilt from the segments, not taken from the +// response's `text` field: see joinSegments. func parseVerbose(r io.Reader) (domain.Transcription, error) { var v verboseJSON if err := json.NewDecoder(r).Decode(&v); err != nil { return domain.Transcription{}, fmt.Errorf("decode response: %w", err) } - text := strings.TrimSpace(v.Text) + raw := make([]string, 0, len(v.Segments)) + for _, s := range v.Segments { + raw = append(raw, s.Text) + } + text := joinSegments(raw) + if text == "" { + text = collapseSpace(v.Text) + } if text == "" { return domain.Transcription{}, ErrEmptyTranscript } @@ -145,3 +154,32 @@ func parseVerbose(r io.Reader) (domain.Transcription, error) { } return t, nil } + +// joinSegments rebuilds the running text from raw (untrimmed) segment texts. +// +// whisper.cpp wraps its output at ~60 characters and a wrap can land INSIDE a word +// ("nyt" | "t godkännande"): a segment that begins with a space starts a new word, +// one that does not continues the previous one, and the response's own `text` +// field joins them with newlines, which breaks the word. So when any segment +// carries a leading space the raw texts are concatenated as they are. Backends +// that return whole sentences with no leading space (berget) are joined with a +// space instead — concatenating those would glue sentences together. +func joinSegments(raw []string) string { + leadingSpace := false + for _, s := range raw { + if strings.HasPrefix(s, " ") { + leadingSpace = true + break + } + } + sep := " " + if leadingSpace { + sep = "" + } + return collapseSpace(strings.Join(raw, sep)) +} + +// collapseSpace turns every run of whitespace (including newlines) into one space. +func collapseSpace(s string) string { + return strings.Join(strings.Fields(s), " ") +}