21 KiB
speech-to-speech — Descripción del Proyecto
Paquete:
Vocero speech-to-speechVersión:0.2.11Autor: Alessandro Valenti Licencia: Apache-2.0 Python: 3.10 – 3.12 Lema: Pipeline Speech-to-Speech de baja latencia end-to-end para construir agentes de voz en tiempo real.
1. ¿Qué es Vocero speech-to-speech?
Vocero speech-to-speech es una pipeline de audio modular y de baja latencia que convierte la entrada hablada del usuario en una respuesta hablada encadenando cuatro etapas de IA:
VAD → STT → LLM → TTS.
De fábrica expone un endpoint WebSocket compatible con el protocolo OpenAI Realtime (ws://host:port/v1/realtime),
por lo que cualquier navegador o SDK que sepa hablar el protocolo Realtime de OpenAI puede conectarse y empezar a tener
conversaciones naturales en cuestión de segundos — sin escribir una sola línea de código en el servidor.
La pipeline está diseñada para ser agnóstica del backend en cada etapa: puedes intercambiar distintos modelos de STT / LLM / TTS según el hardware disponible (NVIDIA CUDA, Apple Silicon MLX o solo CPU), los idiomas de tus usuarios y el compromiso entre latencia y calidad que necesites.
Un subsistema opcional integrado de RAG (Retrieval Augmented Generation) del lado servidor permite que las respuestas del LLM se basen en tu propia base de conocimientos privada (documentos Markdown / TXT / JSONL), mientras permanece 100 % transparente para el cliente. Las actualizaciones dinámicas de la base de conocimientos se exponen en el mismo servidor HTTP mediante una API REST de 11 endpoints.
2. Características principales
| Característica | Descripción |
|---|---|
| Protocolo de voz en tiempo real | Soporte nativo del WebSocket OpenAI v1/realtime: session.update, response.create, llamadas a funciones, deltas de audio, interrupciones. Compatible directamente con el SDK Realtime oficial y con los "playgrounds" web. |
| 4 modos de ejecución | local (micrófono → altavoces), socket (IPC TCP), websocket (WS audio crudo), realtime (protocolo OpenAI — predeterminado). |
| 6 backends de STT intercambiables | Whisper / Whisper-MLX / MLX-Audio-Whisper / Faster-Whisper / Parakeet TDT (predeterminado) / Paraformer. |
| 4 backends de LLM intercambiables | transformers (local) · mlx-lm (Apple Silicon) · responses-api (endpoint OpenAI con tool-calling) · chat-completions (cualquier servidor /v1/chat/completions compatible con OpenAI). |
| 5 backends de TTS intercambiables | ChatTTS · Facebook MMS · Pocket (muy pequeño, CPU) · Kokoro · Qwen3-TTS (predeterminado, voz personalizada de 1.7B). |
| Multiplataforma | NVIDIA CUDA (Linux), Apple Silicon MLX + MPS (macOS), fallback CPU en cada etapa. |
| Pool de N pipelines aisladas | --num_pipelines N ejecuta N sesiones independientes en paralelo (cada una con sus propios VAD / STT / LLM / TTS y estado de conversación). Ideal para despliegues pequeños con múltiples conexiones simultáneas. |
| VAD e interrupción integrados | Detección de actividad vocal con umbral configurable; finalización del STT por silencio; el LLM se puede interrumpir mientras habla y se cancela de forma limpia mediante un CancelScope. |
| Detección automática de idioma | A través de lingua-language-detector para el idioma de la respuesta del asistente. |
| Transcripción parcial en vivo | Parakeet-TDT emite transcripciones parciales cada 500 ms para que los clientes muestren el texto "el usuario está hablando…". |
| Soporte completo de tool-calling | Con el backend responses-api: entrega en streaming de argumentos de función, tool calls paralelos, voces personalizadas sobre TTS — todo conforme a la superficie de la Responses API de OpenAI. |
| Inyección RAG opcional en el servidor | Hook transparente de recuperación en cada turno del LLM. Embeddings con Sentence-Transformers, persistencia del índice NPZ, top-k / umbral coseno configurables, inyección como mensaje del sistema o del usuario. Multilingüe por defecto (español / italiano / inglés listos para usar). |
| API REST para base de conocimientos dinámica | 11 endpoints para listar, buscar, hacer upsert, actualizar, eliminar y recargar contenidos de la KB en tiempo real — con persistencia entre reinicios mediante kb/_dynamic.jsonl. |
| Configuración estructurada CLI / JSON | Cada parámetro es un argumento @dataclass de HfArgumentParser con valores predeterminados razonables, o un fichero JSON de configuración completo que puedes pasar como único argumento. |
| Empaquetado y flujo de publicación PyPI | Configurado uv build + twine check + GitHub Actions (la etiqueta vX.Y.Z activa la subida). |
3. Arquitectura a vista de pájaro
Un turno único de conversación en modo realtime se ve así:
┌─────────────────────────────────────────────────────────┐
│ uvicorn + FastAPI │
│ ┌───────────────────────────────────────────────────┐ │
Mic / Navegador ──► WS │ │ RealtimeService (sesión + rutas + eventos) │ │
(audio entradas + │ │ └───────────────────────────────────────────────────┘ │
eventos) │ │ │
│ │ Pool de pipelines ─► PipelineUnit #1 ─► PipelineUnit #N│
│ └─────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────┐
│ Una unidad de pipeline (una por usuario) │
│ │
│ 1. VAD ──► inicio / fin voz │
│ 2. STT (5 sabores) ──► último texto usuario│
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ HOOK BÚSQUEDA RAG │ ◄─── kb/_index.npz
│ │ (inyecta solo si ≥ N) │ + md/txt/jsonl
│ └────────────────────────┘ + _dynamic.jsonl
│ │ │
│ ▼ │
│ 3. LLM (4 sabores) ──► texto + tool calls│
│ │ │
│ ▼ │
│ 4. TTS (5 sabores) ──► PCM audio streaming│
└──────────────────────────────────────────────┘
│
▼
◄── WS audio / delta events
Invariantes clave:
- Cada conexión = una
PipelineUnit(asignada de forma atómica; colas y estado aislados; si las N unidades están ocupadas, la conexión N+1 se rechaza). - La etapa LLM nunca ve audio crudo. La pipeline solo envía buffers de texto al manejador del LLM, por lo que cambiar de proveedor o de modelo sigue siendo transparente.
- La etapa RAG es un efecto lateral puro sobre el búfer de texto: lee el texto más reciente del usuario, ejecuta un embedding + top-k por coseno + filtro por umbral, y antepone el conocimiento relevante como un mensaje adicional del sistema (o del usuario) — con una línea de registro clara para ver siempre exactamente qué se inyectó.
- TTS en streaming (todos los backends modernos): los deltas de audio se emiten a medida que se sintetizan, por lo que el primer byte de una respuesta llega al altavoz mucho antes de que el LLM termine de generar el texto.
4. Modos de ejecución (--mode)
Configurables mediante ModuleArguments.mode
(predeterminado: realtime).
| Modo | Entrada | Salida | Ideal para |
|---|---|---|---|
local |
Micrófono local vía sounddevice / miniaudio |
Altavoces locales | Demos de escritorio, prototipado rápido en portátil. |
socket |
Chunks PCM crudos en socket TCP | PCM crudos por socket TCP | Sistemas legacy / embebidos, transporte personalizado. |
websocket |
PCM crudo sobre endpoint WS | PCM crudo sobre WS | Frontend a medida mínimo que solo envía audio. |
realtime |
WS con protocolo OpenAI Realtime (/v1/realtime) |
Mismo protocolo + API REST RAG en el mismo servidor HTTP | Producción e integración con SDK. Todos los clientes que soportan la Realtime API (OpenAI SDK, playgrounds web, wrappers Swift / Kotlin / JS) se conectan aquí. |
5. Backends soportados
5.1 STT — Speech to Text
Nombre --stt |
Familia de modelos | Modelo predeterminado | Ventajas hardware |
|---|---|---|---|
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 |
Whisper cuantizada CTranslate2 | tiny.en |
CPU con latencia muy baja |
parakeet-tdt |
(predeterminado) HuggingFace Parakeet TDT | parakeet-tdt-1.1b |
Streaming amigable + transcripción parcial en vivo sobre CUDA |
paraformer |
Alibaba Paraformer | paraformer-zh |
Despliegues solo chino |
5.2 LLM — Modelo de lenguaje
--llm_backend |
Descripción | Modelo predeterminado |
|---|---|---|
transformers |
Generación local HF Transformers en proceso | Qwen/Qwen3-4B-Instruct-2507 |
mlx-lm |
Inferencia local cuantizada 4-bit / 8-bit en Apple Silicon | mlx-community/... |
responses-api |
(predeterminado) Endpoint remoto compatible con OpenAI Responses API (superficie completa de tool-calling + argumentos de función en streaming) | gpt-5.4-mini |
chat-completions |
Cualquier remoto /v1/chat/completions compatible con OpenAI — enchufa vLLM, TGI, Ollama, servidor llama.cpp, SGLang, TabbyAPI, etc. |
(auto-detecta vía base_url + /v1/models) |
5.3 TTS — Text to Speech
Nombre --tts |
Backend | Modelo predeterminado | Puntos fuertes |
|---|---|---|---|
chatTTS |
ChatTTS (grupo opcional chattts) |
— | Muy conversacional, inglés + chino, prosodia muy expresiva |
facebookMMS |
Facebook MMS (grupo opcional facebook-mms) |
facebook/mms-tts-eng |
Ultraligero, cubre más de 1.100 idiomas |
pocket |
PocketTTS (pequeño CPU) | — | Sin dependencias en dispositivo, ideal para embebidos / poca RAM |
kokoro |
Kokoro TTS (grupo opcional kokoro) |
— | Estado del arte de TTS neural en inglés y japonés |
qwen3 |
(predeterminado) Qwen3-TTS vía GGML faster-qwen3-tts |
Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice |
Multilingüe, soporta clonación por voz de referencia, tasa de token 12 Hz → latencia ultrabaja. |
Todos los backends de TTS se acceden a través de una interfaz de streaming común, por lo que el resto de la pipeline es agnóstica al backend.
6. RAG del lado servidor (complemento opcional)
El subsistema RAG está completamente documentado en:
- 🌍 Guía lógica IT/EN: RAG_SERVER_SIDE.md
- 🇪🇸 Traducción al español: RAG_SERVER_SIDE.es.md
En resumen:
$ ./start_pipeline_rag.sh # arranca pipeline + API REST RAG
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'
Puntos clave:
- Cero cambios en el cliente — la inyección ocurre en el servidor, dentro del búfer de chat del LLM, antes de cada turno.
- Modelos de embedding intercambiables con un modelo multilingüe predeterminado (
paraphrase-multilingual-MiniLM-L12-v2, 384 dims) y alternativas recomendadas para español-only y para bases de conocimientos muy grandes. - Persistencia del índice NPZ — se reconstruye solo si el contenido cambió. Soporte de
--rag_force_rebuild. - Tres formatos de fuente: Markdown / TXT con chunking recursivo automático (tamaño + solapamiento configurables), o JSONL para chunks artesanales.
- Comportamiento de recuperación configurable:
--rag_top_k,--rag_threshold,--rag_inject_as (system/user),--rag_language (es/it/en). - API REST dinámica thread-safe (11 endpoints):
/status,/sources,/chunks,/chunks/list,/chunks/update,/upsert/document,/search,/add/document,/add/chunks,/remove,/reload. - Persistencia entre reinicios: las llamadas a la API con
persist=truese adjuntan atómicamente akb/_dynamic.jsonly se re-indexan automáticamente en el siguiente arranque. - Patrón CRUD idempotente:
/upsert/documentgestiona crear / reemplazar / eliminar (text="") de forma atómica para que los clientes solo necesiten una URL. - Falla con elegancia: cualquier excepción de recuperación se registra pero nunca rompe el turno de conversación.
7. Arranque rápido
7.1 Instalación (instalación editable estilo PyPI)
git clone https://github.com/huggingface/speech-to-speech.git
cd speech-to-speech
# Paquete base (incluye STT parakeet-tdt + TTS qwen3 por defecto)
uv pip install -e .
# Opcional: añade el subsistema RAG para recuperación en la KB
uv pip install -e ".[rag]"
# Opcional: selecciona los extras de TTS / STT que quieras
uv pip install -e ".[rag,chattts,kokoro,faster-whisper]"
7.2 Arrancar en modo Realtime (predeterminado) con un LLM externo
Esta es la configuración más común: el LLM corre en un servidor remoto compatible con OpenAI
(p.ej. http://127.0.0.1:8001/v1 con vLLM o TGI), STT + TTS corren localmente sobre CUDA / MLX.
Crea un pequeño wrapper de shell (véase start_pipeline.sh para la plantilla completa):
#!/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"
Ejecútalo y después conecta cualquier cliente Realtime de OpenAI a:
ws://<host>:12345/v1/realtime
7.3 Misma pipeline + RAG activado
Añade estos flags (véase §6 para la referencia completa y el script auxiliar start_pipeline_rag.sh):
--rag_enabled \
--rag_kb_path ./kb \
--rag_top_k 3 \
--rag_threshold 0.25 \
--rag_language es \
--rag_inject_as system
La API REST de RAG aparece inmediatamente en el mismo servidor HTTP:
curl http://127.0.0.1:12345/v1/rag/status | jq
8. Estructura del proyecto (puntos destacados)
speech-to-speech/
├── pyproject.toml ← metadatos del paquete + grupos de deps opcionales
├── LICENSE ← Apache-2.0
├── start_pipeline.sh ← script de arranque de referencia
├── start_pipeline_rag.sh ← script de arranque con RAG activado
│
├── src/speech_to_speech/
│ ├── s2s_pipeline.py ← punto de entrada (main), constructor de pipeline, pool realtime
│ ├── baseHandler.py
│ ├── chat.py
│ ├── pipeline/ ← tipos de cola, CancelScope, tipos de handler
│ │
│ ├── arguments_classes/ ← args HfArgumentParser @dataclass (uno por handler)
│ │ ├── module_arguments.py ← mode / stt / tts / llm_backend / num_pipelines
│ │ ├── rag_arguments.py ← 12 flags específicos de RAG
│ │ └── ... (Whisper, Qwen3, ChatTTS, VAD, …)
│ │
│ ├── STT/ ← seis manejadores STT
│ ├── TTS/ ← cinco manejadores TTS
│ ├── LLM/
│ │ ├── base_openai_compatible_language_model.py ← Hook de inyección RAG + tool-calling completo
│ │ ├── chat_completions_language_model.py
│ │ ├── responses_api_language_model.py
│ │ └── language_model.py ← manejadores locales transformers / mlx-lm
│ │
│ ├── RAG/
│ │ ├── retriever.py ← singleton, embeddings, NPZ, search, CRUD completo
│ │ └── router.py ← 11 endpoints FastAPI /v1/rag
│ │
│ └── api/openai_realtime/
│ ├── websocket_router.py ← FastAPI + Realtime + montaje condicional RAG
│ ├── service.py ← Bucle de eventos Realtime + rutas de sesión / handlers
│ └── ...
│
├── kb/
│ ├── 01_faq_producto.md ← ejemplo FAQ en español (incluido en scaffold)
│ ├── 02_politicas_internas.md ← ejemplo políticas en español
│ ├── README.md ← guía de formatos
│ └── _dynamic.jsonl ← chunks añadidos por API (auto-generado)
│
└── docs/
├── RAG_SERVER_SIDE.md ← Guía RAG en profundidad IT/EN (más de 600 líneas)
└── RAG_SERVER_SIDE.es.md ← Traducción al español
9. Versionado y publicación
El repositorio trae una pipeline completa de lanzamiento a PyPI en
.github/workflows/publish.yml:
- Sube la
versionen pyproject.toml y__version__en src/speech_to_speech/init.py. - Fusiona una PR de release que contenga solo esos dos cambios.
- Etiqueta y sube:
git checkout main && git pull origin main git tag -a vX.Y.Z -m "Release vX.Y.Z" git push origin vX.Y.Z - El workflow ejecuta automáticamente
uv build+twine check --strict+ subida a PyPI.
Consulta AGENTS.md para las reglas de release a nivel de repositorio.
10. Siguientes pasos
- Empieza con los scripts de arranque: start_pipeline.sh (base) y start_pipeline_rag.sh (con RAG).
- Profundiza en el subsistema RAG: RAG_SERVER_SIDE.md / RAG_SERVER_SIDE.es.md.
- Ajusta los manejadores por etapa leyendo las clases de argumentos en arguments_classes — cada flag tiene su ayuda en línea.
- Construye tu propio cliente Realtime apoyándote en service.py y websocket_router.py.