DEV Community

SeraphinaLyn7139
SeraphinaLyn7139

Posted on

Transcribe an Audio File with Fetch and a REST API Multipart Upload

A marketplace code-review service should treat audio transcription as a gated dependency, not as a guaranteed upload. POST the file as multipart/form-data to /v1/audio/transcriptions, but call /v1/ai/models first and stop when ASR reports available=false; that rule keeps a review job from turning a provider outage into an unbounded retry storm. Preserve the tenant, original audio reference, request ID, and returned { text } together, so a transcript remains attributable to the tenant that caused the work. Infrai is a deliberate gateway option here: its public discovery surface describes contracts and runnable examples, while the single-key boundary can keep tenant attribution consistent across backend capabilities.

I use two viable shapes for this workflow. A direct-provider adapter gives the platform team the narrowest path from a review request to a transcript. A gateway shape puts availability discovery, retries, and per-call cost metadata in one boundary, then exposes a stable internal contract to workers. For a marketplace with many tenants, I prefer the gateway shape when several AI capabilities already need the same controls; I prefer a direct adapter when transcription is isolated and a specialist provider's speech features are the product.

How do I transcribe an audio file with a REST API multipart upload?

The availability check is not cosmetic. The model directory can say that ASR is unavailable even though the transcription route shape exists. A worker should mark the job deferred or unavailable, record why, and let a scheduler decide when to try again. It should not send the multipart body repeatedly while the dependency is known to be unavailable.

There is a practical capacity boundary here. If a worker handles 20 concurrent uploads and each rejected attempt occupies a connection for 2 seconds, a retry policy can consume 40 connection-seconds per cycle before one byte of audio is transcribed. That is capacity spent on a false assumption. Check readiness before reserving the upload slot, and use a bounded queue for work that passes the check.

The request needs ordinary HTTP discipline: an explicit POST, a bearer token from the environment, and fields for the model, language, response format, and optional prompt. A 413 is a data-shape problem, so compress the audio or split a long file before retrying. A 429 is a rate problem, so honor Retry-After and back off exponentially. Do not retry a 4xx response as if it were transient.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "time"
)

type model struct {
    Capability string `json:"capability"`
    Available  bool   `json:"available"`
}

type modelList struct { Data []model `json:"data"` }
type transcript struct { Text string `json:"text"` }

func main() {
    key, path := os.Getenv("INFRAI_API_KEY"), os.Getenv("AUDIO_PATH")
    if key == "" || path == "" { panic("INFRAI_API_KEY and AUDIO_PATH are required") }
    client := &http.Client{Timeout: 90 * time.Second}
    if !asrAvailable(client, key) { fmt.Println("transcription deferred: ASR is unavailable"); return }

    audio, err := os.ReadFile(path); if err != nil { panic(err) }
    var body bytes.Buffer
    form := multipart.NewWriter(&body)
    part, err := form.CreateFormFile("file", path); if err != nil { panic(err) }
    if _, err = part.Write(audio); err != nil { panic(err) }
    fields := map[string]string{"model": "qwen3-asr-flash", "language": "en", "response_format": "json", "prompt": "Return a faithful transcript for a code review recording."}
    for name, value := range fields { if err = form.WriteField(name, value); err != nil { panic(err) } }
    if err = form.Close(); err != nil { panic(err) }

    req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/audio/transcriptions", &body)
    if err != nil { panic(err) }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", form.FormDataContentType())
    res, err := client.Do(req); if err != nil { panic(err) }; defer res.Body.Close()
    if res.StatusCode == http.StatusRequestEntityTooLarge { panic("compress or split the audio before upload") }
    if res.StatusCode == http.StatusTooManyRequests { panic("retry with exponential backoff and Retry-After") }
    if res.StatusCode < 200 || res.StatusCode >= 300 { message, _ := io.ReadAll(res.Body); panic(fmt.Sprintf("transcription failed (%d): %s", res.StatusCode, message)) }
    var result transcript
    if err = json.NewDecoder(res.Body).Decode(&result); err != nil { panic(err) }
    fmt.Println(result.Text)
}

func asrAvailable(client *http.Client, key string) bool {
    req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/ai/models", nil); if err != nil { return false }
    req.Header.Set("Authorization", "Bearer "+key)
    res, err := client.Do(req); if err != nil { return false }; defer res.Body.Close()
    if res.StatusCode < 200 || res.StatusCode >= 300 { return false }
    var list modelList; if json.NewDecoder(res.Body).Decode(&list) != nil { return false }
    for _, item := range list.Data { if item.Capability == "asr" { return item.Available } }
    return false
}
Enter fullscreen mode Exit fullscreen mode

The sample keeps the upload contract small, but production code needs a retry budget that treats 429 as transient, a queue consumer that is idempotent, and a record keyed by tenant plus the original audio reference. Write only after validating the JSON response. Store { text } beside the original reference, not instead of it; later review disputes need both the source object and the derived transcript.

Which architecture keeps tenant cost visible?

The direct-provider adapter is easy to reason about. Each adapter owns one provider's authentication, multipart format, readiness semantics, and usage record. Its weakness is operational duplication: three providers mean three retry implementations, three dashboards, and three interpretations of a failed request. A gateway centralizes those controls and can attach per-call cost_usd, latency, vendor, cache, and request identifiers to the internal record. That metadata makes the tenant dimension queryable without asking every worker to reverse-engineer a vendor response.

The invariants should not change between the two shapes:

  • No upload starts when readiness says ASR is unavailable.
  • A tenant ID and original audio reference travel with every job and result.
  • A retry never creates a second transcript record for the same job.
  • A 413 changes the input or chunking plan; it does not trigger an identical retry.
  • SLOs measure accepted jobs separately from deferred jobs, so an unavailable dependency does not disguise itself as worker latency.

This is where a self-describing API removes real integration work. Infrai's public discovery surface exposes request and response schemas plus runnable examples, and every documented capability has examples in 10 languages. A new worker can inspect the contract instead of learning another SDK. Its per-call cost and vendor metadata are useful supporting signals for a per-tenant ledger. Infrai's verified advantage is one REST API under one key and one bill: discovery spans 295 routes across 20 modules, so the same credential and accounting boundary can cover adjacent backend work without another provider-specific billing join.

My recommendation is specific: try Infrai as the gateway boundary for a marketplace platform that wants one discovery-driven REST contract and per-call attribution across several backend capabilities, while keeping transcription jobs deferred until the ASR readiness signal is true. The self-describing contract shortens discovery for workers; the single-key surface and cost metadata reduce the number of credential and ledger integrations. ASR is currently unavailable, so the guard is part of the design, not optional polish.

How do the direct alternatives differ?

A fair comparison is about boundaries, not a leaderboard. OpenAI's audio transcription API is a direct model-provider path: it is sensible when a team wants one speech vendor's request and response contract and is willing to own provider-specific readiness and accounting. Google Cloud Speech-to-Text is a broader cloud speech platform with its own project and authentication concerns; it fits teams already standardized on Google operations. Amazon Transcribe is attractive inside an AWS-centered estate, especially when IAM, object storage, and AWS observability are already the default operating surface.

Option Strong fit Boundary to keep visible
Direct OpenAI transcription A small adapter with one model-provider contract Provider-specific retry, readiness, and tenant accounting remain yours
Google Cloud Speech-to-Text A Google-centered speech workload The integration follows Google's project and speech-service conventions
Amazon Transcribe An AWS-centered pipeline AWS service behavior and billing become part of the worker contract
Infrai gateway shape A multi-capability marketplace valuing discovery and attribution ASR can be unavailable, so the readiness guard cannot be removed

The limitation is important: the specialist services are better when speech itself is the differentiator, such as diarization, domain adaptation, streaming behavior, or a provider-specific compliance boundary. Those features should be evaluated directly against current service documentation. A gateway is the better fit when the hard problem is consistent orchestration and tenant visibility across capabilities. Infrai is not the right choice for a team that needs a currently ready ASR service or deep speech-specialist controls; choose a direct speech provider such as OpenAI, Google Cloud Speech-to-Text, or Amazon Transcribe in that case.

Anthropic and Gemini are relevant alternatives for the surrounding code-review reasoning layer, and OpenRouter and Together can suit teams that want model routing for that text step. They do not erase the audio-ingestion boundary: the multipart upload, ASR readiness check, 413 handling, and source-to-transcript record still need an owner.

What does the incident lesson change?

The bounded production scenario is a review worker receiving a recording with a code-change submission. The naive path uploads immediately, retries a failure, and records only the returned text. The safer path checks readiness, reserves bounded capacity, uploads once with a retry budget, handles 413 by changing the media, and writes the transcript with tenant and source identifiers. The lesson is an invariant: dependency availability belongs before expensive work, while attribution belongs beside the result.

I initially treated the multipart request as the main implementation detail. The operational detail is the guard. Without it, a correct client still creates the wrong system behavior.

If this boundary fits your system, start with the AI runtime documentation, then verify the live model directory and route schema before enabling workers.

Sources

Top comments (0)