Paxa Labs

ถอดเสียงสดภาษาไทย

ถอดเสียงภาษาไทยระหว่างที่ยังพูดอยู่ เปิด WebSocket เดียวส่งเสียงขึ้นไป ระบบหาขอบเขตรอบพูดให้ แล้วส่งบทถอดที่จบแล้วกลับมาทีละรอบ ใช้ได้กับวอยซ์เอเจนต์ คำบรรยายสด และการมอนิเตอร์สายโทรศัพท์

บทถอดเสียงเขียนว่าอย่างไร

ทวนการจอง

spokenขอจองโต๊ะวันเสาร์ที่ สิบห้า สำหรับ สี่ ท่านค่ะ

writtenขอจองโต๊ะวันเสาร์ที่ 15 สำหรับ 4 ท่านค่ะ

หนึ่งรอบพูดจากลูกค้า แบบเขียนใส่วันที่และจำนวนคนเป็นตัวเลข ซึ่งเป็นรูปแบบที่ระบบจองต้องกรอก

เลขพัสดุที่สะกดทีละตัว

spokenพัสดุเลขที่ ที เอช หนึ่ง สอง สาม สี่ ห้า หก ส่งถึงไหนแล้วคะ

writtenพัสดุเลขที่ TH123456 ส่งถึงไหนแล้วคะ

เลขอ้างอิงที่พูดสะกดทีละตัวกลางประโยคไทย บทถอดรอบสุดท้ายมาเป็นข้อความเดียวที่เอาไปค้นต่อได้

ทวนยอดเงิน

spokenยอดที่ต้องชำระคือ หนึ่งพันสองร้อยห้าสิบ บาท ใช่ไหมคะ

writtenยอดที่ต้องชำระคือ 1,250 บาท ใช่ไหมคะ

ลูกค้าทวนตัวเลขกลับมา ตัวเลขลงช่องที่ระบบชำระเงินอ่าน ตั้งแต่รอบพูดที่ลูกค้าพูดมัน

บทถอดทุกคู่บนหน้านี้เป็นตัวอย่างที่เขียนขึ้นเพื่อแสดงว่าตัวเลือกรูปแบบเปลี่ยนอะไร และตรวจกับผลลัพธ์จริงก่อนเผยแพร่ หน้า Speech-to-Text มีบทถอดจริงของโมเดลจากไฟล์เสียงจริง พร้อมวันที่บันทึก

วิธีที่ระบบสดใช้ซ็อกเก็ตนี้

ไฟล์บันทึกรอได้ บทสนทนาที่กำลังเกิดขึ้นรอไม่ได้ เมื่อขั้นตอนถัดไปต้องใช้บทถอดเป็นตัวตั้ง ไม่ว่าจะเป็นวอยซ์เอเจนต์ที่ต้องตัดสินใจว่าจะพูดอะไร หัวหน้าทีมที่เฝ้าดูสายที่กำลังไปผิดทาง หรือคำบรรยายใต้เวทีที่ต้องขึ้นทัน ระยะห่างระหว่างคำพูดกับข้อความคือตัวสินค้าเอง การถอดเสียงสดตัดไฟล์ออกจากตรงกลาง เสียงเดินทางขึ้น WebSocket ทันทีที่ไมโครโฟนหรือสายโทรศัพท์ผลิตออกมา และบทถอดเดินทางกลับมาทีละรอบพูดขณะที่คนยังพูดต่อ สิ่งที่ทำให้ใช้งานได้จริงคือรอบพูด ระบบหาจุดที่ผู้พูดหยุดแล้วส่งประโยคที่จบแล้วออกมา โค้ดปลายทางจึงอ่านประโยคไทยทั้งประโยคโดยเชื่อจุดจบได้ ภาษาไทยทำให้การตัดสินนี้ยากกว่าที่คิด เพราะเขียนติดกันโดยไม่มีช่องว่างระหว่างคำ และสัญญาณว่าความคิดจบแล้วมักเป็นคำลงท้ายมากกว่าเครื่องหมายวรรคตอน

  1. เปิดหนึ่งซ็อกเก็ตต่อหนึ่งบทสนทนา

    ส่งเฟรม start ระบุโมเดล การเข้ารหัสเสียง และแซมเปิลเรต แล้วส่งเฟรมเสียงขึ้นไปเรื่อย ๆ ตามที่บันทึกได้ API key อยู่ที่เซิร์ฟเวอร์ของคุณ ส่วนเบราว์เซอร์ส่งเสียงจากไมโครโฟนผ่านรีเลย์ของคุณ

  2. แสดงข้อความระหว่างทาง ทำงานกับฉบับจบ

    ถ้ามีหน้าจอ ให้วาดบทถอดระหว่างทางไปก่อน แล้วสั่งขั้นตอนถัดไปด้วยบทถอดฉบับจบของแต่ละรอบพูด ซึ่งมีหมายเลขรอบ ข้อความ และตำแหน่งในเสียงมาให้พร้อม

  3. ปิดเซสชันแล้วอ่านยอดรวม

    ส่งเฟรม end เมื่อสายวางแล้ว อีเวนต์ done รายงานเสียงที่รับเข้ามา จำนวนรอบพูด และเครดิตที่การเชื่อมต่อนี้ต้องจ่าย ทั้งการเชื่อมต่อลงในบันทึกคำขอเป็นบรรทัดเดียว

หนึ่งบทสนทนาบนสาย

session
client → {"type":"start","model":"paxa-stt-lite-realtime-v1-preview"}
server ← {"type":"started","connection":"7d9c2b7e"}
client → เฟรมเสียง ตลอดเวลาที่สายยังเปิดอยู่
server ← {"type":"speech_started","turn":1}
server ← {"type":"speech_ended","turn":1}
server ← {"type":"transcript","turn":1,"is_final":true}
client → {"type":"end"}
server ← {"type":"done","turns":12}

การเชื่อมต่อเดียวรับบทสนทนาทั้งบท อีเวนต์ของทุกรอบพูดกลับมาทางซ็อกเก็ตเดียวกัน สลับกับเสียงที่กำลังส่งขึ้นไป

สิ่งที่ฝั่งเซิร์ฟเวอร์ตัดสินให้

16 หรือ 48 kHz

เสียงที่รับเข้า

เฟรม start ระบุการเข้ารหัสเสียงและแซมเปิลเรต PCM แบบ 16 บิตมีเครื่องหมายรับได้ทั้งสองอัตรา ส่วนเสียง mulaw จากสายโทรศัพท์รับที่ 8 kHz จากนั้นเสียงเดินทางเป็นเฟรมไบนารี

2 อีเวนต์

ขอบเขตรอบพูด

เซิร์ฟเวอร์บอกจุดที่ผู้พูดเริ่มและจุดที่หยุด ระบบปลายทางไม่ต้องเฝ้าความเงียบหรือเดาว่าประโยคไทยจบตรงไหน

1 ฉบับจบ

ระหว่างทางก่อน แล้วค่อยฉบับจบ

บทถอดระหว่างทางมาระหว่างที่รอบพูดยังไม่จบ ซึ่งเป็นสิ่งที่คำบรรยายสดต้องใช้เพื่อให้ข้อความขยับตลอด ส่วนฉบับจบมาเมื่อรอบพูดปิด และเป็นข้อความที่ควรเอาไปทำงานต่อ

4 ชั่วโมง

เพดานต่อหนึ่งเซสชัน

การเชื่อมต่อเดียวรับเสียงได้เท่านี้ก่อนจะปิดเซสชันเหมือนที่เฟรม end ทำ ถ้ายาวกว่านั้นให้เปิดการเชื่อมต่อใหม่ ซึ่งจะเริ่มนับหมายเลขรอบพูดใหม่

การหาขอบเขตรอบพูดทำงานอยู่ฝั่งเซิร์ฟเวอร์ ไคลเอนต์มีหน้าที่ส่งเสียงและอ่านอีเวนต์ สัญญาเดียวกันนี้ใช้ได้ทั้งกับรีเลย์ในเบราว์เซอร์ สะพาน SIP และไฟล์ที่เล่นตามจังหวะจริง

หนึ่งรอบพูด จากระหว่างทางถึงฉบับจบ

turn 2
transcript    turn 2   interim   สวัสดีค่ะ
transcript    turn 2   interim   สวัสดีค่ะ อยากสอบถาม
transcript    turn 2   interim   สวัสดีค่ะ อยากสอบถามเรื่องบิล
speech_ended  turn 2
charged       seq 2
transcript    turn 2   final     สวัสดีค่ะ อยากสอบถามเรื่องบิลเดือนนี้ค่ะ

ข้อความระหว่างทางคือร่างของรอบพูดที่ยังไม่จบ และถูกแทนที่ด้วยชุดถัดไป จุดจบของรอบพูดเป็นจุดตัดช่วงคิดเงิน และบทถอดฉบับจบตามหลังการคิดเงินนั้น ส่วนรอบพูดที่โมเดลถอดไม่สำเร็จจะส่งอีเวนต์ error มาแทนฉบับจบ และเสียงของรอบนั้นถูกตัดออกจากยอดเรียกเก็บ

ค่าฟัง

ค่าฟัง
ปริมาณงานเสียงที่รับเข้ามาค่าใช้จ่าย
หนึ่งสาย 5 นาที5 นาที$0.06
100 สาย8.3 ชม.$6.25
รับสาย 1,000 สายประมาณ $62.50

12.5 เครดิตต่อนาทีของเสียงที่รับเข้ามา รวมช่วงเงียบ คำนวณที่ 5 นาทีต่อหนึ่งสายที่รับ รอบพูดที่ถอดไม่สำเร็จไม่มีค่าใช้จ่าย และทั้งเซสชันแสดงเป็นหนึ่งบรรทัดในบันทึกคำขอ

สำหรับนักพัฒนา

ตัวอย่าง TypeScript และ Python เปิดซ็อกเก็ต ส่งเสียงขึ้นไปตามจังหวะจริง แล้วอ่านแต่ละรอบพูดจากบทถอดฉบับจบ ส่วนแท็บที่สามคือสายอีเวนต์ที่หนึ่งรอบพูดสร้างขึ้น

เอกสารอ้างอิง API ฉบับเต็ม
import { open } from "node:fs/promises";

// 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: relay the audio through your
// server.
const socket = new WebSocket("wss://api.paxalabs.com/v1/stt/live", {
  headers: { Authorization: `Bearer ${process.env.PAXA_API_KEY}` },
});

socket.onopen = async () => {
  socket.send(JSON.stringify({
    type: "start",
    model: "paxa-stt-lite-realtime-v1-preview",
    audio: { encoding: "pcm_s16le", sample_rate: 16000 },
    language: "th",
  }));
  // 16 kHz signed 16-bit mono is 32,000 bytes a second. A microphone
  // delivers frames at this pace on its own; a file has to be paced.
  const file = await open("caller.pcm");
  const frame = new Uint8Array(640); // 20 ms
  for (;;) {
    const { bytesRead } = await file.read(frame, 0, frame.length);
    if (bytesRead === 0) break;
    socket.send(frame.subarray(0, bytesRead));
    await new Promise((resolve) => setTimeout(resolve, 20));
  }
  socket.send(JSON.stringify({ type: "end" }));
};

socket.onmessage = (event) => {
  const message = JSON.parse(String(event.data));
  switch (message.type) {
    case "speech_started":
      // The user is talking: stop the agent's audio, cancel its reply.
      break;
    case "transcript":
      if (message.is_final) console.log(`turn ${message.turn}: ${message.text}`);
      break;
    case "done":
      socket.close();
  }
};

โมเดลฟังอะไรบนสายสด

Paxa STT Lite Realtime ถอดภาษาไทยเป็นหลัก และถอดภาษาอื่นตามที่พูด รวมถึงประโยคที่สลับภาษากลางทาง ระบบอ่านขอบเขตรอบพูดจากตัวเสียงเอง โดยไม่ต้องรับสัญญาณจากไคลเอนต์ และยังไม่ประกาศตัวเลขความแม่นยำจนกว่าจะเผยแพร่วิธีวัดควบคู่กันไป

คำถามที่พบบ่อย

เซสชันสดคิดเงินอย่างไร

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

ถ้ารอบพูดหนึ่งล้มเหลวจะเกิดอะไรขึ้น

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

เบราว์เซอร์เปิดซ็อกเก็ตเองได้ไหม

ห้ามใส่ API key ไว้ในเบราว์เซอร์ เสียงจึงต้องผ่านเซิร์ฟเวอร์ของคุณ หน้าเว็บส่งเฟรมเสียงไปยังรีเลย์ที่คุณเป็นเจ้าของ แล้วรีเลย์เป็นฝ่ายถือคีย์และถือซ็อกเก็ต

ถ้ามีไฟล์บันทึกอยู่แล้วต้องใช้เอนด์พอยต์ไหน

ใช้แบบบัฟเฟอร์ POST /v1/stt รับไฟล์ยาวได้ถึง 60 นาที ตอบครั้งเดียวพร้อมบทถอดทั้งหมด และคิดค่าบริการต่อนาทีถูกกว่า เพราะไม่ต้องเดินตามจังหวะของผู้พูด

เริ่มต้นด้วยเครดิตฟรี

เข้าสู่ระบบด้วย Google, GitHub หรือ Hugging Face แล้วใช้เครดิตฟรี 100 เครดิตกับข้อความของคุณเอง