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 }