Ang upload ay nagbabalik ng job, hindi ng transcript. Maglagay ng Idempotency-Key sa bawat create para hindi makagawa o makasingil ng pangalawang transcription ang retry; pagkatapos, i-poll ang job hanggang maging done, at basahin ang mga sentence, word o export gamit ang iisang id.
Ang pag-transcribe ng file ang pinakasimpleng ginagawa ng VocalStack API, at may hugis ito ng anumang asynchronous job: i-upload, maghintay, bumasa. Agad na sumasagot ang upload ng isang transcription at ng job nito, pero wala pang salita. Habang ini-poll ng iyong programa ang job, mas mabilis ng maraming beses ang pag-transcribe ng server kaysa sa takbo ng audio. Kapag ang state ay done, binabasa mo ang transcript bilang mga sentence na may speaker at timing, o ini-export ito bilang file sa isa sa apat na format, sa orihinal na wika o isinalin.
Ang anumang batch API ay may ganitong hugis: isang create na nagbabalik ng id, isang status na i-poll mo, at isang resulta na kukunin mo pagkatapos. Ang mga pangalan ng field lang ang tiyak sa VocalStack, at dadaanan ito ng tutorial na ito gamit ang TypeScript at Python SDK, at ang mga HTTP call kung saan nakakatulong ang mga ito.
Buksan ang Developers → API keys sa Dashboard at gumawa ng key na may mga permiso na Read at Write. Ipinapakita ang key nang isang beses lang; kopyahin ito sa isang environment variable:
export VOCALSTACK_API_KEY="<your API key>"
Para sa mga server at script ang key. Ang anumang ipinapadala sa browser ay may kasamang key, kaya ilayo ito sa page code at sa mga URL, na napupunta sa mga log. Para mag-stream mula sa browser, gagawa ang iyong server ng realtime session para dito; iyon ay ibang tutorial.
Bawat tawag ay nagpapatotoo gamit ang key bilang bearer token:
Authorization: Bearer vs_key_...
Kapag nasa environment mo na ang key, ang susunod ay client na magpapadala nito.
May dalawang SDK. May tig-isang client ang bawat isa, VocalStack, at ang mga method nito ay nakapangkat ayon sa resource: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime at ilan pang iba. Tumatakbo ang TypeScript SDK sa Node 20 o mas bago at sa mga browser; ang Python SDK ay nakabatay sa asyncio at nangangailangan ng Python 3.10 o mas bago.
Wala sa npm o PyPI ang mga SDK. Inihahatid ng API ang mga build sa bawat account. Nasa seksyong Install ng quickstart sa API reference ang mga install command, na nakahanda para sa iyong environment, at ipinapadala lang nila ang iyong key sa API host. Kung ayaw mong mag-install ng kahit ano, kailangan lang ng mga HTTP call sa ibaba ang curl at jq.
Alinman ang pipiliin mo, ang unang tawag ay ang upload.
Kinukuha ng POST /v1/transcriptions ang file bilang multipart/form-data: isang file part, at opsyonal na options part na may JSON. Sumasagot ito ng 201 na may transcription. Ang job.recordingId nito ang id ng recording na ginawa mula sa file, at sa id na ito tinutukoy ang bawat tawag na susunod.
Dalawang setting ang mahalaga para sa unang takbo, pareho sa loob ng options.settings:
spokenLanguage: isang code gaya ng en. Kung iiwan mong wala, ita-tukoy ang wika. Ang pagbanggit dito ay nagpapabuti ng katumpakan sa maiingay na recording at malakas na accent.speakerLabels: auto, on o off. Sa on, may dalang speaker number ang bawat sentence; sa auto, lalabas lang ang mga label kapag may narinig nang pangalawang speaker.Karamihan sa audio at video file ay tinatanggap: mp3, wav, flac, ogg, m4a, mp4 at iba pa. Audio lang ang itinatago ng video. Halos walang epekto ang format sa katumpakan, dahil gumagana ang speech recognition sa 16 kHz mono at nire-resample ang lahat sa ganoon pagdating. Ang isang 48 kHz stereo WAV ng isang taong nagsasalita ay gumagamit ng halos lahat ng bytes nito para ilarawan ang katahimikan sa mataas na fidelity. Mai-transcribe ito nang eksaktong kasing-ganda ng 64 kbps mono MP3 o Opus file ng parehong usapan, pero mga dalawampung beses itong mas malaki, kaya dalawampung beses din ang tagal ng upload. Kung nagko-convert ka bago mag-upload, ang mono at katamtamang bitrate ang dalawang setting na sulit baguhin.
Magpadala ng Idempotency-Key sa bawat upload, at unawain kung bakit. Ang network timeout ay hindi nagsasabi kung ano ang ginawa ng server: maaaring hindi nito nakita ang request, o maaaring naitabi na nito ang file at naipila ang job bago pa mawala ang sagot. Kaya ang blind retry ay may panganib ng pangalawang transcription at pangalawang singil. Inaayos iyon ng key. Kapag inulit ang request na may parehong key at body sa loob ng 24 na oras, ibinabalik ng server ang unang sagot sa halip na gumawa ng bago, at nagiging "eksaktong isang beses" ang "kahit isang beses." Kaya ang patakaran ay isang key bawat lohikal na upload, ginawa bago ang unang pagsubok at ginagamit muli sa bawat retry. Ang bagong key sa bawat pagsubok ay gagawing bagong upload ang bawat retry, at walang maipoprotektahan ang 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
Ang parehong tawag gamit ang 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)
Ang 201 ay dumarating bago pa may salitang na-transcribe; ang hawak mo ay job id, kaya ang susunod na hakbang ay ang paghihintay dito.
Ang job ay gumagalaw mula queued → transcribing → done, o nagtatapos sa failed na may error na mensahe at, kapag machine-readable ang sanhi, may errorCode. Ang ikaapat na state, cropping, ay nangangahulugang nakahinto ang job hanggang may pumili kung aling bahagi ng recording ang ita-transcribe, gaya ng ginagawa ng trim step ng Dashboard. Hindi ito inaabot ng karaniwang upload, bagaman nagbabalik din ang jobs.wait kapag naabot ito ng job. Basahin ang GET /v1/recordings/{id}/job hanggang ang state ay done o failed.
Walang webhook, at sinadya iyon bilang trade-off. Kailangan ng webhook ng pampublikong HTTPS endpoint sa panig mo, ng signature check para walang ibang makatawag dito, at ng retry policy para sa oras na down ang endpoint mo, na maraming plumbing para sa job na tumatagal ng isang minuto. Walang kailangan sa polling. Itatanong ng script mo ang "tapos na ba?" tuwing dalawang segundo, parang batang nagtatanong sa likod ng kotse, at hindi tulad ng bata, porsyento ang ibinabalik nito.
Ang web page na bukas na ay maaaring sumunod sa events socket ng transcription (WS /v1/transcriptions/{id}/events, isang frame na job.progress habang umuusad ang job). Para sa script, polling ang tamang gamitin.
Ginagawa ng mga SDK ang polling: binabasa ng vs.jobs.wait ang job kada 2 segundo at ibinibigay ang bawat basa sa onProgress (on_progress sa Python). Ang progress ng job ay may pct (0 hanggang 100) at processedS, ang mga segundo ng media na na-transcribe na. Nagpi-print lang ng linya ang mga programa sa ibaba kapag nagbago ang state o ang porsyento.
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}")
Tandaan na hindi nagra-raise ang wait sa failed; ibinabalik nito ang job at ikaw ang bahala sa pagsuri. Sapat na ang dalawang segundo para sa isang usapan. Para sa tatlong oras na recording, o batch na daan-daan, palawakin ang agwat (intervalMs, interval_s) o unti-unting ipagpaliban habang tumatakbo ang job: ang mga unang basa ang pinakamaraming sinasabi, at ang mga susunod ay kadalasang kumukumpirma lang na abala pa rin ang server. Kung walang SDK, isulat mo ang parehong loop nang mano-mano:
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
Kapag ang state ay nagbasa ng done, handa na ang transcript.
Ibinabalik ng GET /v1/recordings/{id}/transcript ang buong transcript sa iisang dokumento:
sentences: ang ipapakita sa mga tao. May text, speaker, at start at end sa segundo ang bawat isa.segments: ang mga linya ng engine, bawat isa ay may timing sa antas ng salita sa words (may text, start at end ang bawat salita).speakers: isang entry bawat speaker number, na may name nito.Nagsisimula sa 0 ang numbering ng mga speaker. Ang speaker na -1 ay nangangahulugang naka-off ang speaker label para sa recording. Idagdag ang ?lang=es para basahin ang transcript na isinalin sa Spanish; ang mga sentence ay ibabalik sa wikang iyon.
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'
Dalawang antas ng timing ang may dalawang gawain. Ang mga sentence ang yunit ng pagbabasa, kaya sila ang ipinapakita ng subtitle o transcript page. Ang mga salita ang yunit ng pagtuturo, kaya ang karaoke-style na highlight, search hit o clip cut ay dumadaan sa segments[].words. Ang mga export format ay binubuo mula sa mga sentence.
Ang GET /v1/recordings/{id}/export ay nagbabalik ng totoong file. Ang format ay isa sa srt, json, xlsx o pdf, at kasama sa file ang anumang correction. Idagdag ang lang= para sa isinaling file: babasahin ang export sa wikang iyon, isang linya bawat sentence, at anumang sentence na hindi pa naisasalin ay isasalin muna.
Sulit kilalanin ang SRT dahil ito ang subtitle format na tinatanggap ng halos lahat ng player, at plain text ito. Ang file ay listahan ng cue na pinaghihiwalay ng blangkong linya, at ang bawat cue ay may counter, saklaw ng oras, at teksto. Ang mga timestamp ay gumagamit ng kuwit bago ang milliseconds, isang gawi mula sa Pranses na hindi na inalis ng format:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Ang isang sentence bawat cue ang tamang laki: nababasa ang bawat cue sa oras na nasa screen ito, at walang napuputol sa kalagitnaan ng isip. Kapag may speaker label ang recording, ang teksto ng bawat cue ay nagsisimula sa pangalan ng speaker at tutuldok.
Sa TypeScript SDK, ibinabalik ng vs.recordings.export ang isang Response; basahin ang bytes nito gamit ang arrayBuffer(). Sa Python, ibinabalik nito ang 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
Nakaimbak ang transcription, kaya lumalabas ito sa ilalim ng Transcriptions sa Dashboard, maibabahagi sa pamamagitan ng link, at mababasa muli anumang oras sa hinaharap. Iyon ang pangunahing ikot: create, wait, read, export. Sakop ng nalalabing bahagi ng tutorial na ito ang mga variation: media na online na, ang mga error na makakaharap mo, at kung magkano ang halaga ng file.
Kung online na ang media, ibigay sa API ang link at kukunin nito ang file mismo: isang pampublikong http(s) address (isang mp3, video, o episode ng podcast) o isang YouTube video. Pareho ang tawag, ang POST /v1/transcriptions, na may JSON body na { "url": "..." } sa halip na multipart upload, at ang name at settings gaya ng dati. Sa mga SDK, ginagawa ng vs.transcriptions.createFromUrl(url) (TypeScript) at vs.transcriptions.create(url=...) (Python) ang parehong request.
Bago gumawa ng kahit ano, sinasabi sa iyo ng GET /v1/transcriptions/probe?url= kung ano ang link. Ang kind nito ay isa sa tatlo:
file: recording na may katapusan; i-transcribe dito.live: YouTube live stream, radio stream na walang katapusan, o HLS playlist na walang dulo. Hindi natatapos ang live link, kaya hindi ito maaaring i-transcribe bilang file; gamitin ito bilang pinagmulan ng live transcription.unknown: hindi matukoy ng server.Kapag hindi talaga nabubuksan bilang media ang link (halimbawa, isang web page), false ang ok at ipinapaliwanag ng problem ang dahilan sa isang pangungusap. Opsyonal ang probe, at limitado sa 60 tawag kada minuto.
Nagsisimula ang link job sa fetching bago lumipat sa transcribing at done. Habang kumukuha, sinasabi ng progress.phase kung ano ang ginagawa ng server: fetching habang dumarating ang bytes (kasama ang fetchedBytes at fetchTotalBytes kapag sinasabi ng kabilang panig kung ilan), o extracting habang kinukuha ang audio mula sa video. Sinasabi ng progress.phasePct kung gaano na kalayo ang job sa phase na iyon. Matagal ang mahabang video, kaya mag-poll kada ilang segundo; tumatanggap ang vs.jobs.wait ng intervalMs (interval_s sa Python) para diyan.
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}%`);
},
});
Ang gabay na Transcribe a link sa reference ay may buong programa sa dalawang wika at sa HTTP. Dahil nagdaragdag ng mga paraan ng pagkabigo ang pagkuha mula sa link, narito ang mga error na talagang makikita mo.
Bawat nabigong tawag ay sumasagot ng iisang JSON envelope. Ang code ang string na sinusuri mo sa kondisyon, at ang status ang HTTP status; ang message ay para sa tao at ang requestId ay para sa support. Sa ilalim ng data, sinasabi ng retryable kung makakatulong ba ang pagpapadala muli ng parehong request, at naglalaman ang details ng anumang kailangan ng code. Naghahagis ang mga SDK ng isang uri ng error, VocalStackError, na may parehong field.
Tatlong code ang sulit hawakan sa unang araw.
402 insufficient_credit: ubos na ang credit ng account, at walang nasingil. Ang details.balance ay ang balanse sa dolyar, at ang details.topUpUrl ay kung saan magdadagdag ng credit. Kapag alam na ang halaga nang maaga, gaya ng file na kilala ang haba, sinasabi ng details.required kung magkano sana ito.429 rate_limited: maghintay ng Retry-After na segundo. Kapag creates_per_hour ang details.metric, naabot mo ang rate ng paggawa ng plan (mga bagong transcription kada oras), hindi burst limit; sinasabi ng limit at resetsAt kung ano ang cap at kailan ito magkakaroon ulit ng bakante.503 unavailable: pansamantalang hindi available ang serbisyo. Ipadala muli ang request pagkatapos ng Retry-After.Nire-retry ng mga SDK ang 429 at 503 pagkatapos ng Retry-After, dalawang beses bilang default (maxRetries / max_retries sa client). Ang 503, gayunpaman, ay nire-retry lamang sa tawag na ligtas ipadala nang dalawang beses: isang GET, o isang create na may kasamang Idempotency-Key. Ang create na walang key ay iniiwan sa iyo, dahil hindi malalaman ng SDK kung dumaan ang unang pagsubok. Iyon ang praktikal na dahilan kung bakit maglagay ng key sa bawat upload.
Bawat response ay may dala ring RateLimit-Limit, RateLimit-Remaining at RateLimit-Reset, kaya ang abalang pipeline ay maaaring bumagal bago pa dumating ang 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…"
}
Ang buong talaan ng mga code ay nasa Errors, retries and limits. Ang 402 na iyon ang tanging error na may kinalaman sa pera, kaya narito kung magkano ang halaga ng file.
Ang mga file ay $0.20 bawat oras ng audio, sinisingil bawat segundo ($0.0033 bawat minuto) na may 5-minutong minimum: ang 30-minutong file ay $0.10. Sampung sentimo iyon, mas mura pa kaysa sa kapeng iinumin mo habang pumupuno ang progress bar. Pareho ang rate sa lahat ng plan, at pareho sa API at sa Dashboard. Nagsisimula ang bagong account sa isang beses na $5 signup credit, at hindi kailangan ng card.
Tatlong limit ng plan ang tumatama sa tutorial na ito.
GET /v1/realtime/capabilities bilang limits.maxUploadBytes. Ang mas maliit sa dalawa ang masusunod, at ang file na lalagpas doon ay tinatanggihan ng 413 bago pa ma-transcribe ang anuman. Para sa mahabang recording, isa pang dahilan ito para mag-upload ng compressed na mono file sa halip na WAV.Anumang oras, iniuulat ng GET /v1/billing ang plan, balanse at mga limit mo; ang buong talaan ay nasa pricing page.
Mula rito, ang mga susunod na hakbang ay pag-stream ng audio sa real time kapag hindi mo kayang maghintay na matapos ang file, o pagkonekta ng AI agent sa parehong account sa pamamagitan ng MCP server, para ito na ang mag-transcribe at maghanap para sa iyo nang walang kahit alin sa code sa itaas.