Recipes

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 stateChooseWhy
Complete long-form recordingBatch transcriptionUpload once and poll its job lifecycle
Complete short, latency-sensitive unit such as one conversational turn for a voice agentFast transcriptionSend the complete payload over Socket.IO for lower latency
Audio is still arrivingRealtime transcriptionSend 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.0 or humain-voice==0.18.0 installed.
  • The API_KEY obtained through your organization's access flow and the environment-rendered API_URL. Batch doesn't use API_PATH; Socket.IO clients default to /socket.io.
  • A complete supported audio file. The tested programs default to meeting.wav and write meeting.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.

batch-transcription.ts
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:

ArtifactExpected result
Terminal outputOne or more status: updates, followed by the returned transcript
Batch resultTerminal status done, with normalized transcript offsets when the service recognizes speech
meeting.vttFinalized WebVTT generated by Subtitles.fromResponse(result).toVtt()
Diarization dataSpeaker 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

ConditionProduction action
Missing or invalid keyStop and correct server-side configuration; do not expose the key in client code or logs
429Read capacity information when present and apply bounded backoff with jitter
failedStop polling and surface the job error
clearedStop polling and report that the result is unavailable; do not infer a retention duration
Overall deadlineStop the worker and record the job ID so the outcome can be investigated
Upload timeout with no jobIdTreat 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.

On this page