ถอดเสียงบันทึกเป็นข้อความ
ปลายทางถอดเสียงแปลงไฟล์บันทึกเสียงภาษาไทยหรือภาษาอื่นเป็นบทถอดตามที่พูดจริง พร้อมเวลาระดับคำเมื่อร้องขอ คิดเครดิตตามความยาวของเสียง ตัดเครดิตก่อนถอดและคืนให้อัตโนมัติเมื่อไม่สำเร็จ
POST /v1/stt อ่านไฟล์บันทึกเสียงแล้วคืนข้อความที่พูด ส่งไฟล์เป็น base64 ในฟิลด์ audio พร้อมรหัสโมเดล paxa-stt-lite-v1-preview บทถอดคืนมาตามที่พูดจริงในภาษาที่พูด ประโยคภาษาไทยที่สลับเป็นอังกฤษกลางทางจะถูกเขียนตามที่พูด ไม่มีการแปล เช่นเดียวกับทุกปลายทาง ระบบตัดเครดิตก่อนประมวลผลและคืนให้อัตโนมัติเมื่อประมวลผลไม่สำเร็จ (เครดิต)
หมายเหตุ
paxa-stt-lite-v1-preview เป็นพรีวิวงานวิจัย คำขอคิดเครดิตตามอัตราที่ประกาศ คุณภาพบทถอดและชุดตัวเลือกอาจเปลี่ยนแปลงได้ในรุ่นถัดไป โดยบันทึกการเปลี่ยนแปลงในหน้าผลิตภัณฑ์ จะระบุทุกครั้ง
# Encode without line wrapping: wrapped base64 breaks the JSON string.
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
# --max-time covers an hour-long recording; curl defaults to no limit.
curl -X POST https://api.paxalabs.com/v1/stt \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\"}"import { readFile } from "node:fs/promises";
const audio = (await readFile("meeting.m4a")).toString("base64");
const response = await fetch("https://api.paxalabs.com/v1/stt", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ audio, model: "paxa-stt-lite-v1-preview" }),
// A long recording can run for minutes; give it room.
signal: AbortSignal.timeout(300_000),
});
if (!response.ok) throw new Error(`Transcription failed: ${response.status}`);
const { text, usage } = await response.json();
console.log(text, `${usage.credits} credits`);import base64
import os
import requests
with open("meeting.m4a", "rb") as file:
audio = base64.b64encode(file.read()).decode()
response = requests.post(
"https://api.paxalabs.com/v1/stt",
headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
json={"audio": audio, "model": "paxa-stt-lite-v1-preview"},
# A long recording can run for minutes; give it room.
timeout=300,
)
response.raise_for_status()
body = response.json()
print(body["text"], body["usage"]["credits"], "credits"){
"text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
"usage": {
"seconds": 11.952,
"credits": 1.66
}
}การคิดเครดิต#
ไฟล์เสียงคิด 8.33 เครดิตต่อนาทีของเสียง หรือ 500 เครดิตต่อชั่วโมง ตามความยาวจริงของเสียง ระบบอ่านความยาวจากตัวไฟล์ก่อนตัดเครดิต จึงรู้ค่าใช้จ่ายก่อนเริ่มฟังเสมอ ทุกคำขอคิดขั้นต่ำ 0.1 เครดิต เวลาระดับคำ ช่วงผู้พูด สไตล์ คำใบ้ภาษา รูปแบบการเขียน และคลังคำศัพท์ไม่คิดเพิ่ม
ยอดที่ตัดรายงานอยู่ใน usage.credits ของเนื้อหาคำตอบเท่านั้น คู่กับ usage.seconds คือความยาวที่คิดเครดิต ปลายทางนี้ไม่ส่งเฮดเดอร์ x-credits-charged คำตอบที่เร็วและช้าจึงมีรูปแบบเดียวกัน เมื่อถอดเสียงไม่สำเร็จหลังตัดเครดิต ระบบคืนเครดิตอัตโนมัติและตอบ 502 provider_error ไฟล์ที่ไม่มีเสียงพูดเลยจะได้บทถอดว่างเปล่าเป็นผลลัพธ์ที่ส่งมอบแล้ว และคิดเครดิตตามความยาว
ไฟล์เสียงและรูปแบบ#
ฟิลด์ audio คือไบต์ของไฟล์ในรูป base64 ระบบอ่านเจ็ดรูปแบบ ได้แก่ MP3, WAV (PCM), FLAC, Ogg (Opus หรือ Vorbis), M4A, AAC (ADTS) และ WebM โดยตรวจรูปแบบและความยาวจากไบต์ของไฟล์เอง ไม่เคยเชื่อฟิลด์ที่ประกาศมา ไฟล์ที่อ่านเป็นเจ็ดรูปแบบนี้ไม่ได้จะตอบ 400 audio_invalid เช่นเดียวกับไฟล์ที่อ่านความยาวจากคอนเทนเนอร์ไม่ได้ เช่น FLAC ที่เขียนโดยไม่มีจำนวนตัวอย่างเสียง ทุกกรณีข้างต้นไม่ตัดเครดิต
base64 ทำให้ขนาดโตขึ้นราวหนึ่งในสาม และไฟล์ที่บีบอัดแล้วเดินทางเร็วกว่า Opus ที่ 24 kbps หรือ MP3 ที่ 64 kbps บรรจุเสียงพูดหนึ่งชั่วโมงได้ภายในขีดจำกัดขนาด ไฟล์ที่บันทึกในเบราว์เซอร์ด้วย MediaRecorder เป็น WebM ซึ่งระบบอ่านความยาวจากบล็อกของไฟล์ได้ จึงไม่ต้องแปลงก่อนส่ง
ขีดจำกัดความยาวและขนาด#
หนึ่งคำขอถอดเสียงได้ไม่เกิน 60 นาที ไฟล์ที่ยาวกว่านั้นตอบ 400 audio_too_long ไฟล์หลังถอดรหัส base64 ต้องไม่เกิน 25 MiB ไฟล์ที่ใหญ่กว่าตอบ 413 audio_too_large การปฏิเสธทั้งสองแบบไม่ตัดเครดิต ไฟล์ที่ยาวกว่านี้ให้ตัดตรงช่วงเงียบแล้วส่งเป็นหลายคำขอ
การแยกผู้พูด มีขีดจำกัดของตัวเองที่สั้นกว่านั้น ไฟล์ยาวตั้งแต่ 9 นาทีขึ้นไปที่ตั้ง diarization เป็น true จะตอบ 400 diarization_audio_too_long โดยไม่ตัดเครดิต ไฟล์เดียวกันถอดเสียงได้ตามปกติเมื่อปิดฟิลด์นี้
ภาษา#
ฟิลด์ language บอกโมเดลว่าควรคาดหวังภาษาใด เป็นแท็ก BCP 47 เช่น "th" หรือ "en" ค่านี้เป็นคำใบ้และไม่กรองสิ่งใด เสียงพูดภาษาอื่นยังถูกถอดตามที่พูด ไฟล์ภาษาไทยที่มีคำและวลีอังกฤษปนอยู่จึงไม่ต้องตั้งค่าใด หากไม่ระบุ โมเดลตรวจจับภาษาเอง ระบุแท็กเมื่อการตรวจจับเลือกภาษาผิดสำหรับไฟล์นั้น
เวลาระดับคำ#
ตั้ง timestamps เป็น "word" แล้วคำตอบจะมีอาร์เรย์ words หนึ่งช่วงต่อหนึ่งคำที่รู้จำได้ พร้อมข้อความและเวลาเริ่มกับสิ้นสุดเป็นวินาทีบนนาฬิกาของไฟล์เอง ระบบตัดคำภาษาไทยให้ ส่วนคำละตินและตัวเลขมาทั้งคำ เครื่องหมายวรรคตอนติดไปกับคำก่อนหน้า หรือคำถัดไปเมื่อเป็นเครื่องหมายเปิด เป็นโครงสร้างช่วงเวลาเดียวกับที่ API เสียงพูด คืน ไฟล์คำบรรยายที่สร้างจากฝั่งหนึ่งจึงใช้กับอีกฝั่งได้พอดี ภาษาไทยไม่เว้นวรรคระหว่างคำ ช่วงเวลาเหล่านี้จึงเป็นตัวบอกว่าคำจบตรงไหน หากต้องการไฟล์คำบรรยาย ฟิลด์ subtitles ตัดให้จากช่วงเวลาชุดนี้ ไฟล์ที่ไม่มีเสียงพูดจะได้อาร์เรย์ว่าง
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"timestamps\": \"word\"}"{
"text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
"words": [
{
"text": "สวัสดี",
"start": 0.12,
"end": 0.44
},
{
"text": "ค่ะ",
"start": 0.52,
"end": 0.66
},
{
"text": "คุณ",
"start": 1.08,
"end": 1.24
},
{
"text": "สมชาย",
"start": 1.32,
"end": 1.88
},
{
"text": "ยอด",
"start": 2.36,
"end": 2.56
},
{
"text": "ชำระ",
"start": 2.6,
"end": 2.879
},
{
"text": "ของ",
"start": 3,
"end": 3.14
},
{
"text": "ท่าน",
"start": 3.2,
"end": 3.359
},
{
"text": "จำนวน",
"start": 3.48,
"end": 3.879
},
{
"text": "พันสองร้อยห้าสิบ",
"start": 4.24,
"end": 5.4
},
{
"text": "บาท",
"start": 5.56,
"end": 5.759
},
{
"text": "ครบ",
"start": 6.32,
"end": 6.52
},
{
"text": "กำหนด",
"start": 6.56,
"end": 6.84
},
{
"text": "วัน",
"start": 6.92,
"end": 7.039
},
{
"text": "ที่",
"start": 7.04,
"end": 7.119
},
{
"text": "สิบห้า",
"start": 7.24,
"end": 7.48
},
{
"text": "สิงหาคม",
"start": 7.68,
"end": 8.26
},
{
"text": "นี้",
"start": 8.36,
"end": 8.5
},
{
"text": "ชำระ",
"start": 9.08,
"end": 9.36
},
{
"text": "ผ่าน",
"start": 9.48,
"end": 9.68
},
{
"text": "แอป",
"start": 9.76,
"end": 9.92
},
{
"text": "ได้",
"start": 10,
"end": 10.14
},
{
"text": "ตลอด",
"start": 10.2,
"end": 10.46
},
{
"text": "ยี่สิบสี่",
"start": 10.56,
"end": 10.96
},
{
"text": "ชั่วโมง",
"start": 11.08,
"end": 11.4
},
{
"text": "ค่ะ",
"start": 11.52,
"end": 11.66
}
],
"usage": {
"seconds": 11.952,
"credits": 1.66
}
}คำบรรยาย#
ตั้ง subtitles เป็น "srt" หรือ "vtt" แล้วคำตอบจะมีสตริง subtitles ซึ่งเป็นเนื้อไฟล์ทั้งไฟล์ พร้อมบันทึกลงดิสก์ได้ทันที SubRip ใส่หมายเลขให้แต่ละช่วงและใช้จุลภาคคั่นมิลลิวินาที ส่วน WebVTT ขึ้นต้นด้วยลายเซ็นของตัวเองและใช้จุด ระบบอ่านเวลาระดับคำให้เอง จึงไม่ต้องตั้ง timestamps และถ้าตั้งด้วยก็จะได้อาร์เรย์ words มาพร้อมไฟล์ ทั้งสองอย่างไม่มีค่าใช้จ่ายเพิ่ม
ช่วงคำบรรยายตัดตรงจุดเปลี่ยนผู้พูด จุดจบประโยค และช่วงเว้นจังหวะ จากนั้นจึงตัดตามความยาวบรรทัด หนึ่งช่วงมีไม่เกินสองบรรทัด บรรทัดละ 60 คอลัมน์ หรือ 32 คอลัมน์ (อักษรจีน 16 ตัว) เมื่อถ้อยคำส่วนใหญ่เป็นภาษาจีน ญี่ปุ่น หรือเกาหลี ค้างบนจออย่างน้อยหนึ่งวินาที และหยุดก่อนช่วงถัดไปเสมอ
ทุกจุดขึ้นบรรทัดใหม่อยู่ตรงขอบคำ ซึ่งเป็นจุดชี้ขาดของภาษาไทย เพราะภาษาไทยไม่เว้นวรรค คำบรรยายที่ตัดตามจำนวนตัวอักษรจึงผ่ากลางคำ ช่วงเวลาระดับคำเป็นตัวบอกว่าคำจบตรงไหน จุดขึ้นบรรทัดจึงไปลงตรงนั้นแทน สระและวรรณยุกต์ไทยที่อยู่บนและล่างพยัญชนะไม่กินความกว้าง ระบบจึงไม่นับด้วย ส่วนอักษรจีน ญี่ปุ่น และเกาหลีนับตัวละสองคอลัมน์ บรรทัดภาษาไทยกับภาษาอังกฤษที่ 60 คอลัมน์จึงเต็มกรอบเท่ากัน
ตั้ง subtitle_line_chars เป็นค่ามาตรฐานของงานคุณได้ ระหว่าง 10 ถึง 120 เมื่อเปิด diarization ด้วย ช่วงคำบรรยายหนึ่งช่วงจะไม่คร่อมการเปลี่ยนผู้พูด และไม่มีการระบุชื่อผู้พูดในไฟล์ ไฟล์ที่ไม่มีเสียงพูดจะได้ไฟล์ SubRip เปล่า หรือไฟล์ WebVTT ที่มีเพียงลายเซ็น
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"subtitles\": \"srt\"}" \
| jq -r .subtitles > meeting.srt{
"text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
"subtitles": "1\n00:00:00,120 --> 00:00:01,000\nสวัสดีค่ะ\n\n2\n00:00:01,080 --> 00:00:02,080\nคุณสมชาย\n\n3\n00:00:02,360 --> 00:00:05,759\nยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาท\n\n4\n00:00:06,320 --> 00:00:08,500\nครบกำหนดวันที่ สิบห้า สิงหาคมนี้\n\n5\n00:00:09,080 --> 00:00:11,660\nชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ\n",
"usage": {
"seconds": 11.952,
"credits": 1.66
}
}แบบพูดหรือแบบเขียน#
ฟิลด์ convention เลือกว่าบทถอดจะเขียนสิ่งที่พูดอย่างไร ค่าเริ่มต้นคือ "spoken" ถ้อยคำเหมือนกันทั้งสองแบบ สิ่งที่ต่างคือตัวเลข หน่วย วันที่ และคำซ้ำ ไฟล์เสียงเบื้องหลังตัวอย่างในหน้านี้พูดถึงยอดเงิน วันที่ และช่วงเวลา:
| สิ่งที่พูด | spoken (ค่าเริ่มต้น) | written |
|---|---|---|
| หนึ่งพันสองร้อยห้าสิบบาท | พันสองร้อยห้าสิบบาท | 1,250บาท |
| วันที่สิบห้าสิงหาคม | สิบห้าสิงหาคม | 15สิงหาคม |
| ยี่สิบสี่ชั่วโมง | ยี่สิบสี่ชั่วโมง | 24ชั่วโมง |
บทถอดทั้งประโยคแบบ "spoken" อ่านว่า สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ ส่วนแบบ "written" อ่านว่า สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ
"spoken" เขียนตัวเลขตามที่ออกเสียง และคำที่พูดสองครั้งเป็นสองคำ เหมาะกับบันทึกคำต่อคำ คำบรรยายที่ต้องตรงกับเสียง หรือข้อความที่จะป้อนให้โมเดลภาษาอ่าน เพราะแบบพูดเก็บสิ่งที่ผู้พูดพูดไว้ครบถ้วน ส่วน "written" คงรูปแบบการแสดงผล คือตัวเลขกับหน่วย และไม้ยมก ๆ เหมาะกับข้อความบอกเขียนที่จะถูกอ่านบนหน้ากระดาษ เช่น บันทึกหรือข้อความ เวลาระดับคำเหมือนกันทั้งสองแบบ
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"convention\": \"written\"}"{
"text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ",
"usage": {
"seconds": 11.952,
"credits": 1.66
}
}คำต่อคำหรือเรียบเรียง#
ฟิลด์ style เลือกว่าบทถอดจะตามเสียงใกล้แค่ไหน ค่าเริ่มต้นคือ "verbatim" คำอุทาน คำพูดติดขัด และการพูดแก้ถูกเขียนตามที่พูด ส่วน "clean" ตัดคำอุทานออกและรวมวลีที่ขาดตอนเป็นวลีเดียว เป็นตัวเลือกเพื่อความอ่านง่าย บทถอดแบบเรียบเรียงห่างจากสิ่งที่พูดมากขึ้น และการวัดของเราเองพบว่าตรงน้อยกว่า เลือกสำหรับบทถอดที่ผู้อ่านจะกวาดตาอ่าน บันทึกหลักฐานใช้ค่าเริ่มต้น ค่าอื่นตอบ 400 validation รูปแบบการเขียนใช้ทับบนสไตล์ใดก็ได้
ผู้พูด#
ตั้ง diarization เป็น true แล้วคำตอบจะมีอาร์เรย์ segments หนึ่งรายการต่อหนึ่งช่วงพูด พร้อมหมายเลข speaker ข้อความ text และเวลาเริ่มกับสิ้นสุดเป็นวินาที ผู้พูดนับจาก 0 ตามลำดับที่ปรากฏครั้งแรกในไฟล์ ไม่มีการระบุตัวตนของใคร ข้อความของแต่ละช่วงเป็นส่วนหนึ่งของบทถอด ทั้งสองจึงไม่มีวันขัดกัน หากตั้ง timestamps เป็น "word" ด้วย ทุกช่วงคำจะระบุ speaker ของตัวเอง ไฟล์ที่แยกผู้พูดถูกอ่านรวดเดียวแทนการอ่านเป็นท่อนขนานกัน หมายเลขผู้พูดจึงตรงกันทั้งไฟล์ การอ่านรวดเดียวนี้เองคือข้อจำกัด ไฟล์ที่แยกผู้พูดต้องสั้นกว่า 9 นาที ไฟล์ตั้งแต่ความยาวนั้นขึ้นไปตอบ 400 diarization_audio_too_long ก่อนตัดเครดิต และใช้เวลาตอบนานกว่าไฟล์ที่ไม่แยกผู้พูด ไฟล์ที่ยาวกว่านี้ให้ตัดตรงช่วงเงียบ แล้วอ่านหมายเลขผู้พูดแยกกันในแต่ละท่อน เพราะแต่ละท่อนนับจาก 0 ใหม่ ไฟล์เสียงเบื้องหลังตัวอย่างในหน้านี้มีเสียงเดียว จึงคืนมาเป็นช่วงเดียว:
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"diarization\": true}"{
"text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
"segments": [
{
"speaker": 0,
"text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
"start": 0.12,
"end": 11.66
}
],
"usage": {
"seconds": 11.952,
"credits": 1.66
}
}คลังคำศัพท์#
ฟิลด์ vocabulary คือรายการคำที่น่าจะปรากฏในไฟล์และต้องการให้โมเดลรู้จำตามรูปเขียนนั้น เช่น ชื่อสินค้า ชื่อคน ชื่อสถานที่ ศัพท์เฉพาะ ภาษาไทยหรือภาษาอื่นก็ได้ ไม่เกิน 50 คำ คำละไม่เกิน 50 ตัวอักษร รายการหรือคำที่ยาวกว่านั้นตอบ 400 validation คำเหล่านี้ช่วยโน้มการรู้จำ ไม่เคยถูกแทรกเข้าไปเอง คำที่ไม่ได้พูดจะไม่ปรากฏ นี่คือคันโยกสำหรับเนื้อหาเฉพาะทาง การประชุมเรื่องสินค้าของคุณเองจะถอดชื่อสินค้าออกมาตามที่คุณสะกด คลังคำศัพท์ใช้ร่วมกับทุกตัวเลือกอื่นได้
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
--max-time 300 \
-H "Authorization: Bearer $PAXA_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"language\": \"th\", \"vocabulary\": [\"Paxa Labs\", \"quarterly review\"]}"ความเสถียรและการส่งซ้ำ#
คำตอบเป็น JSON ก้อนเดียว ไฟล์ยาวหนึ่งชั่วโมงอาจใช้เวลาหนึ่งถึงสองนาที เพราะระบบอ่านไฟล์เป็นหลายท่อนขนานกันโดยตัดตรงช่วงเงียบ เวลารอจึงโตช้ากว่าความยาว ตั้งไทม์เอาต์ของไคลเอนต์ให้เผื่อไว้ ตัวอย่างด้านบนเผื่อ 300 วินาที
คำขอที่ใช้เวลานานพอจะเขียนส่วนหัวของคำตอบออกไปก่อนที่จะรู้ผลลัพธ์ เพื่อไม่ให้พร็อกซีระหว่างทางตัดการเชื่อมต่อที่ยังทำงานอยู่ คำตอบลักษณะนี้มีสถานะ 200 เสมอ และรายงานความล้มเหลวไว้ในเนื้อคำตอบด้วยเอกสารปัญหาชุดเดียวกับที่คำขอล้มเหลวได้รับ คือมี title และ status แทนที่ตำแหน่งของ text ไฟล์ยาวคือกรณีที่ไปถึงจุดนี้ ให้ถือว่าเนื้อคำตอบที่มี title คือข้อผิดพลาดตามที่ระบุ ส่วนไฟล์ที่สั้นกว่านั้นแจ้งสถานะไว้ที่บรรทัดสถานะเสมอ
ส่งซ้ำได้อย่างปลอดภัยด้วย Idempotency-Key การส่งซ้ำของคำขอที่ส่งมอบแล้วจะได้บทถอดเดิมโดยไม่ตัดเครดิตซ้ำ คีย์ครอบคลุมทั้งไฟล์เสียงและทุกตัวเลือก การส่งซ้ำด้วยรูปแบบการเขียน สไตล์ หรือคลังคำศัพท์ที่ต่างไปจึงถูกปฏิเสธ เมื่อถอดเสียงไม่สำเร็จ ระบบคืนเครดิตให้ และคำขอใหม่ต้องใช้คีย์ใหม่
เรียลไทม์พร้อมตรวจจับรอบพูด#
WS /v1/stt/live ถอดเสียงขณะที่กำลังพูด ผ่าน WebSocket เดียวที่เปิดค้างไว้ตลอดบทสนทนา โมเดลคือ paxa-stt-lite-realtime-v1-preview ซึ่งปรับมาสำหรับ voice agent โดยเฉพาะ มันฟังเสียงจากไมโครโฟน สังเกตเองว่าผู้ใช้เริ่มพูดและหยุดพูดเมื่อไร แล้วส่งบทถอดที่จบแล้วหนึ่งชุดต่อหนึ่งรอบพูด agent ไม่ต้องตัดสินว่ารอบพูดจบตรงไหนและไม่ต้องหั่นเสียงเป็นคำขอ ส่ง start พร้อมรหัสโมเดลและรูปแบบเสียง แล้วสตรีมเสียงดิบเป็นเฟรมไบนารีตามที่จับได้ รองรับ PCM 16 บิตที่ 16 หรือ 48 kHz หรือ mu-law ที่ 8 kHz ตามที่ระบบโทรศัพท์ส่งมา แบบโมโนทั้งคู่ ส่ง end เมื่อบทสนทนาจบแล้วรอ done
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();
}
};import json
import os
import time
from websockets.sync.client import connect # pip install websockets
with connect(
"wss://api.paxalabs.com/v1/stt/live",
additional_headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
) as socket:
socket.send(json.dumps({
"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 file has to
# be paced the way a microphone paces itself.
with open("caller.pcm", "rb") as audio:
while frame := audio.read(640): # 20 ms
socket.send(frame)
time.sleep(0.02)
socket.send(json.dumps({"type": "end"}))
for message in socket:
event = json.loads(message)
if event["type"] == "transcript" and event["is_final"]:
print(event["turn"], event["text"])
elif event["type"] == "done":
breakแต่ละรอบพูดให้เหตุการณ์สามอย่างตามลำดับ speech_started มาทันทีที่ผู้ใช้เริ่มพูด agent ที่กำลังพูดอยู่ควรหยุดเล่นเสียงและยกเลิกคำตอบที่กำลังสร้าง speech_ended มาเมื่อผู้ใช้หยุดพูด จากนั้น transcript ที่ is_final เป็น true ตามมาพร้อมข้อความของรอบนั้น ให้ agent ส่งไปยังโมเดลภาษา ระหว่างเหตุการณ์แรกกับสุดท้าย อาจมี transcript ที่ is_final เป็น false มาตรงจังหวะหยุดตามธรรมชาติ พร้อมข้อความเท่าที่พูดมา ใช้เตรียมคำตอบล่วงหน้าแล้วแทนที่ด้วยฉบับสุดท้าย ทุกเหตุการณ์ระบุ turn ของตัวเอง นับจาก 1 คำตอบที่สร้างไว้สำหรับรอบ 3 จึงทิ้งได้ทันทีที่รอบ 4 เริ่ม เวลาเป็นวินาทีบนนาฬิกาของการเชื่อมต่อ ซึ่งเริ่มนับที่เฟรมเสียงแรก
{"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}เซสชันตัดสินเองว่ารอบพูดจบตรงไหน ไม่มีค่าให้ตั้ง เมื่อ agent เล่นเสียงของตัวเองในห้องเดียวกัน ให้ส่ง playback โดย speaking เป็น true ขณะเล่นและ false เมื่อหยุด ตัวตรวจจับรอบพูดจะเผื่อให้ผู้ใช้พูดแทรก และไม่นับเสียงสะท้อนของ agent เป็นรอบพูด ตัวเลือก language style convention vocabulary และ timestamps ทำงานเหมือนกับไฟล์บันทึก และคงที่ตลอดการเชื่อมต่อ
การถอดเสียงแบบเรียลไทม์คิด 12.5 เครดิตต่อนาทีของเสียงที่รับเข้ามา หรือ 750 เครดิตต่อชั่วโมง รวมช่วงเงียบ เพราะโมเดลฟังทั้งหมด ระบบตัดเครดิตเป็นช่วง คือเมื่อจบแต่ละรอบพูด ทุกหนึ่งนาทีของเสียงที่ไม่มีรอบพูด และเมื่อปิดการเชื่อมต่อ แต่ละช่วงรายงานด้วยเหตุการณ์ charged พร้อมจำนวนวินาทีที่ครอบคลุมและยอดสะสม และ done สรุปยอดรวมของการเชื่อมต่อ บทถอดฉบับสุดท้ายของรอบพูดส่งหลังจากตัดเครดิตของช่วงนั้นแล้วเท่านั้น รอบพูดที่โมเดลถอดไม่ได้ไม่มีค่าใช้จ่ายและรายงานเป็น error ที่ระบุรอบนั้น การเชื่อมต่อที่ส่งเสียงมาแล้วคิดขั้นต่ำ 0.1 เครดิต การเชื่อมต่อที่เปิดอยู่ถือหนึ่งช่องคำขอพร้อมกันฝั่งถอดเสียงของแพ็กเกจ และรับเสียงได้สูงสุด 4 ชั่วโมงก่อนปิดด้วย done รหัสปิดและทุกฟิลด์ของเฟรมอยู่ในอ้างอิงปลายทาง