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 địnhMô tả
--host127.0.0.1Địa chỉ bind. Đổi sang 0.0.0.0 để mở ra trên LAN.
--port8080Cổng TCP.
--preloadtắtTả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

Mã nguồn