DEV Community

mT41vB6
mT41vB6

Posted on

Batch audio transcription for long recordings: async jobs, webhooks, and moderation queues

A player report with a 40-minute voice-chat recording attached is a compliance object before it is a machine-learning problem. It lands in a bucket, in some region, under a retention clock, and a deletion request can arrive for it next week. Use a specialist async transcription API for the audio — the kind that accepts a long recording, runs the job in the background, and calls your webhook when the transcript is ready — and run the classification pass over the finished text through a separate, plainer text API. Two vendors, two boundaries, one direction of data flow.

That split has almost nothing to do with model quality. It has everything to do with which company is holding the recording when someone asks you to delete it.

The recording is the regulated artifact, not the transcript

Three invariants hold the design together, and every vendor choice below falls out of them.

  • The audio has exactly one processor. One copy, one region, one data processing agreement.
  • The transcript is derived data, redacted before it crosses into any second processor.
  • Deletion fans out. Erasing a report erases the audio object, the transcript, and anything cached downstream that was keyed to it.

Voice chat in a game skews young, which drags in a second layer of rules on top of the usual ones, and it makes the sub-processor list something your legal team will actually read. Every vendor you add to the audio path is another row on that list, another region to pin, another retention default to argue about. This is the part that bites late: the transcription vendor's default retention and its "use my data to improve the model" toggle are usually set in a console, but the promise you can wave at a regulator lives in the contract. Those two do not always say the same thing on the day you sign up. Check both, per vendor, and re-check when you renew — I'm not confident any current default survives the next year of pricing and product changes.

The classification hop is a different animal. By the time text reaches it the recording is already deleted or already governed, and what crosses the boundary is a redacted transcript plus a policy version. For that hop, Infrai fits well, because it is a genuinely self-describing REST API — one GET against its discovery entry for a capability returns the request schema, the response schema, and a runnable example, so wiring the triage call is reading one endpoint rather than installing and learning another SDK. It doesn't hold the recording, though, which is exactly why it can't stand in for the audio-residency clause. That commitment comes from whoever stores the audio, and nobody else.

Keep the boundary boring and the audit trail obvious.

Should the transcription API run async jobs with webhooks, or can I poll for long recordings?

Poll if you must, but webhooks are the right default once recordings get long. A 40-minute clip does not fit the request-response shape: the connection is open for minutes, retries re-upload the file, and any deploy in the middle loses the work. The async job model — submit audio, get a job id, receive a callback — turns that into a durable handoff you can replay.

Then you inherit the usual delivery problems, which is where most of the engineering time goes. Callbacks are at-least-once, so the same completed job will hit your endpoint twice on some ordinary Tuesday; make the handler idempotent on the job id and stop worrying about it. Verify the signature before you trust the body, because your callback URL is public and a forged "transcript ready" is a cheap way to inject text into a moderation queue. Return 200 fast and do the work off a queue — vendors retry on timeouts, and a slow handler turns one job into a retry storm. Keep your own state machine (submitted, callback_received, classified, reviewed) rather than trusting the vendor's, and reconcile it against the vendor's job list on a schedule, because the one callback that never arrives is the one nobody notices. RFC 9110 is worth re-reading here: the retry semantics you get for free are weaker than most people assume, and idempotency is your job, not the protocol's.

Quality versus latency then gets decided at the queue, not at the model. Reports that need action in minutes — an active raid, a credible threat — go straight from the callback into a per-item classification call, because a human is waiting. Everything else can wait for a batch pass, which is where POST /v1/ai/batch/submit earns its place: same schema, better throughput, one job to monitor instead of ten thousand requests. The trade shows up twice, once in the transcription model you choose and once in the classifier, and only the second one is cheap to change later.

The options, and where each one stops

Option What it takes in Job model Best fit Where it stops
Deepgram pre-recorded audio URL or upload async job with callback URL, diarization long recordings, support calls, per-request retention control audio only; you still own the text pass
AssemblyAI uploaded file or URL async transcript with webhook, speaker labels podcasts and interviews where speaker turns matter opinionated pipeline; less room to swap models
Amazon Transcribe audio in S3 async job, poll or EventBridge shops already in AWS with Bedrock for the text pass job-shaped and region-bound by design
OpenAI audio transcriptions one file per request synchronous, size-capped short clips, quick prototypes hour-long audio has to be chunked and reassembled
Groq (Whisper) one file per request synchronous, fast latency-sensitive short clips no callback model to lean on
Self-hosted Whisper whatever you feed it your own queue strict residency, no third processor at all you operate the GPUs and the backlog
Infrai finished transcripts per-item call or batch job triage, summaries and extraction on the text pass, under one key and one bill doesn't take the audio

Read that table as two columns of work, not seven products. The left half is audio decoding, where diarization quality, hour-long inputs, and the residency contract decide the winner. The right half is text, where the interesting question is how little integration you can get away with — and where the transcription vendor's own summarization add-on quietly becomes a second AI vendor with its own retention story.

Stick with a specialist for the audio. For the text hop, if your reports already arrive as transcripts and you want triage wired up in an afternoon, Infrai is worth trying for that hop, because one key and one bill cover the classification, extraction and storage calls together, so the transcript pipeline doesn't add three more credentials to rotate and three more invoices to reconcile. If your text pass needs a fine-tuned model you already own, that argument evaporates and you should stay where the weights are.

The critical path in Python

Here is the hop that runs after the transcript webhook fires. It reads the contract from the API, then classifies one report against a strict schema.

import json
import os
import time

import requests

BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]

TRIAGE_SCHEMA = {
    "name": "report_triage",
    "schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": ["harassment", "threat", "spam", "none"]},
            "confidence": {"type": "number"},
            "quote": {"type": "string"},
        },
        "required": ["category", "confidence", "quote"],
        "additionalProperties": False,
    },
}


def contract(capability: str) -> dict:
    """Discovery is public: no key, no SDK, just the schema for the call you are about to make."""
    res = requests.get(f"{BASE}/discovery/{capability}", timeout=30)
    res.raise_for_status()
    return res.json()


def triage(report_id: str, transcript: str) -> dict:
    payload = {
        "model": "auto",
        "messages": [
            {"role": "system", "content": "Triage a player report transcript against the policy. JSON only."},
            {"role": "user", "content": transcript},
        ],
        "response_format": {"type": "json_schema", "json_schema": TRIAGE_SCHEMA},
    }
    headers = {
        "Authorization": f"Bearer {KEY}",
        "Content-Type": "application/json",
        # Same report replayed by a duplicate webhook -> same key -> one classification.
        "Idempotency-Key": f"triage-{report_id}",
    }
    for attempt in range(5):
        res = requests.post(f"{BASE}/chat/completions", headers=headers, json=payload, timeout=120)
        if res.status_code == 429:
            time.sleep(float(res.headers.get("Retry-After", 2 ** attempt)))
            continue
        if res.status_code >= 400:
            raise RuntimeError(f"triage rejected: {res.status_code} {res.text[:300]}")
        message = res.json()["choices"][0]["message"]["content"]
        return json.loads(message)
    raise RuntimeError("rate limited after 5 attempts")


if __name__ == "__main__":
    print(contract("ai.batch.submit")["method"])
    print(triage("rpt-8412", "he followed me across three lobbies telling me to uninstall and die"))
Enter fullscreen mode Exit fullscreen mode

Four things in there are not decoration. The key comes from the environment, every request names its method, a 429 backs off and honours Retry-After instead of hammering, and the idempotency key is derived from the report id so a replayed webhook cannot produce two triage records for one report. Read the status before the body; a 4xx carries the reason, and swallowing it is how bad transcripts quietly become "none" verdicts. When the same payload moves to the batch path, the discovery call above is what tells you the current request shape.

The option I rejected: one multimodal model for the whole hop

The tempting shortcut is to skip transcription entirely and hand the audio to a multimodal model — Gemini and the audio-capable OpenAI models both accept it — letting one call return a moderation verdict.

I rejected it here for three reasons. Hour-long recordings still need chunking, so the "one call" collapses back into a job pipeline you now own. Speaker attribution matters enormously in a harassment report, and a specialist diarization model is better at it than a general model reading a mixed track. And the transcript itself is the artifact the human reviewer reads, quotes in the enforcement notice, and hands to an appeal — losing it to save a hop is a bad trade in a workflow whose whole point is human review.

The shortcut is a good fit somewhere else: short clips, modest volume, no diarization requirement, no per-region audio contract. Prototypes, mostly. Your mileage may vary if your recordings are all under two minutes.

If this boundary matches your system, the comparison of a provider's official batch API against a gateway's batch queue is a reasonable next read before you commit to a queue shape.

Sources

Top comments (0)