Paxa Labs
คู่มือ

ถอดเสียงสดภาษาไทย กับคำถามว่ารอบพูดจบตรงไหน

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

· อ่าน 5 นาที

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

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

รอบพูดคืออะไร#

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

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

turn 2
speech_started  turn 2
transcript      turn 2  interim  ผม
transcript      turn 2  interim  ผมโอนไปแล้ว
transcript      turn 2  interim  ผมโอนไปแล้วเมื่อเช้า
speech_ended    turn 2
transcript      turn 2  final    ผมโอนไปแล้วเมื่อเช้านี้ครับ

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

ทำไมภาษาไทยทำให้ขอบเขตนี้ยาก#

ลักษณะสามข้อของเสียงพูดภาษาไทยขัดกับกฎหาจุดจบที่เขียนขึ้นสำหรับภาษาอังกฤษ

  • ไม่มีเครื่องหมายจบประโยค ประโยคไทยจบเพราะมันจบ และหน้ากระดาษทำเครื่องหมายไว้อย่างมากก็แค่ช่องว่าง กฎที่รอเครื่องหมายปิดจึงรอไปตลอด
  • ประโยคสุภาพปิดด้วยคำ ครับ และ ค่ะ ปิดรอบพูด เช่นเดียวกับ นะคะ และ แล้วครับ คำเหล่านี้มาหลังเนื้อความ แปลว่าส่วนที่มีสาระของประโยคจบก่อนที่ผู้พูดจะจบ
  • การหยุดมักตกที่รอยต่อของวลี ภาษาไทยเว้นวรรคระหว่างวลี และผู้พูดก็หยุดตรงตำแหน่งเดียวกันนั้นกลางประโยค ช่องว่างจึงอยู่ห่างจากจุดจบของความคิดอยู่มาก

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

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

ข้อความระหว่างทางมีไว้ทำอะไร#

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

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

หมายเหตุ

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

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

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

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

ไฟล์บันทึกที่มีอยู่แล้วควรไปที่เอนด์พอยต์แบบบัฟเฟอร์ Paxa STT Lite รับไฟล์ยาวได้ถึง 60 นาที ตอบครั้งเดียวพร้อมบทถอดทั้งหมด และคิดค่าบริการต่อนาทีถูกกว่า เพราะไม่ต้องเดินตามจังหวะของผู้พูด

เปิดซ็อกเก็ต#

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

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();
  }
};
Server events
{"type":"started","connection":"7d9c2b7e-4a41-4c8e-9a35-1f26d8c04b7a","model":"paxa-stt-lite-realtime-v1-preview"}
{"type":"speech_started","turn":1,"time":0.3}
{"type":"speech_ended","turn":1,"time":12.252}
{"type":"charged","seq":1,"seconds":12.252,"credits":2.56,"total_credits":2.56}
{"type":"transcript","turn":1,"is_final":true,"text":"สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ","start":0.3,"end":12.252}
{"type":"done","total_seconds":12.252,"turns":1,"total_credits":2.56}

ขั้นต่อไป#

อ่านต่อ

คู่มืออื่น