การอัปโหลดจะตอบกลับด้วยงาน ไม่ใช่ข้อความถอดเสียง ใส่ Idempotency-Key ในทุกคำสั่งสร้าง เพื่อให้การลองใหม่ไม่สร้างหรือเรียกเก็บเงินสำหรับการถอดเสียงครั้งที่สองได้ จากนั้นตรวจสอบงานจนเสร็จ แล้วอ่านประโยค คำ หรือไฟล์ส่งออกจากรหัสงานเดียวกัน
การถอดเสียงไฟล์เป็นสิ่งที่ง่ายที่สุดที่ VocalStack API ทำได้ และมีรูปแบบของงานอะซิงโครนัสทั่วไป คือ อัปโหลด รอ แล้วอ่าน การอัปโหลดตอบกลับทันทีด้วยการถอดเสียงและงานของมัน แต่ยังไม่มีคำพูดใด ๆ ขณะที่โปรแกรมของคุณตรวจสอบงาน เซิร์ฟเวอร์ถอดเสียงไฟล์เร็วกว่าการเล่นเสียงหลายเท่า เมื่อสถานะเป็น done คุณจะอ่านข้อความถอดเสียงเป็นประโยคพร้อมผู้พูดและเวลา หรือส่งออกเป็นไฟล์ในหนึ่งในสี่รูปแบบ ทั้งในภาษาที่พูดหรือในภาษาที่แปลแล้ว
ทุกอย่างในรูปแบบนี้ใช้ได้กับ API แบบกลุ่มใด ๆ คือ การสร้างที่คืนค่าเป็นรหัส สถานะที่คุณตรวจสอบ และผลลัพธ์ที่ดึงมาภายหลัง มีเพียงชื่อฟิลด์เท่านั้นที่เฉพาะของ VocalStack บทช่วยสอนนี้จะอธิบายด้วย SDK ของ TypeScript และ Python และคำสั่ง HTTP ในจุดที่ช่วยได้
เปิด Developers → API keys ใน Dashboard แล้วสร้างคีย์ที่มีสิทธิ์ Read และ Write คีย์จะแสดงเพียงครั้งเดียว ให้คัดลอกไปไว้ในตัวแปรสภาพแวดล้อม:
export VOCALSTACK_API_KEY="<your API key>"
คีย์ใช้สำหรับเซิร์ฟเวอร์และสคริปต์ ส่วนใดก็ตามที่ถูกส่งไปยังเบราว์เซอร์จะนำคีย์ไปด้วย ดังนั้นอย่าใส่คีย์ในโค้ดหน้าเว็บ และอย่าใส่คีย์ใน URL เพราะ URL จะไปอยู่ในบันทึก หากต้องการสตรีมจากเบราว์เซอร์ เซิร์ฟเวอร์ของคุณจะสร้างเซสชันเรียลไทม์ให้ ซึ่งอยู่ใน บทช่วยสอนอีกเรื่อง
การเรียกทุกครั้งยืนยันตัวตนด้วยคีย์ในรูปแบบ bearer token:
Authorization: Bearer vs_key_...
เมื่อมีคีย์ในสภาพแวดล้อมแล้ว ขั้นต่อไปคือไคลเอนต์ที่จะส่งคีย์นั้น
มี SDK สองตัว ทั้งสองมีไคลเอนต์เดียวชื่อ VocalStack โดยเมธอดแบ่งตามทรัพยากร ได้แก่ vs.transcriptions, vs.jobs, vs.recordings, vs.realtime และอื่น ๆ อีกไม่กี่ตัว SDK ของ TypeScript ทำงานบน Node 20 ขึ้นไปและในเบราว์เซอร์ ส่วน SDK ของ Python สร้างบน asyncio และต้องใช้ Python 3.10 ขึ้นไป
SDK ไม่ได้อยู่บน npm หรือ PyPI API ให้บิลด์ SDK แก่ทุกบัญชี คำสั่งติดตั้งที่กรอกค่าสำหรับสภาพแวดล้อมของคุณอยู่ในส่วนการติดตั้งของ quickstart ในเอกสาร API และจะส่งคีย์ของคุณไปยังโฮสต์ API เท่านั้น หากคุณไม่อยากติดตั้งอะไรเลย คำสั่ง HTTP ด้านล่างต้องการเพียง curl และ jq
ไม่ว่าจะเลือกวิธีใด การเรียกแรกคือการอัปโหลด
POST /v1/transcriptions รับไฟล์เป็น multipart/form-data คือส่วน file และส่วน options ที่เป็นทางเลือกซึ่งเก็บ JSON ตอบกลับด้วย 201 พร้อมการถอดเสียง job.recordingId คือรหัสของการบันทึกที่สร้างจากไฟล์ และการเรียกทุกครั้งหลังจากนั้นจะใช้รหัสนี้
มีสองการตั้งค่าที่สำคัญสำหรับการรันครั้งแรก ทั้งอยู่ภายใน options.settings:
spokenLanguage: รหัสภาษา เช่น en หากไม่ใส่ ระบบจะตรวจจับภาษาให้ การระบุภาษาจะช่วยให้ความแม่นยำดีขึ้นกับการบันทึกที่มีเสียงรบกวนและสำเนียงหนักspeakerLabels: auto, on หรือ off เมื่อเป็น on ทุกประโยคจะมีหมายเลขผู้พูด เมื่อเป็น auto ป้ายกำกับจะปรากฏก็ต่อเมื่อได้ยินผู้พูดคนที่สองแล้วเท่านั้นไฟล์เสียงและวิดีโอส่วนใหญ่ได้รับการยอมรับ เช่น mp3, wav, flac, ogg, m4a, mp4 และอื่น ๆ วิดีโอจะเก็บเฉพาะเสียง รูปแบบแทบไม่ส่งผลต่อความแม่นยำ เพราะการรู้จำเสียงพูดทำงานที่ 16 kHz โมโน และทุกอย่างถูกปรับอัตราสุ่มตัวอย่างเป็นค่านั้นเมื่อมาถึง ไฟล์ WAV สเตอริโอ 48 kHz ของคนคนเดียวที่พูดอยู่ใช้ไบต์ส่วนใหญ่ไปกับการบรรยายความเงียบด้วยความละเอียดสูง มันถอดเสียงได้ดีเท่ากับไฟล์ MP3 หรือ Opus โมโน 64 kbps ของการพูดเดียวกัน แต่ใหญ่กว่าราวยี่สิบเท่า จึงใช้เวลาอัปโหลดนานกว่ายี่สิบเท่า หากคุณแปลงไฟล์ก่อนอัปโหลด การตั้งเป็นโมโนและบิตเรตปานกลางคือสองการตั้งค่าที่คุ้มค่าแก่การเปลี่ยน
ส่ง Idempotency-Key ไปกับทุกการอัปโหลด และเข้าใจเหตุผลด้วย การหมดเวลาของเครือข่ายไม่บอกอะไรเลยว่าเซิร์ฟเวอร์ทำอะไรไปแล้ว อาจไม่เคยได้รับคำขอเลย หรืออาจบันทึกไฟล์และเข้าคิวงานไปแล้วก่อนที่คำตอบจะหายไป การลองใหม่แบบไม่ดูอะไรเลยจึงเสี่ยงให้เกิดการถอดเสียงครั้งที่สองและการเรียกเก็บเงินครั้งที่สอง คีย์นี้แก้ปัญหานั้น เมื่อคำขอถูกส่งซ้ำด้วยคีย์และเนื้อหาเดียวกันภายใน 24 ชั่วโมง เซิร์ฟเวอร์จะคืนคำตอบแรกโดยไม่สร้างอะไรใหม่ ซึ่งเปลี่ยนจาก "อย่างน้อยหนึ่งครั้ง" เป็น "มีผลเพียงครั้งเดียวในทางปฏิบัติ" กฎคือหนึ่งคีย์ต่อการอัปโหลดหนึ่งครั้ง สร้างก่อนความพยายามแรกและใช้ซ้ำในทุกการลองใหม่ คีย์ใหม่ทุกครั้งที่พยายามจะทำให้การลองใหม่แต่ละครั้งกลายเป็นการอัปโหลดใหม่ และหัวข้อนั้นจะไม่ป้องกันอะไรเลย
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: '{{api}}', apiKey: process.env.VOCALSTACK_API_KEY });
const uploadKey = crypto.randomUUID(); // one key for this upload, however many attempts it takes
const tx = await vs.transcriptions.create(
{ file: './talk.mp3', name: 'Talk', settings: { spokenLanguage: 'en', speakerLabels: 'on' } },
{ idempotencyKey: uploadKey },
);
const recordingId = tx.job!.recordingId;
import asyncio
import os
import uuid
from vocalstack import VocalStack
async def main() -> None:
upload_key = str(uuid.uuid4()) # one key for this upload, however many attempts it takes
async with VocalStack("{{api}}", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
tx = await vs.transcriptions.create(
file="./talk.mp3",
name="Talk",
settings={"spokenLanguage": "en", "speakerLabels": "on"},
idempotency_key=upload_key,
)
assert tx.job is not None
recording_id = tx.job.recording_id
คำสั่งเดียวกันด้วย curl:
API={{api}}
AUTH="Authorization: Bearer $VOCALSTACK_API_KEY"
UPLOAD_KEY=$(uuidgen)
REC=$(curl -sSf "$API/v1/transcriptions" -H "$AUTH" -H "Idempotency-Key: $UPLOAD_KEY" \
-F file=@talk.mp3 \
-F 'options={"name":"Talk","settings":{"spokenLanguage":"en","speakerLabels":"on"}}' \
| jq -r .job.recordingId)
201 ตอบกลับก่อนที่คำพูดจะถูกถอดเสียงแม้แต่คำเดียว สิ่งที่คุณได้รับคือรหัสงาน ดังนั้นขั้นต่อไปคือรอให้งานนั้นเสร็จ
งานเปลี่ยนสถานะจาก queued → transcribing → done หรือจบที่ failed พร้อมข้อความ error และ errorCode เมื่อสาเหตุอ่านได้โดยเครื่อง สถานะที่สี่คือ cropping หมายความว่างานถูกพักไว้จนมีคนเลือกช่วงของการบันทึกที่จะถอดเสียง เหมือนขั้นตอนตัดของ Dashboard การอัปโหลดทั่วไปจะไม่เข้าสถานะนี้ แม้ว่า jobs.wait จะคืนค่าเมื่องานไปถึงสถานะนี้ก็ตาม ให้อ่าน GET /v1/recordings/{id}/job จนกว่าสถานะจะเป็น done หรือ failed
ไม่มี webhook และนั่นเป็นการแลกที่ตั้งใจไว้ webhook ต้องใช้ endpoint HTTPS สาธารณะที่ฝั่งคุณ การตรวจลายเซ็นเพื่อให้ไม่มีใครอื่นเรียกได้ และนโยบายการลองใหม่เมื่อ endpoint ของคุณล่ม ซึ่งเป็นงานติดตั้งจำนวนมากสำหรับงานที่ใช้เวลาหนึ่งนาที การ polling ไม่ต้องการสิ่งเหล่านั้น สคริปต์ของคุณจะถามว่า "เสร็จหรือยัง" ทุกสองวินาที เหมือนเด็กที่นั่งเบาะหลังถามระหว่างเดินทาง แต่ต่างจากเด็ก มันได้เปอร์เซ็นต์กลับมาด้วย
หน้าเว็บที่เปิดอยู่แล้วสามารถติดตามซ็อกเก็ตเหตุการณ์ของการถอดเสียงได้แทน (WS /v1/transcriptions/{id}/events โดยมีเฟรม job.progress เมื่องานก้าวหน้า) สำหรับสคริปต์ การ polling คือเครื่องมือที่เหมาะสม
SDK ทำการ polling ให้: vs.jobs.wait อ่านงานทุก 2 วินาที และส่งการอ่านแต่ละครั้งให้ onProgress (on_progress ใน Python) progress ของงานมี pct (0 ถึง 100) และ processedS ซึ่งคือวินาทีของสื่อที่ถอดเสียงไปแล้ว โปรแกรมด้านล่างจะพิมพ์บรรทัดเมื่อสถานะหรือเปอร์เซ็นต์เปลี่ยนเท่านั้น
let last = '';
const job = await vs.jobs.wait(recordingId, {
onProgress: (j) => {
const line = `${j.state} ${j.progress.pct}%`;
if (line !== last) console.log((last = line));
},
});
if (job.state !== 'done') throw new Error(`transcription ${job.state}: ${job.error ?? ''}`);
last = ""
def on_progress(j) -> None:
nonlocal last
line = f"{j.state} {j.progress.pct}%"
if line != last:
last = line
print(line)
job = await vs.jobs.wait(recording_id, on_progress=on_progress)
if job.state != "done":
raise RuntimeError(f"transcription {job.state}: {job.error}")
โปรดสังเกตว่า wait ไม่โยนข้อผิดพลาดเมื่อ failed แต่คืนงานกลับมาและปล่อยให้คุณตรวจเอง สองวินาทีเหมาะกับการบรรยาย สำหรับการบันทึกสามชั่วโมงหรือชุดไฟล์หลายร้อยไฟล์ ให้เพิ่มช่วงเวลา (intervalMs, interval_s) หรือค่อย ๆ ลดความถี่ขณะงานกำลังทำงาน การอ่านช่วงแรกบอกคุณได้มากที่สุด ส่วนการอ่านช่วงหลังส่วนใหญ่ยืนยันเพียงว่าเซิร์ฟเวอร์ยังไม่ว่าง หากไม่ใช้ SDK คุณเขียนลูปเดียวกันด้วยมือได้:
while :; do
JOB=$(curl -sSf "$API/v1/recordings/$REC/job" -H "$AUTH")
STATE=$(jq -r .state <<<"$JOB")
echo "$STATE $(jq -r .progress.pct <<<"$JOB")%"
case $STATE in
done) break ;;
failed) jq -r .error <<<"$JOB" >&2; exit 1 ;;
esac
sleep 2
done
เมื่อสถานะขึ้นว่า done ข้อความถอดเสียงก็พร้อมแล้ว
GET /v1/recordings/{id}/transcript ตอบกลับด้วยข้อความถอดเสียงทั้งหมดในเอกสารเดียว:
sentences: สิ่งที่จะแสดงให้ผู้คนอ่าน แต่ละรายการมี text, speaker และ start กับ end เป็นวินาทีsegments: บรรทัดจากเอนจิน แต่ละบรรทัดมีเวลาระดับคำใน words (ทุกคำมี text, start และ end)speakers: หนึ่งรายการต่อหมายเลขผู้พูด พร้อม nameผู้พูดนับจาก 0 ค่า speaker เป็น -1 หมายความว่าป้ายกำกับผู้พูดถูกปิดสำหรับการบันทึกนั้น เพิ่ม ?lang=es เพื่ออ่านข้อความถอดเสียงที่แปลเป็นภาษาสเปน ประโยคจะกลับมาเป็นภาษานั้น
const { sentences } = await vs.recordings.transcript(recordingId);
for (const s of sentences) console.log(s.speaker >= 0 ? `[speaker ${s.speaker}] ${s.text}` : s.text);
snapshot = await vs.recordings.transcript(recording_id)
for s in snapshot.sentences:
print(f"[speaker {s.speaker}] {s.text}" if s.speaker >= 0 else s.text)
curl -sSf "$API/v1/recordings/$REC/transcript" -H "$AUTH" \
| jq -r '.sentences[] | if .speaker >= 0 then "[speaker \(.speaker)] \(.text)" else .text end'
ระดับเวลาทั้งสองทำหน้าที่สองอย่าง ประโยคคือหน่วยของการอ่าน จึงเป็นสิ่งที่คำบรรยายหรือหน้าข้อความถอดเสียงแสดง คำคือหน่วยของการชี้ ดังนั้นการไฮไลต์แบบคาราโอเกะ ผลการค้นหา หรือการตัดคลิป จะเดินผ่าน segments[].words แทน รูปแบบการส่งออกทั้งหมดสร้างจากประโยค
GET /v1/recordings/{id}/export คืนค่าเป็นไฟล์จริง format เป็นหนึ่งใน srt, json, xlsx หรือ pdf และไฟล์รวมการแก้ไขใด ๆ ไว้ด้วย เพิ่ม lang= เพื่อให้ได้ไฟล์ที่แปลแล้ว ไฟล์นั้นจะอ่านในภาษานั้น หนึ่งบรรทัดต่อหนึ่งประโยค และประโยคใดที่ยังไม่ได้แปลจะถูกแปลก่อน
SRT ควรรู้จักไว้เพราะเป็นรูปแบบคำบรรยายที่เครื่องเล่นเกือบทุกตัวรองรับ และเป็นข้อความธรรมดา ไฟล์คือรายการของคิวที่คั่นด้วยบรรทัดว่าง และแต่ละคิวประกอบด้วยหมายเลข ช่วงเวลา และข้อความ เวลาใช้จุลภาคก่อนมิลลิวินาที ซึ่งเป็นธรรมเนียมจากฝรั่งเศสที่รูปแบบนี้ยังไม่เคยเลิก
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
หนึ่งประโยคต่อหนึ่งคิวเป็นความละเอียดที่เหมาะสม แต่ละคิวอ่านได้ทันในเวลาที่อยู่บนจอ และไม่มีคิวใดตัดกลางความคิด เมื่อการบันทึกมีป้ายกำกับผู้พูด ข้อความของแต่ละคิวจะขึ้นต้นด้วยชื่อผู้พูดและเครื่องหมายโคลอน
ใน SDK ของ TypeScript vs.recordings.export คืนค่าเป็น Response ให้อ่านไบต์ด้วย arrayBuffer() ใน Python คืนค่าเป็น bytes
import { writeFile } from 'node:fs/promises';
const srt = await vs.recordings.export(recordingId, 'srt');
await writeFile('talk.srt', Buffer.from(await srt.arrayBuffer()));
const pdf = await vs.recordings.export(recordingId, 'pdf', { lang: 'es' });
await writeFile('talk.es.pdf', Buffer.from(await pdf.arrayBuffer()));
srt = await vs.recordings.export(recording_id, "srt")
with open("talk.srt", "wb") as f:
f.write(srt)
pdf = await vs.recordings.export(recording_id, "pdf", lang="es")
with open("talk.es.pdf", "wb") as f:
f.write(pdf)
asyncio.run(main())
curl -sSf "$API/v1/recordings/$REC/export?format=srt" -H "$AUTH" -o talk.srt
curl -sSf "$API/v1/recordings/$REC/export?format=pdf&lang=es" -H "$AUTH" -o talk.es.pdf
การถอดเสียงถูกเก็บไว้ จึงปรากฏใต้ Transcriptions ใน Dashboard แชร์ผ่านลิงก์ได้ และอ่านซ้ำได้ทุกเมื่อในภายหลัง นั่นคือวงจรหลัก คือ สร้าง รอ อ่าน ส่งออก ส่วนที่เหลือของบทช่วยสอนนี้ครอบคลุมรูปแบบต่าง ๆ ได้แก่ สื่อที่มีออนไลน์อยู่แล้ว ข้อผิดพลาดที่คุณจะพบ และไฟล์หนึ่งไฟล์มีค่าใช้จ่ายเท่าไร
หากสื่อมีออนไลน์อยู่แล้ว ให้ส่งลิงก์ให้ API และ API จะดึงไฟล์เอง ได้แก่ที่อยู่ http(s) สาธารณะ (ไฟล์ mp3 วิดีโอ หรือตอนพอดแคสต์) หรือวิดีโอ YouTube การเรียกคือ POST /v1/transcriptions เดิม โดยใช้เนื้อหา JSON { "url": "..." } แทนการอัปโหลดแบบ multipart ส่วน name และ settings เหมือนเดิม ใน SDK vs.transcriptions.createFromUrl(url) (TypeScript) และ vs.transcriptions.create(url=...) (Python) ส่งคำขอเดียวกัน
ก่อนสร้างอะไร GET /v1/transcriptions/probe?url= จะบอกว่าลิงก์คืออะไร kind มีสามค่า:
file: การบันทึกที่มีจุดสิ้นสุด ถอดเสียงที่นี่ได้live: สตรีม YouTube สด สตรีมวิทยุที่ไม่มีที่สิ้นสุด หรือเพลย์ลิสต์ HLS ที่ไม่มีจุดจบ ลิงก์สดไม่มีวันจบ จึงถอดเสียงเป็นไฟล์ไม่ได้ ให้ใช้เป็นแหล่งของการถอดเสียงสดแทนunknown: เซิร์ฟเวอร์ไม่สามารถระบุได้เมื่อลิงก์เปิดเป็นสื่อไม่ได้เลย (เช่น หน้าเว็บ) ok จะเป็น false และ problem จะอธิบายสาเหตุในหนึ่งประโยค การ probe เป็นทางเลือก และจำกัดไว้ที่ 60 การเรียกต่อนาที
งานจากลิงก์เริ่มที่ fetching ก่อนจะย้ายไป transcribing และ done ขณะดึงไฟล์ progress.phase บอกว่าเซิร์ฟเวอร์กำลังทำอะไรอยู่ คือ fetching ขณะที่ไบต์ไหลเข้ามา (พร้อม fetchedBytes และ fetchTotalBytes เมื่อปลายทางบอกขนาด) หรือ extracting ขณะดึงเสียงออกจากวิดีโอ progress.phasePct บอกว่างานไปถึงไหนในช่วงนั้น วิดีโอยาวใช้เวลาพักใหญ่ ให้ polling ทุกไม่กี่วินาที และ vs.jobs.wait รับ intervalMs (interval_s ใน Python) สำหรับเรื่องนี้
const probe = await vs.transcriptions.probe(url);
if (!probe.ok) throw new Error(`that link did not open: ${probe.problem}`);
if (probe.kind === 'live') throw new Error('a live link: record it with a live transcription instead');
const linkKey = crypto.randomUUID();
const tx = await vs.transcriptions.createFromUrl(url, { name: 'From a link' }, { idempotencyKey: linkKey });
const job = await vs.jobs.wait(tx.job!.recordingId, {
intervalMs: 3000,
onProgress: (j) => {
const p = j.progress;
if (j.state === 'fetching') console.log(`${p.phase ?? 'fetching'} ${p.phasePct ?? 0}%`);
else console.log(`${j.state} ${p.pct}%`);
},
});
คู่มือ Transcribe a link ในเอกสารอ้างอิงมีโปรแกรมฉบับเต็มทั้งสองภาษาและ HTTP การดึงจากลิงก์เพิ่มวิธีที่จะล้มเหลวได้ ดังนั้นนี่คือข้อผิดพลาดที่คุณจะพบจริง ๆ
ทุกการเรียกที่ล้มเหลวตอบกลับด้วยซองข้อมูล JSON หนึ่งชุด code คือสตริงที่ควรใช้แยกกรณี และ status คือสถานะ HTTP message สำหรับมนุษย์อ่าน และ requestId สำหรับฝ่ายสนับสนุน ภายใต้ data, retryable บอกว่าการส่งคำขอเดิมซ้ำอาจช่วยได้หรือไม่ และ details เก็บสิ่งที่รหัสนั้นต้องการ SDK โยนข้อผิดพลาดชนิดเดียว คือ VocalStackError ที่มีฟิลด์เดียวกัน
มีสามรหัสที่ควรจัดการตั้งแต่วันแรก
402 insufficient_credit: เครดิตของบัญชีหมด และไม่มีการเรียกเก็บเงิน details.balance คือยอดคงเหลือเป็นดอลลาร์ และ details.topUpUrl คือที่เติมเครดิต เมื่อทราบค่าใช้จ่ายล่วงหน้า เช่นไฟล์ที่รู้ความยาว details.required จะบอกว่าจะต้องใช้เท่าไร429 rate_limited: รอ Retry-After วินาที เมื่อ details.metric เป็น creates_per_hour หมายความว่าคุณชนขีดจำกัดอัตราการสร้างของแพ็กเกจ (การถอดเสียงใหม่ต่อชั่วโมง) ไม่ใช่ขีดจำกัดการระเบิดของคำขอ limit และ resetsAt บอกว่าเพดานคืออะไรและจะว่างเมื่อใด503 unavailable: บริการไม่พร้อมชั่วคราว ส่งคำขอใหม่หลัง Retry-AfterSDK ลอง 429 และ 503 ใหม่หลัง Retry-After โดยค่าเริ่มต้นสองครั้ง (maxRetries / max_retries บนไคลเอนต์) อย่างไรก็ตาม 503 จะถูกลองใหม่เฉพาะการเรียกที่ปลอดภัยที่จะส่งสองครั้ง คือ GET หรือการสร้างที่มี Idempotency-Key การสร้างที่ไม่มีคีย์จะปล่อยให้คุณจัดการเอง เพราะ SDK ไม่รู้ว่าความพยายามแรกผ่านไปแล้วหรือไม่ นั่นคือเหตุผลเชิงปฏิบัติที่ควรใส่คีย์ในทุกการอัปโหลด
ทุกการตอบกลับยังมี RateLimit-Limit, RateLimit-Remaining และ RateLimit-Reset ดังนั้นไปป์ไลน์ที่ทำงานหนักสามารถลดความเร็วก่อนจะถึง 429 ได้
{
"code": "insufficient_credit",
"status": 402,
"message": "this account has no credit left; add credit to continue",
"data": {
"retryable": false,
"details": { "balance": -0.42, "required": 0.075, "currency": "usd", "plan": "payg", "topUpUrl": "https://…" }
},
"requestId": "01J…"
}
ตารางรหัสทั้งหมดอยู่ใน ข้อผิดพลาด การลองใหม่ และขีดจำกัด 402 นั้นเป็นข้อผิดพลาดเดียวที่เกี่ยวกับเงิน ดังนั้นนี่คือค่าใช้จ่ายของไฟล์หนึ่งไฟล์
ไฟล์มีราคา $0.20 ต่อชั่วโมงของเสียง คิดเป็นรายวินาทีที่ $0.0033 ต่อนาที โดยมีขั้นต่ำ 5 นาที ไฟล์ 30 นาทีมีค่าใช้จ่าย $0.10 นั่นคือสิบเซ็นต์ น้อยกว่ากาแฟที่คุณจะดื่มระหว่างที่แถบความคืบหน้าค่อย ๆ เต็ม อัตราเดียวกันในทุกแพ็กเกจ และเหมือนกันทั้งผ่าน API และใน Dashboard บัญชีใหม่เริ่มด้วยเครดิตสมัครใช้งานครั้งเดียว $5 และไม่ต้องใช้บัตร
มีขีดจำกัดของแพ็กเกจสามข้อที่กระทบบทช่วยสอนนี้
GET /v1/realtime/capabilities รายงานเป็น limits.maxUploadBytes ระบบใช้ค่าที่น้อยกว่าในสองค่า และไฟล์ที่เกินจะถูกปฏิเสธด้วย 413 ก่อนที่จะมีการถอดเสียงใด ๆ สำหรับการบันทึกที่ยาว นี่เป็นอีกเหตุผลหนึ่งที่ควรอัปโหลดไฟล์โมโนที่บีบอัดแล้วแทน WAVGET /v1/billing รายงานแพ็กเกจ ยอดคงเหลือ และขีดจำกัดของคุณได้ทุกเมื่อ ตารางเต็มอยู่ในหน้าราคา
จากนี้ ขั้นต่อไปคือ สตรีมเสียงแบบเรียลไทม์ เมื่อคุณไม่อยากรอให้ไฟล์เสร็จ หรือ เชื่อมต่อ AI agent เข้ากับบัญชีเดียวกันผ่านเซิร์ฟเวอร์ MCP เพื่อให้มันถอดเสียงและค้นหาให้คุณโดยไม่ต้องใช้โค้ดข้างต้นเลย