Streams the WAV from disk with a computed Content-Length (no buffering, no chunking), posts verbose_json with field `file` and language=sv, clamps the final segment to the audio duration, and treats an empty transcript as a failure. The chain falls back on ANY primary error, records the serving endpoint, and never falls back once the context is cancelled. gateway.go and chain.go are one commit: the chain's tests use the gateway's StatusError and ErrEmptyTranscript, so neither compiles alone. Mutation-checked: field name, clamp, cancel-fallback, content length and the recorded endpoint each turn a test red. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
54 lines
1.8 KiB
Go
54 lines
1.8 KiB
Go
package transcribe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/domain"
|
|
)
|
|
|
|
// Backend is one transcription endpoint. *Gateway satisfies it; Name is the value
|
|
// recorded as the serving endpoint.
|
|
type Backend interface {
|
|
Name() string
|
|
Transcribe(ctx context.Context, wavPath string) (domain.Transcription, error)
|
|
}
|
|
|
|
// Chain tries a primary (local) backend and, if it fails for ANY reason, a
|
|
// fallback. The fallback exists because iguana can wedge permanently on a Metal
|
|
// OOM (infra#273) — which is also why a fallback is a data-egress decision: the
|
|
// berget endpoint is a cloud service, so the result records which one served it.
|
|
type Chain struct {
|
|
primary Backend
|
|
fallback Backend // nil disables the fallback
|
|
}
|
|
|
|
// NewChain builds a Chain. A nil fallback means a primary failure is the result.
|
|
func NewChain(primary, fallback Backend) *Chain {
|
|
return &Chain{primary: primary, fallback: fallback}
|
|
}
|
|
|
|
// Transcribe returns the first backend's success. It falls back on any primary
|
|
// error — transport, timeout, non-2xx, empty transcript — and never inspects a
|
|
// response body to decide. A cancelled context stops the chain: a job the user or
|
|
// shutdown cancelled must not ship its audio to a cloud endpoint as a last act.
|
|
func (c *Chain) Transcribe(ctx context.Context, wavPath string) (domain.Transcription, error) {
|
|
t, err := c.primary.Transcribe(ctx, wavPath)
|
|
if err == nil {
|
|
t.Endpoint = c.primary.Name()
|
|
return t, nil
|
|
}
|
|
if c.fallback == nil || ctx.Err() != nil {
|
|
return domain.Transcription{}, fmt.Errorf("%s: %w", c.primary.Name(), err)
|
|
}
|
|
t, ferr := c.fallback.Transcribe(ctx, wavPath)
|
|
if ferr != nil {
|
|
return domain.Transcription{}, errors.Join(
|
|
fmt.Errorf("%s: %w", c.primary.Name(), err),
|
|
fmt.Errorf("%s: %w", c.fallback.Name(), ferr))
|
|
}
|
|
t.Endpoint = c.fallback.Name()
|
|
return t, nil
|
|
}
|