अपलोड से ट्रांसक्रिप्ट नहीं, एक जॉब मिलता है। हर क्रिएट कॉल पर Idempotency-Key लगाएँ, ताकि दोबारा कोशिश से दूसरा ट्रांसक्रिप्शन कभी न बने और दोबारा बिलिंग न हो; फिर जॉब पूरा होने तक पोल करें, और उसी id से वाक्य, शब्द या एक्सपोर्ट पढ़ें।
फ़ाइल का ट्रांसक्रिप्शन VocalStack API का सबसे सरल काम है, और उसका ढाँचा किसी भी असिंक्रोनस जॉब जैसा है: अपलोड करें, इंतज़ार करें, पढ़ें। अपलोड तुरंत एक ट्रांसक्रिप्शन और उसका जॉब लौटा देता है, लेकिन अभी कोई शब्द नहीं होता। जब आपका प्रोग्राम जॉब को पोल करता है, तब सर्वर फ़ाइल को ऑडियो चलने की रफ़्तार से कहीं तेज़ ट्रांसक्राइब करता है। जब स्टेट done हो जाए, तो आप ट्रांसक्रिप्ट को स्पीकर और समय के साथ वाक्यों के रूप में पढ़ सकते हैं, या चार फ़ॉर्मैट में से किसी एक में फ़ाइल एक्सपोर्ट कर सकते हैं, बोली गई भाषा में या अनुवादित रूप में।
इस ढाँचे की हर चीज़ किसी भी बैच API पर लागू होती है: एक क्रिएट कॉल जो id लौटाए, एक स्टेटस जिसे आप पोल करें, और एक नतीजा जिसे बाद में लाएँ। सिर्फ़ फ़ील्ड के नाम VocalStack के लिए ख़ास हैं, और यह ट्यूटोरियल TypeScript और Python SDK के साथ, और जहाँ मदद करें वहाँ HTTP कॉल के साथ, इन्हें समझाता है।
डैशबोर्ड में Developers → API keys खोलें और Read और Write अनुमतियों वाली एक key बनाएँ। key एक बार ही दिखाई जाती है; उसे एक environment variable में कॉपी करें:
export VOCALSTACK_API_KEY="<your API key>"
key सर्वर और स्क्रिप्ट के लिए है। जो भी ब्राउज़र तक जाता है, उसके साथ key भी जाती है, इसलिए key को पेज के कोड से और URL से बाहर रखें, क्योंकि URL लॉग में पहुँच जाते हैं। ब्राउज़र से स्ट्रीम करने के लिए आपका सर्वर उसके लिए एक realtime session बनाता है; वह एक अलग ट्यूटोरियल है।
हर कॉल key को bearer token की तरह भेजकर ख़ुद को प्रमाणित करती है:
Authorization: Bearer vs_key_...
key आपके environment में हो, तो अगला कदम है उसे भेजने वाला एक client।
दो SDK हैं। हर एक में एक ही client है, VocalStack, जिसके methods resource के हिसाब से समूहित हैं: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime और कुछ और। TypeScript SDK Node 20 या उससे नए पर और ब्राउज़र में चलता है; Python SDK asyncio पर बना है और उसके लिए Python 3.10 या उससे नया चाहिए।
SDK npm या PyPI पर नहीं हैं। API हर अकाउंट को ये builds देता है। आपके environment के लिए भरे हुए इंस्टॉल कमांड API reference के quickstart के Install सेक्शन में हैं, और वे आपकी key सिर्फ़ API होस्ट को भेजते हैं। अगर आप कुछ भी इंस्टॉल नहीं करना चाहते, तो नीचे के HTTP कॉल्स के लिए सिर्फ़ curl और jq चाहिए।
जो भी तरीका चुनें, पहला कॉल अपलोड ही है।
POST /v1/transcriptions फ़ाइल को multipart/form-data के रूप में लेता है: एक file part, और वैकल्पिक options part जिसमें JSON होता है। यह 201 के साथ ट्रांसक्रिप्शन लौटाता है। इसके job.recordingId में उस फ़ाइल से बनी रिकॉर्डिंग का id होता है, और आगे की हर कॉल इसी id से होती है।
पहली बार चलाने के लिए दो सेटिंग्स मायने रखती हैं, दोनों options.settings के अंदर:
spokenLanguage: en जैसा एक कोड। इसे छोड़ दें तो भाषा अपने-आप पहचान ली जाती है। इसे बताने से शोर वाली रिकॉर्डिंग और भारी लहजे पर सटीकता बेहतर होती है।speakerLabels: auto, on या off। on के साथ हर वाक्य पर स्पीकर का नंबर होता है; auto के साथ लेबल तभी दिखते हैं जब दूसरा स्पीकर सुनाई दे चुका हो।ज़्यादातर ऑडियो और वीडियो फ़ाइलें स्वीकार होती हैं: mp3, wav, flac, ogg, m4a, mp4 और दूसरी। वीडियो में सिर्फ़ उसका ऑडियो रहता है। फ़ॉर्मैट से सटीकता पर ज़्यादा असर नहीं पड़ता, क्योंकि स्पीच रिकग्निशन 16 kHz मोनो पर काम करता है और आने पर सब कुछ उसी में resample हो जाता है। एक इंसान की बात वाली 48 kHz स्टीरियो WAV फ़ाइल के ज़्यादातर बाइट्स बहुत बारीकी से ख़ामोशी का वर्णन करते हैं। वह उतनी ही सटीकता से ट्रांसक्राइब होती है जितनी उसी बात की 64 kbps मोनो MP3 या Opus फ़ाइल, लेकिन वह लगभग बीस गुना बड़ी है, इसलिए उसे अपलोड होने में भी बीस गुना समय लगता है। अगर आप अपलोड से पहले बदलाव करते हैं, तो मोनो और मध्यम bitrate ही वे दो सेटिंग्स हैं जो बदलने लायक हैं।
हर upload के साथ Idempotency-Key भेजें, और यह समझें कि क्यों। नेटवर्क timeout से आपको यह नहीं पता चलता कि सर्वर ने क्या किया: हो सकता है उसने request देखी ही न हो, या फ़ाइल सेव कर ली हो और जॉब queue कर दिया हो, और जवाब रास्ते में खो गया हो। बिना सोचे दोबारा भेजने से दूसरा ट्रांसक्रिप्शन और दूसरा चार्ज बन सकता है। key यही रोकती है। जब वही key और वही body 24 घंटे के भीतर दोबारा भेजे जाते हैं, तो सर्वर कुछ नया नहीं बनाता, बल्कि पहला जवाब लौटा देता है, और "कम से कम एक बार" को "असल में एक बार" में बदल देता है। नियम यह है: हर तार्किक upload के लिए एक key, जो पहली कोशिश से पहले बने और हर दोबारा कोशिश में वही इस्तेमाल हो। हर कोशिश पर नई key लगाने से हर retry एक नया upload बन जाएगा, और header कुछ भी नहीं बचाएगा।
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 बिना एक भी शब्द ट्रांसक्राइब हुए आता है; आपके हाथ में एक job id है, इसलिए अगला कदम है उस जॉब का इंतज़ार।
जॉब queued → transcribing → done के क्रम में चलता है, या failed पर ख़त्म होता है, जिसमें error संदेश होता है और, अगर कारण मशीन-पठनीय हो, तो errorCode भी। एक चौथी स्टेट, cropping, का मतलब है कि जॉब रुका हुआ है, जब तक कोई यह न चुने कि रिकॉर्डिंग का कौन सा हिस्सा ट्रांसक्राइब होना है, जैसा डैशबोर्ड का trim step करता है। सामान्य upload इस स्टेट में कभी नहीं जाता, हालाँकि jobs.wait तब भी लौट आता है जब जॉब यहाँ पहुँचे। GET /v1/recordings/{id}/job को तब तक पढ़ें जब तक स्टेट done या failed न हो।
यहाँ कोई webhook नहीं है, और यह सोच-समझकर लिया गया फ़ैसला है। webhook के लिए आपकी ओर एक सार्वजनिक HTTPS endpoint चाहिए, एक signature check ताकि कोई और उसे कॉल न कर सके, और इस बात के लिए retry policy कि जब आपका endpoint डाउन हो तब क्या हो; एक ऐसे जॉब के लिए जो एक मिनट में पूरा होता है, यह काफ़ी झंझट है। पोलिंग को इनमें से कुछ भी नहीं चाहिए। आपकी स्क्रिप्ट हर दो सेकंड में पूछती रहेगी "हो गया?", पीछे की सीट पर बैठे बच्चे की तरह, लेकिन बच्चे से अलग उसे प्रतिशत भी मिलेगा।
जो वेब पेज पहले से खुला है, वह इसके बजाय ट्रांसक्रिप्शन का events socket (WS /v1/transcriptions/{id}/events, जो जॉब आगे बढ़ने पर job.progress फ़्रेम भेजता है) सुन सकता है। स्क्रिप्ट के लिए पोलिंग ही सही औज़ार है।
SDK पोलिंग ख़ुद करते हैं: vs.jobs.wait हर 2 सेकंड में जॉब पढ़ता है और हर रीड को onProgress (Python में on_progress) को देता है। जॉब के 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 पर error नहीं फेंकता; वह जॉब लौटा देता है और जाँच आप पर छोड़ता है। एक बातचीत-भरी रिकॉर्डिंग के लिए दो सेकंड ठीक हैं। तीन घंटे की रिकॉर्डिंग या सैकड़ों फ़ाइलों के batch के लिए अंतराल बढ़ाएँ (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 का मतलब है कि उस रिकॉर्डिंग के लिए speaker labels बंद थे। ट्रांसक्रिप्ट को स्पेनिश में अनुवादित पढ़ने के लिए ?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'
दो समय-स्तर दो काम के लिए हैं। वाक्य पढ़ने की इकाई हैं, इसलिए सबटाइटल या ट्रांसक्रिप्ट पेज उन्हीं को दिखाता है। शब्द इशारा करने की इकाई हैं, इसलिए karaoke-शैली की हाइलाइट, सर्च का मिलान या क्लिप काटना segments[].words में चलता है। एक्सपोर्ट फ़ॉर्मैट वाक्यों से ही बनते हैं।
GET /v1/recordings/{id}/export असली फ़ाइल लौटाता है। format इनमें से एक है: srt, json, xlsx या pdf, और फ़ाइल में कोई भी सुधार (corrections) शामिल होता है। अनुवादित फ़ाइल के लिए lang= जोड़ें: तब एक्सपोर्ट उस भाषा में पढ़ा जाता है, हर वाक्य एक पंक्ति में, और जो वाक्य अभी अनुवादित नहीं हुआ, उसे पहले अनुवादित किया जाता है।
SRT को पहचानना उपयोगी है, क्योंकि यह वह सबटाइटल फ़ॉर्मैट है जिसे लगभग हर प्लेयर स्वीकार करता है, और यह सादा टेक्स्ट है। फ़ाइल ख़ाली लाइनों से अलग किए गए cues की सूची है, और हर cue में एक क्रम-संख्या, एक समय-सीमा और टेक्स्ट होता है। timestamps में मिलीसेकंड से पहले कॉमा लगता है, एक फ़्रेंच आदत जिसे यह फ़ॉर्मैट कभी छोड़ नहीं पाया:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
हर cue में एक वाक्य रखना सही आकार है: हर cue स्क्रीन पर रहने के समय में पढ़ा जा सकता है, और कोई भी बीच विचार में नहीं कटता। जब रिकॉर्डिंग में speaker labels थे, तो हर cue का टेक्स्ट स्पीकर के नाम और एक कोलन से शुरू होता है।
TypeScript SDK में 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 के नीचे दिखता है, लिंक से साझा किया जा सकता है, और बाद में कभी भी दोबारा पढ़ा जा सकता है। यही मूल चक्र है: बनाएँ, इंतज़ार करें, पढ़ें, एक्सपोर्ट करें। इस ट्यूटोरियल का बाक़ी हिस्सा इसके बदलावों को समझाता है: जो मीडिया पहले से ऑनलाइन है, वे त्रुटियाँ जो आपको मिलेंगी, और फ़ाइल की लागत।
अगर मीडिया पहले से ऑनलाइन है, तो API को लिंक दे दें, और वह फ़ाइल ख़ुद लाता है: एक सार्वजनिक http(s) पता (mp3, वीडियो, पॉडकास्ट एपिसोड) या एक YouTube वीडियो। कॉल वही POST /v1/transcriptions है, multipart upload की जगह JSON body { "url": "..." } के साथ, और name और settings पहले की तरह। SDK में vs.transcriptions.createFromUrl(url) (TypeScript) और vs.transcriptions.create(url=...) (Python) वही request भेजते हैं।
कुछ भी बनाने से पहले, GET /v1/transcriptions/probe?url= बताता है कि लिंक क्या है। उसका kind तीन में से एक होता है:
file: एक ऐसी रिकॉर्डिंग जिसका अंत है; इसे यहाँ ट्रांसक्राइब करें।live: YouTube लाइव स्ट्रीम, बिना अंत वाला रेडियो स्ट्रीम या HLS playlist जिसका कोई अंत नहीं। लाइव लिंक कभी ख़त्म नहीं होता, इसलिए उसे फ़ाइल की तरह ट्रांसक्राइब नहीं किया जा सकता; उसे लाइव ट्रांसक्रिप्शन के स्रोत के रूप में इस्तेमाल करें।unknown: सर्वर तय नहीं कर पाया।जब लिंक मीडिया के रूप में खुलता ही नहीं (जैसे एक वेब पेज), तो ok false होता है और problem एक वाक्य में बताता है कि ऐसा क्यों है। probe वैकल्पिक है, और यह प्रति मिनट 60 कॉल तक सीमित है।
लिंक वाला जॉब fetching में शुरू होता है, फिर transcribing और done में जाता है। फ़ेच होते समय progress.phase बताता है कि सर्वर क्या कर रहा है: बाइट्स आते समय fetching (जब दूसरा छोर बताए कि कुल कितने हैं, तो fetchedBytes और fetchTotalBytes के साथ), या वीडियो से ऑडियो निकाला जाते समय extracting। progress.phasePct बताता है कि जॉब उस चरण में कितना आगे है। लंबे वीडियो में समय लगता है, इसलिए हर कुछ सेकंड में पोल करें; उसके लिए vs.jobs.wait intervalMs (Python में interval_s) लेता है।
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 envelope लौटाता है। code वह स्ट्रिंग है जिस पर आप शाखा बनाते हैं और status HTTP status है; message लोगों के लिए है और requestId सपोर्ट के लिए। data के अंदर retryable बताता है कि वही request दोबारा भेजने से मदद मिल सकती है या नहीं, और details में वह सब है जो उस code को चाहिए। SDK एक ही error प्रकार फेंकते हैं, VocalStackError, उन्हीं फ़ील्ड्स के साथ।
पहले दिन से तीन codes सँभालने लायक हैं।
402 insufficient_credit: अकाउंट का क्रेडिट ख़त्म हो गया है, और कुछ भी चार्ज नहीं हुआ। details.balance डॉलर में बैलेंस है और details.topUpUrl वह जगह है जहाँ क्रेडिट जोड़ा जाता है। जब लागत पहले से पता हो, जैसे किसी ज्ञात लंबाई की फ़ाइल के लिए होती है, तब details.required बताता है कि वह कितनी होती।429 rate_limited: Retry-After सेकंड इंतज़ार करें। जब details.metric creates_per_hour हो, तो आप प्लान की creation rate (प्रति घंटे नए ट्रांसक्रिप्शन) की सीमा तक पहुँच चुके हैं, न कि किसी burst limit तक; limit और resetsAt बताते हैं कि सीमा क्या है और वह कब खाली होगी।503 unavailable: सेवा थोड़ी देर के लिए उपलब्ध नहीं है। Retry-After के बाद request फिर भेजें।SDK 429 और 503 को Retry-After के बाद दोबारा भेजते हैं, डिफ़ॉल्ट रूप से दो बार (client पर maxRetries / max_retries)। हालाँकि 503 सिर्फ़ उस कॉल पर दोबारा भेजा जाता है जिसे दो बार भेजना सुरक्षित है: एक GET, या Idempotency-Key वाला create। बिना key वाला create आप पर छोड़ा जाता है, क्योंकि SDK नहीं जान सकता कि पहली कोशिश पहुँची या नहीं। हर upload पर key लगाने का व्यावहारिक कारण यही है।
हर response में RateLimit-Limit, RateLimit-Remaining और RateLimit-Reset भी आते हैं, ताकि एक व्यस्त pipeline 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…"
}
codes की पूरी तालिका Errors, retries and limits में है। वह 402 एकमात्र त्रुटि है जो पैसे से जुड़ी है, इसलिए अब देखें कि एक फ़ाइल की लागत क्या है।
फ़ाइलें $0.20 प्रति घंटा ऑडियो के हिसाब से लगती हैं, प्रति सेकंड बिल होती हैं ($0.0033 प्रति मिनट) और न्यूनतम 5 मिनट के साथ: 30 मिनट की फ़ाइल की लागत $0.10 है। यह दस सेंट है, उस कॉफ़ी से भी कम जो आप प्रोग्रेस बार भरने के दौरान पिएँगे। दर हर प्लान पर एक जैसी है, और API के ज़रिए भी वही है जो डैशबोर्ड में। नए अकाउंट को एक बार का $5 साइनअप क्रेडिट मिलता है, और कार्ड की ज़रूरत नहीं होती।
तीन प्लान सीमाएँ इस ट्यूटोरियल को छूती हैं।
GET /v1/realtime/capabilities limits.maxUploadBytes के रूप में बताता है। दोनों में से छोटी सीमा लागू होती है, और उससे बड़ी फ़ाइल कुछ भी ट्रांसक्राइब होने से पहले 413 के साथ लौटा दी जाती है। लंबी रिकॉर्डिंग के लिए यह एक और कारण है कि WAV के बजाय कम्प्रेस्ड मोनो फ़ाइल अपलोड करें।GET /v1/billing किसी भी समय आपका प्लान, बैलेंस और सीमाएँ बताता है; पूरी तालिका pricing page पर है।
आगे के कदम: जब आप फ़ाइल पूरी होने का इंतज़ार नहीं कर सकते, तो रीयल टाइम में ऑडियो स्ट्रीम करना, या उसी अकाउंट को MCP server के ज़रिए किसी AI एजेंट से जोड़ना, ताकि वह ऊपर के कोड के बिना आपके लिए ट्रांसक्राइब और खोज करे।