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:
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:
| Configuration | Output sample rate |
|---|---|
| Chatterbox | 24,000 Hz, or modelConfig.outputSampleRate when set |
| Supertonic | 44,100 Hz, or modelConfig.outputSampleRate when set |
| Parler | 44,100 Hz, or modelConfig.outputSampleRate when native streaming is disabled; native streaming requires 44,100 Hz |
| LavaSR enhancer enabled | 48,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,
},
});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)For model constants, see SDK — Models.
LavaSR
With either Chatterbox or Supertonic, 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:
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:
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:
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);
}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:
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:
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:
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.