ข้อความถอดเสียงแบบเรียลไทม์มาถึงสองครั้ง ครั้งแรกเป็นฉบับร่างที่เปลี่ยนแปลงอยู่เรื่อย ๆ แล้วจึงเป็นบรรทัดสุดท้ายที่ไม่เปลี่ยนอีก แสดงฉบับร่างเป็นส่วนท้ายที่แทนที่ได้ และแสดงบรรทัดสุดท้ายเป็นประวัติ ส่วนที่เหลือของมุมมองคำบรรยายสดก็เป็นเพียงงานเชื่อมต่อ
เซสชันแบบเรียลไทม์เป็นสิ่งที่เล็กที่สุดที่ VocalStack API ทำได้ เซิร์ฟเวอร์ของคุณเรียก HTTP หนึ่งครั้งและได้ streamUrl ที่มีลายเซ็นกลับมา จากนั้นไคลเอนต์เปิด WebSocket ไปยัง URL นั้น ส่งเสียงดิบเข้าไปขณะที่ถูกบันทึก และอ่านข้อความถอดเสียงกลับมาเป็นเฟรมข้อความ JSON ที่ตามหลังผู้พูดเพียงเสี้ยววินาที เมื่อเสียงจบ ให้ส่ง eof เซิร์ฟเวอร์จะเคลียร์ข้อมูลที่ค้างอยู่ รายงานสถิติที่วัดได้ของเซสชัน แล้วปิดการเชื่อมต่อ
ไม่มีอะไรถูกเก็บ ข้อความถอดเสียงมีอยู่เฉพาะบนสายเท่านั้น ซึ่งเป็นสิ่งที่มุมมองคำบรรยาย การซ้อนทับในที่ประชุม หรือฟีเจอร์เสียงในแอปของคุณต้องการ หากอยากเก็บคำพูด แชร์ และส่งออกภายหลัง คุณต้องใช้การถอดเสียงแบบสดและการถ่ายทอดสดแทน ส่วนสุดท้ายจะชี้ไปทางนั้น
รูปแบบนี้เป็นแบบที่ API สตรีมมิงเสียงพูดส่วนใหญ่ใช้ร่วมกัน คือการแลกเปลี่ยนข้อมูลยืนยันตัวตนผ่าน HTTP ซ็อกเก็ตที่อยู่ได้นานโดยมีเสียงไหลเข้าและ JSON ไหลออก และวิธีบอกว่าข้อความใดเป็นการคาดเดาชั่วคราวและข้อความใดเป็นข้อความสุดท้ายแล้ว ชื่อของแต่ละ API อาจต่างกัน แต่ชิ้นส่วนหลักแทบไม่ต่างกัน ต่อไปนี้คือขั้นตอนตามลำดับ:
POST /v1/realtime/sessions พร้อมคีย์ API ของคุณ บนเซิร์ฟเวอร์ของคุณ คำตอบคือ { sessionId, streamUrl, expiresAt }streamUrlsession.loading จนกว่าจะได้ session.readytranscript.draft (แทนที่ส่วนท้าย) และ transcript.committed (ต่อท้ายเป็นบรรทัดสุดท้าย){"type":"eof"} แล้วอ่าน session.doneมีเพียงขั้นตอนที่ 1 เท่านั้นที่ต้องใช้คีย์ของคุณ ดังนั้นขั้นตอนนั้นจึงเกิดขึ้นบนเซิร์ฟเวอร์ของคุณ
สร้างคีย์ภายใต้ Developers, API keys พร้อมสิทธิ์ Write คีย์ใช้สำหรับเซิร์ฟเวอร์และสคริปต์เท่านั้น API จะปฏิเสธคีย์ที่ส่งมาจากหน้าเว็บหรืออยู่ใน URL ดังนั้นคุณจึงสร้างเซสชันหลังเส้นทาง (route) ของคุณเอง และหน้าเว็บจะเห็นเพียง streamUrl เท่านั้น
URL มีโทเค็นของมันเองเนื่องจากข้อจำกัดของเบราว์เซอร์ที่ควรรู้ไว้โดยทั่วไป ตัวสร้าง WebSocket ของเบราว์เซอร์ไม่สามารถตั้งค่า header ของคำขอได้ จึงไม่มี header Authorization ให้ใส่คีย์ลงไป API เสียงพูดที่ใช้งานผ่านเบราว์เซอร์ทุกตัวจึงยืนยันตัวตนซ็อกเก็ตด้วยโทเค็นอายุสั้นที่อยู่ใน URL (หรือซ่อนอยู่ในชื่อ subprotocol) อายุสั้นคือประเด็นสำคัญ เพราะ URL มักไปปรากฏในล็อกและประวัติ สิ่งที่อยู่ใน URL จึงต้องหมดอายุ เชื่อมต่อก่อนถึง expiresAt
SDK ดาวน์โหลดจากพื้นที่ Developers ไม่ใช่จาก package registry ส่วน Install ของ คู่มือเริ่มต้นอย่างรวดเร็ว แสดงคำสั่งของสภาพแวดล้อมนี้ ด้วย TypeScript SDK เส้นทางฝั่งเซิร์ฟเวอร์มีหน้าตาดังนี้:
// On your server, behind a route of yours (POST /api/transcribe, say):
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: '{{api}}', apiKey: process.env.VOCALSTACK_API_KEY });
export async function createStream(): Promise<{ sessionId: string; streamUrl: string }> {
const { sessionId, streamUrl } = await vs.realtime.createSession({ spokenLanguage: 'en' });
return { sessionId, streamUrl };
}
หากไม่ใช้ SDK ก็เป็นเพียง fetch ตัวเดียว ตัวอย่างนี้ขอภาษาสเปนและปิดการประกอบประโยคด้วย ให้ตรงกับตัวอย่าง wire ที่อยู่ด้านล่าง:
const res = await fetch('{{api}}/v1/realtime/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOCALSTACK_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ spokenLanguage: 'en', translate: ['es'], sentences: { enabled: false } }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // the error envelope: { code, message, … }
const { streamUrl } = (await res.json()) as { sessionId: string; streamUrl: string; expiresAt: string };
ทุกตัวเลือกมีค่าเริ่มต้น ตัวที่ควรรู้ตั้งแต่ต้นคือ spokenLanguage (หากไม่ระบุ ระบบจะตรวจจับภาษาเอง) translate (รายการภาษาปลายทาง) speakerLabels และ latencyMode ส่วนที่เหลือจะอธิบายเมื่อจำเป็นในส่วนถัดไป
ส่ง streamUrl ให้สิ่งที่ใช้จับเสียง และจากจุดนี้เป็นต้นไปไม่มีคีย์เกี่ยวข้องอีก
เชื่อมต่อไปยัง streamUrl ตามที่เป็นอยู่ โดยไม่มี header ขณะที่เซสชันกำลังเริ่มต้น เซิร์ฟเวอร์จะส่งสัญญาณ session.loading เป็นระยะ แล้วจึงตามด้วย session.ready อย่าส่งเสียงก่อนหน้านั้น เสียงที่ส่งเร็วเกินไปจะไม่ถูกเก็บไว้ให้ทีหลัง เซิร์ฟเวอร์จะถือว่าเป็นข้อผิดพลาด ส่งเฟรม error และปิดซ็อกเก็ต
เมื่อซ็อกเก็ตเปิดแล้ว ความล้มเหลวทุกครั้งจะอยู่ในรูปของเฟรมนั้น: {"type":"error","code":"…","message":"…","retryable":true|false} code เป็นรายการปิดตัว (invalid_audio, invalid_options, invalid_token, capacity_exceeded, session_expired, slow_consumer, engine_unavailable, internal) message ไว้สำหรับล็อกของคุณ และ retryable บอกว่าควรลองเซสชันใหม่หรือไม่ จัดการตั้งแต่เฟรมแรก:
const ws = new WebSocket(streamUrl); // the token is in the URL: no header
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', (m: MessageEvent) => {
if (typeof m.data !== 'string') return;
const f = JSON.parse(m.data);
switch (f.type) {
case 'session.loading':
console.error('starting…');
break;
case 'session.ready':
startMicrophone(); // send audio only after session.ready
break;
case 'error':
console.error(`${f.code}: ${f.message}`);
if (f.retryable) scheduleReconnect(); // a new session, with backoff; see "Stop"
break;
}
});
ไคลเอนต์แบบดิบต้องจัดการเองอีกสองเรื่อง SDK จัดการให้อยู่แล้ว ประการแรก การอัปเกรดที่ถูกปฏิเสธจะมาพร้อมซองข้อผิดพลาด HTTP ตามปกติของ API: 401 invalid_token, 410 session_expired หรือ 503 unavailable พร้อม header Retry-After ที่ต้องเคารพ
ประการที่สอง ความอิ่มตัวของระบบจะดูเหมือนความเงียบ ไม่ใช่ข้อผิดพลาด หากไม่มีเฟรมใดมาถึงภายใน 30 วินาที ให้ถือว่าเป็น capacity_exceeded แล้วเชื่อมต่อใหม่ เซสชันใหม่มักผ่านได้ หากยังมีเฟรม loading มาอยู่แต่ session.ready ไม่มาสักที ให้ถือว่าเป็น engine_unavailable ซึ่งลองใหม่ได้เช่นกัน ใน SDK RealtimeSession.connect จะ resolve เมื่อถึง session.ready และจะล้มเหลวหลัง readyDeadlineMs พร้อมโค้ดที่ถูกต้อง
เมื่อ session.ready มาถึง เซิร์ฟเวอร์จะคาดหวังเสียงในจังหวะเดียวกับที่พูด
เสียงถูกส่งเป็นเฟรมไบนารีของ PCM ดิบ: 32-bit float แบบ little-endian, 16 kHz, โมโน (pcm_f32le ซึ่งเป็นค่าเริ่มต้นของ audio ในเซสชัน) ส่งตามที่จับได้ เป็นชิ้นราว 100 ถึง 250 ms โดยไม่ต้องรอข้อความถอดเสียงระหว่างแต่ละชิ้น
ควรคำนวณขนาดข้อมูลสักครั้ง 16,000 ตัวอย่างต่อวินาที ตัวอย่างละ 4 ไบต์ เท่ากับ 64,000 ไบต์ต่อวินาที ดังนั้น 100 ms คือ 6,400 ไบต์ และ 250 ms คือ 16,000 ไบต์ โมเดลการรู้จำเสียงพูดมักถูกเทรนด้วยเสียงโมโน 16 kHz นั่นคือเหตุผลที่ API แบบเรียลไทม์จำนวนมากขอรูปแบบนี้ ขณะที่ไมโครโฟนและเบราว์เซอร์มักบันทึกที่ 44.1 หรือ 48 kHz และมักเป็นสเตอริโอ ดังนั้นต้องมีอะไรสักอย่างที่แปลงอัตราสุ่มตัวอย่างและรวมช่องเสียงให้เป็นหนึ่งก่อนส่งไบต์ออกไป ในที่นี้คือ ffmpeg และในเบราว์เซอร์คือตัวช่วยไมโครโฟนของ SDK การส่งเสียง 48 kHz แต่ประกาศว่าเป็น 16 kHz ไม่ทำให้เกิดข้อผิดพลาด แต่จะได้การถอดเสียงที่เพี้ยนเหมือนพูดช้าลงและผิดพลาดมาก
ใน Node ffmpeg เป็นอุปกรณ์จับเสียงที่ง่ายที่สุด:
import { spawn } from 'node:child_process';
function startMicrophone(): void {
// Raw PCM: f32le, 16 kHz, mono, as binary frames. macOS: -f avfoundation -i :0, Linux: -f pulse -i default.
const mic = spawn('ffmpeg', ['-loglevel', 'error', '-f', 'avfoundation', '-i', ':0', '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
mic.stdout.on('data', (chunk: Buffer) => {
// Backpressure: bytes the socket has accepted but not yet put on the wire.
// Two seconds of audio (128,000 bytes) queued means the link, not the server, is behind.
if (ws.bufferedAmount > 128_000) console.error(`link behind by ${ws.bufferedAmount} bytes`);
ws.send(new Uint8Array(chunk));
});
mic.on('close', () => ws.send(JSON.stringify({ type: 'eof' })));
process.once('SIGINT', () => mic.kill());
}
ws.send ไม่เคยบล็อก มันเพียงเข้าคิว bufferedAmount คือขนาดของคิวนั้น และบนลิงก์ที่ปกติจะอยู่ใกล้ศูนย์ เสียงสดไม่สามารถหยุดชั่วคราวได้ ดังนั้นการตอบสนองที่มีประโยชน์ต่อคิวที่โตขึ้นคือการสังเกตเห็น ไม่ใช่การทิ้งเฟรม เซิร์ฟเวอร์ก็ใช้ backpressure เช่นกัน มันรับเสียงล่วงหน้าเพียงไม่กี่วินาที (limits.audioBufferS) แล้วหยุดอ่านจนกว่าจะตามทัน ในทิศทางกลับ คอมมิตจะถูกเข้าคิวโดยไม่สูญหาย ขณะที่ฉบับร่างจะถูกรวมเข้าด้วยกัน ผู้อ่านที่ช้าจึงเห็นฉบับร่างน้อยลงแต่สดใหม่กว่า ไคลเอนต์ที่ตามคอมมิตไม่ทันแม้จะรับเฟรม error ที่มี slow_consumer และ retryable: false แล้วถูกตัดการเชื่อมต่อ หากคุณเห็นเหตุการณ์นี้ แปลว่าตัวจัดการข้อความของคุณทำงานหนักเกินไป
ในเบราว์เซอร์ streamMicrophone จาก @vocalstack/sdk/mic จะขออนุญาตใช้ไมโครโฟน ปรับอัตราเป็น 16 kHz และส่งชิ้นเสียงพร้อมเฟรม clock ซึ่งช่วยให้เซิร์ฟเวอร์ทำเครื่องหมายช่องว่างเมื่อเบราว์เซอร์หน่วงแท็บ หน้าเว็บรับเซสชันจากเส้นทางของคุณและไม่เคยเห็นคีย์:
import { RealtimeSession } from '@vocalstack/sdk';
import { streamMicrophone } from '@vocalstack/sdk/mic';
export async function transcribeMicrophone(
stream: { sessionId: string; streamUrl: string },
render: (committed: string, tail: string) => void,
): Promise<void> {
const session = await RealtimeSession.connect(stream);
const mic = await streamMicrophone(session); // asks for the microphone, sends 16 kHz PCM
document.querySelector('#stop')?.addEventListener('click', () => {
void mic.stop();
session.eof();
});
let committed = '';
for await (const ev of session) {
if (ev.type === 'transcript.draft') render(committed, ev.segments.map((s) => s.text).join(' '));
if (ev.type === 'transcript.committed') {
committed += `${ev.segment.text} `;
render(committed, '');
}
}
}
render(committed, tail) คือรูปแบบ UI ทั้งหมด ส่วนถัดไปจะอธิบายว่าเพราะเหตุใด
มีเฟรมสองประเภทที่สร้างข้อความถอดเสียง และการสับสนระหว่างสองประเภทนี้คือความผิดพลาดที่พบบ่อยที่สุด
ก่อนอื่น ทำไมจึงมีสองประเภท โมเดลการรู้จำเสียงตัดสินว่าคำหนึ่งคืออะไรบางส่วนจากคำที่ตามมา "I scream" กับ "ice cream" ฟังเหมือนกันจนกว่าประโยคจะไปต่อ ดังนั้นตัวรู้จำแบบเรียลไทม์จะปล่อยการเดาออกมาเร็ว แล้วแก้ไขเมื่อได้ยินเสียงเพิ่ม และจะนิ่งเมื่อได้ยินมากพอ API การสตรีมเสียงพูดทุกตัวแสดงสิ่งนี้ในรูปแบบใดรูปแบบหนึ่ง บางตัวเรียกการเดาว่า "interim" หรือ "partial" และทำเครื่องหมายผลที่นิ่งแล้วด้วย is_final คำศัพท์ต่างกันไป แต่หลักการเดียวกัน ที่นี่ การเดาคือฉบับร่าง และข้อความที่นิ่งแล้วคือคอมมิต
transcript.draft คือ ส่วนท้ายสด: คำที่ได้ยินตั้งแต่คอมมิตล่าสุด และยังเปลี่ยนอยู่ ฉบับร่างแต่ละฉบับเป็นการแทนที่ฉบับก่อนหน้าทั้งหมด ให้วาดส่วนท้ายใหม่ด้วย segments อย่าต่อท้ายเด็ดขาด ฉบับร่างไม่มี seq โดยตั้งใจ ดังนั้นอย่าพยายามจับคู่มันกับคอมมิตที่มันจะกลายเป็น
transcript.committed คือ คอมมิต ซึ่งเป็นข้อความสุดท้าย: segment หนึ่งชิ้นที่มี seq (0, 1, 2 ต่อเนื่องไม่มีช่องว่าง), start, end, text, speaker และ words พร้อมเวลาของแต่ละคำ ให้ต่อท้ายและทิ้งฉบับร่างที่มันครอบคลุม ฉบับร่างถัดไปจะเริ่มจากจุดที่คอมมิตนี้จบลง
committed: [seq 0] [seq 1]
draft: "how are" ← frame 1
draft: "how are you do" ← frame 2 REPLACES frame 1
committed: [seq 0] [seq 1] [seq 2 "How are you doing?"]
← the commit consumes the tail; drop the draft
UI ที่ต่อท้ายฉบับร่างจะแสดงข้อความซ้ำและกระพริบ มุมมองคำบรรยายของคุณจะเกิดอาการกระตุกที่ผู้พูดไม่เคยมี ให้แสดงคอมมิตทั้งหมดก่อน แล้วตามด้วยฉบับร่างล่าสุดหลังจากนั้น และล้างฉบับร่างเมื่อคอมมิตมาถึง คอมมิตอาจมีคำที่ไม่เคยปรากฏในฉบับร่างใดเลย
นี่คือตัวอย่างคำพูดไม่กี่วินาทีบนสาย จากตัวอย่าง fetch ข้างต้น (translate: ["es"] และปิดการประกอบประโยค):
{"type":"session.loading"}
{"type":"session.ready"}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good","speaker":0,"tentative":true}]}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good morning every","speaker":0,"tentative":true}]}
{"type":"transcript.committed","segment":{"seq":0,"start":0.0,"end":1.4,"text":"Good morning, everyone.","speaker":0,"words":[{"start":0.0,"end":0.3,"text":"Good"},{"start":0.3,"end":0.8,"text":"morning,"},{"start":0.8,"end":1.4,"text":"everyone."}]}}
{"type":"transcript.translated","seq":0,"translations":{"es":"Buenos días a todos."}}
{"type":"transcript.draft","segments":[{"start":1.6,"text":"Thanks for","speaker":0,"tentative":true}]}
มีเฟรมอีกสองประเภทอยู่รอบ ๆ เหล่านี้ transcript.amended แก้ไขคอมมิตที่มีอยู่แล้วในที่เดิม (seq เดิม พร้อม text หรือ speaker ใหม่) เมื่อคำในนั้นถูกแก้ไข ให้นำไปใช้กับบรรทัดที่วาดไว้แล้ว transcript.words ส่งเวลาของคำสำหรับส่วนท้ายก่อนจะคอมมิต ต้องใช้ events.words ซึ่งเปิดเป็นค่าเริ่มต้น และคอมมิตจะมี words อยู่เสมอ ไม่ว่าตัวเลือกเหตุการณ์จะตั้งค่าใดก็ตาม สุดท้าย ให้ข้ามเฟรมประเภทที่คุณไม่รู้จัก เพราะประเภทใหม่จะถูกเพิ่มโดยไม่แจ้งล่วงหน้า
นี่คือลูปการอ่านของ Python SDK โดยใช้เทอร์มินัลเป็นมุมมองคำบรรยาย ปิดการประกอบประโยคเช่นกัน ดังนั้นการแปลจะมาเป็น transcript.translated และเฟรม error จะถูกโยนเป็น VocalStackError ออกจากลูป:
import asyncio
import os
from vocalstack import RealtimeSession, VocalStack, VocalStackError
MIC = ["ffmpeg", "-loglevel", "error", "-f", "avfoundation", "-i", ":0",
"-f", "f32le", "-ac", "1", "-ar", "16000", "-"]
async def main() -> None:
async with VocalStack("{{api}}", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
created = await vs.realtime.create_session(
spoken_language="en", translate=["es"], sentences={"enabled": False}
)
session = await RealtimeSession.connect(created.session_id, created.stream_url)
mic = await asyncio.create_subprocess_exec(*MIC, stdout=asyncio.subprocess.PIPE)
async def feed() -> None:
assert mic.stdout is not None
while chunk := await mic.stdout.read(8000): # 125 ms of audio
await session.send_audio(chunk)
await session.eof() # the server flushes, then sends session.done
feeder = asyncio.create_task(feed())
try:
async with session:
async for event in session:
if event.type == "transcript.draft":
# The live tail: replace it, never append it.
tail = " ".join(s.text for s in event.segments)
print(f"\r\x1b[K\x1b[2m{tail}\x1b[0m", end="", flush=True)
elif event.type == "transcript.committed":
# Final: append it (seq 0, 1, 2...), and the tail it covers is gone.
print(f"\r\x1b[K{event.segment.text}", flush=True)
elif event.type == "transcript.translated":
print(f" es: {event.translations.get('es')}")
await feeder
print(f"done ({session.done_reason})", session.stats)
except VocalStackError as e:
# An `error` frame mid-stream, or a refused connect.
print(f"\nerror {e.code}: {e.message} (retryable: {e.retryable})")
finally:
if mic.returncode is None:
mic.kill()
asyncio.run(main())
การแปลรายเซ็กเมนต์ตามที่แสดงข้างต้นคือรูปแบบที่เรียบง่าย รูปแบบที่ผู้อ่านชอบมีหัวข้อของตัวเอง
translate: ["es", "fr"] ขอคำแปลในระหว่างเซสชัน วิธีที่คำแปลมาถึงขึ้นอยู่กับการประกอบประโยค (sentences: { enabled: true }) ซึ่งเปิดเป็นค่าเริ่มต้น
เมื่อปิดการประกอบประโยค คอมมิตแต่ละตัวจะตามด้วยเฟรม transcript.translated ไม่นานหลังจากนั้น โดยระบุ seq ของมันและมี translations ที่จัดกลุ่มตามภาษา ให้แนบคำแปลเหล่านั้นกับเซ็กเมนต์นั้น นี่คือสิ่งที่ตัวอย่างก่อนหน้าใช้ มันเรียบง่ายและอ่านเหมือนบันทึกคอมมิต คือหนึ่งเซ็กเมนต์ต่อหนึ่งคำแปล และขอบเขตประโยคอยู่ตรงจุดที่ผู้พูดหายใจ
เมื่อเปิดการประกอบประโยค เซิร์ฟเวอร์จะรวมคอมมิตเป็นประโยคเต็ม แล้วแปลประโยคเหล่านั้น ซึ่งอ่านได้ดีกว่ามาก เฟรม transcript.sentence แต่ละเฟรมแทนที่ประโยคหนึ่งทั้งประโยค โดยระบุด้วย sentenceId status เปลี่ยนจาก open เป็น closed (หรือ dropped ซึ่งหมายความว่าให้ทิ้งเสีย) และ translations เป็นฉบับร่างขณะที่ประโยคยังเปิดอยู่ และเป็นค่าสุดท้ายเมื่อปิดแล้ว ให้ upsert ด้วย sentenceId อย่าต่อท้าย TypeScript SDK มี applySentence สำหรับกรณีนี้โดยเฉพาะ:
import { applySentence, type SentenceSegment } from '@vocalstack/sdk';
let sentences: SentenceSegment[] = [];
for await (const ev of session) {
if (ev.type !== 'transcript.sentence') continue;
sentences = applySentence(sentences, ev.sentence); // upsert by sentenceId
if (ev.sentence.status === 'closed') console.log(`${ev.sentence.text}\n es: ${ev.sentence.translations?.es}`);
}
คำแปลจะตามหลังข้อความถอดเสียงอยู่หนึ่งถึงสองวินาที หากต้องการให้อ่านออกเสียงด้วย ให้เพิ่ม voice: ["es"] (ต้องใช้การประกอบประโยค) ภาษาสเปนของแต่ละประโยคที่ปิดแล้วจะมาเป็นเฟรม transcript.voice ที่มีคลิปเป็น Ogg/Opus แบบ base64 ที่ 24 kHz โมโน ประโยคยาวจะมาเป็นหลายชิ้น (part จาก parts) ให้เล่นต่อกัน ข้อความฉบับร่างจะไม่ถูกอ่านออกเสียงเด็ดขาด คู่มือการแปลสด ครอบคลุมเสียงพูดอย่างละเอียด และ เวลาทั้งหมดหายไปไหน อธิบายห่วงโซ่ของความหน่วง
ภาษาปลายทางไม่ได้ถูกกำหนดตายตัวตอนสร้าง
เฟรมข้อความ session.update เปลี่ยน spokenLanguage, translate, speakerLabels, latencyMode, voice หรือ voiceByLang ตั้งแต่จุดนั้นเป็นต้นไป ฟิลด์ที่คุณไม่ใส่จะคงเดิม
{"type":"session.update","translate":["es","fr"],"latencyMode":"fast"}
SDK เปิดเผยเป็น session.update(…) translate แทนที่รายการทั้งหมด และ [] จะปิดการแปล เซ็กเมนต์ที่ส่งไปแล้วจะไม่ถูกถอดเสียงหรือแปลซ้ำ และหากเปิดป้ายชื่อผู้พูด ผู้พูดจะถูกระบุตั้งแต่จุดนั้นเท่านั้น สิ่งที่พูดไปแล้วยังคงอยู่เช่นเดิม การตั้งค่าจะควบคุมเฉพาะสิ่งที่จะมาถัดไป
เมื่อเสียงจบ ให้ส่ง {"type":"eof"} (ใน SDK คือ session.eof()) เซิร์ฟเวอร์จะถอดเสียงส่วนที่ยังค้างอยู่ ส่งคอมมิตสุดท้าย จากนั้นส่ง session.done และปิดการเชื่อมต่อ การส่ง cancel แทนจะหยุดทันทีและทิ้งส่วนที่เหลือ
{"type":"session.done","reason":"eof","stats":{"audioS":30.0,"processingS":30.6,"realtimeFactor":0.98}}
reason คือ eof, max_duration (เซสชันถึงความยาวสูงสุดของแพ็กเกจและทำการ flush แบบเดียวกัน) หรือ cancelled stats วัดโดยเซิร์ฟเวอร์: audioS คือเสียงที่คุณส่ง processingS คือเวลานาฬิกาที่เซสชันทำงาน และ realtimeFactor คืออัตราส่วนของทั้งสอง เซสชันสดมักได้ค่าต่ำกว่า 1 เล็กน้อย เพราะเวลานาฬิการวมการ flush ครั้งสุดท้ายหลัง eof ของคุณ หากค่านั้นต่ำกว่ามาก แปลว่าเซสชันใช้เวลานานกว่าความยาวของเสียงอย่างเห็นได้ชัด จุดแรกที่ควรดูคือเสียงมาถึงเร็วแค่ไหน สำหรับไฟล์ ตัวเลขเดียวกันบอกว่าการถอดเสียงกลับมาเร็วกว่าการเล่นกี่เท่า
เซสชันอยู่ได้นานสุดที่ limits.maxDurationS จาก GET /v1/realtime/capabilities และเสียงของมันใช้เครดิตของคุณตามอัตราสดของแพ็กเกจ โดยมีขั้นต่ำ 5 นาทีเช่นเดียวกับการถอดเสียงทุกประเภท ทุกเซสชันที่รับเสียงจะถูกเรียกเก็บอย่างน้อย 5 นาที (ราคา) ซ็อกเก็ตเดียวกันยังรับไฟล์ทั้งไฟล์ได้ด้วย สร้างเซสชันด้วย input: { kind: "file" } ส่งไบต์ของไฟล์นั้นเอง (mp3, wav, flac, ogg, m4a) และ eof แล้วข้อความถอดเสียงจะกลับมาเร็วกว่าการเล่นเสียงหลายเท่า พร้อมเฟรม transcript.progress ระหว่างทาง
ไม่มีการกลับมาต่อ (resume) การเชื่อมต่อที่หลุดจะจบเซสชัน การเชื่อมต่อใหม่หมายถึงการสร้างเซสชันใหม่ ซึ่งถูกเรียกเก็บขั้นต่ำของตัวเองอีกครั้ง แล้วสตรีมใหม่จากแหล่งสดของคุณ โดย seq เริ่มที่ 0 อีกครั้ง การเชื่อมต่อใหม่เป็นรูปแบบมาตรฐานของ API ซ็อกเก็ตทุกตัว และควรทำให้ถูกต้องสักครั้ง:
retryable: true บนเฟรม error, 503 พร้อม Retry-After หรือการไม่มีเฟรมใดนาน 30 วินาที ซึ่งแทน capacity_exceeded invalid_token และ session_expired หมายถึงต้องขอ streamUrl ใหม่จากเซิร์ฟเวอร์ของคุณ ไม่ใช่ลองอีกครั้งด้วยค่าเดิมทำให้เส้นทางที่สร้างเซสชันเรียกได้ง่ายและถูก และถือว่าการปิดคือการเริ่มใหม่ การไม่มี resume คือราคาของการไม่เก็บอะไรเลย หากข้อความถอดเสียงต้องคงอยู่เกินอายุของซ็อกเก็ต ให้เก็บมันไว้เอง
เมื่อต้องการเก็บสตรีมเดียวกันเป็นการบันทึก แก้ไขภายหลัง ส่งออก และให้ผู้ฟังที่ใช้ภาษาของตนเองรับชม ให้เผยแพร่ไปยังการถอดเสียงแบบสดแทน คู่มือการถ่ายทอดสด แสดงวิธีส่งซ็อกเก็ต PCM เดียวกันผ่านการถอดเสียงและลิงก์ถ่ายทอดสด และ คู่มือการรับฟัง แสดงฝั่งผู้สมัครรับ ซึ่งผู้ฟังทุกคนเลือกภาษาได้ และผู้ที่เข้ามาช้าจะตามทันจากตัวชี้ lastSeq รูปแบบของสิ่งนี้สำหรับห้องหนึ่งห้องเป็นหัวข้อของ ลิงก์เดียว ทุกภาษา
สำหรับไฟล์ที่ต้องการเก็บไว้ เส้นทางที่บันทึกคือ ถอดเสียงไฟล์ด้วย API เอกสารอ้างอิงเฟรมฉบับเต็ม พร้อมทุกฟิลด์ คือคู่มือ สตรีมไมโครโฟน ในเอกสารอ้างอิง API