Transcribe a Recording with Speaker Labels
Process a complete long-form recording with bounded polling, speaker reconciliation, and subtitle output.
This recipe takes one complete recording through a production-shaped batch workflow: submit, poll to a terminal state, reconcile speaker segments, write captions, and clean up on every path.
When to use this recipe
Use BatchTranscribeClient when the whole recording already exists, especially
for long-form or large media such as meetings, podcasts, calls, and archives.
Batch defaults bound each request at 512 MiB of request bytes and 4 hours of
decoded audio; Fast transcription has its own separate upload limits. A request
exactly at a configured limit is accepted and only a request that exceeds it is
rejected, but deployed limits can be lower than these defaults, so validate
representative media. The Batch API defines no result-retention duration, so
retrieve results promptly and don't design around an undocumented retention
window.
| Audio state | Choose | Why |
|---|---|---|
| Complete long-form recording | Batch transcription | Upload once and poll its job lifecycle |
| Complete short, latency-sensitive unit such as one conversational turn for a voice agent | Fast transcription | Send the complete payload over Socket.IO for lower latency |
| Audio is still arriving | Realtime transcription | Send PCM chunks and handle provisional and final results |
Fast transcription isn't the long-media path. Use batch for this meeting, podcast, or archive workflow.
Prerequisites
@humain-voice/sdk@0.18.0orhumain-voice==0.18.0installed.- The
API_KEYobtained through your organization's access flow and the environment-renderedAPI_URL. Batch doesn't useAPI_PATH; Socket.IO clients default to/socket.io. - A complete supported audio file. The tested programs default to
meeting.wavand writemeeting.vtt. - A server-side JavaScript runtime or Python 3.10 or newer. The direct Python
polling example also uses
httpx. - A writable output directory and an app deadline appropriate for the recording and worker environment.
The tested programs select Language.ArEn with
BatchTranscriptionModel.BayanArEn. Change the language and model together if
your recording requires another supported combination.
1. Run the tested SDK path
Choose one program and save it under the displayed filename. Both programs enable diarization, poll every two seconds with a 300-second polling-loop threshold, print the returned transcript, write finalized WebVTT, and close the client in cleanup. Submission and an in-flight request can extend wall time.
import { readFile, writeFile } from 'node:fs/promises';
import {
BatchDiarization,
BatchTranscribeClient,
BatchTranscriptionModel,
Language,
Subtitles,
} from '@humain-voice/sdk';
function requiredEnv(name: 'API_KEY' | 'API_URL'): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is required`);
return value;
}
async function main(): Promise<void> {
const inputPath = process.argv[2] ?? 'meeting.wav';
const outputPath = process.argv[3] ?? 'meeting.vtt';
const client = new BatchTranscribeClient({
api_url: requiredEnv('API_URL'),
api_key: requiredEnv('API_KEY'),
api_version: process.env.API_VERSION ?? 'v1',
});
try {
const result = await client.transcribe(
await readFile(inputPath),
Language.ArEn,
{
asr: BatchTranscriptionModel.BayanArEn,
diarization: BatchDiarization.On,
saveResult: true,
pollInterval: 2,
timeout: 300,
onProgress: ({ status }) => console.info('status:', status),
},
);
console.info(result.results?.transcript ?? '');
await writeFile(outputPath, Subtitles.fromResponse(result).toVtt(), 'utf8');
} finally {
await client.close();
}
}
void main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Run the program you saved:
- JavaScript / TypeScript in the Node.js 24 documentation verification
environment:
node batch-transcription.ts meeting.wav meeting.vtt - Python:
python batch_transcription.py meeting.wav meeting.vtt
2. Confirm the expected artifacts
On a successful job:
| Artifact | Expected result |
|---|---|
| Terminal output | One or more status: updates, followed by the returned transcript |
| Batch result | Terminal status done, with normalized transcript offsets when the service recognizes speech |
meeting.vtt | Finalized WebVTT generated by Subtitles.fromResponse(result).toVtt() |
| Diarization data | Speaker segments returned with the legacy result shape used by the released SDK |
Audio with no recognized speech can produce an empty transcript. Treat a
created caption file and a done job as successful processing. Validate whether
the content is useful separately.
The SDK helper succeeds on done, raises on failed, and reaches its configured
timeout while a job remains queued, processing, or cleared. It doesn't
surface cleared immediately. Use direct polling when the app must
distinguish that state as soon as it appears.
3. Bound direct API polling
After submitting multipart/form-data to
POST /v1/transcribe/{lang}, poll the recommended V2 result operation,
GET /v1/transcribe/{job_id}. The loop needs both a per-request timeout and an
overall deadline.
// `jobId` is the value returned by the submission request in step 2.
const jobId = process.env.JOB_ID!;
const deadline = Date.now() + 5 * 60_000;
let job;
while (Date.now() < deadline) {
const response = await fetch(`${process.env.API_URL}/v1/transcribe/${jobId}?save_result=true`, {
headers: {
"x-api-key": process.env.API_KEY!,
Origin: process.env.API_URL!,
},
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) throw new Error(`poll failed: HTTP ${response.status}`);
({ data: job } = await response.json());
if (job.status === "done") break;
if (job.status === "failed") {
throw new Error("transcription failed");
}
if (job.status === "cleared") {
throw new Error("transcription result is unavailable (cleared)");
}
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
if (!job || job.status !== "done") throw new Error("poll deadline exceeded");queued and processing are non-terminal; done, failed, and cleared are
terminal. Use results only for done, surface the job failure for failed, and
treat cleared as an unavailable result.
These loops set save_result=true before terminal retrieval so a lost done
or failed response can be fetched again. The default false can clear stored
fields after building that response. The option does not guarantee a retention
duration.
The two-second interval, ten-second request timeout, and five-minute deadline
above are application choices, not service guarantees. Handle 429 using the
reported capacity and bounded backoff. Retry a result read only when
save_result=true preserved it; do not blindly repeat a timed-out upload
because it may already have created a job.
4. Reconcile words and speakers
The V2 response keeps final_word_segments and diarization_segments
separate. The following explicit application policy assigns a word to the
segment containing its midpoint. If there is no match, it preserves
UNKNOWN_SPEAKER.
function speakerFor(word, segments) {
const midpoint = (word.start_time + word.end_time) / 2;
return segments.find(
(segment) =>
segment.start_time <= midpoint && midpoint < segment.end_time,
)?.speaker ?? "UNKNOWN_SPEAKER";
}
const attributed = job.final_word_segments.map((word) => ({
...word,
speaker: speakerFor(word, job.diarization_segments ?? []),
}));Temporal midpoint matching is an application rule, not an identity guarantee. Document a different nearest-segment or overlap rule if you choose one. Speaker labels distinguish turns; they do not identify real people.
The legacy V1 route can return speaker directly on word offsets when its
force-alignment behavior is enabled. Keep V1 and V2 response types separate
rather than mixing their field names.
5. Produce subtitles
The tested SDK path already writes meeting.vtt from normalized word offsets.
Use toSrt() instead of toVtt() when the consumer requires SubRip. For a
direct V2 client, first normalize the returned word timing into the subtitle
renderer's input shape; do not pass the V2 wrapper to a helper that expects the
released SDK's legacy TranscriptionResponse.
Caption text and a speaker timeline are separate artifacts. WebVTT and SRT are subtitle formats; RTTM is a diarization format.
6. Handle failure and cleanup
| Condition | Production action |
|---|---|
| Missing or invalid key | Stop and correct server-side configuration; do not expose the key in client code or logs |
429 | Read capacity information when present and apply bounded backoff with jitter |
failed | Stop polling and surface the job error |
cleared | Stop polling and report that the result is unavailable; do not infer a retention duration |
| Overall deadline | Stop the worker and record the job ID so the outcome can be investigated |
Upload timeout with no jobId | Treat the outcome as ambiguous; do not blindly upload the same media again |
The verified JavaScript fixture closes its client in finally; the Python
fixture uses an async context manager. The direct Python poller uses a sync
context manager for its HTTP client. Preserve those cleanup boundaries when
adding storage, queues, or subtitle publishing.
Next steps
If the SDK path fits, re-run it against representative recordings and exercise deadlines, retries, and cleanup. If your worker owns submission and polling separately, continue to the Batch REST contract before implementing the upload side.