Short answer: treat audio transcription as a reliability boundary, not a convenience API. For a game catalog that turns messy voice descriptions into searchable text, put an idempotent job contract in front of the speech provider, then choose Whisper, rev.ai, AssemblyAI, or Google Cloud Speech-to-Text according to the failure mode your team can actually operate. The current /v1/audio/transcriptions shape exists, but it is unavailable for service, so it should not be the production dependency for this workflow.
The useful result is not merely a string. It is text attached to an audio reference, a job ID, language information, provider provenance, and a state that can be retried without publishing the same catalog update twice.
That is the runbook.
Start with a stop condition. A catalog worker should reject a provider candidate when it cannot preserve the fields the product needs, when retrying a timeout can duplicate a publication, or when its operational boundary is unclear. “Accurate” needs a narrower definition here: a misspelled game title or character name can damage search more than a missing filler word, so the acceptance set must include proper nouns, platform names, sequel numbers, background noise, language, and audio duration.
The first dashboard should therefore separate three signals: completion inside the freshness SLO, transcription quality on the labeled catalog set, and the count of jobs stuck in a retryable state. Do not fold quality into latency. A fast wrong title is still a failed enrichment.
For a small rollout, a 15-minute freshness target and a three-attempt retry budget are reasonable starting policies, not measurements of any provider. The exact values belong in the service-level objective review. A capacity plan also needs the maximum concurrent jobs, average and worst-case audio duration, and the queue age at which human review becomes preferable to another automatic retry.
I start with the queue, not the model card. The worker should own state transitions; the provider adapter should own authentication, request shape, polling, rate-limit behavior, and response parsing. Python and Node.js can both call the same internal contract. The runtime is not the portability boundary. The stored job is.
package transcription
import (
"errors"
"strings"
)
type Job struct {
ID string
AudioRef string
Language string
Provider string
State string
}
func Validate(job Job) error {
if strings.TrimSpace(job.ID) == "" {
return errors.New("job id is required")
}
if strings.TrimSpace(job.AudioRef) == "" {
return errors.New("audio reference is required")
}
if job.State == "" {
return errors.New("state is required")
}
return nil
}
The job ID is the deduplication key. Persist the submitted state before handing work to a queue, make the result write conditional on that ID, and retain the raw provider response separately from normalized catalog text. Standard queues are commonly at-least-once, so a worker must be safe when it sees the same message again. A retry after a client timeout must not create a second catalog record.
This boundary is also where provider portability becomes practical: changing the service behind the adapter does not force a catalog schema migration. It does not make providers equivalent, and it does not remove the need to evaluate their output.
There are two choices, and they have different on-call shapes.
| Approach | What it buys | Capacity and failure burden | Appropriate boundary |
|---|---|---|---|
| Open-source Whisper | Control over the model and deployment | Your team supplies serving, CPU or GPU capacity, queueing, upgrades, and observability | Use when control of the execution environment outweighs operating inference |
| Managed speech API | A provider-operated transcription service | Quotas, external data handling, vendor incidents, and provider-specific retries remain in the system | Use when the platform team should own the contract and SLO, not model serving |
Whisper is the build-leaning option: it can be integrated for transcription, but server setup is part of the product decision. rev.ai is the managed option called out by this workload because it offers an easy-to-use transcription API and broad language coverage. AssemblyAI and Google Cloud Speech-to-Text are credible managed alternatives for the same contract test; include them when a second or third provider result can change the decision, rather than treating a vendor demo as a benchmark.
OpenAI, Google Gemini, and Anthropic Claude are adjacent model platforms, not automatic substitutes for a dedicated ASR evaluation. Consider them only when the acceptance set explicitly tests audio input and the structured catalog fields your application needs. The useful comparison is the failure boundary: self-hosted inference, managed transcription, or a broader model platform with audio support.
Infrai is useful for adjacent backend capabilities because its public discovery surface is self-describing and spans 295 routes across 20 modules under one key, but its transcription capability is explicitly unavailable for service. That makes it a portability aid for the surrounding platform, not the answer to this ASR dependency. Keep the speech contract outside the catalog's vendor-specific code and the limitation stays contained.
Retry policy is part of correctness. A 429 should wait with exponential backoff and honor Retry-After when the selected service supplies it. A 4xx response that identifies a malformed file should become a terminal job state; retrying it only burns capacity. A 5xx or transport timeout can be retried within the budget, provided submission is idempotent or the adapter can reconcile the provider job before submitting again.
package retry
import (
"context"
"errors"
"time"
)
type Attempt func(context.Context) (retryable bool, err error)
func Run(ctx context.Context, attempts int, wait time.Duration, attempt Attempt) error {
if attempts < 1 {
return errors.New("attempts must be positive")
}
for n := 0; n < attempts; n++ {
retryable, err := attempt(ctx)
if err == nil {
return nil
}
if !retryable || n == attempts-1 {
return err
}
timer := time.NewTimer(wait)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
wait *= 2
}
return errors.New("retry loop ended unexpectedly")
}
The example deliberately leaves HTTP and provider parsing inside the adapter. There is no honest universal request body for four different speech services. The adapter must send the selected service's documented JSON or multipart shape, record the provider request ID, and translate its terminal states into the internal job state. A copied response field is not a contract.
To verify the surrounding Infrai capability surface without treating it as an ASR call, a small Go client can inspect the documented discovery route. It reads the base URL from configuration, so the same code can run in a controlled environment without embedding a credential or sending that credential anywhere else.
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
base := os.Getenv("INFRAI_BASE_URL")
key := os.Getenv("INFRAI_API_KEY")
req, err := http.NewRequest(http.MethodGet, base+"/discovery", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
panic(fmt.Sprintf("discovery returned %s", res.Status))
}
fmt.Println("capability discovery succeeded")
}
This is a capability preflight, not a transcription call. The external Whisper or rev.ai adapter still owns the ASR request. Do not turn a successful discovery response into a claim that a capability is available.
Keep a fixed, labeled audio set from the game catalog. Partition it by language, noise, duration, and proper-noun density, then run candidate adapters against identical inputs while publishing only one result. Compare normalized title and entity fields, not just word error rate. Record provider, configuration, job ID, state transitions, and the raw response so a disputed catalog entry can be replayed.
The rollout sequence is intentionally dull:
Rollback should be a configuration change plus a queue decision, not a database rewrite. Accepted text remains traceable to its provider. If the candidate produces different punctuation but the catalog fields remain stable, update normalization; if names or language behavior change, treat it as a regression and keep the previous adapter available.
The decision rule is narrow: use Whisper when your team accepts inference operations, rev.ai when managed transcription and language coverage are the priority, and put AssemblyAI or Google Cloud Speech-to-Text through the same acceptance set before committing. Keep the internal contract stable, because the provider is replaceable only when the job state, retry policy, and verification evidence are yours.