Speech Server

รันไทม์แบบเนทีฟสำหรับ Apple · PocketTTS

PocketTTS เพิ่มการสังเคราะห์เสียง CoreML แบบเนทีฟให้ speech-swift ทุกขั้นตอนใช้ CPU เท่านั้นโดยค่าเริ่มต้น เสียง Alba ภาษาอังกฤษส่งเฟรมขนาด 1,920 ตัวอย่างที่ 24 kHz หรือ 80 มิลลิวินาทีต่อเฟรม

การใช้ Swift และ CLI

ใช้ 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.pcm

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

แฟล็กบรรทัดคำสั่ง

แฟล็กค่าเริ่มต้นคำอธิบาย
--host127.0.0.1ที่อยู่ bind เปลี่ยนเป็น 0.0.0.0 เพื่อเปิดให้เข้าถึงผ่าน LAN
--port8080พอร์ต 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

ซอร์สโค้ด

การจัดการโมเดลแบบเลือกใช้

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

อ่านคู่มือ Voxtral TTS

การเติมพรอมป์ต์ล่วงหน้าของ 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 เดิม