Speech Server
speech-server là một máy chủ HTTP + WebSocket cục bộ, mở mọi mô hình Soniqo qua một REST API đơn giản, kèm theo một WebSocket tương thích OpenAI Realtime API tại /v1/realtime. Nó được đóng gói trong cùng Homebrew bottle với CLI speech — lệnh brew install speech đặt cả hai vào PATH của bạn.
Cài đặt và chạy
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)
Cờ dòng lệnh
| Cờ | Mặc định | Mô tả |
|---|---|---|
--host | 127.0.0.1 | Địa chỉ bind. Đổi sang 0.0.0.0 để mở ra trên LAN. |
--port | 8080 | Cổng TCP. |
--preload | tắt | Tải tất cả mô hình ngay khi khởi động. Boot chậm hơn (~30–60 giây) nhưng request đầu tiên không bị trễ. |
Mô hình được tải xuống ở lần dùng đầu tiên và được cache trong ~/Library/Caches/qwen3-speech/. Request đầu tiên cho một mô hình phải chịu chi phí tải xuống + nạp (30 giây – 2 phút tùy kích thước mô hình); các request tiếp theo đã ấm.
Các endpoint REST
POST /transcribe — Chuyển giọng nói thành văn bản
Chấp nhận hoặc body WAV thô, hoặc một JSON envelope với âm thanh đã mã hóa 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"}'
Phản hồi: {"text": "…", "language": "en", "confidence": 0.93}.
POST /speak — Chuyển văn bản thành giọng nói
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
Body phản hồi là một blob WAV. Các giá trị engine được hỗ trợ: qwen3 (mặc định), cosyvoice, kokoro.
POST /respond — Giọng nói sang giọng nói
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
Chạy PersonaPlex 7B — giọng nói vào, giọng nói ra. Transcript được trả về qua header X-Response-Text. Xem hướng dẫn PersonaPlex để biết tên các voice preset.
POST /enhance — Cải thiện giọng nói
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. Đầu vào được resample nếu cần.
GET /health — Kiểm tra hoạt động
curl http://127.0.0.1:8080/health
# {"status":"ok"}
WebSocket: /v1/realtime
Tương thích trực tiếp với OpenAI Realtime API — cùng lược đồ sự kiện JSON (session.update, input_audio_buffer.append, response.create, response.audio.delta, …) với âm thanh PCM16 mã hóa base64 ở 24 kHz. Các client được viết cho Realtime SDK của OpenAI hoạt động với speech-server mà không cần thay đổi code (chỉ cần đổi URL của WebSocket).
Lượt nói tự động bằng VAD phía máy chủ
input_audio_buffer.commit thủ công vẫn là mặc định. Đặt session.turn_detection thành server_vad để chạy Silero VAD trên máy chủ và tự động chép lời mỗi lượt nói đã kết thúc. Âm thanh tiền tố được giữ lại, bộ đệm khi rảnh được giới hạn, các sự kiện speech_started, speech_stopped và committed được gửi theo thứ tự, còn lượt nói liên tục sẽ bị buộc kết thúc ở giới hạn đã cấu hình.
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
}
}
}));
Ví dụ 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);
}
};
Ví dụ 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())
Ghi chú triển khai
- Không xác thực mặc định.
speech-serverbind vào127.0.0.1và tin tưởng mọi caller. Đặt nó sau một reverse proxy (Caddy, nginx, tailscale) nếu bạn muốn mở ra ngoài localhost. - Mô hình được tải lười. Request đầu tiên cho
/transcribekích hoạt tải xuống + nạp ASR (~700 MB, 30–60 giây). Dùng--preloadđể không có cold start nhưng boot chậm hơn. - Chưa có phiên âm streaming.
POST /transcribeyêu cầu toàn bộ file WAV ngay từ đầu. Dùng/v1/realtimeđể streaming. - Systemd / launchd. Hiện chưa có service unit đi kèm. Một
nohup speech-server &đơn giản hoặc trình giám sát tiến trình quen thuộc của bạn là đủ.
Mã nguồn
- Sources/AudioServer — Router HTTP dựa trên Hummingbird, registry mô hình tải lười, handler WebSocket
/v1/realtime. - Sources/AudioServerCLI — Entry point
@main, argument parser. - Upstream: tham chiếu OpenAI Realtime API.