ریئل ٹائم ٹرانسکرپٹ دو بار آتا ہے: پہلے ایک ڈرافٹ کے طور پر جو مسلسل بدلتا رہتا ہے، پھر ایک حتمی لائن کے طور پر جو نہیں بدلتی۔ ڈرافٹ کو ایک ایسی دم کے طور پر دکھائیں جسے بدلا جا سکے، اور حتمی لائن کو تاریخ کے طور پر، باقی لائیو کیپشن ویو کا سارا کام محض پس پردہ کا عام کام ہے۔
ریئل ٹائم سیشن VocalStack API کا سب سے چھوٹا کام ہے۔ آپ کا سرور ایک HTTP کال کرتا ہے اور دستخط شدہ streamUrl واپس لیتا ہے۔ آپ کا کلائنٹ اس URL پر ایک WebSocket کھولتا ہے، جیسے ہی آڈیو ریکارڈ ہوتی ہے خام آڈیو بھیجتا ہے، اور بولنے والے سے چند لمحے پیچھے رہ کر JSON ٹیکسٹ فریمز کی صورت میں ٹرانسکرپٹ واپس پڑھتا ہے۔ آڈیو ختم ہونے پر آپ eof بھیجتے ہیں، سرور بقایا ڈیٹا بھیج دیتا ہے، سیشن کے ناپے گئے اعداد و شمار بتاتا ہے اور کنکشن بند کر دیتا ہے۔
کچھ بھی محفوظ نہیں ہوتا۔ ٹرانسکرپٹ صرف تار پر موجود رہتا ہے، اور کیپشن ویو، میٹنگ اوورلے یا آپ کی اپنی ایپ میں وائس فیچر کو یہی چاہیے۔ اگر آپ چاہتے ہیں کہ الفاظ بعد میں محفوظ، شیئر اور ایکسپورٹ ہوں، تو آپ کو اس کے بجائے ایک لائیو ٹرانسکرپشن اور ایک براڈکاسٹ چاہیے؛ آخری حصہ وہیں کی طرف اشارہ کرتا ہے۔
یہ ساخت زیادہ تر اسٹریمنگ اسپیچ APIs میں مشترک ہے: HTTP پر ایک اسناد کا تبادلہ، ایک طویل مدتی ساکٹ جس میں آڈیو اندر جاتی ہے اور JSON باہر آتا ہے، اور کسی نہ کسی طریقے سے عارضی متن کو حتمی متن سے الگ پہچاننا۔ ناموں میں ہر API کا فرق ہوتا ہے؛ کام کرنے والے حصے شاذ ہی بدلتے ہیں۔ یہ رہے، ترتیب سے:
POST /v1/realtime/sessions کو اپنی API کلید کے ساتھ، اپنے سرور پر بھیجیں۔ جواب { sessionId, streamUrl, expiresAt } ہوگا۔streamUrl پر ایک WebSocket کھولیں۔session.loading ہارٹ بیٹس کے دوران انتظار کریں، یہاں تک کہ session.ready آ جائے۔transcript.draft پڑھیں (دم کی جگہ بدلیں) اور transcript.committed پڑھیں (ایک حتمی لائن شامل کریں)۔{"type":"eof"} بھیجیں؛ session.done پڑھیں۔صرف پہلے مرحلے کو آپ کی کلید درکار ہے، اس لیے یہ آپ کے سرور پر ہوتا ہے۔
Developers, API keys کے تحت Write اجازت کے ساتھ ایک کلید بنائیں۔ کلید سرورز اور اسکرپٹس کے لیے ہوتی ہے: API ایسی کلید کو رد کر دیتی ہے جو کسی ویب پیج سے یا URL میں بھیجی گئی ہو، اس لیے آپ سیشن اپنے کسی روٹ کے پیچھے بناتے ہیں، اور صفحہ صرف streamUrl دیکھتا ہے۔
URL اپنا ٹوکن خود لے کر چلتا ہے، اور یہ ایک براؤزر کی حد کی وجہ سے ہے جسے عمومی طور پر جاننا مفید ہے۔ براؤزر کا WebSocket کنسٹرکٹر ریکوئسٹ ہیڈرز سیٹ نہیں کر سکتا، اس لیے کلید رکھنے کے لیے کوئی Authorization ہیڈر نہیں ہوتا؛ براؤزر کی طرف کام کرنے والا ہر اسپیچ API اپنے ساکٹ کی توثیق URL میں موجود مختصر مدت کے ٹوکن سے کرتا ہے (یا سب پروٹوکول کے نام میں چھپا کر)۔ مختصر مدت ہی اصل نکتہ ہے: URL لاگز اور ہسٹری میں جا پہنچتا ہے، اس لیے جو ٹوکن اس میں ہو اس کی میعاد ختم ہونی چاہیے۔ expiresAt سے پہلے کنیکٹ کریں۔
SDKs ڈیولپرز ایریا سے ڈاؤن لوڈ ہوتے ہیں، کسی پیکیج رجسٹری سے نہیں؛ کوئیک اسٹارٹ کا 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 ہے۔ یہ والا ہسپانوی زبان بھی مانگتا ہے اور جملوں کی تشکیل بند کر دیتا ہے، جو تار پر دکھائے گئے نیچے کے نمونے سے مماثل ہے:
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 سے بغیر کسی ہیڈر کے، جیسا ہے ویسا کنیکٹ کریں۔ جب تک سیشن شروع ہو رہا ہوتا ہے، سرور 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;
}
});
ایک خام کلائنٹ کو خود دو مزید چیزیں ہینڈل کرنی ہوتی ہیں؛ SDKs یہ پہلے ہی کرتے ہیں۔ پہلی بات: رد کیا گیا اپ گریڈ API کا معمول کا HTTP خرابی لفافہ لے کر آتا ہے: 401 invalid_token، 410 session_expired، یا 503 unavailable کے ساتھ Retry-After ہیڈر جس کا احترام کرنا ہے۔
دوسری بات: سیچوریشن خرابی کی بجائے خاموشی کی طرح نظر آتی ہے۔ اگر 30 سیکنڈ کے اندر کوئی بھی فریم نہ آئے، تو اسے capacity_exceeded سمجھیں اور دوبارہ کنیکٹ کریں؛ نیا سیشن عموماً کامیاب ہو جاتا ہے۔ اگر لوڈنگ فریمز آتے رہیں لیکن session.ready کبھی نہ آئے، تو اسے engine_unavailable سمجھیں، جو دوبارہ کوشش کے قابل بھی ہے۔ SDKs میں، RealtimeSession.connect session.ready پر حل ہوتا ہے اور readyDeadlineMs کے بعد درست کوڈ کے ساتھ ناکام ہو جاتا ہے۔
session.ready آنے کے بعد، سرور توقع کرتا ہے کہ آڈیو اسی رفتار سے آئے جس رفتار سے بولا جا رہا ہے۔
آڈیو خام PCM کے بائنری فریمز کی صورت میں جاتی ہے: 32-bit float، little-endian، 16 kHz، mono (pcm_f32le، سیشن کا پہلے سے طے شدہ audio فارمیٹ)۔ اسے جیسے ہی ریکارڈ ہو بھیجیں، تقریباً 100 سے 250 ملی سیکنڈ کے ٹکڑوں میں، اور ہر ٹکڑے کے درمیان ٹرانسکرپٹ کا انتظار کیے بغیر۔
بائٹس کا حساب ایک بار کرنے کے قابل ہے۔ ایک سیکنڈ میں 16,000 سیمپلز، ہر سیمپل 4 بائٹس کا، یعنی 64,000 بائٹس فی سیکنڈ؛ اس لیے 100 ms یعنی 6,400 بائٹس، اور 250 ms یعنی 16,000 بائٹس۔ اسپیچ ماڈلز عام طور پر 16 kHz mono پر تربیت یافتہ ہوتے ہیں، یہی وجہ ہے کہ اتنے سارے ریئل ٹائم APIs اسی کا مطالبہ کرتے ہیں، جبکہ مائکروفونز اور براؤزرز اکثر 44.1 یا 48 kHz پر اور stereo میں ریکارڈ کرتے ہیں۔ بائٹس بھیجنے سے پہلے کسی کو آڈیو کا ری سیمپل کرنا اور چینلز کو ایک میں ملانا ہوتا ہے: یہاں یہ کام 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 اس قطار کا سائز ہے، اور صحت مند لنک پر یہ تقریباً صفر کے قریب رہتا ہے۔ لائیو آڈیو کو روکا نہیں جا سکتا، اس لیے بڑھتی ہوئی قطار پر مفید ردعمل یہ ہے کہ اسے نوٹس کیا جائے، فریمز ڈراپ نہ کیے جائیں۔ سرور بھی بیک پریشر لاگو کرتا ہے۔ وہ حقیقی وقت سے چند سیکنڈ آگے تک آڈیو قبول کرتا ہے (limits.audioBufferS)، پھر جب تک پیچھے رہ جانے والا حصہ پورا نہ ہو، پڑھنا روک دیتا ہے۔ واپسی کے راستے پر، کمٹس بغیر نقصان کے قطار میں رہتے ہیں جبکہ ڈرافٹس یکجا کر دیے جاتے ہیں، تو سست ریڈر کو کم مگر تازہ تر ڈرافٹس ملتے ہیں۔ جو کلائنٹ کمٹس کے ساتھ بھی ساتھ نہ دے سکے، اسے slow_consumer اور retryable: false کے ساتھ error فریم ملتا ہے اور وہ ڈسکنیکٹ کر دیا جاتا ہے؛ اگر آپ کو یہ نظر آئے، تو آپ کا میسج ہینڈلر ضرورت سے زیادہ کام کر رہا ہے۔
براؤزر میں، @vocalstack/sdk/mic کا streamMicrophone مائکروفون کی اجازت مانگتا ہے، 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 جب تک جملہ کھلا ہو عارضی رہتے ہیں اور بند ہونے پر حتمی ہو جاتے ہیں۔ 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 فریم کے طور پر آتا ہے، جو کلپ کو 24 kHz mono پر base64 Ogg/Opus میں لے کر چلتا ہے۔ لمبا جملہ ٹکڑوں میں آتا ہے (parts کا part) تاکہ یکے بعد دیگرے چلایا جا سکے۔ عارضی متن کبھی نہیں بولا جاتا۔ لائیو ترجمے کی گائیڈ آوازوں کی تفصیل دیتی ہے، اور سیکنڈ کہاں جاتے ہیں تاخیر کی زنجیر کی وضاحت کرتا ہے۔
ہدف زبانیں تخلیق کے وقت طے نہیں ہوتیں۔
ایک session.update ٹیکسٹ فریم اس نقطے سے spokenLanguage، translate، speakerLabels، latencyMode، voice یا voiceByLang بدل دیتا ہے؛ جو فیلڈز آپ چھوڑ دیں وہ جوں کی توں رہتی ہیں۔
{"type":"session.update","translate":["es","fr"],"latencyMode":"fast"}
SDKs اسے session.update(…) کے طور پر پیش کرتے ہیں۔ translate پوری فہرست کی جگہ لیتا ہے، اور [] ترجمہ بند کر دیتا ہے۔ جو سیگمنٹس پہلے ہی پہنچ چکے ہیں انہیں دوبارہ ٹرانسکرپٹ یا ترجمہ نہیں کیا جاتا، اور اگر آپ اسپیکر لیبلز فعال کریں تو اسپیکرز صرف اسی نقطے سے لیبل ہوتے ہیں۔ جو کہا جا چکا وہ کہا ہوا ہی رہتا ہے؛ سیٹنگز صرف آنے والی چیزوں پر اثر ڈالتی ہیں۔
جب آڈیو ختم ہو، تو {"type":"eof"} بھیجیں (SDKs میں session.eof())۔ سرور جو کچھ اس کے پاس ابھی باقی ہے اس کی ٹرانسکرپشن کرتا ہے، آخری کمٹس بھیجتا ہے، پھر session.done بھیج کر بند ہو جاتا ہے۔ اس کے بجائے cancel بھیجنے سے وہ فوراً رک جاتا ہے اور باقی حصہ ضائع کر دیتا ہے۔
{"type":"session.done","reason":"eof","stats":{"audioS":30.0,"processingS":30.6,"realtimeFactor":0.98}}
reason یا تو eof ہوتا ہے، یا max_duration (سیشن نے پلان کی زیادہ سے زیادہ مدت چھو لی اور وہی فلش چلا) یا cancelled۔ stats سرور ناپتا ہے: audioS وہ آڈیو ہے جو آپ نے بھیجی، processingS وہ وال کلاک وقت ہے جو سیشن نے لیا، اور realtimeFactor ان دونوں کی نسبت ہے۔ لائیو سیشن 1 سے ذرا کم رہتا ہے، کیونکہ وال کلاک وقت میں آپ کے eof کے بعد کا آخری فلش شامل ہوتا ہے۔ اس سے کافی کم عدد کا مطلب ہے کہ سیشن آڈیو کی مدت سے نمایاں طور پر زیادہ وقت میں چلا؛ سب سے پہلے دیکھیں کہ آڈیو کتنی تیزی سے پہنچی۔ فائل کے لیے، وہی عدد بتاتا ہے کہ ٹرانسکرپٹ پلے بیک سے کتنی گنا تیزی سے واپس آیا۔
ایک سیشن زیادہ سے زیادہ GET /v1/realtime/capabilities سے حاصل ہونے والی limits.maxDurationS تک چلتا ہے، اور اس کی آڈیو آپ کے پلان کی لائیو شرح پر فی سیکنڈ آپ کے کریڈٹ سے کٹتی ہے، ہر ٹرانسکرپشن کی طرح اسی 5 منٹ کی کم از کم حد کے ساتھ: جو ہر سیشن آڈیو وصول کرتا ہے اس کا کم از کم 5 منٹ بل ہوتا ہے (قیمتیں)۔ اسی ساکٹ پر پوری فائل بھی جا سکتی ہے: سیشن input: { kind: "file" } کے ساتھ بنائیں، فائل کے اپنے بائٹس (mp3، wav، flac، ogg، m4a) اور eof بھیجیں، اور ٹرانسکرپٹ آڈیو چلنے سے کئی گنا تیزی سے واپس آتا ہے، راستے میں transcript.progress فریمز کے ساتھ۔
دوبارہ شروع کرنے کی کوئی سہولت نہیں۔ ٹوٹا ہوا کنکشن سیشن ختم کر دیتا ہے؛ دوبارہ کنیکٹ کرنے کا مطلب ہے نیا سیشن بنانا، جس کا اپنا کم از کم بل ہوتا ہے، اور اپنے لائیو ذریعے سے دوبارہ سٹریم کرنا، جہاں seq پھر 0 سے شروع ہوتا ہے۔ دوبارہ کنیکٹ کرنا خود کسی بھی ساکٹ API کا معیاری طریقہ ہے، اور اسے ایک بار درست کر لینا مفید ہے:
error فریم پر retryable: true، Retry-After کے ساتھ 503، یا بغیر فریم کے 30 سیکنڈ، جو capacity_exceeded کے مترادف ہے۔ invalid_token اور session_expired کا مطلب ہے کہ آپ کے سرور سے نیا streamUrl لیا جائے، پرانے کے ساتھ دوبارہ کوشش نہیں۔سیشن بنانے والے روٹ کو سستا رکھیں اور بند ہونے کو نئی شروعات سمجھیں۔ دوبارہ شروع کی سہولت نہ ہونا کچھ بھی محفوظ نہ کرنے کی قیمت ہے؛ اگر ٹرانسکرپٹ کو ساکٹ سے زیادہ دیر زندہ رہنا ہے، تو اسے خود محفوظ کریں۔
جب آپ چاہیں کہ وہی سٹریم بطور ریکارڈنگ محفوظ ہو، بعد میں درست کی جائے، ایکسپورٹ ہو، اور سامعین اپنی زبانوں میں پڑھیں، تو اسے اس کے بجائے ایک لائیو ٹرانسکرپشن پر شائع کریں۔ لائیو جانے کی گائیڈ اسی PCM ساکٹ کو ایک ٹرانسکرپشن اور ایک براڈکاسٹ لنک کے ذریعے سمجھاتی ہے، اور سننے کی گائیڈ سبسکرائبر کا رخ دکھاتی ہے، جہاں ہر سامع ایک زبان چنتا ہے اور دیر سے شامل ہونے والا lastSeq کرسر سے پیچھے رہ جانے کو پورا کرتا ہے۔ کسی کمرے کے لیے یہ کیسا دکھتا ہے، یہ ایک لنک، ہر زبان کا موضوع ہے۔
کسی فائل کو محفوظ رکھنا ہو تو محفوظ شدہ راستہ API کے ساتھ فائل کی ٹرانسکرپشن ہے۔ ہر فیلڈ سمیت مکمل فریم حوالہ، API ریفرنس میں مائکروفون سٹریم کریں گائیڈ ہے۔