---
title: Quickstart
icon: Rocket
description: Install SDK 0.18.0, complete a first batch transcription, then choose fast or realtime delivery.
---

The shortest path to a first result is **batch transcription** of a complete
audio file. It accepts a supported audio container and does not require you to
prepare realtime PCM chunks. Complete that path first, then choose fast or
realtime delivery if your product needs it.

## Before you start

Have these ready before running a command:

- An API key and Socket.IO path obtained through your organization's
  [access flow](/en/authentication). The page renders the base URL configured
  for its environment.
- A complete, supported audio file. The examples below use `meeting.wav` and
  write captions to `meeting.vtt`.
- One supported SDK runtime:
  - JavaScript / TypeScript: a server-side Node.js or Bun runtime that supports
    ES2021, `fetch`, `FormData`, and `Blob`. The SDK does not publish a minimum
    Node.js or Bun version. The fixtures are checked with Node.js 24 and Bun
    1.3.14; the direct `node` commands below assume that Node.js 24 verification
    environment.
  - Python 3.10 or newer.
- `ffmpeg` only if you plan to try the optional realtime path.

Keep the API key in server-side configuration. Do not put it in browser or
mobile code.

## 1. Install SDK 0.18.0

Choose one language. The page uses the same JavaScript and Python tab labels for
every alternative.

<CodeBlockTabs defaultValue="JavaScript">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="JavaScript">JavaScript / TypeScript</CodeBlockTabsTrigger>
    <CodeBlockTabsTrigger value="Python">Python</CodeBlockTabsTrigger>
  </CodeBlockTabsList>
  <CodeBlockTab value="JavaScript">

```bash
npm install @humain-voice/sdk@0.18.0
```

  </CodeBlockTab>
  <CodeBlockTab value="Python">

```bash
python -m pip install humain-voice==0.18.0
```

  </CodeBlockTab>
</CodeBlockTabs>

**Expected:** the package manager completes successfully and records the exact
SDK version `0.18.0`.

## 2. Configure the environment

Run these exports in the same shell that will run the example. Replace the key
with the value configured for your organization.

```bash
export API_URL="https://api.voice.humain.com"
export API_PATH="/socket.io"
export API_KEY="YOUR_API_KEY"
export API_VERSION="v1"

test -n "$API_URL" && test -n "$API_PATH" && test -n "$API_KEY" && echo "HUMAIN Voice environment ready"
```

**Expected:** the last command prints `HUMAIN Voice environment ready`.

This environment publishes Socket.IO at `/socket.io`. Release `0.18.0`
defaults every Socket.IO client to `/socket.io`. Set
`API_PATH` only when a self-hosted or proxied deployment requires an override.
The legacy `sautech.humain.com` endpoint requires `/realtime/socket.io`.
The batch client uses `API_URL`, `API_KEY`, and `API_VERSION` only.

## 3. Run a batch transcription

Use the code-block copy button and save the selected fixture under its displayed
filename. It submits `meeting.wav`, polls with a five-minute deadline, enables
diarization, prints the returned transcript, and writes WebVTT captions.

<CodeBlockTabs defaultValue="JavaScript">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="JavaScript">JavaScript / TypeScript</CodeBlockTabsTrigger>
    <CodeBlockTabsTrigger value="Python">Python</CodeBlockTabsTrigger>
  </CodeBlockTabsList>
  <CodeBlockTab value="JavaScript">

```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;
});
```

  </CodeBlockTab>
  <CodeBlockTab value="Python">

```python
from __future__ import annotations

import asyncio
import os
import sys
from pathlib import Path

from humain_voice import stt
from humain_voice.stt.batchtranscription import BatchDiarization


def required_env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"{name} is required")
    return value


async def main() -> None:
    input_path = Path(sys.argv[1] if len(sys.argv) > 1 else "meeting.wav")
    output_path = Path(sys.argv[2] if len(sys.argv) > 2 else "meeting.vtt")

    async with stt.BatchTranscribeClient(
        api_url=required_env("API_URL"),
        api_key=required_env("API_KEY"),
        api_version=os.environ.get("API_VERSION", "v1"),
    ) as client:
        result = await client.transcribe(
            input_path,
            lang=stt.Language.ArEn,
            asr=stt.BatchTranscriptionModel.BayanArEn,
            diarization=BatchDiarization.On,
            save_result=True,
            poll_interval=2.0,
            timeout_seconds=300.0,
            on_progress=lambda response: print("status:", response.status.value),
        )

    print(result.results.transcript if result.results else "")
    output_path.write_text(
        stt.Subtitles.from_response(result).to_vtt(),
        encoding="utf-8",
    )


if __name__ == "__main__":
    asyncio.run(main())
```

  </CodeBlockTab>
</CodeBlockTabs>

Run the command for the file you saved:

- JavaScript / TypeScript: `node batch-transcription.ts meeting.wav meeting.vtt`
- Python: `python batch_transcription.py meeting.wav meeting.vtt`

**Expected:** on a successful job, the terminal prints one or more `status:`
updates followed by the returned transcript, and `meeting.vtt` is created. Audio
with no recognized speech can produce an empty transcript.

<Callout type="info">
Your first HUMAIN Voice request is complete when the job reaches `done` and the
caption file is written.
</Callout>

## After the first result

- The fixture polls every two seconds with a 300-second polling-loop threshold.
  Submission and an in-flight fetch can extend wall time. Choose deadlines for
  your own workload; a request timeout is not an overall workflow deadline.
- SDK `0.18.0` returns on `done` and raises on `failed` or timeout. Its
  `transcribe()` helper does not stop specially on `cleared`, so a cleared job
  reaches the configured timeout. A direct poller must stop on `done`, `failed`,
  and `cleared` explicitly.
- The fixture closes its client even when submission or polling fails. Preserve
  that cleanup pattern in production.

The [batch recording recipe](/en/recipes/transcribe-a-recording) expands the
polling, speaker-label, and subtitle patterns.

## Choose the next delivery mode

| Mode | Use it when | Audio delivery | Result flow |
|---|---|---|---|
| Batch transcription | A complete recording, including longer meetings, calls, or podcasts | Upload once | Poll a job until `done`, `failed`, or `cleared` |
| Fast transcription | A short, already-complete audio payload needs lower latency, such as one agentic conversation turn | Send the complete payload once over Socket.IO | Receive upload and transcription events, ending with a final result |
| Realtime transcription | Audio is still arriving from a microphone, call, or live source | Send PCM16 little-endian, 16 kHz, mono chunks | Replace provisional text until a final or speech-final response arrives |

Fast transcription is not the long-audio or podcast path. Use batch for those
complete recordings; use fast when the complete payload is short and latency
matters.

## Optional: run realtime transcription

Realtime input must already be raw PCM16 little-endian, 16 kHz, mono. Convert a
recording for this walkthrough:

```bash
ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ar 16000 -ac 1 speech.pcm
```

**Expected:** `ffmpeg` exits successfully and creates `speech.pcm`. Raw PCM has
no playable file header.

Save the selected fixture under its displayed filename:

<CodeBlockTabs defaultValue="JavaScript">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="JavaScript">JavaScript / TypeScript</CodeBlockTabsTrigger>
    <CodeBlockTabsTrigger value="Python">Python</CodeBlockTabsTrigger>
  </CodeBlockTabsList>
  <CodeBlockTab value="JavaScript">

```ts
import { readFile, writeFile } from 'node:fs/promises';

import {
  type ErrorResponse,
  Language,
  RealtimeClient,
  Subtitles,
  type WordSegment,
} from '@humain-voice/sdk';

const CHUNK_BYTES = 3_200; // 100 ms of PCM16LE, 16 kHz, mono audio.

function requiredEnv(name: 'API_KEY' | 'API_PATH' | 'API_URL'): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
}

async function pause(milliseconds: number): Promise<void> {
  await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
}

async function main(): Promise<void> {
  const inputPath = process.argv[2] ?? 'speech.pcm';
  const outputPath = process.argv[3] ?? 'speech.vtt';
  const client = new RealtimeClient({
    api_url: requiredEnv('API_URL'),
    api_path: requiredEnv('API_PATH'),
    api_key: requiredEnv('API_KEY'),
  });
  const finalizedWords: WordSegment[] = [];
  let serverError: ErrorResponse | undefined;
  let protocolFinalObserved = false;

  try {
    const stream = await client.startStream(Language.ArEn, {
      onResponse: (response) => {
        const kind = response.is_final
          ? 'final'
          : response.is_speech_final
            ? 'speech-final'
            : 'partial';
        console.info(`${kind}:`, response.transcription);
        if (response.is_final) protocolFinalObserved = true;
        if (response.is_final || response.is_speech_final) {
          // The current public Realtime contract does not guarantee increasing
          // seq values, so collect final words in arrival order instead of
          // asking RealtimeSubtitles to deduplicate by id:seq.
          finalizedWords.push(...response.words);
        }
      },
      // The released SDK can invoke a stream handler more than once for one
      // routed error, so keep this callback idempotent.
      onError: (error) => {
        serverError = error;
      },
    });

    const pcm = await readFile(inputPath);
    for (let offset = 0; offset < pcm.length; offset += CHUNK_BYTES) {
      await stream.send(pcm.subarray(offset, offset + CHUNK_BYTES));
      await pause(100);
    }

    // close() sends the last frame and waits for protocol is_final, a routed
    // error, or this timeout. It resolves rather than throwing on timeout.
    await stream.close(5);
    if (serverError) {
      throw new Error(serverError.message ?? serverError.code ?? 'Realtime stream failed');
    }
    if (!protocolFinalObserved) {
      throw new Error('Realtime stream ended before protocol is_final');
    }
    await writeFile(outputPath, Subtitles.fromWords(finalizedWords).toVtt(), 'utf8');
  } finally {
    await client.disconnect();
  }
}

void main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});
```

  </CodeBlockTab>
  <CodeBlockTab value="Python">

```python
from __future__ import annotations

import asyncio
import os
import sys
from pathlib import Path

from humain_voice import stt


CHUNK_BYTES = 3_200  # 100 ms of PCM16LE, 16 kHz, mono audio.


def required_env(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"{name} is required")
    return value


async def main() -> None:
    input_path = Path(sys.argv[1] if len(sys.argv) > 1 else "speech.pcm")
    output_path = Path(sys.argv[2] if len(sys.argv) > 2 else "speech.vtt")
    finalized_words: list[stt.WordSegment] = []
    server_error: stt.ErrorResponse | None = None
    protocol_final_observed = False

    def handle_response(response: stt.RtTranscribeResponse) -> None:
        nonlocal protocol_final_observed
        if response.is_final:
            kind = "final"
        elif response.is_speech_final:
            kind = "speech-final"
        else:
            kind = "partial"
        print(f"{kind}:", response.transcription)
        if response.is_final:
            protocol_final_observed = True
        if response.is_final or response.is_speech_final:
            # The current public Realtime contract does not guarantee increasing
            # seq values, so collect final words in arrival order instead of
            # asking RealtimeSubtitles to deduplicate by id:seq.
            finalized_words.extend(response.words)

    def handle_error(error: stt.ErrorResponse | None) -> None:
        # The released SDK can invoke a stream handler more than once for one
        # routed error, so keep this callback idempotent.
        nonlocal server_error
        server_error = error

    client = stt.RealtimeClient(
        api_url=required_env("API_URL"),
        api_path=required_env("API_PATH"),
        api_key=required_env("API_KEY"),
    )
    async with client:
        stream = await client.start_stream(
            language=stt.Language.ArEn,
            on_response=handle_response,
            on_error=handle_error,
        )
        pcm = input_path.read_bytes()
        for offset in range(0, len(pcm), CHUNK_BYTES):
            await stream.send(pcm[offset : offset + CHUNK_BYTES])
            await asyncio.sleep(0.1)


        # close() sends the last frame and waits for protocol is_final, a routed
        # error, or this timeout. It returns rather than raising on timeout.
        await stream.close(timeout_seconds=5.0)

    if server_error is not None:
        raise RuntimeError(server_error.message or server_error.code or "Realtime stream failed")
    if not protocol_final_observed:
        raise RuntimeError("Realtime stream ended before protocol is_final")
    output_path.write_text(
        stt.Subtitles.from_words(finalized_words).to_vtt(),
        encoding="utf-8",
    )


if __name__ == "__main__":
    asyncio.run(main())
```

  </CodeBlockTab>
</CodeBlockTabs>

Run the command for the file you saved:

- JavaScript / TypeScript: `node realtime-transcription.ts speech.pcm speech.vtt`
- Python: `python realtime_transcription.py speech.pcm speech.vtt`

**Expected:** the terminal labels responses as `partial:`, `final:`, or
`speech-final:`, and the successful stream writes finalized captions to
`speech.vtt`.

After success, keep provisional UI text separate and replace it as result events
arrive. The fixture collects words only from final or speech-final events, then
uses `Subtitles` to render them; it does not rely on `seq`, whose ordering is not
part of the current Realtime wire contract. It closes the stream, waits up to
five seconds for protocol-level `is_final`, and disconnects the client in
cleanup. A timeout is reported as incomplete instead of writing a normal
caption file.

## Next steps

Continue with the path that matches your product. Before production traffic,
re-run it with representative inputs and exercise deadlines, terminal states,
disconnects, retries, and cleanup regardless of delivery mode.

<Cards>
  <Card href="/en/recipes/transcribe-a-recording" title="Deepen the batch flow" description="Add bounded direct polling, speaker reconciliation, and subtitle output." />
  <Card href="/en/sdk" title="Use fast transcription" description="Send a short complete payload through the SDK fast client." />
  <Card href="/en/recipes/realtime-transcription" title="Build realtime transcription" description="Handle PCM framing, provisional text, final captions, and cleanup." />
</Cards>
