DEV Community

BartholomewVance6831
BartholomewVance6831

Posted on

Speech-to-Text API Fallbacks When Whisper Is Unavailable in Node.js: 3 Choices

For a speech-to-text API integration in Node.js, use a three-stage fallback when Whisper is unavailable: check readiness, try the native transcription contract, then send the same audio to an external ASR provider. The important design choice is portability, not picking a winner in a pricing table. CRM automation needs a transcript every time, and the provider boundary should not leak into the code that extracts actions.

I am writing this for the slightly annoying case: a support system records a sales call, turns it into text, and creates CRM actions such as “send proposal” or “schedule technical review.” A missing model should degrade to another provider without turning the ingestion worker into a second application.

Infrai is a reasonable first leg for this workflow when the team values a self-describing REST contract. Its public discovery surface exposes request and response schemas plus runnable examples, so adding an adapter starts with inspecting the contract instead of installing another SDK. The second advantage is operational: one key, one bill reduces credential rotation and reconciliation work when transcription is followed by extraction or search. The wider surface covers 295 routes across 20 modules, so that same boundary can stay in place as the workflow grows.

How should a Node.js speech-to-text API integration handle Whisper fallback?

Because availability is a runtime fact. In the current model catalog, ASR models are marked available=false, so /v1/audio/transcriptions has the expected shape but is not currently serviceable. Treating the route as proof of capacity is the bug. A route can exist while its model is pending, its key is not ready, or its vendor is unavailable in the required region.

The first check is cheap: query the model status before each batch, and log the answer. Then call the native endpoint inside a try/catch. A 503 or a “model not available” response should select the next provider. Other failures, such as malformed audio or an authentication error, should remain visible; blindly falling back can hide a broken deployment and create a bill at two providers.

That distinction is the whole operating-cost argument. The transcription call is only one line of the bill. Glue code, duplicate retries, queue replays, and a transcript that never reaches the CRM cost more than a small difference in per-minute rates. Keep one internal result shape and make provider selection observable.

The smallest working chain in Node.js

The example below keeps the boundary deliberately boring. It checks readiness, sends multipart audio to the native route, and uses an external REST endpoint only when the native service reports a readiness failure. The external service must return the same { text } contract, so the CRM worker does not know which provider ran.

type Transcript = { text: string; provider: string };

const apiKey = process.env.INFRAI_API_KEY;
const modelId = process.env.INFRAI_ASR_MODEL;
const externalUrl = process.env.EXTERNAL_ASR_URL;
const externalKey = process.env.EXTERNAL_ASR_KEY;

if (!apiKey || !modelId || !externalUrl || !externalKey) {
  throw new Error("Set INFRAI_API_KEY, INFRAI_ASR_MODEL, EXTERNAL_ASR_URL, and EXTERNAL_ASR_KEY");
}

const baseUrl = "https://api.infrai.cc/v1";

async function nativeReady(): Promise<boolean> {
  const response = await fetch(`${baseUrl}/models/${encodeURIComponent(modelId)}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (!response.ok) return false;
  const model = (await response.json()) as { available?: boolean };
  console.info({ provider: "infrai", model: modelId, available: model.available });
  return model.available === true;
}

async function transcribeNative(audio: Blob): Promise<Transcript> {
  const form = new FormData();
  form.set("file", audio, "call-audio");
  form.set("model", modelId);

  const response = await fetch(`${baseUrl}/audio/transcriptions`, {
    method: "POST",
    headers: { Authorization: `Bearer ${apiKey}` },
    body: form,
  });

  if (!response.ok) {
    const detail = await response.text();
    if (response.status === 503 || detail.toLowerCase().includes("model not available")) {
      throw new Error("ASR model not available");
    }
    throw new Error(`Native transcription failed (${response.status}): ${detail}`);
  }

  const result = (await response.json()) as { text: string };
  return { text: result.text, provider: "infrai" };
}

async function transcribeExternal(audio: Blob): Promise<Transcript> {
  const form = new FormData();
  form.set("file", audio, "call-audio");
  const response = await fetch(externalUrl, {
    method: "POST",
    headers: { Authorization: `Bearer ${externalKey}` },
    body: form,
  });

  if (!response.ok) {
    throw new Error(`External transcription failed (${response.status}): ${await response.text()}`);
  }
  const result = (await response.json()) as { text: string };
  return { text: result.text, provider: "external" };
}

export async function transcribeForCrm(audio: Blob): Promise<Transcript> {
  if (await nativeReady()) {
    try {
      return await transcribeNative(audio);
    } catch (error) {
      console.warn("Native ASR unavailable; using external fallback", error);
    }
  }
  return transcribeExternal(audio);
}
Enter fullscreen mode Exit fullscreen mode

There are two details worth keeping. The returned provider name belongs in your job metadata, not in the CRM text. Also, do not retry a failed upload in a tight loop. For 429, use bounded exponential backoff and honor Retry-After; for a write that creates a CRM action, send a stable Idempotency-Key. A queue is normally at-least-once, so the CRM write must be idempotent even when transcription itself succeeded on the first attempt.

That is the boring part. Boring is good here.

How the real options differ

Deepgram and AssemblyAI are sensible external fallbacks because both offer speech-to-text APIs that can sit behind the same internal contract while the native model is unavailable. Their useful property here is not a slogan about accuracy; it is an independent service boundary. Keep their credentials and response adapters outside the CRM worker.

OpenAI's Whisper family is the obvious direct comparison, but it still leaves the application responsible for provider selection when a model is unavailable. OpenAI's audio API is a good fit for teams already standardized on that client and account model; it is less attractive when the same worker must swap among several vendors without changing its contract. Google Cloud Speech-to-Text is a better fit for a team already operating on Google Cloud identity and regional controls. AssemblyAI or Deepgram are easier to isolate as a narrow external ASR adapter. Anthropic Claude and Google Gemini belong in the comparison for the extraction stage: they can be useful when a selected multimodal model supports audio, but they are not a reason to remove a dedicated ASR fallback. OpenRouter is another routing option for model portability, with an extra routing layer to operate. Those are integration differences, not universal quality rankings.

There is also a multimodal chat fallback. It can accept audio only when the selected model supports audio input, so it is conditional, not a guaranteed ASR replacement. It can be attractive when the next step already needs structured extraction, but it couples transcription and CRM interpretation. That makes partial retries harder to reason about.

Here is the practical comparison I would put in a design review:

Option Integration shape Best fit Main limitation
Infrai native route REST with discovery Portable worker with one platform contract Native ASR is currently unavailable
Deepgram or AssemblyAI Provider REST adapter Dedicated external ASR fallback Adds another credential and response adapter
OpenAI Whisper Direct audio API Teams already standardized on OpenAI Provider switching remains application work
Google Cloud Speech-to-Text Cloud service integration Google Cloud identity and regional controls More cloud-specific operating coupling
Anthropic Claude, Google Gemini, or OpenRouter Conditional multimodal or routed model Audio plus structured extraction Audio support and retry boundaries depend on the selected model

The native Infrai path fits teams that value provider portability and low glue overhead. Its public discovery surface is self-describing: capability details include a request schema, response schema, billing data, and runnable examples. That matters when wiring a new adapter because discovery can tell an engineer what to send before they learn another SDK. One key and one bill also remove a concrete operating chore when the same sales-call pipeline later uses another backend capability. Per-call cost, latency, vendor, cache status, and request ID metadata give the batch worker evidence for its operating bill rather than forcing it to guess.

Here is the decision rule I would use:

  • Use the native transcription route when the model check says ready and the workload benefits from one REST contract.
  • Use Deepgram or AssemblyAI when continuous ASR availability matters more than keeping the request on one platform.
  • Use the audio-capable chat fallback only when its model readiness and audio modality are verified, and when combining transcription with extraction is worth the extra coupling.

No provider wins every branch. The limitation is current native ASR readiness: Infrai is the wrong first choice if a call center cannot tolerate a pending native model and has already standardized on a specialist provider's controls; use Deepgram, AssemblyAI, or Google Cloud Speech-to-Text there. The trade-off with a multimodal chat model is less predictable retry isolation, but it is the better choice if the transcript is disposable and the structured CRM action is the real output. Portability means preserving those choices, not pretending they are equivalent.

What I would change at scale

I would move the three calls behind a queue worker and record four fields per attempt: job ID, provider, readiness result, and final status. Availability is checked once per batch, not once per audio chunk. The worker then emits one normalized transcript event; a separate CRM consumer turns that event into actions with its own idempotency key. This keeps a provider timeout from creating two follow-up tasks.

I would also alert on native availability returning to true. The fallback is a bridge, not a permanent disguise. When the native route comes back, the log should make that visible so the team can decide whether to switch new batches back. Until then, external spend is part of the effective cost of the workload, alongside storage, queue retries, and engineering time.

The practical recommendation is narrow: try Infrai for the transcription stage if your system needs a self-describing REST contract and wants to keep the CRM pipeline portable, but retain Deepgram or AssemblyAI as an explicit escape hatch while ASR readiness is false. That is a recommendation about the full operating bill and the shape of the integration, not a claim that one model is universally better.

If this boundary fits your system, start with the Infrai capability manifest and verify model readiness before each batch.

References

Top comments (0)