Four-State Candidate Audio Files - Batch Transcription Through an API Queue
Short answer: use a durable API queue to batch transcribe multiple candidate audio files, give each job an idempotency key, and make transcription completion a state transition rather than an HTTP timeout.
For a B2B SaaS recruiting workflow, the useful unit is not "an audio file was uploaded"; it is "this candidate recording produced a validated transcript that is safe to score against the job rubric."
The queue should enforce bounded concurrency, tenant-aware fairness, and a retry policy that distinguishes transport failure from bad input. Store the raw result, the normalized transcript, and the validation decision separately.
That gives the on-call engineer something to inspect when a recruiter sees a missing score, without asking a model to repair an operational ambiguity.
How Should a Node.js API Batch Transcribe Multiple Audio Files?
The Node.js API should accept file references and return a job identifier; it should not keep a request open while a worker transcribes the batch.
Multiple files can share a batch identifier for user-facing progress, while each file keeps its own idempotency key and terminal state. That distinction lets one bad recording fail without erasing the useful results from the rest.
What Should a Queue Promise When a Recording Fails?
A practical contract has four states: queued, running, succeeded, and failed. The API may accept a request quickly, but acceptance is not completion.
A client receives a stable job identifier and can poll or subscribe to a status change; the worker owns the transition to a terminal state.
The important boundary is between retryable and terminal errors. A dropped connection, a temporary upstream rejection, or a worker restart can be retried. An unsupported codec, an empty object, or a transcript that cannot satisfy the required schema needs a terminal record with an actionable reason.
Retrying the latter five times only increases load and hides the real data-quality problem.
For candidate scoring, I would also record tenant_id, candidate_id, recording_id, rubric_version, and the content hash as job metadata. The hash is useful for deduplication; the rubric version prevents a later rubric edit from silently changing the meaning of an earlier score.
Keep those fields outside the prompt text and outside the transcript itself.
The SLO should describe the complete workflow. For example, define an availability target for job creation, a latency target from queued to succeeded, and a correctness target for schema validation. A fast transcript with the wrong fields is a failed outcome.
Keep it boring. No magic. That boundary matters.
How Do You Make Repeated Submissions Harmless?
Use an idempotency key whose scope is explicit, such as tenant_id plus a client-generated request key. On the first accepted request, persist the key and the job record in one transaction. On a repeated request, return the original job instead of creating another transcription.
The same principle applies when a worker retries a provider request: the application must not publish two completion events or overwrite a successful result with a later failure.
HTTP semantics matter here. A retry is not a magic fix for every method, and an application has to define the key and replay behavior rather than infer them from a client library. RFC 9110 describes idempotent methods and the conditions under which a client can retry after a communication failure; your queue contract should make the business operation idempotent even when the underlying transcription operation is not.
A minimal worker can make that boundary visible. The example uses a generic speech interface, so the queue is not coupled to one provider or one deployment model. The transcript is accepted only after it has been decoded and checked.
package transcription
import (
"context"
"errors"
)
type SpeechService interface {
Transcribe(ctx context.Context, objectURL string) (string, error)
}
type Result struct {
Transcript string
Words int
}
func Process(ctx context.Context, svc SpeechService, objectURL string) (Result, error) {
text, err := svc.Transcribe(ctx, objectURL)
if err != nil {
return Result{}, err // classify before deciding whether to retry
}
if len(text) == 0 {
return Result{}, errors.New("empty transcript")
}
return Result{Transcript: text, Words: countWords(text)}, nil
}
func countWords(text string) int {
count := 0
inWord := false
for _, r := range text {
if r == ' ' || r == '\n' || r == '\t' {
inWord = false
continue
}
if !inWord {
count++
inWord = true
}
}
return count
}
The production part is the persistence around this function: claim one job with a lease, increment an attempt counter, and write the result with a conditional update such as "only if the job is still running." A lease expiry makes a stuck job eligible for recovery; it does not make two workers safe to write arbitrary final states. The conditional write does that.
Do not pass the whole candidate object through every queue message. Pass a stable object reference and identifiers, then fetch authorized data inside the worker. This keeps messages small and makes retention decisions visible. It also gives the service one place to enforce tenant isolation.
Which Build Choice Fits the Failure Budget?
The limitation is material: a managed API is a poor fit when retention, region, or dependency policy forbids sending recordings outside the service boundary. Self-hosting is a poor fit when the team cannot carry model upgrades, capacity failures, and 24-hour operational ownership. The right choice is the one whose failure budget the platform team can actually support.
The choice is a boundary decision, not a branding exercise. A managed speech API usually reduces the operational surface for codecs, model serving, and capacity spikes, while a self-hosted engine can offer more control over data placement and upgrade timing. Neither choice removes the queue, the schema contract, or the need to measure correctness.
| Decision area | Managed speech API | Self-hosted speech engine |
|---|---|---|
| Capacity planning | Plan request quotas, concurrency, and external latency | Plan CPU/GPU, memory, replicas, and model load time |
| On-call load | Fewer serving components; more dependency and quota risk | More control; more patching, capacity, and degradation work |
| Data boundary | Verify retention, region, and deletion semantics | Operate storage, access control, and model artifacts directly |
| Lock-in | Abstract the job contract and keep provider adapters narrow | Own the runtime and accept a larger platform surface |
| Correctness | Validate returned structure and language assumptions | Validate model version, decoding, and resource pressure |
Capacity planning should start with arrival rate and service time, not with a convenient worker count. If 40 recordings arrive in a burst and the measured service time is 30 seconds, four workers cannot drain that burst in 30 seconds; the queue needs a stated latency target, a maximum in-flight count, and a policy for what happens when that policy is impossible.
That policy might reject new work, defer low-priority tenants, or keep accepting jobs while explicitly allowing the latency SLO to breach. Each option moves pressure somewhere else: rejection becomes a client concern, deferral becomes a fairness concern, and unbounded acceptance becomes an incident waiting to happen.
Backpressure is an operating decision, not a dashboard color.
For this recruiting workflow, I would keep provider-specific code behind one interface and make the normalized transcript an internal contract. That makes a later engine change a migration with an evaluation set, rather than a rewrite of job accounting. It also prevents the scoring service from depending on incidental response fields.
How Do You Verify a Transcript Before Scoring It?
Verification needs two gates. The first is mechanical: the job reached a terminal state, the transcript is non-empty, the encoding is valid, and the metadata matches the requested candidate and rubric version. The second is semantic: required sections exist, fields have the expected types, and the score input can be reproduced from the stored transcript and rubric.
Treat structured output as a contract. Parse into a typed value, reject unknown or missing required fields according to the contract, and persist the validation result beside the raw response. Do not let a downstream scorer guess whether a missing field means "not mentioned," "not transcribed," or "the response was malformed." Those are different outcomes.
The useful metrics are small in number:
- queue age
- processing latency
- retry count
- terminal failure rate
- schema rejection rate
- duplicate-suppression count
Break them down by tenant, recording duration bucket, language, and runtime version where those dimensions are permitted. A single average can look healthy while one tenant waits behind a noisy neighbor.
Test the transitions, not only the happy path. A compact test matrix includes:
- duplicate submission
- worker loss after the upstream call
- an expired lease
- a malformed response
- a successful retry
- a late failure after success
The last case is where conditional state updates earn their keep.
What Is the Rollback Path for a Bad Deployment?
Rollback begins with the queue contract. Stop new workers on the suspect version, leave accepted jobs durable, and start the previous version only if it can read the same job and result schema. If the schema changed incompatibly, pause finalization and run an explicit migration; do not replay blindly.
A safe deployment separates code rollback from data rollback. Raw recordings and raw transcription responses should be immutable under normal operation. Derived normalized transcripts and rubric scores can be regenerated when their version is recorded. This is more auditable than deleting a questionable result and hoping a retry produces the same answer.
The status endpoint should expose enough information for an operator to act: state, attempt count, timestamps, failure class, and a correlation identifier. It should not expose raw candidate audio or unrestricted transcript content by default. Authorization belongs at every read boundary, including administrative replay tools.
Before enabling a new runtime or model version, replay a fixed evaluation set that represents real duration, silence, accents, and overlapping speech. Compare schema validity and rubric-relevant fields, then canary by tenant or queue partition. The release gate is the agreed correctness SLO, not a pleasing sample transcript.
The decision rule is straightforward: use a durable queue whenever transcription can outlive an HTTP request, make duplicate work harmless before adding concurrency, and refuse to score data that has not passed the transcript contract. That sequence keeps the platform honest. It also leaves room to change the speech backend without changing what a candidate result means.
Comments
No comments yet. Start the discussion.