@humain-voice/sdk
v0.16.1
Published
TypeScript SDK for HUMAIN Voice ASR and TTS services
Readme
HUMAIN Voice JavaScript/TypeScript SDK
Official TS/JS SDK for HUMAIN Voice ASR and TTS services. It supports:
- Fast Transcription (file-based)
- Realtime (streaming audio via websockets)
- Batch Transcription (HTTP polling)
- Live Diarization
- ASR subtitle helpers for SRT and WebVTT
- Text-to-Speech (websocket streaming, Node/Bun)
Installation
Using npm:
npm install @humain-voice/sdkUsing pnpm:
pnpm add @humain-voice/sdkUsing bun:
bun add @humain-voice/sdkUse @humain-voice/sdk for new code. The deprecated compatibility package
still works for now, but emits a deprecation warning and re-exports this
package.
Changelog
Release notes are maintained in the repository root
CHANGELOG.md. The npm README is generated during
publish and includes the full changelog. The package also ships
CHANGELOG.md.
Configuration
Provide environment variables at runtime:
API_URL: Base URL of the ASR service (e.g.https://api.example.com)API_PATH: Socket.IO path for the service you are calling. Required, with no default — each service has its own path (e.g./realtime/socket.iofor realtime,/fast-transcription/socket.iofor fast transcription,/tts/socket.iofor TTS). It must be passed to every client.API_KEY: Your API key
Example (Bash — the path below is the realtime example):
export API_URL="https://api.example.com"
export API_PATH="/realtime/socket.io"
export API_KEY="<your_api_key>"Fast Transcription
import { ASRModel, FastTranscriptionClient, Language } from '@humain-voice/sdk';
import { readFile } from 'node:fs/promises';
async function main() {
const apiUrl = process.env.API_URL ?? '';
const apiPath = process.env.API_PATH ?? '';
const apiKey = process.env.API_KEY ?? '';
const client = new FastTranscriptionClient({
api_url: apiUrl,
api_path: apiPath,
api_key: apiKey,
onConnect: () => console.info('connected'),
onFileUpload: (resp) => console.info('file upload:', resp),
onError: (err) => console.error('error:', err),
});
await client.connect();
const audio = await readFile('samples/king_fahad_test.wav');
const result = await client.transcribe(
audio,
Language.ArEn,
ASRModel.BayanArEn,
{ onResponse: (resp) => console.info('response:', resp) }
);
console.info('final result:', result);
await client.close();
}
main().catch(console.error);Realtime Streaming
import { Language, RealtimeClient, RealtimeStream } from '@humain-voice/sdk';
import { readFile } from 'node:fs/promises';
async function run() {
const apiUrl = process.env.API_URL ?? '';
const apiPath = process.env.API_PATH ?? '';
const apiKey = process.env.API_KEY ?? '';
const client = new RealtimeClient({
api_url: apiUrl,
api_path: apiPath,
api_key: apiKey,
});
const stream = await client.startStream(Language.ArEn, {
onConnect: () => console.info('connected'),
onDisconnect: () => console.info('disconnected'),
onResponse: (t) => console.info('response:', t),
onError: (e) => console.error('error:', e),
});
const audioBytes = await readFile('samples/king_fahad_test.wav');
const bytes = new Uint8Array(
audioBytes.buffer,
audioBytes.byteOffset,
audioBytes.byteLength
);
const sampleRate = 16000; // Hz
const bytesPerSample = 2; // 16-bit PCM
const chunkDurationSec = 0.1;
const chunkSize = Math.floor(sampleRate * bytesPerSample * chunkDurationSec);
for (let start = 0; start < bytes.length; start += chunkSize) {
const end = Math.min(start + chunkSize, bytes.length);
const chunk = bytes.subarray(start, end);
await stream.send(chunk);
await new Promise((r) => setTimeout(r, chunkDurationSec * 1000));
}
await stream.close(1);
}
run().catch(console.error);Parallel streams (single client)
import { Language, RealtimeClient, RealtimeStream } from '@humain-voice/sdk';
import { readFile } from 'node:fs/promises';
async function streamAudio(stream: RealtimeStream, audioBytes: Uint8Array) {
const sampleRate = 16000;
const bytesPerSample = 2;
const chunkDurationSec = 0.1;
const chunkSize = Math.floor(sampleRate * bytesPerSample * chunkDurationSec);
for (let start = 0; start < audioBytes.length; start += chunkSize) {
const end = Math.min(start + chunkSize, audioBytes.length);
const chunk = audioBytes.subarray(start, end);
await stream.send(chunk);
await new Promise((r) => setTimeout(r, chunkDurationSec * 1000));
}
await stream.close(1);
}
async function runParallel() {
const apiUrl = process.env.API_URL ?? '';
const apiPath = process.env.API_PATH ?? '';
const apiKey = process.env.API_KEY ?? '';
const client = new RealtimeClient({
api_url: apiUrl,
api_path: apiPath,
api_key: apiKey,
});
const streamA = await client.startStream(Language.ArEn, {
onResponse: (t) => console.info('stream A:', t),
});
const streamB = await client.startStream(Language.ArEn, {
onResponse: (t) => console.info('stream B:', t),
});
const audioBytes = await readFile('samples/king_fahad_test.wav');
const bytes = new Uint8Array(
audioBytes.buffer,
audioBytes.byteOffset,
audioBytes.byteLength
);
await Promise.all([streamAudio(streamA, bytes), streamAudio(streamB, bytes)]);
}
runParallel().catch(console.error);Live Diarization
A standalone RealtimeDiarizationClient identifies speakers in real time over the same realtime socket, accumulating a full per-speaker timeline as audio arrives.
import {
DIARIZATION_RECOMMENDED_CHUNK_BYTES,
RealtimeDiarizationClient,
type DiarizationUpdate,
} from '@humain-voice/sdk';
import { readFile } from 'node:fs/promises';
async function run() {
const client = new RealtimeDiarizationClient({
api_url: process.env.API_URL ?? '',
api_key: process.env.API_KEY ?? '',
});
const stream = await client.startStream({
onError: (err) => console.error('stream error:', err),
});
// feed audio and consume updates concurrently
const feeder = (async () => {
const wav = await readFile('samples/king_fahad_test.wav');
// skip 44-byte WAV header; payload must be raw PCM16LE @ 16 kHz mono
const pcm = new Uint8Array(wav.buffer, wav.byteOffset + 44, wav.byteLength - 44);
for (let off = 0; off < pcm.length; off += DIARIZATION_RECOMMENDED_CHUNK_BYTES) {
await stream.send(pcm.subarray(off, off + DIARIZATION_RECOMMENDED_CHUNK_BYTES));
await new Promise((r) => setTimeout(r, 480)); // 480 ms real-time pace
}
const timeline = await stream.close();
console.log('final timeline:', timeline);
})();
for await (const update of stream) {
const u: DiarizationUpdate = update;
console.log(
`segments=${u.segments.length} newlyFinalized=${u.newlyFinalized.length} ` +
`actives=${u.activeSegments.length} final=${u.isFinal}`,
);
}
await feeder;
}
run().catch(console.error);Notes:
- Stream raw PCM16LE mono 16 kHz. The recommended chunk size is 15360 bytes (480 ms = one model inference step). Other sizes are accepted but produce a less steady result cadence.
final_segmentsarrive as per-response deltas; the SDK accumulates them.update.segmentsis the full best-known timeline;update.newlyFinalizedis the per-response delta.- Active segments may be revised on any update and may overlap (simultaneous speakers).
stream.close()sends the terminator frame, waits up to 5 s, and returns the best-known timeline instead of throwing on timeout.
Subtitles (SRT / VTT)
Fast, realtime, and batch ASR responses include timed word offsets. The subtitle helpers group those words into readable cues and render SRT or WebVTT without adding speaker labels.
Fast and batch responses can be converted with Subtitles.fromResponse():
import { Subtitles } from '@humain-voice/sdk';
const result = await ftClient.transcribe(...);
const srt = Subtitles.fromResponse(result).toSrt();
const vtt = Subtitles.fromResponse(result).toVtt();
const batchResult = await batchClient.transcribe(...);
const batchSrt = Subtitles.fromResponse(batchResult).toSrt();For one-shot conversion, use the exported helpers:
import { toSrt, toVtt } from '@humain-voice/sdk';
const srt = toSrt(result);
const vtt = toVtt(batchResult);Realtime streams can collect stable final responses automatically:
import { RealtimeSubtitles } from '@humain-voice/sdk';
const captions = new RealtimeSubtitles();
const stream = await rtClient.startStream(Language.ArEn, { subtitles: captions });
// send audio, then close the stream
await stream.close();
const vtt = captions.toVtt();Lower-level helpers are available when you already have words or cues:
import { cuesToSrt, cuesToVtt, wordsToCues } from '@humain-voice/sdk';
const cues = wordsToCues(result.words);
const srt = cuesToSrt(cues);
const vtt = cuesToVtt(cues);Speaker labels are never rendered. When batch offsets include speaker metadata, speaker changes are used only as cue boundaries.
Defaults follow common caption readability heuristics:
- up to two lines per cue
- 42 characters per line
- splits on sentence endings, long pauses, long cues, and speaker changes
- forgiving cleanup for common ASR timing issues
Use { strict: true } when you want validation errors instead of normalization:
const captions = Subtitles.fromResponse(result, { strict: true });
const vtt = captions.toVtt({ strict: true });Strict validation applies to both SRT and VTT output.
Object render methods are intentionally named as conversions: toSrt() and
toVtt(). The lower-level helpers keep input-specific names such as
cuesToSrt() and wordsToCues().
Text-to-Speech (TTS)
import { TTSClient, TtsModel } from '@humain-voice/sdk';
import { writeFile } from 'node:fs/promises';
async function tts() {
const apiUrl = process.env.API_URL ?? '';
const apiPath = process.env.API_PATH ?? '';
const apiKey = process.env.API_KEY ?? '';
const client = new TTSClient({
api_url: apiUrl,
api_path: apiPath,
api_key: apiKey,
});
// Optional: fetch available voices (UUID + label)
const voices = await client.listVoices({ timeoutSeconds: 5 });
if (voices.length === 0) {
throw new Error('No voices available');
}
// Single-shot bytes
const bytes = await client.synthesize('Hello from HUMAIN Voice TTS', {
voice_id: voices[0].id,
model: TtsModel.Nebula,
});
await writeFile('out_single.pcm', bytes);
// Streaming iterator
const chunks: Uint8Array[] = [];
for await (const r of client.synthesizeStream(
'Streaming sample from HUMAIN Voice TTS',
{
voice_id: voices[0].id,
model: TtsModel.Nebula,
}
)) {
chunks.push(r.audio);
}
await client.close();
}
tts().catch(console.error);Error handling
The SDK exposes a structured error contract aligned with the platform's ErrorResponse. The fields id, message, code, retryable, and timestamp are all optional. The error callback always fires; whether an in-flight context is terminated depends on the code's ownership (see ADR-0003).
Error code constants
import {
ASR_TRANSCRIPTION_FAILED,
ASR_MODEL_UNAVAILABLE,
RATE_LIMIT_EXCEEDED,
TTS_VOICE_LIST_FAILED,
SERVER_INTERNAL,
isAsrCode,
isTtsCode,
isRequestScopedCode,
isRealtimeOwned,
isTtsOwned,
} from '@humain-voice/sdk'Unknown future codes pass through as strings. Legacy aliases such as RATE_LIMITED, VALIDATION_FAILED, and INTERNAL_ERROR remain exported for older deployments.
Socket.IO errors (TTS / realtime STT / fast STT)
The onError callback always receives an ErrorResponse. Branch on code to decide your policy:
import { ASR_MODEL_UNAVAILABLE, ASR_TRANSCRIPTION_FAILED } from '@humain-voice/sdk'
const onError = (err) => {
console.log(`code=${err.code} retryable=${err.retryable} msg=${err.message}`)
if (err.code === ASR_MODEL_UNAVAILABLE) {
// Safe to retry later.
} else if (err.code === ASR_TRANSCRIPTION_FAILED) {
// Terminal — no retry.
}
}The realtime adapter only terminates an ASR stream context for ASR-shaped codes. A TTS_VOICE_LIST_FAILED arriving on the realtime socket fires the global onError but does not kill the stream.
Batch transcription HTTP errors
BatchTranscribeError (and its subclasses) carry a structured payload plus per-field accessors:
import {
BatchTranscribeClient,
BatchTranscribeError,
BatchTranscribeRateLimitError,
} from '@humain-voice/sdk'
try {
const result = await client.submit(audioBytes, 'ar')
} catch (err) {
if (err instanceof BatchTranscribeRateLimitError) {
console.log(`rate-limited; retryAfter=${err.retryAfter}s capacity=${err.capacity}`)
} else if (err instanceof BatchTranscribeError) {
console.log(`status=${err.statusCode}`)
console.log(`code=${err.code} retryable=${err.retryable}`)
console.log(`detail=${err.detail} jobId=${err.jobId}`)
console.log('rawBody=', err.rawBody) // always preserved
}
}User-facing message preference: detail → message → error → raw text. err.message already follows that order.
Note. The
maxRetriesconstructor option is deprecated and is a no-op. The SDK never retries — callers decide policy based onerr.retryable,err.code, anderr.capacity.
Types
Common enums are exported from the root entrypoint, e.g. Language, ASRModel.
Examples
See javascript/examples/ft_client.ts and javascript/examples/rt_client.ts for full runnable samples with subtitle output.
Also see javascript/examples/tts_client.ts for TTS sample. The batch example (javascript/examples/batch_transcribe_client.ts) shows structured-error handling and SRT/VTT rendering end-to-end.
javascript/examples/diarization_client.ts demonstrates live diarization streaming (feeder + async-iterator pattern).
Changelog
All notable changes to the HUMAIN Voice SDK (Python humain-voice, JS @humain-voice/sdk,
Go module) are documented here. The format follows Keep a Changelog,
and the project adheres to Semantic Versioning.
Versions are shared across all three language packages; a release tags
python/vX.Y.Z, javascript/vX.Y.Z, and golang/vX.Y.Z together. The Teams
release notification posts the section matching the tag's version, so keep the
top section here current before tagging.
[Unreleased]
[0.16.1-beta.2] - 2026-06-18
Changed
- Rebranded repository, package, and release documentation to HUMAIN Voice across the root, Python, JavaScript, and Go docs.
- Python, JavaScript, and Go examples, comments, and release metadata now use HUMAIN Voice wording instead of the old product name.
- The deprecated JavaScript compatibility package now emits
HUMAIN_VOICE_COMPAT_PACKAGE_DEPRECATEDand uses neutral compatibility wording in its warning message. - The legacy Python import path now emits
HumainVoiceCompatibilityWarningwith neutral compatibility wording. - Example package manifests now depend on the registry package names instead of local source paths that expose old package-directory names.
- Archived live-diarization planning docs were condensed so the public docs no longer preserve obsolete package paths.
[0.16.1-beta.1] - 2026-06-18
Added
- The Python SDK can now be imported as
humain_voice, matching thehumain-voicedistribution name. - The deprecated JavaScript compatibility package now wraps
@humain-voice/sdk. - PyPI and npm publish jobs now generate registry READMEs that append this root changelog, so package registry pages show release notes from the same source as the repository.
- The Go module now includes a checked-in
golang/CHANGELOG.mdmirror so tagged Go source includes release notes for Go consumers and pkg.go.dev.
Changed
- JavaScript examples now import
@humain-voice/sdk. - Go dependencies were refreshed and the Go CI/container baseline moved to Go 1.25.
- Go README and release notes no longer reference the non-existent
pkg/fasttranscriptioncompatibility wrapper.
Deprecated
- The legacy Python import path remains available for compatibility, but now
emits a deprecation warning. New code should import from
humain_voice. - The legacy JavaScript compatibility package remains available, but now emits
a deprecation warning. New code should import from
@humain-voice/sdk.
[0.16.1] - 2026-06-15
Changed
- We now recommend selecting the Arabic/English model with the unversioned
BayanArEnconstant instead of pinning toBayanArEnV1. The unversioned constant always maps to the current production model, so you automatically benefit from accuracy improvements without a code change. If you have a specific reason to stay on a fixed model version, theBayanArEnV1andBayanArEnV2constants remain available — nothing you ship today breaks.
[0.16.0] - 2026-06-11
⚠️ Breaking Changes
ASR and diarization model identifiers were renamed. The SDK constants used to select a model changed, so any code that selects a model must be updated before upgrading. Functionality is unchanged; these are the same models under new, brand-aligned names. Always select models through the SDK constants below rather than raw strings. Migration from 0.15.x:
| What you used (0.15.x) | New constant | | ---------------------- | -------------------------- | |
ASRModel.Ar_1|ASRModel.NidaAr| |ASRModel.Ar_2|ASRModel.BayanAr| |ASRModel.Ar_8k_1|ASRModel.NidaArTelephony| |ASRModel.ArEn|ASRModel.BayanArEn| |ASRModel.ArEn_1|ASRModel.BayanArEnV1| |ASRModel.ArEn_2|ASRModel.BayanArEnV2| |BatchDiarization.M1|BatchDiarization.D1| |BatchDiarization.M2|BatchDiarization.D2|Go callers use the package-prefixed forms (e.g.
ASRModelBayanArEnV1,BatchDiarizationD1). The old constants are no longer accepted — code using them will need to migrate to the new names.The batch transcription client no longer retries failed requests automatically. Previous versions retried internally (via
tenacityin Python andp-retryin JS). The client now makes exactly one HTTP call per operation and raises immediately on failure. Themax_retries/maxRetriesconstructor option is kept for compatibility but is now a no-op; passing a non-zero value emits a deprecation warning. If you depend on retries, you must now implement your own policy — every raised error exposesretryable,retry_after, andcapacity(for HTTP 429) so you can back off correctly.Error handling moved to a structured, typed exception hierarchy. Errors are now routed by error code to the client that owns them, and failures raise a specific exception type instead of a generic error. For batch transcription: HTTP 401 raises an auth error, HTTP 429 raises a rate-limit error (carrying
retry_afterandcapacity), and all other failures raise the baseBatchTranscribeError. The full server payload is available on the exception (payload/raw_body) for inspection. If you currently catch a broad error type or parse error strings, switch to catching the typed exceptions and reading their attributes.
Added
- Realtime speaker diarization over the streaming transport — receive per-speaker labels on transcripts as audio streams in.
- SRT and VTT subtitle helpers in Python, JS, and Go to turn transcription results directly into ready-to-use subtitle files.
[0.15.1] - 2026-05-13
Added
- Official Go SDK, at full feature parity with the Python and JavaScript packages: realtime speech-to-text, fast transcription, text-to-speech, and batch transcription, all over the same streaming transport. If you work in Go, you no longer need to call the platform API directly.
[0.15.0] - 2026-05-06
⚠️ Breaking Changes
- Client imports, event names, and configuration shapes changed. Internally the STT and TTS clients were rebuilt on a shared connection layer, and that shifted parts of the public surface — some import paths, streaming event names, and configuration option shapes are different. Review your client setup and event handlers against the updated examples when upgrading from 0.14.x.
disconnect_socketiowas removed. Connection teardown is now handled by the client itself; if you importeddisconnect_socketiodirectly, drop the call — closing the client cleans up the connection for you.
Changed
- STT and TTS clients now share a single connection and lifecycle, which makes connection handling more consistent and predictable across both. This is an internal change, but it is the reason for the surface adjustments noted above.
[0.14.5] - 2026-02-03
Added
- Run multiple concurrent streams from a single
RealtimeClient. You can now open several independent realtime sessions from one client instance instead of constructing a separate client per stream.
Changed
- Invalid ASR and TTS request fields are now rejected up front. The SDK validates request fields — including voice-reference inputs for TTS — before sending, so mistakes surface immediately as a clear client-side error rather than failing partway through a request. Requests that previously appeared to start before failing server-side may now raise a validation error sooner; fix the flagged field and the request proceeds as normal.
