Speech Server
รันไทม์แบบเนทีฟสำหรับ Apple · PocketTTS
PocketTTS เพิ่มการสังเคราะห์เสียง CoreML แบบเนทีฟให้ speech-swift ทุกขั้นตอนใช้ CPU เท่านั้นโดยค่าเริ่มต้น เสียง Alba ภาษาอังกฤษส่งเฟรมขนาด 1,920 ตัวอย่างที่ 24 kHz หรือ 80 มิลลิวินาทีต่อเฟรม
ใช้ pocket-tts กับเสียง alba การสตรีม HTTP ต้องใช้ PCM ส่วน WebSocket ส่งชิ้นส่วนเสียง โมเดลเริ่มต้นเดิมยังคงเดิม
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 เป็นเซิร์ฟเวอร์ HTTP + WebSocket แบบ local ที่เปิดโมเดล Soniqo ทุกตัวผ่าน REST API ที่เรียบง่าย พร้อมกับ WebSocket ที่เข้ากันได้กับ OpenAI Realtime API ที่ /v1/realtime มันถูกบรรจุใน Homebrew bottle เดียวกับ CLI speech — คำสั่ง brew install speech จะติดตั้งทั้งสองตัวลงใน PATH ของคุณ
การติดตั้งและการรัน
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)
แฟล็กบรรทัดคำสั่ง
| แฟล็ก | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
--host | 127.0.0.1 | ที่อยู่ bind เปลี่ยนเป็น 0.0.0.0 เพื่อเปิดให้เข้าถึงผ่าน LAN |
--port | 8080 | พอร์ต TCP |
--preload | ปิด | โหลดโมเดลทั้งหมดทันทีตอน startup boot ช้ากว่า (~30–60 วินาที) แต่ request แรกไม่หน่วง |
โมเดลถูกดาวน์โหลดเมื่อใช้ครั้งแรกและ cache ไว้ใน ~/Library/Caches/qwen3-speech/ request แรกสำหรับโมเดลแต่ละตัวต้องเสียค่าใช้จ่ายดาวน์โหลด + โหลด (30 วินาที – 2 นาที ขึ้นอยู่กับขนาดโมเดล) request ถัดไปจะอุ่นแล้ว
REST endpoint
POST /transcribe — เสียงเป็นข้อความ
รับได้ทั้ง WAV body ดิบ หรือ JSON envelope ที่มีเสียงเข้ารหัส 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"}'
การตอบกลับ: {"text": "…", "language": "en", "confidence": 0.93}
POST /speak — ข้อความเป็นเสียง
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 ของการตอบกลับเป็น WAV blob ค่า engine ที่รองรับ: qwen3 (ค่าเริ่มต้น), cosyvoice, kokoro
POST /respond — เสียงเป็นเสียง
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
รัน PersonaPlex 7B — เสียงเข้า เสียงออก Transcript ถูกส่งกลับผ่าน header X-Response-Text ดู คู่มือ PersonaPlex สำหรับชื่อ voice preset
POST /enhance — การปรับปรุงคุณภาพเสียงพูด
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 อินพุตจะถูก resample หากจำเป็น
GET /health — ตรวจสอบสถานะ
curl http://127.0.0.1:8080/health
# {"status":"ok"}
การทดสอบ HTTP และ WebSocket แบบเรียลไทม์จะรอให้ /health ส่ง HTTP 200 พร้อม {"status":"ok"} กลับมา โดยกำหนดเวลารอเริ่มต้นสูงสุด 10 วินาที ข้อผิดพลาดในการเริ่มต้นจะทำให้ขั้นตอนเตรียมการทดสอบล้มเหลว และขั้นตอนเก็บกวาดจะรอให้เซิร์ฟเวอร์หยุดก่อนใช้พอร์ตเดิมอีกครั้ง
WebSocket: /v1/realtime
เข้ากันได้โดยตรงกับ OpenAI Realtime API — schema event JSON เดียวกัน (session.update, input_audio_buffer.append, response.create, response.audio.delta, …) กับเสียง PCM16 เข้ารหัส base64 ที่ 24 kHz client ที่เขียนสำหรับ Realtime SDK ของ OpenAI ทำงานกับ speech-server โดยไม่ต้องเปลี่ยน code (แค่เปลี่ยน URL ของ WebSocket)
แบ่งช่วงสนทนาอัตโนมัติด้วย VAD ฝั่งเซิร์ฟเวอร์
ค่าเริ่มต้นยังคงเป็น input_audio_buffer.commit แบบกำหนดเอง ตั้ง session.turn_detection เป็น server_vad เพื่อเรียกใช้ Silero VAD บนเซิร์ฟเวอร์และถอดเสียงแต่ละช่วงคำพูดที่จบแล้วโดยอัตโนมัติ ระบบเก็บเสียงก่อนเริ่มพูด จำกัด buffer ขณะว่าง ส่ง event speech_started, speech_stopped และ committed ตามลำดับ และบังคับจบช่วงที่มีเสียงต่อเนื่องเมื่อถึงเวลาสูงสุดที่กำหนด
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
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
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())
หมายเหตุการ deploy
- ไม่มีการยืนยันตัวตนตามค่าเริ่มต้น
speech-serverbind ที่127.0.0.1และเชื่อใจ caller ทุกตัว วางไว้หลัง reverse proxy (Caddy, nginx, tailscale) หากคุณต้องการเปิดสู่ภายนอก localhost - โมเดลถูกโหลดแบบ lazy request แรกสำหรับ
/transcribeจะกระตุ้นการดาวน์โหลด + โหลด ASR (~700 MB, 30–60 วินาที) ใช้--preloadเพื่อไม่มี cold start แต่ boot ช้ากว่า - ยังไม่มีการถอดเสียงแบบ streaming
POST /transcribeต้องการไฟล์ WAV ทั้งหมดตั้งแต่ต้น ใช้/v1/realtimeสำหรับ streaming - Systemd / launchd ยังไม่มี service unit แนบมาด้วย
nohup speech-server &ง่าย ๆ หรือตัว supervisor ที่คุ้นเคยก็เพียงพอ
ซอร์สโค้ด
- Sources/AudioServer — Router HTTP ที่ใช้ Hummingbird, registry โมเดลแบบโหลด lazy, handler WebSocket
/v1/realtime - Sources/AudioServerCLI — Entry point
@main, argument parser - Upstream: เอกสารอ้างอิง OpenAI Realtime API
การจัดการโมเดลแบบเลือกใช้
SpeechRuntime รองรับการโหลดร่วมกัน การรับงาน FIFO ที่ยกเลิกได้ และการรันตามลำดับ Qwen3ASRRuntime เลือก MLX หรือ CoreML แบบเต็มผ่านตัวอธิบายที่ชัดเจน โดยยังใช้ API ดั้งเดิมและตัวเลือก CLI ได้
เซิร์ฟเวอร์ถือสิทธิ์รันตลอดคำขอ รวมถึงเอาต์พุตสตรีม และให้รอได้สูงสุด 32 คำขอ ความล้มเหลว ASR ส่งเป็นข้อผิดพลาด HTTP หรือเหตุการณ์ผิดพลาด WebSocket ส่วนผลถอดเสียงว่างที่ถูกต้องยังสำเร็จ การเล่นเสียงเก็บตัวอย่างที่ล้นไว้ โมเดลในแคชและเสียงรอเล่นยังใช้หน่วยความจำ
Voxtral TTS
Voxtral 4B TTS 2603 ทำงานในเครื่องบน Apple Silicon ผ่านโมดูล VoxtralTTS รองรับเสียงที่ตั้งไว้ล่วงหน้า 20 เสียง 9 ภาษา และเสียงโมโน 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รองรับเฉพาะเสียงที่กำหนดไว้ล่วงหน้า ยังไม่รองรับการโคลนจากเสียงอ้างอิง SDK, CLI, WebSocket และ HTTP PCM ส่งเสียงได้ระหว่างการสร้าง ต้องส่งข้อความอินพุตทั้งหมดตั้งแต่เริ่มต้น น้ำหนักโมเดลใช้สัญญาอนุญาต CC BY-NC 4.0
การเติมพรอมป์ต์ล่วงหน้าของ Gemma 4 12B
บริการอนุมานแบบเนทีฟโหลดโมเดล Gemma 4 12B Unified INT4 ที่เตรียมไว้ผ่าน agentDirectory โดยเลือกใช้ในการสนทนาด้วยรหัสโมเดล gemma4-12b-it
ขั้นตอน prefill ของ Gemma 4 12B เติมสถานะ KV เป็นชุดละไม่เกิน 256 โทเคน และคำนวณ logits สำหรับการสุ่มครั้งแรกจากโทเคนสุดท้ายของพรอมป์ต์ โดยข้ามการฉายไปยังคลังคำศัพท์ของตำแหน่งระหว่างกลาง แคช KV ของคำขอจะถูกปล่อยเมื่อคำขอเสร็จสิ้นหรือถูกยกเลิก
สามารถเก็บคำนำหน้าระบบ/เครื่องมือที่ไม่เปลี่ยนแปลงหนึ่งชุดเป็นบล็อกเต็มชุดละ 256 โทเคน แต่ละคำขอขยายสำเนาทำงานแยกกัน ข้อความผู้ใช้ ประวัติสนทนา และผลลัพธ์เครื่องมือจะไม่ถูกเพิ่มในสถานะที่เก็บไว้ SPEECH_SWIFT_GEMMA4_PREFIX_CACHE_MB จำกัดหน่วยความจำเป็น MiB และ 0 ปิดการใช้ซ้ำ clearPrefixCache() ปล่อยสถานะที่เก็บไว้ HTTP รายงานจำนวนโทเคนที่ใช้ซ้ำใน cachedPrefixTokens และข้อมูลแหล่งที่มาบันทึก gemma4_12b_prefix_cache_budget_bytes คำขอแบบเย็นยังคงประมวลผลพรอมป์ต์ทั้งหมด
ประวัติเครื่องมือแบบเนทีฟของ Gemma เก็บผลลัพธ์เครื่องมือและคำตอบต่อเนื่องของผู้ช่วยไว้ในเทิร์นเดียวของโมเดล หากมีข้อความผู้ใช้ตามมา ระบบจะปิดเทิร์นโมเดลนั้นก่อนเปิดเทิร์นผู้ใช้ถัดไป ส่งผลลัพธ์แต่ละรายการพร้อม toolCallID เดิม