Translation pending

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).

Uso de Swift y CLI

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.pcm

speech-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

FlagDefaultDescription
--host127.0.0.1Bind address. Change to 0.0.0.0 to expose on LAN.
--port8080TCP port.
--preloadoffLoad 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

Source

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.wav

Solo 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.

Leer la guía de Voxtral TTS

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.