La traducción de esta página aún no está disponible. El contenido a continuación está en inglés — referencia: la versión en inglés.
Speech Server
Motor nativo para Apple · PocketTTS
PocketTTS añade síntesis nativa con CoreML a speech-swift. Todas las etapas usan solo CPU por defecto. La voz inglesa Alba transmite bloques de 1.920 muestras a 24 kHz (80 ms por bloque).
Use pocket-tts y la voz alba. HTTP en tiempo real requiere PCM; WebSocket emite fragmentos de audio. Los modelos predeterminados se mantienen.
curl http://localhost:8080/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"model":"pocket-tts","voice":"alba","input":"Hello world.","response_format":"pcm","stream":true}' \
-o hello.pcmspeech-server is a local HTTP + WebSocket server that exposes every Soniqo model through a simple REST API, plus an OpenAI Realtime API-compatible WebSocket at /v1/realtime. It ships in the same Homebrew bottle as the speech CLI — brew install speech drops both on your PATH.
Install and run
brew install speech
speech-server --port 8080
# Starting server on http://127.0.0.1:8080
# Endpoints:
# POST /transcribe - Speech-to-text (WAV body or JSON with audio_base64)
# POST /speak - Text-to-speech (JSON: {text, engine?, language?})
# POST /respond - Speech-to-speech (WAV body, voice/max_steps via query)
# POST /enhance - Speech enhancement (WAV body)
# GET /health - Health check
# WS /v1/realtime - OpenAI Realtime API (JSON events, base64 PCM16 audio)
Command-line flags
| Flag | Default | Description |
|---|---|---|
--host | 127.0.0.1 | Bind address. Change to 0.0.0.0 to expose on LAN. |
--port | 8080 | TCP port. |
--preload | off | Load all models eagerly at startup. Slower boot (~30–60 s) but zero first-request latency. |
Models are downloaded on first use and cached in ~/Library/Caches/qwen3-speech/. The first request for a given model pays the download + load cost (30 s – 2 min depending on model size); subsequent requests are warm.
REST endpoints
POST /transcribe — Speech-to-text
Accepts either a raw WAV body or a JSON envelope with base64-encoded audio.
# WAV body (preferred — lower overhead)
curl -X POST http://127.0.0.1:8080/transcribe \
-H "Content-Type: audio/wav" \
--data-binary @recording.wav
# JSON with base64
curl -X POST http://127.0.0.1:8080/transcribe \
-H "Content-Type: application/json" \
-d '{"audio_base64":"'"$(base64 -i recording.wav)"'","language":"en"}'
Response: {"text": "…", "language": "en", "confidence": 0.93}.
POST /speak — Text-to-speech
curl -X POST http://127.0.0.1:8080/speak \
-H "Content-Type: application/json" \
-d '{"text":"Hello, world!","engine":"kokoro","language":"en"}' \
--output hello.wav
Response body is a WAV blob. Supported engine values: qwen3 (default), cosyvoice, kokoro.
POST /respond — Speech-to-speech
curl -X POST "http://127.0.0.1:8080/respond?voice=en_female_calm&max_steps=256" \
-H "Content-Type: audio/wav" \
--data-binary @question.wav \
--output answer.wav
Runs PersonaPlex 7B — speech in, speech out. Transcript is returned as an X-Response-Text header. See PersonaPlex guide for voice preset names.
POST /enhance — Speech enhancement
curl -X POST http://127.0.0.1:8080/enhance \
-H "Content-Type: audio/wav" \
--data-binary @noisy.wav \
--output clean.wav
DeepFilterNet3 at 48 kHz. Input is resampled if needed.
GET /health — Liveness probe
curl http://127.0.0.1:8080/health
# {"status":"ok"}
Las pruebas HTTP y de WebSocket en tiempo real esperan a que /health devuelva HTTP 200 con {"status":"ok"}, con un plazo de inicio de 10 segundos. Los errores de inicio hacen fallar la preparación de la prueba, y la limpieza espera a que el servidor termine antes de reutilizar su puerto.
WebSocket: /v1/realtime
Drop-in compatible with the OpenAI Realtime API — the same JSON event schema (session.update, input_audio_buffer.append, response.create, response.audio.delta, …) with base64-encoded PCM16 audio at 24 kHz. Clients written against OpenAI's Realtime SDK work against speech-server without code changes (just switch the WebSocket URL).
Turnos automáticos con VAD del servidor
El input_audio_buffer.commit manual sigue siendo el valor predeterminado. Define session.turn_detection como server_vad para ejecutar Silero VAD en el servidor y transcribir automáticamente cada turno de voz completado. Se conserva el audio previo, el búfer inactivo queda acotado, los eventos speech_started, speech_stopped y committed se emiten en orden y un turno continuamente activo se cierra al alcanzar el máximo configurado.
ws.send(JSON.stringify({
type: "session.update",
session: {
turn_detection: {
type: "server_vad",
threshold: 0.5,
prefix_padding_ms: 300,
silence_duration_ms: 500,
max_turn_duration_ms: 120000
}
}
}));
JavaScript example
const ws = new WebSocket("ws://127.0.0.1:8080/v1/realtime");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "session.update",
session: { modalities: ["audio", "text"] }
}));
// Stream PCM16 mono 24kHz audio from the mic:
const audioBase64 = await capturePCM16Chunk();
ws.send(JSON.stringify({
type: "input_audio_buffer.append",
audio: audioBase64
}));
ws.send(JSON.stringify({ type: "response.create" }));
};
ws.onmessage = ev => {
const msg = JSON.parse(ev.data);
if (msg.type === "response.audio.delta") {
playPCM16Base64(msg.delta);
}
};
Python example
import asyncio, base64, json, wave, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8080/v1/realtime") as ws:
await ws.send(json.dumps({"type": "session.update",
"session": {"modalities": ["audio", "text"]}}))
with wave.open("question.wav", "rb") as wav:
pcm16 = wav.readframes(wav.getnframes())
await ws.send(json.dumps({"type": "input_audio_buffer.append",
"audio": base64.b64encode(pcm16).decode()}))
await ws.send(json.dumps({"type": "response.create"}))
async for raw in ws:
msg = json.loads(raw)
if msg["type"] == "response.audio.delta":
open("answer.pcm", "ab").write(base64.b64decode(msg["delta"]))
elif msg["type"] == "response.done":
break
asyncio.run(main())
Deployment notes
- No auth by default.
speech-serverbinds127.0.0.1and trusts every caller. Put it behind a reverse proxy (Caddy, nginx, tailscale) if you expose it beyond localhost. - Models are lazy-loaded. First request for
/transcribetriggers the ASR download + load (~700 MB, 30–60 s). Use--preloadfor zero cold-start but slower boot. - No streaming transcription yet.
POST /transcribeexpects the full WAV up front. Use/v1/realtimefor streaming. - Systemd / launchd. No service unit ships today. A plain
nohup speech-server &or your usual process supervisor works.
Source
- Sources/AudioServer — Hummingbird-based HTTP router, lazy model registry,
/v1/realtimeWebSocket handler. - Sources/AudioServerCLI —
@mainentry point, argument parser. - Upstream: OpenAI Realtime API reference.
Gestión opcional de modelos
SpeechRuntime ofrece carga compartida, admisión FIFO cancelable y ejecución serializada. Qwen3ASRRuntime selecciona MLX o CoreML completo mediante descriptores explícitos; las API nativas y opciones CLI siguen disponibles.
El servidor mantiene un permiso durante toda la solicitud, incluida la salida en streaming, con hasta 32 solicitudes en espera. Los fallos ASR generan errores HTTP o eventos de error WebSocket; las transcripciones vacías válidas siguen siendo correctas. La reproducción conserva las muestras excedentes. Los modelos en caché y el audio pendiente siguen ocupando memoria.
Voxtral TTS
Voxtral 4B TTS 2603 se ejecuta localmente en Apple Silicon mediante el módulo VoxtralTTS: 20 voces predefinidas, nueve idiomas y voz mono a 24 kHz.
curl http://localhost:8080/v1/audio/speech \
-H 'Content-Type: application/json' \
-d '{"model":"voxtral-tts","input":"Hello, welcome to Voxtral.","voice":"casual_male","response_format":"wav"}' \
-o hello.wavSolo voces predefinidas; no se admite clonación con audio de referencia. El SDK, la CLI, WebSocket y HTTP PCM transmiten audio durante la generación. El texto de entrada debe proporcionarse completo al principio. Los pesos usan CC BY-NC 4.0.
Prefill del prompt de Gemma 4 12B
El servicio de inferencia nativo carga el modelo Gemma 4 12B Unified INT4 preparado mediante agentDirectory. Para chat, selecciónelo con el ID gemma4-12b-it.
El prefill de Gemma 4 12B llena el estado KV en lotes de hasta 256 tokens y calcula los primeros logits de muestreo a partir del último token del prompt. Se omiten las proyecciones al vocabulario de las posiciones intermedias. La caché KV de la solicitud se libera al completarse o cancelarse.
Puede conservarse un prefijo de sistema y herramientas sin cambios en bloques completos de 256 tokens. Cada solicitud amplía una copia independiente; los mensajes del usuario, el historial y los resultados de herramientas nunca entran en la instantánea conservada. SPEECH_SWIFT_GEMMA4_PREFIX_CACHE_MB limita los MiB retenidos; 0 desactiva la reutilización. clearPrefixCache() libera la instantánea. HTTP informa los tokens reutilizados en cachedPrefixTokens y la procedencia registra gemma4_12b_prefix_cache_budget_bytes. Las solicitudes en frío siguen procesando el prompt completo.
El historial nativo de herramientas de Gemma mantiene los resultados y la continuación del asistente dentro del mismo turno del modelo. Si después llega un mensaje del usuario, cierra ese turno antes de abrir el siguiente turno del usuario. Envíe cada resultado con su toolCallID original.