A catalog team can transcribe a supplier's voice memo in Node.js in a few lines. The hard part starts after the text arrives: product attributes must remain traceable when the transcription provider changes, retries happen, or a description is too messy to parse.
Short answer: put transcription behind a small provider-neutral interface, persist the raw transcript with an explicit schema version, and treat attribute extraction as a separate observable step. That gives an e-commerce catalog pipeline a clean before/after shape: before, one request owns every failure; after, audio, transcription, normalization, and publication have separate contracts.
This is a portability decision, not a model-shopping exercise. The useful unit is the boundary around the capability.
Small boundary.
The important detail is what happens when a supplier recording contains two products. A single successful transcription can still produce an unsafe catalog draft if the enrichment step assigns the first material or dimension to both items. Keeping one transcript as evidence, one draft as a reviewable artifact, and one publication event per product makes that mistake visible: the team can correct the split without asking the audio service to repeat work, while the lifecycle metrics still show that transcription succeeded and enrichment needs attention.
How should a Node.js Whisper audio transcription API handle catalog failures?
Picture the flow as four boxes in a line: audio object -> transcript -> catalog draft -> published attribute. Each arrow has a different failure mode. A missing object is storage trouble. An empty transcript is transcription trouble. A malformed size is data-quality trouble. A rejected catalog update is a publication trouble. Combining those into one promise makes the logs vague and the retry policy dangerous.
I use one narrow interface for the second box. It knows about an audio reference and a result; it does not know which hosted service, local process, or future provider produced that result. The adapter owns provider-specific request details. The catalog code owns neither.
| Stage | Owns | Retry decision |
|---|---|---|
| Audio object | Input reference and media type | Fix storage or input metadata first |
| Transcription | Verbatim text and request metadata | Retry a bounded, identifiable attempt |
| Enrichment | Parsed catalog draft and schema version | Re-run rules without re-transcribing |
| Publication | Final catalog write | Retry only an idempotent write |
The table is a design constraint, not a dashboard. It tells the on-call engineer which evidence to inspect before replaying work.
type AudioInput = {
objectKey: string;
contentType: string;
};
type Transcript = {
text: string;
language?: string;
providerRequestId?: string;
};
type Transcriber = (input: AudioInput) => Promise<Transcript>;
type CatalogDraft = {
title: string;
material?: string;
dimensions?: string;
sourceTranscript: string;
schemaVersion: 1;
};
The optional request identifier is operational metadata, not a business key. A provider can omit it. The transcript remains useful without it. That small distinction prevents a portability refactor from leaking an external identifier into product records.
Why separate transcription from catalog enrichment?
Because a transcript is evidence, not truth. A supplier might say, "the blue jacket is medium, recycled nylon, two pockets," while the catalog schema expects a controlled material value and a dimension format. The transcription stage should preserve what was heard. The enrichment stage should make its interpretation explicit.
Here is a deliberately small pipeline. It returns a draft instead of publishing directly. That pause is the point: a bad parse should be reviewable without replaying the audio request.
async function enrichProduct(
input: AudioInput,
transcribe: Transcriber,
): Promise<CatalogDraft> {
const transcript = await transcribe(input);
const text = transcript.text.trim();
if (text.length === 0) {
throw new Error("transcription_empty");
}
return {
title: inferTitle(text),
material: inferMaterial(text),
dimensions: inferDimensions(text),
sourceTranscript: text,
schemaVersion: 1,
};
}
function inferTitle(text: string): string {
return text.split(/[.!?]/, 1)[0]?.trim() || "Untitled product";
}
function inferMaterial(text: string): string | undefined {
const match = text.match(/\b(cotton|wool|linen|nylon)\b/i);
return match?.[1]?.toLowerCase();
}
function inferDimensions(text: string): string | undefined {
return text.match(/\b\d+(?:\.\d+)?\s?(?:cm|mm|in)\b/i)?.[0];
}
This parser is intentionally modest. It demonstrates the contract, not a claim that a regular expression can understand every supplier description. In production, I would test extraction against a fixture set containing abbreviations, missing units, multiple products in one recording, and negation such as "not leather." The raw text lets that parser evolve without losing the original input.
There is a hard limit here. This boundary is a poor fit for an interaction that needs word-by-word live captions, because the example is organized around a completed audio object and a durable catalog draft. Use a streaming design for that job; do not stretch a batch-shaped contract until its events become ambiguous.
How should Node.js retries and observability fit?
Retry the smallest failed operation. If an enrichment rule fails, do not transcribe the same audio again. If the transcription request times out, retry only when the adapter can safely identify the attempt and the caller has a bounded retry budget. A timeout is not proof that the remote side did nothing.
The useful log event is a compact lifecycle record: catalog_job_started, transcription_succeeded, enrichment_rejected, or catalog_published. Include a job ID, object key, schema version, duration, and outcome. Keep the transcript out of ordinary logs; it may contain supplier or product information and is already stored as the evidence field.
Metrics should answer three questions: how many jobs reached each stage, how long each stage takes, and where work stops. Counters for stage outcomes and a duration histogram are enough to start. An alert on a sudden rise in transcription_empty is more actionable than an alert on every thrown exception, because it names the data symptom the catalog team can inspect.
Provider portability lives in the dimensions of those signals. Compare adapters by successful completion, empty-result rate, latency distribution, error class, and transcript quality on the same fixture set. Do not hide a provider switch behind one blended success rate; a faster adapter that produces more unusable dimensions may be the worse catalog choice.
What are the two objections to this design?
The first objection is that an adapter adds code before a simple API call. It does. Three functions are enough to keep that cost bounded: one adapter per provider shape, one shared result type, and one test suite that runs against a fake transcriber. The payoff is local reasoning. The enrichment workflow does not change when request serialization changes.
The second objection is that preserving raw transcripts increases storage and retention work. Also true. The answer is a retention rule, not silent deletion: keep the evidence long enough to audit a catalog change, restrict access, and record which schema version produced the draft. If the business cannot retain audio-derived text, store a redacted or hashed reference according to its data policy, but do not pretend that an unverifiable attribute has the same provenance.
I started by thinking the provider call was the implementation. The catalog failure modes changed my mind. The implementation is the chain of contracts around that call. Once those contracts are visible, switching providers becomes an adapter test and a fixture comparison instead of a rewrite of catalog publication.
The trade-off is deliberate: a little more persisted evidence and a few more lifecycle events buy a much clearer correction path. Teams with no review step may choose a shorter pipeline, but they should accept that an incorrect attribute will be harder to explain later.
Sources
References:
Top comments (0)