Paxa Labs

เสียงพูดสดสองทางผ่าน WebSocket

สตรีมสดคือ WebSocket สองทางสำหรับทั้งบทสนทนา ส่งข้อความเข้าเมื่อพร้อม รับเฟรมเสียงกลับต่อเนื่อง พร้อมใบเสร็จค่าบริการบนการเชื่อมต่อเดียวกัน

ดูเป็น Markdown

เอนด์พอยต์สดเปิด WebSocket เดียวค้างไว้ตลอดบทสนทนา: ส่งข้อความเข้ามาได้ทันทีที่มี และรับเฟรมเสียงกลับทันทีที่พร้อม ทั้งสองทิศทางพร้อมกัน เหมาะกับงานที่ข้อความมาทีละส่วน เช่นสตรีมโทเคนจาก LLM หรือประโยคตอบที่กำลังประกอบขึ้น หากข้อความของแต่ละคำขอครบอยู่แล้ว การสตรีมผ่าน HTTP ให้เวลารอเสียงแรกต่ำเท่ากันโดยไม่ต้องดูแลการเชื่อมต่อ

สตรีมผ่าน HTTPการเชื่อมต่อสด
การส่งข้อมูลหนึ่ง POST /v1/tts ต่อหนึ่งคำขอWebSocket เดียวสังเคราะห์ได้หลายรอบ
ข้อความครบทั้งก้อนต่อคำขอทยอยส่งเป็นส่วนๆ ได้
ค่าบริการคิดต่อคำขอ ขั้นต่ำ 0.1 เครดิตต่อครั้งคิดสะสมต่อการเชื่อมต่อ ขั้นต่ำครั้งเดียว
ข้อมูลเวลาtimestamps ต่อคำขอตั้งครั้งเดียวใน start แล้วรับอีเวนต์เวลาทุกรอบ

เปิดเซสชัน#

  1. เชื่อมต่อเปิด WebSocket ไปยังเอนด์พอยต์สดโดยส่งคีย์ API ในเฮดเดอร์ Authorization แบบเดียวกับคำขอ HTTP หากอัปเกรดไม่ผ่าน ระบบตอบเป็น problem+json ตามปกติ
  2. startเฟรมแรกกำหนดค่าการเชื่อมต่อ: model และ voice พร้อม format sample_rate timestamps speed language และ temperature (ถ้าต้องการ) หนึ่งการเชื่อมต่อกำหนดค่าได้ครั้งเดียว แต่ละรอบสังเคราะห์แยกกัน เมื่อ language เป็นค่าเริ่มต้น auto รอบที่มีแต่ตัวเลขจึงอ่านเป็นภาษาไทย ระบุ th หรือ en หากต้องการให้ตัวเลขและรหัสทุกรอบอ่านแบบเดียวกัน
  3. textส่งข้อความเข้ามาทีละส่วนตามที่มี เซิร์ฟเวอร์สะสมไว้และเริ่มสังเคราะห์ตามจังหวะธรรมชาติ: ขึ้นบรรทัดใหม่ เครื่องหมายจบประโยค หรือความยาวถึงเกณฑ์ ภาษาไทยไม่มีเครื่องหมายจบประโยค จึงอาศัยเกณฑ์ความยาวหรือสั่ง flush เมื่อจบประโยคตอบ บนเสียงที่อ่านแท็กอารมณ์หรือแท็กคำกำกับได้ แท็กที่ยังเปิดค้างตรงจุดส่งจะรอจนปิดครบก่อน แท็กที่ยังเปิดค้างเกินเกณฑ์ความยาวจะถูกอ่านเป็นข้อความโดยไม่ถูกตรวจสอบในฐานะแท็ก และแต่ละรอบเริ่มด้วยน้ำเสียงที่รอบก่อนหน้าจบไว้
  4. flushสั่งสังเคราะห์ทุกอย่างที่ค้างอยู่ทันที เช่นเมื่อจบประโยคตอบ flush ตอนบัฟเฟอร์ว่างก็ปลอดภัย ไม่มีผลข้างเคียง
  5. cancelหยุดรอบที่กำลังพูดอยู่และทิ้งข้อความที่ค้างในบัฟเฟอร์ เช่นเมื่อผู้พูดพูดแทรกขึ้นมากลางประโยคตอบ การเชื่อมต่อยังเปิดอยู่พร้อมรับประโยคตอบถัดไป
  6. endบอกว่าไม่มีข้อความเพิ่มแล้ว เซิร์ฟเวอร์สังเคราะห์ส่วนที่เหลือ ส่ง done แล้วปิดการเชื่อมต่อ
// Runs on Bun as-is; on Node, `import WebSocket from "ws"` accepts
// the same constructor options. Browsers cannot send this header, and
// API keys never ship to browsers anyway.
const socket = new WebSocket("wss://api.paxalabs.com/v1/tts/live", {
  headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` },
});
socket.binaryType = "arraybuffer";

socket.onopen = () => {
  socket.send(JSON.stringify({
    type: "start",
    model: "paxa-tts-flash-v2",
    voice: "khanomkrok",
    timestamps: "word",
  }));
  // Send text whenever it becomes available, token by token if you like.
  socket.send(JSON.stringify({ type: "text", text: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs" }));
  socket.send(JSON.stringify({ type: "flush" }));
  socket.send(JSON.stringify({ type: "end" }));
};

socket.onmessage = (event) => {
  if (typeof event.data === "string") {
    const message = JSON.parse(event.data);
    if (message.type === "done") socket.close();
    return;
  }
  player.feed(new Uint8Array(event.data)); // binary frames are audio
};

อีเวนต์จากเซิร์ฟเวอร์#

เฟรมไบนารีคือเสียงในรูปแบบที่เลือกไว้ตอน start ที่เหลือเป็นอีเวนต์ JSON: charged คือใบเสร็จค่าบริการ timing ส่งช่วงเวลาเมื่อเปิดไทม์สแตมป์ error ใช้โค้ดชุดเดียวกับหน้าข้อผิดพลาด flushed ยืนยันว่าบัฟเฟอร์ว่างแล้ว cancelled ยืนยันว่าหยุดรอบที่กำลังพูดตามคำสั่งแล้ว และ done ปิดการสนทนา ข้อมูลอ้างอิง API อธิบายทุกรูปแบบข้อความ ดูการไล่เฟรมของเซสชันสดจริงได้ที่หน้าแปลงข้อความเป็นเสียง

Server events
{"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-tts-flash-v2","voice":"khanomkrok","format":"mp3","sample_rate":44100,"timestamps":"word"}
{"type":"charged","seq":1,"chars":36,"credits":0.45,"total_credits":0.45}
{"type":"timing","seq":1,"spans":[{"text":"สวัสดี","start":0,"end":0.51},{"text":"ครับ","start":0.51,"end":0.85}]}
{"type":"flushed","seq":1}
{"type":"done","total_chars":36,"total_credits":0.45}

หยุดเสียงกลางประโยค#

เฟรม cancel หยุดรอบที่กำลังพูดอยู่และทิ้งข้อความที่ยังค้างในบัฟเฟอร์ เสียงของรอบนั้นจบลง อีเวนต์ cancelled บอกว่าหยุดที่รอบไหน และการเชื่อมต่อยังเปิดอยู่ เฟรม text ถัดไปจึงเริ่มรอบใหม่บนเซสชันเดิมได้เลย นี่คือวิธีรับมือเมื่อผู้พูดพูดแทรกขึ้นมา จะปิดการเชื่อมต่อทิ้งก็ได้ แต่ต้องคืนช่องคำขอพร้อมกันและเสียเวลาอัปเกรดใหม่

รอบที่ถูกหยุดยังคงค่าบริการตามใบเสร็จ charged ที่แจ้งไว้ เพราะข้อความส่วนนั้นสังเคราะห์ไปแล้วตอนที่คำสั่งหยุดมาถึง ส่วนข้อความที่ค้างในบัฟเฟอร์และถูกทิ้งไปพร้อมกันนั้นไม่เคยถูกคิดเงินเลย การส่งทีละประโยคต่อหนึ่งเฟรม text จึงจำกัดต้นทุนของการพูดแทรกไว้ที่ประโยคที่กำลังพูดอยู่ ซึ่งเป็นรูปแบบเดียวกับที่ทำให้เวลารอเสียงแรกต่ำ

Barge-in
// Stop the reply the moment the speaker starts. The sentence being
// spoken ends where it is, whatever was queued behind it is dropped,
// and the same socket carries the next turn.
speech.onSpeechStart = () => {
  socket.send(JSON.stringify({ type: "cancel" }));
  player.discard(); // drop the audio already buffered on the client
};

socket.onmessage = (event) => {
  if (typeof event.data !== "string") {
    player.feed(new Uint8Array(event.data));
    return;
  }
  const message = JSON.parse(event.data);
  if (message.type === "cancelled") {
    // No more audio arrives for message.seq. That leg keeps the charge
    // its receipt reported; the discarded text was never charged.
    return;
  }
  if (message.type === "done") socket.close();
};

ค่าบริการ#

การเชื่อมต่อคิดค่าบริการแบบสะสมในอัตราเดียวกับคำขออื่นทุกคำขอ การสังเคราะห์เสียงคิด 10 เครดิตต่อ 1,000 ตัวอักษร หรือ 1.25 เท่าของอัตรานั้นเมื่อเปิดไทม์สแตมป์ คิดตามจำนวนตัวอักษรจริงและปัดเศษขึ้นถึงหนึ่งในร้อยของเครดิต ขั้นต่ำ 0.1 เครดิตคิดครั้งเดียวต่อการเชื่อมต่อ และคิดครั้งเดียวสำหรับทุกข้อความบนการเชื่อมต่อนั้น ข้อความสั้นหลายชิ้นจึงจ่ายขั้นต่ำเพียงครั้งเดียว

วิธีส่งตัวอักษรที่คิดเงินเครดิต
การเชื่อมต่อสดหนึ่งครั้ง150.15
คำขอ HTTP แยกกัน 3 ครั้ง4, 5, 60.3

อีเวนต์ charged แต่ละครั้งคือใบเสร็จของหนึ่งงวด ยอดรวมของการเชื่อมต่อคือผลรวมของใบเสร็จ และ GET /v1/me สะท้อนยอดสดเสมอ บน WebSocket ไม่มีเฮดเดอร์ x-credits-charged ใบเสร็จทำหน้าที่แทน

จัดการข้อผิดพลาดและปิดการเชื่อมต่อ#

ความล้มเหลวก่อนอัปเกรดเป็นการตอบ HTTP application/problem+json ตามปกติ หลังอัปเกรดแล้ว ความล้มเหลวมาเป็นอีเวนต์ error ที่ code ตรงกับแคตตาล็อกข้อผิดพลาด: กรณีที่กู้คืนได้อย่าง rate_limited หรือ tag_invalid สำหรับเฟรม text ที่มีแท็กซึ่งเสียงอ่านไม่ได้ (บัฟเฟอร์คงข้อความเดิมก่อนเฟรมนั้นไว้) หรือรอบสังเคราะห์ที่ล้มเหลว (provider_error คืนเครดิตให้อัตโนมัติ) การเชื่อมต่อยังอยู่ให้ลองใหม่ ส่วนเครดิตหมดหรือผิดลำดับโปรโตคอล (live_protocol) จะปิดการเชื่อมต่อด้วยโค้ดตามตารางในข้อมูลอ้างอิง การเชื่อมต่อที่ปิดโดยไม่มี done แปลว่าส่งไม่ครบ ให้ถือว่าช่วงท้ายหายไป

หมายเหตุ

การสังเคราะห์แต่ละรอบนับเป็นหนึ่งคำขอต่อลิมิตอัตราคำขอของแพ็กเกจ เท่ากับ POST /v1/tts หนึ่งครั้ง อีเวนต์ rate_limited ไม่ทิ้งบัฟเฟอร์และไม่ปิดการเชื่อมต่อ แต่จะพัก auto-flush ไว้ สั่ง flush หลังครบ retry_after วินาที หรือสั่ง cancel เพื่อทิ้งข้อความในบัฟเฟอร์แล้วเริ่มรอบใหม่ ซึ่งจะกลับมา auto-flush ตามปกติ ตัวการเชื่อมต่อเองถือหนึ่งช่องคำขอพร้อมกันฝั่งเสียงพูดของแพ็กเกจตลอดเวลาที่เปิดอยู่ การอัปเกรดเกินลิมิตถูกปฏิเสธด้วย 429 concurrency_limited จึงควรปิดการเชื่อมต่อที่ไม่ใช้แล้ว