1. Pipeline Architecture
Pocket-S2S is an ultra-low latency, real-time Speech-to-Speech API built entirely without proprietary real-time APIs. The pipeline processes voice inputs and streams back synthesized audio through four tightly coupled, asynchronous layers:
Voice Activity Detection
Adaptive energy-based VAD with dynamic noise floor. Triggers transcription after 600ms of silence.
Groq Whisper Large V3
Transcribes completed audio segments in ~100ms via Groq LPUs, with domain-biased vocabulary prompting.
NVIDIA Nemotron-Mini
Streams LLM tokens shaped by active persona system prompt. TTFT ~200ms.
Kyutai Pocket-TTS
Generates quantized 24kHz PCM audio per clause. Time to first audio ~300ms.
2. WebSocket Connection
Clients establish a single, persistent bidirectional WebSocket connection. Both raw binary audio frames and JSON text frames are multiplexed over the same connection.
ws://<your-server-host>/s2s
/voices
/personas
3. Audio Specifications
To eliminate container overhead (MP3, WAV headers, WebM), raw numerical arrays are sent directly as binary WebSocket frames.
Client → Server (Microphone)
| Format | Raw Float32 PCM (IEEE Float) |
|---|---|
| Sample Rate | 16,000 Hz |
| Channels | 1 (Mono) |
| Chunk Size | 2048 samples (~128ms) |
Server → Client (Synthesized Speech)
| Format | Raw Int16 PCM (Signed Short) |
|---|---|
| Sample Rate | 24,000 Hz |
| Channels | 1 (Mono) |
| Chunk Size | Variable (per TTS output frame) |
4. Voice Selection
Send a config message over the WebSocket at any time to switch the TTS voice for the active session.
Voices are pre-cached via Pocket-TTS's internal LRU cache, so switching is near-instant after first use.
List voices via HTTP
GET /voices
// Example response:
{
"voices": [
{ "id": "cosette", "name": "Cosette", "language": "en", "style": "default", "description": "Neutral, clear English voice (default)" },
{ "id": "alba", "name": "Alba", "language": "en", "style": "casual", "description": "Casual, warm English voice" },
{ "id": "jean", "name": "Jean", "language": "en", "style": "deep", "description": "Deep, authoritative English voice" },
{ "id": "marius", "name": "Marius", "language": "en", "style": "young", "description": "Young, energetic English voice" },
{ "id": "vera", "name": "Vera", "language": "en", "style": "narrator", "description": "Smooth narrator-style English voice" },
{ "id": "giovanni", "name": "Giovanni", "language": "it", "style": "default", "description": "Italian-accented voice" },
{ "id": "lola", "name": "Lola", "language": "es", "style": "default", "description": "Spanish-accented voice" },
{ "id": "juergen", "name": "Juergen", "language": "de", "style": "default", "description": "German-accented voice" },
{ "id": "rafael", "name": "Rafael", "language": "pt", "style": "default", "description": "Portuguese-accented voice" },
{ "id": "estelle", "name": "Estelle", "language": "fr", "style": "default", "description": "French-accented voice" }
],
"default": "cosette",
"total": 10
}
Switch voice over WebSocket
// Send at any time — takes effect immediately for the current session
ws.send(JSON.stringify({ type: "config", voice: "alba" }))
// Server confirms with:
{ "type": "status", "message": "Session updated: voice=alba" }
Language hint (improves STT accuracy)
When using a non-English voice, also set the language field to bias Whisper toward the correct language:
ws.send(JSON.stringify({ type: "config", voice: "lola", language: "es" }))
5. Persona Fine-Tuning
Pocket-S2S supports system-prompt injection to customize the AI's behavior without any model retraining. You can select one of four built-in personas, or supply your own custom system prompt. This shapes tone, vocabulary, response style, and domain knowledge used by the Nemotron LLM.
Built-in Persona Presets
Friendly, extremely concise general-purpose assistant. Best for quick Q&A and information retrieval.
ws.send(JSON.stringify({ type: "config", persona: "assistant" }))
Polite, empathetic, and solution-focused. Acknowledges the user's concern and guides them toward a resolution. Ideal for business deployments.
ws.send(JSON.stringify({ type: "config", persona: "customer_service" }))
Action-oriented and efficient. Responds with clarity about what it will do. Best for tool-use flows, automation, and task completion.
ws.send(JSON.stringify({ type: "config", persona: "agent" }))
Calm, warm, and reflective. Draws on universal themes of peace, compassion, and mindfulness. Ideal for wellness and meditation apps.
ws.send(JSON.stringify({ type: "config", persona: "spiritual" }))
Custom System Prompt
Supply any system prompt to create fully custom AI behavior. Use persona: "custom" alongside a system_prompt string:
ws.send(JSON.stringify({
type: "config",
persona: "custom",
system_prompt: "You are a witty pirate captain named Captain Rex. Respond to all questions with nautical metaphors and dramatic flair, in 1-2 sentences."
}))
// Server confirms:
{ "type": "status", "message": "Session updated: persona=custom" }
Combine voice + persona + language in one message
// Configure a Spanish customer service agent with Lola's voice
ws.send(JSON.stringify({
type: "config",
voice: "lola",
language: "es",
persona: "customer_service"
}))
List personas via HTTP
GET /personas // Returns the active built-in personas and their system prompts
6. Message Schemas
In addition to raw binary audio chunks, the server and client exchange JSON text frames:
Client → Server Messages (JSON Text Frames)
// All fields are optional — send only what you want to change
{
"type": "config",
"voice": "alba", // optional: voice id (see /voices)
"persona": "customer_service", // optional: preset or "custom"
"system_prompt": "...", // required only when persona="custom"
"language": "en" // optional: ISO-639-1 code for STT hint
}
Server → Client Messages (JSON Text Frames)
{ "type": "status", "message": "Listening..." | "Transcribing..." | "Thinking..." | "Session updated: ..." }
{ "type": "transcription", "text": "What is the capital of France?" }
{ "type": "llm_text", "text": "Paris is the capital of France." }
{ "type": "done" }
{ "type": "error", "message": "Unknown voice 'xyz'. Available: cosette, alba, ..." }
Binary Frames (Audio Data)
7. Full JavaScript Example
Complete boilerplate to connect, capture microphone input, stream audio, configure a persona, and play back synthesized speech:
// ─────────────────────────────────────────────────────────────
// 1. Queue-based player for streaming 16-bit PCM (24kHz)
// ─────────────────────────────────────────────────────────────
class QueueAudioPlayer {
constructor() {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.sampleRate = 24000;
this.nextStartTime = 0;
}
playChunk(int16Array) {
const float32 = new Float32Array(int16Array.length);
for (let i = 0; i < int16Array.length; i++) {
float32[i] = int16Array[i] / 32768.0;
}
const buffer = this.audioCtx.createBuffer(1, float32.length, this.sampleRate);
buffer.getChannelData(0).set(float32);
const source = this.audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(this.audioCtx.destination);
const now = this.audioCtx.currentTime;
const playTime = Math.max(now, this.nextStartTime);
source.start(playTime);
this.nextStartTime = playTime + buffer.duration;
}
}
// ─────────────────────────────────────────────────────────────
// 2. Connect and configure the S2S session
// ─────────────────────────────────────────────────────────────
async function startSpeechToSpeech() {
const player = new QueueAudioPlayer();
const ws = new WebSocket("ws://localhost:8000/s2s");
ws.binaryType = "arraybuffer";
ws.onopen = () => {
// Configure voice + persona on connection
// All fields are optional — omit any you don't need
ws.send(JSON.stringify({
type: "config",
voice: "alba", // see GET /voices for full list
persona: "customer_service" // or "assistant", "agent", "spiritual"
}));
// --- OR use a fully custom system prompt ---
// ws.send(JSON.stringify({
// type: "config",
// voice: "jean",
// persona: "custom",
// system_prompt: "You are a no-nonsense financial advisor. Answer in exactly one sentence."
// }));
};
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
// Binary frame: synthesized audio chunk (Int16 PCM 24kHz)
player.playChunk(new Int16Array(event.data));
} else {
// Text frame: status, transcription, llm_text, done, or error
const msg = JSON.parse(event.data);
if (msg.type === "transcription") console.log("You said:", msg.text);
if (msg.type === "llm_text") console.log("Assistant:", msg.text);
if (msg.type === "error") console.warn("S2S Error:", msg.message);
if (msg.type === "done") console.log("--- Turn complete ---");
}
};
// ─────────────────────────────────────────────────────────
// 3. Capture microphone and stream Float32 PCM at 16kHz
// ─────────────────────────────────────────────────────────
const stream = await navigator.mediaDevices.getUserMedia({
audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true }
});
const micCtx = new AudioContext({ sampleRate: 16000 });
const source = micCtx.createMediaStreamSource(stream);
const processor = micCtx.createScriptProcessor(2048, 1, 1);
processor.onaudioprocess = (e) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(e.inputBuffer.getChannelData(0).buffer); // Float32Array
}
};
source.connect(processor);
processor.connect(micCtx.destination);
}
startSpeechToSpeech();
8. Standalone Text-to-Speech (REST API)
If you only need to synthesize text into speech without the full speech-to-speech loop, Pocket-S2S exposes standalone GET and POST REST endpoints. Both endpoints return a standard audio/wav byte stream (24kHz, 16-bit, mono PCM) that can be played or saved directly.
GET Request (Direct Audio URLs)
Useful for direct browser embedding in standard HTML5 audio elements.
/tts?text=YOUR_TEXT_HERE&voice=VOICE_ID
text(Required): The text to synthesize.voice(Optional): The voice ID. Defaults tocosette. See Section 4 for available voices.
<!-- Example of direct embedding in HTML --> <audio src="http://localhost:8000/tts?text=Hello+from+pocket+TTS&voice=alba" controls></audio>
POST Request (Programmatic Synthesis)
Best for sending long text or generating files programmatically.
/tts
application/json
// Request JSON Body Schema
{
"text": "The quick brown fox jumps over the lazy dog.",
"voice": "jean" // optional, defaults to "cosette"
}
JavaScript Example (Programmatic Fetch & Play)
async function playStandaloneTTS(text, voice = "cosette") {
const response = await fetch("/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, voice })
});
if (!response.ok) {
const err = await response.json();
console.error("TTS Failed:", err);
return;
}
// Get audio binary data (WAV)
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
// Play immediately
const audio = new Audio(audioUrl);
audio.play();
}
// Usage:
// playStandaloneTTS("Welcome to the standalone voice synthesizer.", "vera");