# speech-to-speech — Project Overview

> **Package**: `speech-to-speech`
> **Version**: `0.2.11`
> **Author**: Hugging Face
> **License**: Apache-2.0
> **Python**: 3.10 – 3.12
> **Tagline**: Low-latency end-to-end Speech-to-Speech pipeline for building realtime voice agents.

---

## 1. What is `speech-to-speech`?

`speech-to-speech` is a fully modular, **low-latency audio pipeline** that turns a user's spoken input into a spoken reply by chaining four AI stages together:
**VAD → STT → LLM → TTS**.

Out of the box it exposes an **OpenAI-compatible Realtime WebSocket endpoint** (`ws://host:port/v1/realtime`),
so any browser or SDK that knows how to speak the OpenAI Realtime Protocol can connect and start having natural voice conversations in seconds — without writing a single line of server code.

The pipeline is designed to be **backend-agnostic at every stage**: you can plug different STT / LLM / TTS models
depending on the available hardware (NVIDIA CUDA, Apple Silicon MLX, or CPU-only), the language(s) of your users,
and the latency / quality trade-off you need.

A built-in, optional **RAG (Retrieval Augmented Generation) server-side subsystem** lets the LLM answers be grounded
in your own private knowledge base (Markdown / TXT / JSONL documents), while staying 100 % transparent for the client.
Dynamic knowledge base updates are exposed over the same HTTP server via a 11-endpoint REST API.

---

## 2. Key Features

| Feature | Description |
|---|---|
| **Real-time voice protocol** | Native OpenAI `v1/realtime` WebSocket support: `session.update`, `response.create`, function calling, audio deltas, interruptions. Drop-in compatible with the official Realtime SDK and browser playgrounds. |
| **4 execution modes** | `local` (mic → speakers), `socket` (TCP IPC), `websocket` (raw audio WS), `realtime` (OpenAI protocol — default). |
| **6 pluggable STT backends** | Whisper / Whisper-MLX / MLX-Audio-Whisper / Faster-Whisper / Parakeet TDT (default) / Paraformer. |
| **4 pluggable LLM backends** | `transformers` (local) · `mlx-lm` (Apple Silicon) · `responses-api` (OpenAI tool-calling aware endpoint) · `chat-completions` (any OpenAI-compatible `/v1/chat/completions` server). |
| **5 pluggable TTS backends** | ChatTTS · Facebook MMS · Pocket (tiny CPU) · Kokoro · Qwen3-TTS (default, custom voice 1.7B). |
| **Multi-hardware ready** | NVIDIA CUDA (Linux), Apple Silicon MLX + MPS (macOS), CPU fallback for every stage. |
| **Pool of N isolated pipelines** | `--num_pipelines N` runs N independent sessions in parallel (each one with its own VAD / STT / LLM / TTS handlers and conversation state). Ideal for small multi-concurrent deployments. |
| **Built-in VAD & interruption** | Voice-activity detection with configurable threshold and streaming silence → STT finalization; the LLM can be interrupted mid-speech by the user and cancelled cleanly through a `CancelScope`. |
| **Automatic language detection** | Via `lingua-language-detector` for the assistant reply language. |
| **Live partial transcription** | Parakeet-TDT emits live partial transcriptions every 500 ms so clients can display "user is speaking …" text. |
| **Full tool-calling support** | With `responses-api` backend: streamed function argument delivery, parallel tool calls, custom voices over TTS — everything compliant with the OpenAI Responses API surface. |
| **Optional RAG server-side injection** | Transparent retrieval hook on every LLM turn. Sentence-Transformers embeddings, NPZ-index persistence, configurable top-k / cosine threshold, system or user-message injection. Multilingual by default (Spanish / Italian / English out of the box). |
| **Dynamic Knowledge Base REST API** | 11 endpoints to list, search, upsert, update, remove, and reload KB contents at runtime — including cross-restart persistence through `kb/_dynamic.jsonl`. |
| **Structured CLI / JSON configuration** | Every parameter is a `@dataclass` HfArgumentParser argument with sane defaults, or a full JSON config file you can pass in as single argument. |
| **PyPI-ready packaging & release flow** | Configured `uv build` + `twine check` + GitHub Actions publish workflow (tag `vX.Y.Z` triggers upload). |

---

## 3. Architecture at a glance

A single conversation turn in **realtime** mode looks like this:

```
                         ┌─────────────────────────────────────────────────────────┐
                         │                    uvicorn + FastAPI                    │
                         │  ┌───────────────────────────────────────────────────┐  │
   Mic / Browser  ──► WS │  │   RealtimeService (session + routing + events)    │  │
   (audio in +       │   │  └───────────────────────────────────────────────────┘  │
    events)          │   │                                                           │
                     │   │  Pipeline pool  ──►  PipelineUnit #1  ──►  PipelineUnit #N│
                     │   └─────────────────────────────────────────────────────────┘
                     │                      │
                     ▼                      ▼
              ┌──────────────────────────────────────────────┐
              │     Single pipeline unit (one per user)       │
              │                                                │
              │   1. VAD            ──►   speech start / end   │
              │   2. STT (5 flavours) ──►   last user text     │
              │        │                                     │
              │        ▼                                     │
              │   ┌────────────────────────┐                  │
              │   │  RAG SEARCH HOOK        │ ◄─── kb/_index.npz
              │   │  (inject only if ≥ N)   │      + md/txt/jsonl
              │   └────────────────────────┘      + _dynamic.jsonl
              │        │                                     │
              │        ▼                                     │
              │   3. LLM (4 flavours) ──►  text reply + tools│
              │        │                                     │
              │        ▼                                     │
              │   4. TTS (5 flavours) ──►  streamed PCM audio│
              └──────────────────────────────────────────────┘
                                                      │
                                                      ▼
                                              ◄── WS audio / delta events
```

Key invariants:

- **Each connection = one `PipelineUnit`** (allocated atomically; queues and state are isolated; if N units are busy, the `N+1`-th connection is rejected).
- **The LLM stage never sees raw audio**. The pipeline only forwards text buffers to the LLM handler, so model vendors and backend switches remain transparent.
- **The RAG stage is a pure side-effect on the text buffer**: it reads the most recent user text, runs an embedding + cosine top-k + threshold filter, and prepends matching knowledge as an extra system (or user) message — with a clear log line so you can always see exactly what was injected.
- **TTS streams** (all modern backends): audio deltas are emitted as they are synthesised, so the first byte of a reply reaches the speaker well before the LLM has finished generating text.

---

## 4. Execution modes (`--mode`)

Configurable via [ModuleArguments.mode](file:///home/azurian/speech-to-speech/src/speech_to_speech/arguments_classes/module_arguments.py#L11-L16)
(default: `realtime`).

| Mode | Input | Output | Best for |
|---|---|---|---|
| `local` | Local microphone via `sounddevice` / `miniaudio` | Local speakers | Desktop demos, quick prototyping on laptop. |
| `socket` | Raw PCM chunks on TCP socket | Raw PCM on TCP socket | Legacy / embedded setups, custom transport. |
| `websocket` | Raw PCM over WS endpoint | Raw PCM over WS | Minimal custom frontend that just pushes audio. |
| `realtime` | OpenAI Realtime Protocol WS (`/v1/realtime`) | Same protocol + RAG REST API on the same HTTP server | **Production & SDK integration.** All clients that support the Realtime API (OpenAI SDK, web playgrounds, Swift / Kotlin / JS wrappers) connect here. |

---

## 5. Supported backends

### 5.1 STT — Speech to Text

| `--stt` name | Model family | Default model | Device highlights |
|---|---|---|---|
| `whisper` | HuggingFace Transformers Whisper | `distil-whisper/distil-large-v3` | CUDA / CPU |
| `whisper-mlx` | MLX Whisper | `mlx-community/whisper-large-v3-turbo` | Apple Silicon (macOS) |
| `mlx-audio-whisper` | Apple `mlx-audio` | `mlx-community/whisper-large-v3-turbo` | Apple Silicon |
| `faster-whisper` | CTranslate2 Quantized Whisper | `tiny.en` | Very low latency CPU |
| `parakeet-tdt` | **(default)** HuggingFace Parakeet TDT | `parakeet-tdt-1.1b` | Streaming-friendly + **live partial transcription** on CUDA |
| `paraformer` | Alibaba Paraformer | `paraformer-zh` | Chinese-only deployments |

### 5.2 LLM — Language Model

| `--llm_backend` | Description | Default model |
|---|---|---|
| `transformers` | Local, process-bound HF Transformers generation | `Qwen/Qwen3-4B-Instruct-2507` |
| `mlx-lm` | Apple Silicon local 4-bit / 8-bit quantised inference | `mlx-community/...` |
| `responses-api` | **(default)** OpenAI Responses-API compatible remote endpoint (full tool-calling surface + streamed function args) | `gpt-5.4-mini` |
| `chat-completions` | Any OpenAI-compatible `/v1/chat/completions` remote — plug in vLLM, TGI, Ollama, llama.cpp server, SGLang, TabbyAPI, etc. | (auto-detects via `base_url + /v1/models`) |

### 5.3 TTS — Text to Speech

| `--tts` name | Backend | Default model | Strengths |
|---|---|---|---|
| `chatTTS` | ChatTTS (optional group `chattts`) | — | Highly conversational, English + Chinese, very expressive prosody |
| `facebookMMS` | Facebook MMS (optional group `facebook-mms`) | `facebook/mms-tts-eng` | Ultra-lightweight, covers 1,100+ languages |
| `pocket` | PocketTTS (small CPU) | — | Zero-dependency on-device, great for embedded / low-RAM |
| `kokoro` | Kokoro TTS (optional group `kokoro`) | — | State of the art English & Japanese neural TTS |
| `qwen3` | **(default)** Qwen3-TTS via `faster-qwen3-tts` GGML | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | Multilingual, supports reference voice cloning, 12 Hz token rate → ultra-low latency. |

All TTS backends are reachable through a common streaming interface so the rest of the pipeline is backend-agnostic.

---

## 6. RAG Server-Side (optional add-on)

The RAG subsystem is fully documented in:

- 🌍 English / Spanish logic: [RAG_SERVER_SIDE.md](file:///home/azurian/speech-to-speech/docs/RAG_SERVER_SIDE.md)
- 🇪🇸 Spanish translation: [RAG_SERVER_SIDE.es.md](file:///home/azurian/speech-to-speech/docs/RAG_SERVER_SIDE.es.md)

At a glance:

```
$ ./start_pipeline_rag.sh                         # starts pipeline + RAG REST API
RAG: Inizializzazione modello embedding=paraphrase-multilingual-MiniLM-L12-v2 device=cuda su kb_path=/home/.../kb
RAG: Índice cargado desde disco: 9 chunk (shape=(9, 384)).
RAG: Activo. kb=... top_k=3 umbral=0.250 inject_as=system idioma=es
RAG HTTP API montata su prefix='/v1/rag'
```

Highlights:

- **Zero client changes** — injection happens server-side in the LLM chat buffer before every turn.
- **Pluggable embedding models** with a default multilingual model (`paraphrase-multilingual-MiniLM-L12-v2`, 384 dims) and recommended alternatives for Spanish-only and very-large-KB scenarios.
- **NPZ index persistence** — rebuilds only if content changed. Supports `--rag_force_rebuild`.
- **Three source formats**: Markdown / TXT with automatic recursive chunking (size + overlap configurable), or JSONL for hand-crafted chunks.
- **Configurable retrieval behaviour**: `--rag_top_k`, `--rag_threshold`, `--rag_inject_as (system/user)`, `--rag_language (es/it/en)`.
- **Thread-safe dynamic REST API** (11 endpoints): `/status`, `/sources`, `/chunks`, `/chunks/list`, `/chunks/update`, `/upsert/document`, `/search`, `/add/document`, `/add/chunks`, `/remove`, `/reload`.
- **Cross-restart persistence**: API calls with `persist=true` are appended atomically to `kb/_dynamic.jsonl` and automatically re-indexed on the next startup.
- **Idempotent CRUD pattern**: `/upsert/document` handles create / replace / delete (`text=""`) atomically so clients only need one URL.
- **Fails gracefully**: any retrieval exception is logged but never breaks the conversation turn.

---

## 7. Quick start

### 7.1 Install (PyPI-style editable install)

```bash
git clone https://github.com/huggingface/speech-to-speech.git
cd speech-to-speech

# Base package (includes default parakeet-tdt STT + qwen3 TTS)
uv pip install -e .

# Optional: add the RAG subsystem for server-side KB retrieval
uv pip install -e ".[rag]"

# Optional: select which TTS / STT extras you want to bring along
uv pip install -e ".[rag,chattts,kokoro,faster-whisper]"
```

### 7.2 Launch in Realtime mode (default) using an external LLM

This is the most common configuration: LLM runs on a remote OpenAI-compatible server
(e.g. `http://127.0.0.1:8001/v1` with vLLM or TGI), STT + TTS run locally on CUDA / MLX.

Create a small shell wrapper (see [start_pipeline.sh](file:///home/azurian/speech-to-speech/start_pipeline.sh) for the full template):

```bash
#!/bin/bash
set -euo pipefail

LLM_BASE_URL="${LLM_BASE_URL:-http://127.0.0.1:8001/v1}"
LLM_MODEL="${LLM_MODEL:-Qwen/Qwen2.5-14B-Instruct-GPTQ-Int4}"
LLM_API_KEY="${LLM_API_KEY:-placeholder}"
TTS_MODEL="${TTS_MODEL:-Qwen/Qwen3-TTS-12Hz-1.7B-Base}"
STT_MODEL="${STT_MODEL:-distil-whisper/distil-large-v3}"

cd /home/azurian/speech-to-speech

uv run speech-to-speech \
  --mode realtime \
  --ws_host 0.0.0.0 \
  --ws_port 12345 \
  --num_pipelines 2 \
  \
  --llm_backend chat-completions \
  --chat_completions_handler_base_url "$LLM_BASE_URL" \
  --chat_completions_handler_model_name "$LLM_MODEL" \
  --chat_completions_handler_api_key "$LLM_API_KEY" \
  \
  --stt whisper \
  --whisper_stt_model_name "$STT_MODEL" \
  \
  --tts qwen3 \
  --qwen3_tts_model_name "$TTS_MODEL"
```

Run it, then connect any OpenAI Realtime client to:

```
ws://<host>:12345/v1/realtime
```

### 7.3 Same pipeline + RAG enabled

Append these flags (see §6 for the full reference and the helper script [start_pipeline_rag.sh](file:///home/azurian/speech-to-speech/start_pipeline_rag.sh)):

```bash
  --rag_enabled \
  --rag_kb_path ./kb \
  --rag_top_k 3 \
  --rag_threshold 0.25 \
  --rag_language es \
  --rag_inject_as system
```

The RAG REST API appears immediately on the same HTTP server:

```bash
curl http://127.0.0.1:12345/v1/rag/status | jq
```

---

## 8. Project layout (highlights)

```
speech-to-speech/
├── pyproject.toml                                     ← package metadata + optional deps groups
├── LICENSE                                            ← Apache-2.0
├── start_pipeline.sh                                  ← reference launch script
├── start_pipeline_rag.sh                              ← launch script + RAG enabled
│
├── src/speech_to_speech/
│   ├── s2s_pipeline.py                                ← entry point (main), pipeline builder, realtime pool
│   ├── baseHandler.py
│   ├── chat.py
│   ├── pipeline/                                      ← queue types, CancelScope, handler types
│   │
│   ├── arguments_classes/                             ← @dataclass HfArgumentParser args (one per handler)
│   │   ├── module_arguments.py                        ← mode / stt / tts / llm_backend / num_pipelines
│   │   ├── rag_arguments.py                           ← 12 RAG-specific flags
│   │   └── ... (Whisper, Qwen3, ChatTTS, VAD, …)
│   │
│   ├── STT/                                           ← six STT handlers
│   ├── TTS/                                           ← five TTS handlers
│   ├── LLM/
│   │   ├── base_openai_compatible_language_model.py   ← RAG injection hook + full tool-calling
│   │   ├── chat_completions_language_model.py
│   │   ├── responses_api_language_model.py
│   │   └── language_model.py                          ← local transformers / mlx-lm handlers
│   │
│   ├── RAG/
│   │   ├── retriever.py                               ← singleton, embeddings, NPZ, search, full CRUD
│   │   └── router.py                                  ← 11 FastAPI /v1/rag endpoints
│   │
│   └── api/openai_realtime/
│       ├── websocket_router.py                        ← FastAPI + Realtime + conditional RAG mount
│       ├── service.py                                 ← Realtime event loop + session/handler routing
│       └── ...
│
├── kb/
│   ├── 01_faq_producto.md                             ← example Spanish FAQ (included in scaffold)
│   ├── 02_politicas_internas.md                       ← example Spanish policies
│   ├── README.md                                      ← format guide
│   └── _dynamic.jsonl                                 ← API-added chunks (auto-generated)
│
└── docs/
    ├── RAG_SERVER_SIDE.md                             ← Italian/English in-depth RAG guide (600+ lines)
    └── RAG_SERVER_SIDE.es.md                          ← Spanish translation
```

---

## 9. Versioning and publishing

The repository ships with a full PyPI release pipeline in
`.github/workflows/publish.yml`:

1. Bump `version` in [pyproject.toml](file:///home/azurian/speech-to-speech/pyproject.toml#L7)
   and `__version__` in [src/speech_to_speech/__init__.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/__init__.py).
2. Merge a release PR containing only those two changes.
3. Tag and push:
   ```bash
   git checkout main && git pull origin main
   git tag -a vX.Y.Z -m "Release vX.Y.Z"
   git push origin vX.Y.Z
   ```
4. The workflow runs `uv build` + `twine check --strict` + PyPI upload automatically.

See [AGENTS.md](file:///home/azurian/speech-to-speech/AGENTS.md) for the repository-level release rules.

---

## 10. Where to go next

- Start with the launch scripts: [start_pipeline.sh](file:///home/azurian/speech-to-speech/start_pipeline.sh) (base) and [start_pipeline_rag.sh](file:///home/azurian/speech-to-speech/start_pipeline_rag.sh) (RAG enabled).
- Dive into the RAG subsystem: [RAG_SERVER_SIDE.md](file:///home/azurian/speech-to-speech/docs/RAG_SERVER_SIDE.md) / [RAG_SERVER_SIDE.es.md](file:///home/azurian/speech-to-speech/docs/RAG_SERVER_SIDE.es.md).
- Tune the per-stage handlers by reading the argument classes in [arguments_classes](file:///home/azurian/speech-to-speech/src/speech_to_speech/arguments_classes) — every flag has inline help.
- Build your own Realtime client against [service.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/api/openai_realtime/service.py) and [websocket_router.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/api/openai_realtime/websocket_router.py).
