POCKET-S2S DOCS
Open Interactive Client →

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:

01. VAD

Voice Activity Detection

Adaptive energy-based VAD with dynamic noise floor. Triggers transcription after 600ms of silence.

02. STT

Groq Whisper Large V3

Transcribes completed audio segments in ~100ms via Groq LPUs, with domain-biased vocabulary prompting.

03. BRAIN

NVIDIA Nemotron-Mini

Streams LLM tokens shaped by active persona system prompt. TTFT ~200ms.

04. TTS

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 ws://<your-server-host>/s2s
Default port: 8000
GET /voices
Returns available voices JSON
GET /personas
Returns available personas JSON

3. Audio Specifications

To eliminate container overhead (MP3, WAV headers, WebM), raw numerical arrays are sent directly as binary WebSocket frames.

Client → Server (Microphone)

FormatRaw Float32 PCM (IEEE Float)
Sample Rate16,000 Hz
Channels1 (Mono)
Chunk Size2048 samples (~128ms)

Server → Client (Synthesized Speech)

FormatRaw Int16 PCM (Signed Short)
Sample Rate24,000 Hz
Channels1 (Mono)
Chunk SizeVariable (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

🤖 assistant (default)

Friendly, extremely concise general-purpose assistant. Best for quick Q&A and information retrieval.

ws.send(JSON.stringify({ type: "config", persona: "assistant" }))
📞 customer_service

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" }))
⚡ agent

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" }))
🌿 spiritual

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)

Configure Session (voice, persona, language)
// 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)

1. Pipeline status updates
{ "type": "status", "message": "Listening..." | "Transcribing..." | "Thinking..." | "Session updated: ..." }
2. STT transcription result (user speech)
{ "type": "transcription", "text": "What is the capital of France?" }
3. LLM streamed clause (assistant response)
{ "type": "llm_text", "text": "Paris is the capital of France." }
4. Response cycle complete
{ "type": "done" }
5. Error notification
{ "type": "error", "message": "Unknown voice 'xyz'. Available: cosette, alba, ..." }

Binary Frames (Audio Data)

Client → Server
Raw Float32 PCM audio at 16kHz. Send continuously while microphone is active. Server VAD determines segment boundaries.
Server → Client
Raw Int16 PCM audio at 24kHz. Interleaved with JSON messages. Each binary frame is one Pocket-TTS output chunk.

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.

GET /tts?text=YOUR_TEXT_HERE&voice=VOICE_ID
Parameters:
  • text (Required): The text to synthesize.
  • voice (Optional): The voice ID. Defaults to cosette. 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.

POST /tts
Content-Type: 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");