Speech Server

Moteur natif pour Apple · PocketTTS

PocketTTS ajoute la synthèse native CoreML à speech-swift. Toutes les étapes utilisent uniquement le CPU par défaut. La voix anglaise Alba diffuse 1 920 échantillons à 24 kHz par bloc, soit 80 ms.

Utilisation Swift et CLI

Utilisez pocket-tts avec la voix alba. HTTP en flux exige PCM ; WebSocket émet des blocs audio. Les modèles par défaut sont conservés.

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 est un serveur HTTP + WebSocket local qui expose chaque modèle Soniqo via une API REST simple, ainsi qu'un WebSocket compatible avec l'OpenAI Realtime API sur /v1/realtime. Il est livré dans le même paquet Homebrew (bottle) que la CLI speech — brew install speech place les deux dans votre PATH.

Installation et lancement

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)

Options de ligne de commande

OptionPar défautDescription
--host127.0.0.1Adresse d'écoute. Passez à 0.0.0.0 pour exposer le serveur sur le réseau local.
--port8080Port TCP.
--preloaddésactivéCharge tous les modèles dès le démarrage. Démarrage plus lent (~30–60 s) mais aucune latence à la première requête.

Les modèles sont téléchargés à la première utilisation et mis en cache dans ~/Library/Caches/qwen3-speech/. La première requête pour un modèle donné paie le coût du téléchargement + chargement (30 s à 2 min selon la taille du modèle) ; les requêtes suivantes sont servies à chaud.

Endpoints REST

POST /transcribe — Parole vers texte

Accepte soit un corps WAV brut, soit une enveloppe JSON avec l'audio encodé en base64.

# 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"}'

Réponse : {"text": "…", "language": "en", "confidence": 0.93}.

POST /speak — Texte vers parole

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

Le corps de la réponse est un blob WAV. Valeurs prises en charge pour engine : qwen3 (par défaut), cosyvoice, kokoro.

POST /respond — Parole-à-parole

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

Exécute PersonaPlex 7B — parole en entrée, parole en sortie. La transcription est renvoyée dans un en-tête X-Response-Text. Consultez le guide PersonaPlex pour les noms des préréglages de voix.

POST /enhance — Amélioration de la parole

curl -X POST http://127.0.0.1:8080/enhance \
  -H "Content-Type: audio/wav" \
  --data-binary @noisy.wav \
  --output clean.wav

DeepFilterNet3 à 48 kHz. L'entrée est rééchantillonnée si nécessaire.

GET /health — Sonde de vivacité

curl http://127.0.0.1:8080/health
# {"status":"ok"}

Les tests HTTP et WebSocket temps réel attendent que /health renvoie HTTP 200 avec {"status":"ok"}, avec un délai de démarrage de 10 secondes. Les erreurs de démarrage font échouer la préparation du test, et le nettoyage attend l’arrêt du serveur avant de réutiliser son port.

WebSocket : /v1/realtime

Directement compatible avec l'OpenAI Realtime API — le même schéma d'événements JSON (session.update, input_audio_buffer.append, response.create, response.audio.delta, …) avec de l'audio PCM16 encodé en base64 à 24 kHz. Les clients écrits pour le SDK Realtime d'OpenAI fonctionnent avec speech-server sans modifier le code (il suffit de changer l'URL du WebSocket).

Tours automatiques avec le VAD serveur

Le input_audio_buffer.commit manuel reste le comportement par défaut. Définissez session.turn_detection sur server_vad pour exécuter Silero VAD sur le serveur et transcrire automatiquement chaque tour de parole terminé. L’audio de préfixe est conservé, le tampon inactif reste borné, les événements speech_started, speech_stopped et committed sont émis dans l’ordre, et un tour actif en continu est fermé à la limite configurée.

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
    }
  }
}));

Exemple JavaScript

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);
  }
};

Exemple Python

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

Notes de déploiement

Source

Gestion facultative des modèles

SpeechRuntime fournit un chargement partagé, une admission FIFO annulable et une exécution sérialisée. Qwen3ASRRuntime sélectionne MLX ou CoreML complet par descripteurs explicites ; les API natives et options CLI restent disponibles.

Le serveur conserve un permis pendant toute la requête, y compris la sortie en streaming, avec jusqu’à 32 requêtes en attente. Les échecs ASR produisent des erreurs HTTP ou des événements d’erreur WebSocket ; les transcriptions vides valides restent des succès. La lecture conserve les échantillons excédentaires. Les modèles en cache et l’audio en attente occupent toujours de la mémoire.

Voxtral TTS

Voxtral 4B TTS 2603 fonctionne localement sur Apple Silicon via le module VoxtralTTS : 20 voix prédéfinies, neuf langues et une sortie mono à 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

Voix prédéfinies uniquement ; le clonage à partir d’un audio de référence est indisponible. Le SDK, la CLI, WebSocket et HTTP PCM émettent l’audio pendant la génération. Le texte complet doit être fourni au départ. Les poids sont sous licence CC BY-NC 4.0.

Lire le guide Voxtral TTS