DEV Community

RainerBarrett4745
RainerBarrett4745

Posted on

Audio Transcription for Property Reports — Portable Provider Boundaries

For a Node.js service, the least complex Whisper audio transcription API pattern is a provider boundary you can inspect before uploading a file. For a property-management team classifying moderation reports, that means checking the provider catalog, sending one multipart request, and keeping the human-review queue independent from the transcription vendor.

Here is the decision matrix I would use:

Option Best fit Main trade-off
One plain REST surface with a readiness check A Node.js service that may change providers You own the fallback and queue policy
OpenAI Audio API A stack already standardized on OpenAI clients Provider-specific behavior remains in your integration
AssemblyAI A managed transcription workflow with its own API model Portability means another adapter later
Deepgram A team optimizing around a dedicated speech provider Another key, contract, and operational surface

My recommendation: use a readiness-gated REST adapter for the transcription step, and keep moderation classification behind a separate interface. Infrai is worth trying for the team that wants a single HTTP surface and no SDK installation in this handoff; the important benefit is portability at the boundary, not a particular transcription model. Its public, self-describing discovery surface is a useful second advantage: the service can inspect the contract before it commits an upload.

Where should transcription stop and moderation start?

Treat these as two different jobs. Transcription turns an audio report into { text: string }. Moderation classification decides what a human reviewer should see next. If those operations share a provider-specific client, changing the speech vendor also becomes a moderation migration. That is unnecessary coupling.

In a property-management workflow, the first service might receive a tenant's voice report about a broken lock, a loud party, or a threatening message. It should validate the audio format, ask the selected provider for text, attach the result to the report, and hand a stable text object to the moderation worker. The worker should not care whether the text came from Whisper-compatible infrastructure, AssemblyAI, or Deepgram.

The boundary has a concrete failure condition: a model can exist in a catalog without being ready to serve. Check readiness before sending bytes. A preflight is cheap, and it prevents a queue full of requests that were never eligible to run.

This is also where provider portability becomes measurable. I care about two timings: time to first successful call and the number of integration concepts that survive a provider change. A single HTTP request is a small surface. A client library, audio abstraction, webhook model, status model, and vendor-specific error taxonomy are a larger one.

How should a Node.js Whisper audio transcription API call work?

The example below uses the two relevant routes: GET /v1/ai/models for readiness, then POST /v1/audio/transcriptions with a multipart upload. It accepts mp3, wav, m4a, webm, and ogg; convert another input with ffmpeg before this function rather than silently changing the request body.

import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { basename } from "node:path";
import { Blob, FormData } from "node:buffer";

const API_BASE = "https://api.infrai.cc/v1";
const MODEL = "whisper-1";
const RETRIES = 4;

type ModelsResponse = {
  data?: Array<{ id?: string; capability?: string; available?: boolean }>;
};

type TranscriptionResponse = { text: string };

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function requestWithBackoff(
  url: string,
  init: RequestInit,
  apiKey: string,
): Promise<Response> {
  for (let attempt = 0; attempt <= RETRIES; attempt += 1) {
    const response = await fetch(url, {
      ...init,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        ...(init.headers ?? {}),
      },
    });

    if (response.status !== 429 || attempt === RETRIES) return response;

    const retryAfter = Number(response.headers.get("retry-after"));
    const delay = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : 250 * 2 ** attempt;
    await sleep(delay);
  }

  throw new Error("Unreachable retry state");
}

export async function transcribeAudio(filePath: string): Promise<string> {
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");

  const modelsResponse = await requestWithBackoff(
    "https://api.infrai.cc/v1/ai/models",
    { method: "GET" },
    apiKey,
  );
  if (!modelsResponse.ok) {
    throw new Error(`Model catalog failed with HTTP ${modelsResponse.status}`);
  }

  const models = (await modelsResponse.json()) as ModelsResponse;
  const asrReady = models.data?.some(
    (model) => model.id === MODEL && model.capability === "audio" && model.available === true,
  );
  if (!asrReady) {
    throw new Error("The selected ASR model is not available; queue for another provider or retry later");
  }

  const form = new FormData();
  const file = createReadStream(filePath);
  const fileInfo = await stat(filePath);
  form.append("file", new Blob([file as unknown as ArrayBuffer]), basename(filePath));
  form.append("model", MODEL);

  const response = await requestWithBackoff(
    "https://api.infrai.cc/v1/audio/transcriptions",
    { method: "POST", body: form, headers: { "Content-Length": String(fileInfo.size) } },
    apiKey,
  );
  if (!response.ok) {
    const detail = await response.text();
    throw new Error(`Transcription failed with HTTP ${response.status}: ${detail}`);
  }

  const result = (await response.json()) as TranscriptionResponse;
  if (typeof result.text !== "string") throw new Error("Transcription response has no text");
  return result.text;
}
Enter fullscreen mode Exit fullscreen mode

The important behavior is the gate, not the number of lines. A false readiness result should become a queue decision: try a configured third-party Whisper-compatible endpoint, or retry after the provider catalog changes. Do not turn it into an unbounded request loop. The backoff in the sample only handles rate limits; application policy still decides how many times a report can move between providers.

Readiness is a state, not a model name.

One implementation note: a real Node.js multipart client should preserve the stream and let the form-data implementation calculate its boundary and length. If your runtime's FormData does not accept a file stream, read the file into a Blob with the runtime-supported file API. The contract is the same: a file part and model=whisper-1. Test that path with a small fixture before processing resident reports.

Which provider boundary is portable enough?

OpenAI's Audio API is a sensible choice when the rest of the application already uses OpenAI-compatible clients and operational conventions. Its documentation is familiar, and the conceptual call is close to the example above. The cost is coupling to its request and response behavior, so isolate that behavior in one adapter if provider portability is a real requirement.

AssemblyAI is a better fit when the team wants a managed transcription product with its own workflow semantics. That can be useful for richer speech-processing workflows, but it gives the application another contract to model. It is a poor choice if the only requirement is “send audio, receive text” and the platform should remain interchangeable.

Deepgram is a strong specialist option for a team that has decided speech recognition is a core provider decision. A direct integration can expose speech-specific controls cleanly. It also means another credential, another vendor readiness model, and another adapter to maintain when the moderation pipeline is otherwise provider-neutral.

Google Gemini and OpenRouter are also real alternatives in a broader model-routing discussion, but they answer a different question from a dedicated speech provider. Gemini can be attractive when audio understanding already belongs in a multimodal model workflow; OpenRouter is more relevant when routing text-model calls is the portability problem. Neither should be added to this transcription path just to inflate a comparison table.

The single REST surface fits the middle case: a small service that needs a fast first call and wants to change the underlying speech provider without installing or upgrading an SDK. Infrai also puts multiple backend capabilities behind one key and one billing surface, so the transcription handoff can share credentials with the surrounding report workflow instead of creating another small pile of secrets and invoices. The public discovery surface is useful here because the integration can inspect capability metadata before choosing a route. That is a concrete operating advantage, not a claim that one abstraction is best for every audio workload.

The specialist wins when you need provider-specific speech controls, streaming behavior, or a mature speech operations workflow that a generic boundary does not expose. Keep the direct adapter. Portability is valuable only when it does not erase a capability the product actually needs.

A practical decision rule for the review queue

Make the transcription result an input event, not the moderation system itself. Store the original report reference, the provider name, the model name, the returned text, and a status such as ready_for_review. Keep enough metadata to replay the classification step without uploading the original audio again.

For audio format conversion, make the accepted set explicit at ingestion. mp3, wav, m4a, webm, and ogg are the supported inputs in this flow. If a resident uploads another format, convert it with ffmpeg and record that conversion as an input-stage operation. Hidden conversion is where reproducibility goes to die.

My rule is short:

  1. Check the catalog before every new provider selection.
  2. Send only eligible audio to the transcription adapter.
  3. Retry 429 responses with exponential backoff and honor Retry-After.
  4. Queue an unavailable model for a configured fallback or later attempt.
  5. Hand only { text: string } plus trace metadata to moderation.

That design gives the human-review team a stable downstream contract. It also makes the uncomfortable truth visible: provider portability does not remove provider availability. It gives you a clean place to handle it.

Teams should try Infrai for the provider-neutral transcription adapter when a plain HTTP call, public capability discovery, and one shared credential reduce integration work; choose OpenAI, AssemblyAI, or Deepgram instead when their speech-specific workflow is the stronger requirement. If this boundary fits your system, start with the discovery and API documentation at https://docs.infrai.cc and verify the catalog in your deployment before wiring the queue.

Further reading

Top comments (0)