New: VisionPsy-Nano, a 460M vision model that outperforms models twice its size.
QVAC Logo

Text-to-Speech

Speech synthesis for text-to-speech (TTS) — i.e., generate audio using custom voices from written input.

Overview

Text-to-Speech uses @qvac/tts-ggml (GGML) as the inference engine. Load any supported model using modelType: "tts". Then, provide text as input (with inputType: "text") to generate speech audio.

textToSpeech() returns an object containing buffer and, when streaming is enabled, a bufferStream for incremental audio output.

Functions

Use the following sequence of function calls:

  1. loadModel()
  2. textToSpeech()
  3. unloadModel()

For how to use each function, see SDK — API reference.

Audio output

The SDK returns raw, mono, signed 16-bit PCM samples as a plain number[]. With stream: false, the buffer promise resolves to the complete sample array and bufferStream is empty. With stream: true, buffer resolves to [] and bufferStream yields individual samples as they become available. The data has no WAV header or other container metadata; use the example utility to write it as a WAV file.

The response does not include a sample rate. Use the rate implied by the loaded engine and its configuration:

ConfigurationOutput sample rate
Chatterbox24,000 Hz, or modelConfig.outputSampleRate when set
Supertonic44,100 Hz, or modelConfig.outputSampleRate when set
Parler44,100 Hz, or modelConfig.outputSampleRate when native streaming is disabled; native streaming requires 44,100 Hz
CosyVoice324,000 Hz, or modelConfig.outputSampleRate when native streaming is disabled; native streaming requires 24,000 Hz unless the LavaSR enhancer is loaded
Audio844,100 Hz, or modelConfig.outputSampleRate when set
LavaSR enhancer enabled48,000 Hz by default, or modelConfig.outputSampleRate when set

Models

Chatterbox

Chatterbox uses a T3 GGUF as the top-level modelSrc and an S3Gen companion GGUF via modelConfig.s3genModelSrc. Optional referenceAudioSrc supplies a WAV for voice cloning.

await loadModel({
  modelSrc: TTS_T3_TURBO_EN_CHATTERBOX_Q8_0,
  modelType: "tts",
  modelConfig: {
    ttsEngine: "chatterbox",
    language: "en",
    s3genModelSrc: TTS_S3GEN_EN_CHATTERBOX,
  },
});
Omitting ttsEngine defaults to Chatterbox.

Supertonic

Supertonic uses a single GGUF via top-level modelSrc. Set voice, ttsSpeed, and ttsNumInferenceSteps in modelConfig as needed. Multilingual output is selected by the GGUF (e.g. TTS_MULTILINGUAL_SUPERTONIC2_Q8_0) plus language.

await loadModel({
  modelSrc: TTS_EN_SUPERTONIC_Q8_0,
  modelType: "tts",
  modelConfig: {
    ttsEngine: "supertonic",
    language: "en",
    voice: "F1",
  },
});

Parler-TTS

Parler-TTS uses a single GGUF and conditions speech from either a free-text description or structured voice fields. Load-time fields provide defaults; textToSpeech() and textToSpeechStream() can override them per request. The registry includes Mini v1, Large v1, and Indic variants; the Indic model supports 21 languages and script-native digit normalization.

const modelId = await loadModel({
  modelSrc: TTS_MINI_V1_EN_PARLER_TTS_Q8_0,
  modelType: "tts",
  modelConfig: {
    ttsEngine: "parler",
    voice: "Laura",
    seed: 42,
    topK: 1,
  },
});

const result = textToSpeech({
  modelId,
  text: "Welcome to Parler text-to-speech.",
  inputType: "text",
  stream: false,
  emotion: "happy",
  pace: "moderate",
});

const pcm = await result.buffer;

For free-form conditioning, use description or its alias voiceDescription. Otherwise, compose a description from voice, emotion, pitch, pace, expressivity, noise, reverb, and quality.

description and voiceDescription cannot be combined with each other or with the structured voice fields. These per-request fields are Parler-only; using them with Chatterbox or Supertonic returns a request-validation error.

Supported emotions are command, anger, narration, conversation, disgust, fear, happy, neutral, proper noun, news, sad, and surprise.

Parler supports all three SDK streaming surfaces:

  • textToSpeech({ stream: true }) for incremental PCM samples.
  • textToSpeech({ stream: true, sentenceStream: true }) for PCM plus sentence/chunk metadata.
  • textToSpeechStream() when text itself arrives incrementally.

Set streamChunkTokens above zero to enable native chunk streaming; streamFirstChunkTokens only tunes the first chunk and does not enable streaming by itself. Native streaming emits at 44.1 kHz, so omit outputSampleRate or set it to 44100 when streamChunkTokens > 0. Integer generation controls use signed 32-bit values. Parler does not support LavaSR post-processing.

The generated Python client uses the same contract:

from tetherto.qvac_sdk import TextToSpeechRequest, load_model, text_to_speech
from tetherto.qvac_sdk.models import TTS_MINI_V1_EN_PARLER_TTS_Q8_0

model_id = await load_model(
    transport,
    model_src=TTS_MINI_V1_EN_PARLER_TTS_Q8_0,
    model_config={
        "ttsEngine": "parler",
        "voice": "Laura",
        "seed": 42,
        "topK": 1,
    },
)

request = TextToSpeechRequest.model_validate({
    "type": "textToSpeech",
    "modelId": model_id,
    "text": "Welcome to Parler text-to-speech.",
    "inputType": "text",
    "stream": False,
    "emotion": "happy",
})

samples = []
async for response in text_to_speech(transport, request):
    samples.extend(response.buffer)

CosyVoice3

CosyVoice3 uses the LLM GGUF as the top-level modelSrc; its registry companion set auto-downloads the flow and HiFT GGUFs, the baked voice.gguf, and the tokenizer files alongside it. Speech is synthesized with the baked default voice — the engine's reference-audio path is reserved and voice cloning is not yet exposed — and emits native 24 kHz audio.

const modelId = await loadModel({
  modelSrc: TTS_COSYVOICE3_LLM_COSYVOICE_Q8_0,
  modelType: "tts",
  modelConfig: {
    ttsEngine: "cosyvoice3",
    seed: 42,
  },
});

const result = textToSpeech({
  modelId,
  text: "Hey, how are you doing today?",
  inputType: "text",
  stream: false,
  emotion: "happy",
});

const pcm = await result.buffer;

Conditioning uses one of three controls:

  • emotionanger, happy, neutral, or sad.
  • paceslow, moderate, or fast; moderate engages nothing and takes the plain zero-shot path.
  • instruct — a raw instruction string, or a structured object with dialect, volume (loud / soft), or style (peppa / robot), resolved by precedence dialect > volume > style. Supported dialects: cantonese, northeastern, gansu, guizhou, henan, hubei, hunan, jiangxi, minnan, ningxia, shanxi, shaanxi, shandong, shanghai, sichuan, tianjin, and yunnan.

CosyVoice3 accepts one conditioning control per synthesis — an emotion, a non-moderate pace, or an instruct. Combining them is rejected at load and request validation. emotion and pace can also be set per request on textToSpeech(); instruct is load-time only.

Set streamChunkTokens above zero for native chunk streaming (streamFirstChunkTokens tunes the first chunk only). Native streaming emits at 24 kHz, so omit outputSampleRate or set it to 24000 unless the LavaSR enhancer is loaded. useGPU offloads to Metal on macOS/iOS, Vulkan on desktop Linux/Windows, and OpenCL on Adreno Android devices; threads, nGpuLayers, seed, and outputSampleRate (8,000–192,000 Hz) work as on the other engines. CosyVoice3 also supports LavaSR post-processing via lavasrEnhancerModelSrc (48 kHz bandwidth extension) and lavasrDenoiserModelSrc (batch-only; runs before the enhancer).

Audio8

Audio8 uses the DualAR LM GGUF as the top-level modelSrc (TTS_LM_MULTILINGUAL_AUDIO8_Q8_0 or TTS_LM_MULTILINGUAL_AUDIO8_FP16) and a required codec decoder via modelConfig.audio8CodecDecoderModelSrc. The model is multilingual — the language is inferred from the text itself — and emits native 44.1 kHz audio.

const modelId = await loadModel({
  modelSrc: TTS_LM_MULTILINGUAL_AUDIO8_Q8_0,
  modelType: "tts",
  modelConfig: {
    ttsEngine: "audio8",
    audio8CodecDecoderModelSrc: TTS_CODEC_DECODER_AUDIO8_Q8_0,
    temperature: 0.7,
    topP: 0.9,
    seed: 42,
  },
});

const result = textToSpeech({
  modelId,
  text: "Hey, how are you doing today?",
  inputType: "text",
  stream: false,
});

const pcm = await result.buffer;

For zero-shot voice cloning, add the optional codec encoder (audio8CodecEncoderModelSrc, e.g. TTS_CODEC_ENCODER_AUDIO8_Q8_0) and supply referenceAudioSrc (a reference recording) together with referenceText (its exact transcript). The three fields are validated together — a reference without its transcript or without the encoder is rejected at load.

Generation is sampled by default: temperature (default 0.7), topK (50), and topP (0.9) tune it, greedy: true takes the argmax instead and ignores all three, and maxFrames caps generation in codec frames (~21.5 frames per second of audio). seed, threads, nGpuLayers, and outputSampleRate work as on the other engines; useGPU offloads to Metal on macOS/iOS, Vulkan on desktop Linux/Windows, and OpenCL on Android/Adreno — elsewhere it falls back to CPU and sets stats.gpuUnsupported in the response.

Audio8 has no emotion, pace, or description fields (at load or per request), no LavaSR post-processing, and no native chunk streaming. Sentence streaming (sentenceStream: true) and duplex textToSpeechStream() work as with every engine.

For model constants, see SDK — Models.

LavaSR

With Chatterbox, Supertonic, or CosyVoice3, you can choose to perform post-processing using LavaSR models. They are applied to the synthesized audio before it is returned, and each stage is enabled purely by supplying its model source — i.e., there is no separate on/off flag.

  • lavasrDenoiserModelSrc — a denoiser GGUF that cleans the speech (noise reduction). It runs first and is rate-preserving. Constants: TTS_DENOISER_LAVASR_FP16, TTS_DENOISER_LAVASR_FP32.
  • lavasrEnhancerModelSrc — an enhancer GGUF that neurally bandwidth-extends the output to 48 kHz. It runs after the denoiser. Constants: TTS_ENHANCER_LAVASR_FP16, TTS_ENHANCER_LAVASR_FP32.
await loadModel({
  modelSrc: TTS_MULTILINGUAL_SUPERTONIC3_Q8_0,
  modelType: "tts",
  modelConfig: {
    ttsEngine: "supertonic",
    language: "en",
    voice: "F1",
    // Denoiser runs first (rate-preserving)…
    lavasrDenoiserModelSrc: TTS_DENOISER_LAVASR_FP16.src,
    // …then the enhancer bandwidth-extends to 48 kHz.
    lavasrEnhancerModelSrc: TTS_ENHANCER_LAVASR_FP16.src,
  },
});

Examples

Chatterbox

The following script shows an example of Chatterbox TTS with voice cloning from a reference audio file. Use it with utils.js / utils.ts:

tts-chatterbox.js
import { loadModel, textToSpeech, unloadModel, TTS_T3_TURBO_EN_CHATTERBOX_Q8_0, TTS_S3GEN_EN_CHATTERBOX } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// Chatterbox TTS (GGML): voice cloning with optional reference audio.
// Uses registry model constants — downloads automatically from QVAC Registry.
// Usage: node chatterbox.ts [referenceAudioSrc]
const [referenceAudioSrc] = process.argv.slice(2);
const CHATTERBOX_SAMPLE_RATE = 24000;
try {
    const modelId = await loadModel({
        modelSrc: TTS_T3_TURBO_EN_CHATTERBOX_Q8_0,
        modelConfig: {
            ttsEngine: 'chatterbox',
            language: 'en',
            s3genModelSrc: TTS_S3GEN_EN_CHATTERBOX.src,
            streamChunkTokens: 25,
            streamFirstChunkTokens: 10,
            cfmSteps: 1,
            threads: 8,
            ...(referenceAudioSrc ? { referenceAudioSrc } : {})
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing Text-to-Speech...');
    const result = textToSpeech({
        modelId,
        text: `QVAC SDK is the canonical entry point to QVAC. Written in TypeScript, it provides all QVAC capabilities through a unified interface while also abstracting away the complexity of running your application in a JS environment other than Bare. Supported JS environments include Bare, Node.js, Expo and Bun.`,
        inputType: 'text',
        stream: false
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total bytes: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, CHATTERBOX_SAMPLE_RATE, 'tts-output.wav');
    console.log('▸ Audio saved to tts-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, CHATTERBOX_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Supertonic

The following script shows an example of Supertonic TTS for general-purpose speech synthesis. Use it with utils.js / utils.ts:

tts-supertonic.js
import { loadModel, textToSpeech, unloadModel, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// Supertonic 3 TTS (GGML): fast multilingual synthesis with baked-in voices.
// Uses registry model constants — downloads automatically from QVAC Registry.
const SUPERTONIC_SAMPLE_RATE = 44100;
try {
    const modelId = await loadModel({
        modelSrc: TTS_MULTILINGUAL_SUPERTONIC3_Q8_0,
        modelConfig: {
            ttsEngine: 'supertonic',
            language: 'en',
            voice: 'F1',
            ttsSpeed: 1.05,
            ttsNumInferenceSteps: 5
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing Text-to-Speech...');
    const result = textToSpeech({
        modelId,
        text: `QVAC SDK is the canonical entry point to QVAC. Written in TypeScript, it provides all QVAC capabilities through a unified interface while also abstracting away the complexity of running your application in a JS environment other than Bare. Supported JS environments include Bare, Node.js, Expo and Bun.`,
        inputType: 'text',
        stream: false
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total samples: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, SUPERTONIC_SAMPLE_RATE, 'supertonic-output.wav');
    console.log('▸ Audio saved to supertonic-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, SUPERTONIC_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Parler-TTS

The following TypeScript example loads the registry-hosted Parler Mini v1 model, applies per-request emotion conditioning, saves the PCM output, and plays it:

tts-parler.js
import { loadModel, textToSpeech, unloadModel, TTS_MINI_V1_EN_PARLER_TTS_Q8_0 } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// Parler-TTS (GGML): description-conditioned speech with per-call voice controls.
// Uses the registry-hosted Mini v1 Q8_0 model and its native 44.1 kHz output.
const PARLER_SAMPLE_RATE = 44100;
try {
    const modelId = await loadModel({
        modelSrc: TTS_MINI_V1_EN_PARLER_TTS_Q8_0,
        modelConfig: {
            ttsEngine: 'parler',
            voice: 'Laura',
            seed: 42
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing Parler Text-to-Speech...');
    const result = textToSpeech({
        modelId,
        text: 'Hey, how are you doing today?',
        inputType: 'text',
        stream: false,
        emotion: 'happy'
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total samples: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, PARLER_SAMPLE_RATE, 'parler-output.wav');
    console.log('▸ Audio saved to parler-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, PARLER_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

CosyVoice3

The following script loads the registry-hosted CosyVoice3 model (the LLM GGUF downloads its flow/HiFT, voice, and tokenizer companions automatically), applies per-request emotion conditioning, saves the PCM output, and plays it:

tts-cosyvoice3.js
import { loadModel, textToSpeech, unloadModel, TTS_COSYVOICE3_LLM_COSYVOICE_Q8_0 } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// CosyVoice3 (GGML): zero-shot speech with one conditioning control per
// synthesis (emotion, pace, or instruct). The LLM GGUF's registry companion
// set downloads the flow/HiFT models, baked voice and tokenizer alongside it.
// Native output is 24 kHz.
const COSYVOICE3_SAMPLE_RATE = 24000;
try {
    const modelId = await loadModel({
        modelSrc: TTS_COSYVOICE3_LLM_COSYVOICE_Q8_0,
        modelConfig: {
            ttsEngine: 'cosyvoice3',
            seed: 42
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing CosyVoice3 Text-to-Speech...');
    const result = textToSpeech({
        modelId,
        text: 'Hey, how are you doing today?',
        inputType: 'text',
        stream: false,
        emotion: 'happy'
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total samples: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, COSYVOICE3_SAMPLE_RATE, 'cosyvoice3-output.wav');
    console.log('▸ Audio saved to cosyvoice3-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, COSYVOICE3_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Audio8

The following script loads the registry-hosted Audio8 LM together with its codec decoder, synthesizes 44.1 kHz audio with the default sampling settings, saves the PCM output, and plays it:

tts-audio8.js
import { loadModel, textToSpeech, unloadModel, TTS_LM_MULTILINGUAL_AUDIO8_Q8_0, TTS_CODEC_DECODER_AUDIO8_Q8_0 } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// Audio8 (GGML): DualAR LM + codec decoder, native 44.1 kHz output. The
// primary modelSrc is the LM GGUF; the codec decoder loads via modelConfig.
// For zero-shot voice cloning also pass audio8CodecEncoderModelSrc plus
// referenceAudioSrc/referenceText (a recording and its exact transcript).
const AUDIO8_SAMPLE_RATE = 44100;
try {
    const modelId = await loadModel({
        modelSrc: TTS_LM_MULTILINGUAL_AUDIO8_Q8_0,
        modelConfig: {
            ttsEngine: 'audio8',
            audio8CodecDecoderModelSrc: TTS_CODEC_DECODER_AUDIO8_Q8_0,
            temperature: 0.7,
            topP: 0.9,
            seed: 42
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing Audio8 Text-to-Speech...');
    const result = textToSpeech({
        modelId,
        text: 'Hey, how are you doing today?',
        inputType: 'text',
        stream: false
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total samples: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, AUDIO8_SAMPLE_RATE, 'audio8-output.wav');
    console.log('▸ Audio saved to audio8-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, AUDIO8_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Chatterbox with LavaSR enhancer

The following script shows Chatterbox TTS with the LavaSR enhancer, which neurally bandwidth-extends the output to 48 kHz. Use it with utils.js / utils.ts:

tts-chatterbox-enhanced.js
import { loadModel, textToSpeech, unloadModel, TTS_T3_TURBO_EN_CHATTERBOX_Q8_0, TTS_S3GEN_EN_CHATTERBOX, TTS_ENHANCER_LAVASR_FP16 } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// Chatterbox TTS (GGML) with the LavaSR enhancer: synthesized audio is neurally
// bandwidth-extended to 48 kHz. Supplying the enhancer GGUF is what enables
// enhancement — there is no on/off flag — and it forces the output to 48 kHz.
// Usage: node chatterbox-enhanced.ts [referenceAudioSrc]
const [referenceAudioSrc] = process.argv.slice(2);
const ENHANCED_SAMPLE_RATE = 48000;
try {
    const modelId = await loadModel({
        modelSrc: TTS_T3_TURBO_EN_CHATTERBOX_Q8_0,
        modelConfig: {
            ttsEngine: 'chatterbox',
            language: 'en',
            s3genModelSrc: TTS_S3GEN_EN_CHATTERBOX.src,
            lavasrEnhancerModelSrc: TTS_ENHANCER_LAVASR_FP16.src,
            ...(referenceAudioSrc ? { referenceAudioSrc } : {})
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing Text-to-Speech (LavaSR enhancer)...');
    const result = textToSpeech({
        modelId,
        text: `QVAC SDK is the canonical entry point to QVAC. Written in TypeScript, it provides all QVAC capabilities through a unified interface while also abstracting away the complexity of running your application in a JS environment other than Bare. Supported JS environments include Bare, Node.js, Expo and Bun.`,
        inputType: 'text',
        stream: false
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total samples: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, ENHANCED_SAMPLE_RATE, 'chatterbox-enhanced-output.wav');
    console.log('▸ Audio saved to chatterbox-enhanced-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, ENHANCED_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Supertonic with LavaSR denoiser + enhancer

The following script shows Supertonic TTS with the full LavaSR pipeline: the denoiser cleans the signal first, then the enhancer bandwidth-extends it to 48 kHz. Use it with utils.js / utils.ts:

tts-supertonic-enhanced.js
import { loadModel, textToSpeech, unloadModel, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0, TTS_DENOISER_LAVASR_FP16, TTS_ENHANCER_LAVASR_FP16 } from '@qvac/sdk';
import { createWav, playAudio, int16ArrayToBuffer, createWavHeader } from './utils';
// Supertonic 3 TTS (GGML) with LavaSR post-processing: the denoiser cleans the
// synthesized signal first, then the enhancer bandwidth-extends it to 48 kHz.
// Supplying the enhancer GGUF is what enables enhancement — there is no on/off
// flag — and it forces the output to 48 kHz regardless of the engine's native
// rate.
const ENHANCED_SAMPLE_RATE = 48000;
try {
    const modelId = await loadModel({
        modelSrc: TTS_MULTILINGUAL_SUPERTONIC3_Q8_0,
        modelConfig: {
            ttsEngine: 'supertonic',
            language: 'en',
            voice: 'F1',
            // Denoiser runs first (rate-preserving)…
            lavasrDenoiserModelSrc: TTS_DENOISER_LAVASR_FP16.src,
            // …then the enhancer bandwidth-extends to 48 kHz.
            lavasrEnhancerModelSrc: TTS_ENHANCER_LAVASR_FP16.src
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log('▸ Testing Text-to-Speech (LavaSR denoiser + enhancer)...');
    const result = textToSpeech({
        modelId,
        text: `QVAC SDK is the canonical entry point to QVAC. Written in TypeScript, it provides all QVAC capabilities through a unified interface while also abstracting away the complexity of running your application in a JS environment other than Bare. Supported JS environments include Bare, Node.js, Expo and Bun.`,
        inputType: 'text',
        stream: false
    });
    const audioBuffer = await result.buffer;
    console.log(`▸ TTS complete. Total samples: ${audioBuffer.length}`);
    console.log('▸ Saving audio to file...');
    createWav(audioBuffer, ENHANCED_SAMPLE_RATE, 'supertonic-enhanced-output.wav');
    console.log('▸ Audio saved to supertonic-enhanced-output.wav');
    console.log('▸ Playing audio...');
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavBuffer = Buffer.concat([
        createWavHeader(audioData.length, ENHANCED_SAMPLE_RATE),
        audioData
    ]);
    playAudio(wavBuffer);
    console.log('▸ Audio playback complete');
    await unloadModel({ modelId });
    console.log('▸ Model unloaded');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Utils

The following helper script is used by the examples above to convert the raw PCM samples returned by textToSpeech() into a WAV file and play it back:

utils.js
import { writeFileSync, unlinkSync } from 'fs';
import { spawn, spawnSync } from 'child_process';
import { platform, tmpdir } from 'os';
import { join } from 'path';
/**
 * Create WAV header for 16-bit PCM audio
 */
export function createWavHeader(dataLength, sampleRate) {
    const header = Buffer.alloc(44);
    // RIFF header
    header.write('RIFF', 0);
    header.writeUInt32LE(36 + dataLength, 4);
    header.write('WAVE', 8);
    // fmt chunk
    header.write('fmt ', 12);
    header.writeUInt32LE(16, 16); // fmt chunk size
    header.writeUInt16LE(1, 20); // PCM format
    header.writeUInt16LE(1, 22); // mono
    header.writeUInt32LE(sampleRate, 24);
    header.writeUInt32LE(sampleRate * 2, 28); // byte rate
    header.writeUInt16LE(2, 32); // block align
    header.writeUInt16LE(16, 34); // bits per sample
    // data chunk
    header.write('data', 36);
    header.writeUInt32LE(dataLength, 40);
    return header;
}
/**
 * Convert Int16Array to Buffer
 */
export function int16ArrayToBuffer(samples) {
    const buffer = Buffer.alloc(samples.length * 2);
    for (let i = 0; i < samples.length; i++) {
        const value = Math.max(-32768, Math.min(32767, Math.round(samples[i] ?? 0)));
        buffer.writeInt16LE(value, i * 2);
    }
    return buffer;
}
/**
 * Create and save WAV file
 */
export function createWav(audioBuffer, sampleRate, filename) {
    const audioData = int16ArrayToBuffer(audioBuffer);
    const wavHeader = createWavHeader(audioData.length, sampleRate);
    const wavFile = Buffer.concat([wavHeader, audioData]);
    writeFileSync(filename, wavFile);
    console.log(`▸ WAV file saved as: ${filename}`);
}
/**
 * Play a WAV buffer by streaming it into ffplay over stdin.
 *
 * ffplay ships with ffmpeg and is cross-platform (macOS/Linux/Windows), so
 * we avoid the old "write to /tmp then shell out to afplay/aplay/powershell"
 * dance — no temp files, no platform switch, no hardcoded /tmp path (which
 * doesn't exist on Windows). Requires ffplay on PATH.
 */
/**
 * Play one mono s16le PCM chunk (as a minimal WAV) and wait for the player to finish.
 * Chunks are played sequentially when awaited in order — suitable for streaming TTS output.
 */
export function playPcmInt16Chunk(samples, sampleRate) {
    if (samples.length === 0) {
        return Promise.resolve();
    }
    const audioData = int16ArrayToBuffer(samples);
    const wavHeader = createWavHeader(audioData.length, sampleRate);
    const wavFile = Buffer.concat([wavHeader, audioData]);
    // `os.tmpdir()` resolves to the OS-specific temp directory (e.g. `%TEMP%`
    // on Windows), so the Windows branch below no longer tries to read a
    // POSIX-only `/tmp/...` path.
    const tempFile = join(tmpdir(), `qvac-tts-chunk-${Date.now()}-${Math.random().toString(16).slice(2)}.wav`);
    writeFileSync(tempFile, wavFile);
    const currentPlatform = platform();
    let audioPlayer;
    let args;
    switch (currentPlatform) {
        case 'darwin':
            audioPlayer = 'afplay';
            args = [tempFile];
            break;
        case 'linux':
            audioPlayer = 'aplay';
            args = [tempFile];
            break;
        case 'win32':
            audioPlayer = 'powershell';
            args = [
                '-Command',
                `Add-Type -AssemblyName presentationCore; (New-Object Media.SoundPlayer).LoadStream([System.IO.File]::ReadAllBytes('${tempFile}')).PlaySync()`
            ];
            break;
        default:
            audioPlayer = 'aplay';
            args = [tempFile];
    }
    return new Promise(function (resolve, reject) {
        const proc = spawn(audioPlayer, args, { stdio: 'ignore' });
        proc.on('error', function (err) {
            try {
                unlinkSync(tempFile);
            }
            catch {
                // ignore
            }
            reject(err);
        });
        proc.on('close', function (code) {
            try {
                unlinkSync(tempFile);
            }
            catch {
                // ignore
            }
            if (code === 0) {
                resolve();
            }
            else {
                reject(new Error(`Audio player exited with code ${code}`));
            }
        });
    });
}
export function playAudio(audioBuffer) {
    const result = spawnSync('ffplay', ['-hide_banner', '-loglevel', 'error', '-autoexit', '-nodisp', '-i', 'pipe:0'], {
        input: audioBuffer,
        stdio: ['pipe', 'inherit', 'inherit']
    });
    if (result.error) {
        const code = result.error.code;
        if (code === 'ENOENT') {
            throw new Error('ffplay not found on PATH. Install ffmpeg (ffplay ships with it) and retry.');
        }
        throw new Error(`ffplay failed: ${result.error.message}`);
    }
    if (result.status !== 0) {
        throw new Error(`ffplay exited with code ${result.status}`);
    }
}

Tip: all examples throughout this documentation are self-contained and runnable. For instructions on how to run them, see the JS/TS quickstart or the Python quickstart.

On this page

Ask anything about QVAC.