Recipes

Generate a Playable WAV File

Discover a voice, synthesize PCM16 with SDK 0.18.0, and package it as a verified WAV artifact.

This recipe turns one text input containing a Unicode letter or number after trimming into a complete file for a media player. It discovers a voice at runtime, stops safely when none is returned, waits for final audio, writes the correct PCM metadata, and closes the client on every path.

When to use this recipe

Use the buffered workflow when the app needs a complete WAV artifact before it publishes, stores, or plays the result. If playback or processing must begin before synthesis finishes, use the streaming method described below and keep the same final-chunk, container, timeout, error, and cleanup boundaries.

This recipe covers Socket.IO TTS through the released JavaScript and Python SDKs. It does not define the output contract for a different transport.

Before you start

Requirement0.18.0 contract
SDK@humain-voice/sdk@0.18.0 or humain-voice==0.18.0 in a trusted server runtime
ConnectionProvisioned API_URL and API_KEY; SDK defaults to /socket.io
TextAfter trimming, contains at least one Unicode letter or number; whitespace-only and punctuation-only input is invalid
Voice inputExactly one of voice_id or a non-empty voice_references collection
OutputA writable destination for the final WAV artifact
export API_URL="https://api.voice.humain.com"
export API_KEY="YOUR_API_KEY"

The tested path discovers a voice_id; it does not assume that a particular voice or any voice is available. Keep the API key in the trusted runtime; the SDK uses /socket.io by default.

If the app uses voice_references instead, send one reference whose audio is standard-base64 RIFF/WAVE containing non-empty mono PCM16 data.

1. Discover a voice

Call listVoices() / list_voices() before synthesis when the app has not already been given a supported voice reference.

RuntimeReleased defaultThis recipe
JavaScriptlistVoices() defaults to 5 secondsPasses timeoutSeconds: 5 explicitly
Pythonlist_voices() has no timeout unless suppliedPasses timeout_seconds=5.0

The result contains seven multilingual { id, label, profile } entries when all configured variants are available. Treat the list as runtime data:

  1. If the list is empty, stop this voice_id workflow before indexing it.
  2. If a request fails, preserve its structured error state and clean up.
  3. If a voice is returned, pass its exact id; do not derive an ID from the label.

Passing a profile ID lets Platform select its physical variant from the text. For current Arabic/English profiles, any Arabic-script letter selects Arabic; otherwise English is selected. Physical variant IDs are internal and rejected.

An empty list is an operational result, not a promise about future voice availability. Do not invent a fallback voice ID.

2. Run the tested happy path

The programs request voices, reject an empty list, select TtsModel.Nebula, synthesize the text Hello from HUMAIN Voice, derive the model sample rate, write speech.wav, and close the client.

tts-to-wav.ts
import { writeFile } from 'node:fs/promises';

import {
  TTSClient,
  TtsModel,
  getSampleRate,
} from '@humain-voice/sdk';

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

function pcm16ToWav(pcm: Uint8Array, sampleRate: number): Buffer {
  const header = Buffer.alloc(44);
  header.write('RIFF', 0);
  header.writeUInt32LE(36 + pcm.byteLength, 4);
  header.write('WAVE', 8);
  header.write('fmt ', 12);
  header.writeUInt32LE(16, 16);
  header.writeUInt16LE(1, 20); // Linear PCM.
  header.writeUInt16LE(1, 22); // Mono.
  header.writeUInt32LE(sampleRate, 24);
  header.writeUInt32LE(sampleRate * 2, 28);
  header.writeUInt16LE(2, 32);
  header.writeUInt16LE(16, 34);
  header.write('data', 36);
  header.writeUInt32LE(pcm.byteLength, 40);

  return Buffer.concat([
    header,
    Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength),
  ]);
}

async function main(): Promise<void> {
  const outputPath = process.argv[2] ?? 'speech.wav';
  const client = new TTSClient({
    api_url: requiredEnv('API_URL'),
    api_path: requiredEnv('API_PATH'),
    api_key: requiredEnv('API_KEY'),
  });

  try {
    const voices = await client.listVoices({ timeoutSeconds: 5 });
    const voice = voices.find((candidate) => candidate.profile) ?? voices[0];
    if (!voice) throw new Error('No TTS voices are available');
    if (voice.profile) {
      console.log(
        'profile:',
        voice.label,
        voice.profile.speaker.dialect,
        voice.profile.languages,
      );
    }

    const model = TtsModel.Nebula;
    const pcm = await client.synthesize('Hello from HUMAIN Voice', {
      voice_id: voice.id,
      model,
      // This is an inactivity timeout applied while awaiting each audio chunk.
      timeoutSeconds: 30,
      onError: (error) => console.error('server error:', error.code, error.message),
    });

    await writeFile(outputPath, pcm16ToWav(pcm, getSampleRate(model)));
  } finally {
    await client.close();
  }
}

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

A successful run has these observable outcomes:

  1. Voice discovery returns at least one entry for this request.
  2. Buffered synthesis receives the final audio chunk without a recorded server error or inactivity timeout.
  3. speech.wav starts with a valid RIFF/WAVE header followed by all returned PCM bytes and can be opened by a WAV-capable player.
  4. The JavaScript client closes in finally; Python exits its async client context before writing the artifact.

If voice discovery returns no entries, the programs fail clearly and do not create a misleading silent artifact.

3. Package raw PCM as WAV

The SDK return value is audio data, not a ready-made media file:

LayerValue used by the fixtures
SDK audioRaw signed PCM16 little-endian, 24 kHz, mono
Sample width16 bits, or 2 bytes
WAV header44-byte RIFF/WAVE header with PCM format, channel count, sample rate, byte rate, block alignment, and data length
WAV bodyEvery PCM byte returned after synthesis reaches its final chunk

Both programs obtain the sample rate through getSampleRate(model) / get_sample_rate(model) instead of treating a container header as part of the SDK response.

The JavaScript helper accepts the released Uint8Array directly and preserves its byteOffset and byteLength when creating a Node.js Buffer. Python uses the standard-library wave module to write the same metadata.

The 24 kHz PCM contract applies to Socket.IO TTS and these SDK clients. Read the current operation contract before packaging bytes returned by another transport.

4. Choose buffered or streaming synthesis

ModeAPIResult and responsibility
Bufferedsynthesize()Returns one Uint8Array in JavaScript or bytes in Python after the SDK has collected audio through the final chunk. Simple for bounded files, but holds the complete PCM result in memory.
StreamingsynthesizeStream() / synthesize_stream()Yields responses with id, audio, and is_last; process audio earlier, track total bytes, and finalize a valid container only after the final response.

For either mode, the SDK has already removed the 17-byte Socket.IO TTS frame header. Write each response's audio bytes, not the original framed payload. An arbitrary network or iterator boundary is not completion; is_last=true marks the final audio response.

Buffered synthesis can also receive chunks through onAudio / on_audio, but the buffered call itself completes only after final audio is collected. If a stream ends without a final chunk, do not publish the partial file as a complete artifact.

5. Bound inactivity and preserve error state

RuntimeVoice-list timeoutSynthesis timeout
JavaScript5-second defaulttimeoutSeconds defaults to 30 seconds of inactivity
PythonNo defaultNo default; always pass timeout_seconds

The fixtures explicitly use five seconds for voice discovery and 30 seconds for synthesis. The synthesis value is an inactivity timeout while waiting for the next audio chunk, not an overall workflow deadline. Add a separate app deadline for connection, discovery, synthesis, file writing, and cleanup.

The service independently enforces a non-resetting 25-second overall synthesis deadline and a 60-second inactivity watchdog. If the overall deadline wins before final audio, it emits retryable TTS_DEADLINE_EXCEEDED; audio already received remains partial.

TTS exposes two different error surfaces:

SurfaceInformation retained
onError / on_error callbackNormalized ErrorResponse; structured payloads can retain id, code, retryable, timestamp, retry_after_seconds, data, reason, retry_scope, and message. A legacy non-object payload becomes a message.
Rejected synthesisGeneric JavaScript Error or Python RuntimeError containing the message only

Record structured callback fields before cleanup. Do not infer retryability by parsing the generic rejection message, and do not claim a retry is safe when the callback does not provide enough state.

6. Clean up and publish atomically

Close the client even when voice discovery is empty, synthesis fails, the final chunk never arrives, or file writing fails. The JavaScript fixture calls client.close() in finally; the Python fixture uses async with to release Socket.IO and its underlying HTTP resources.

Cleanup closes the client connection, which cancels active synthesis requests owned by that connection and suppresses their later audio and error events. It does not close inference connections shared with other requests.

For a production file workflow, write to a temporary destination and expose the artifact only after final audio, a complete WAV header/body, and a successful file close. Cleanup is still required if publishing the file fails.

Next steps

On this page