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.
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.pcmspeech-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
| Option | Par défaut | Description |
|---|---|---|
--host | 127.0.0.1 | Adresse d'écoute. Passez à 0.0.0.0 pour exposer le serveur sur le réseau local. |
--port | 8080 | Port TCP. |
--preload | dé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
- Pas d'authentification par défaut.
speech-serverécoute sur127.0.0.1et fait confiance à chaque appelant. Placez-le derrière un proxy inverse (Caddy, nginx, tailscale) si vous l'exposez au-delà de localhost. - Les modèles sont chargés à la demande. La première requête vers
/transcribedéclenche le téléchargement + chargement de l'ASR (~700 Mo, 30–60 s). Utilisez--preloadpour supprimer le démarrage à froid, au prix d'un lancement plus lent. - Pas encore de transcription en streaming.
POST /transcribeattend le fichier WAV complet d'emblée. Utilisez/v1/realtimepour le streaming. - Systemd / launchd. Aucune unité de service n'est fournie à ce jour. Un simple
nohup speech-server &ou votre superviseur de processus habituel fait l'affaire.
Source
- Sources/AudioServer — routeur HTTP basé sur Hummingbird, registre de modèles à chargement différé, gestionnaire WebSocket
/v1/realtime. - Sources/AudioServerCLI — point d'entrée
@main, analyseur d'arguments. - En amont : référence de l'OpenAI Realtime API.
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.wavVoix 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.