’n Oplaai gee ’n taak terug, nie ’n transkripsie nie. Sit ’n Idempotency-Key op elke skepping sodat ’n herprobeerpoging nooit ’n tweede transkripsie kan skep of faktureer nie; nagaan dan die taak totdat dit klaar is, en lees sinne, woorde of ’n uitvoer uit dieselfde id.
’n Lêertranskripsie is die eenvoudigste ding wat die VocalStack-API doen, en dit het die vorm van enige asinchrone taak: oplaai, wag, lees. Die oplaai antwoord dadelik met ’n transkripsie en sy taak, maar nog geen woorde nie. Terwyl jou program die taak nagaan, transkribeer die bediener die lêer baie keer vinniger as wat die oudio speel. Wanneer die status done is, lees jy die transkripsie as sinne met sprekers en tydsberekeninge, of voer dit uit as ’n lêer in een van vier formate, óf in die gesproke taal óf vertaal.
Alles in hierdie vorm pas op enige groep-API toe: ’n skepping wat ’n id teruggee, ’n status wat jy nagaan, ’n resultaat wat jy later haal. Slegs die veldname is spesifiek vir VocalStack, en hierdie tutoriaal loop daardeur met die TypeScript- en Python-SDK’s, en die HTTP-oproepe waar dit help.
Maak Developers → API keys in die dashboard oop en skep ’n sleutel met die Read- en Write-toestemmings. Die sleutel word net een keer gewys; kopieer dit na ’n omgewingsveranderlike:
export VOCALSTACK_API_KEY="<your API key>"
’n Sleutel is vir bedieners en skripte. Enigiets wat na ’n blaaier gestuur word, stuur die sleutel saam daarmee, so hou dit uit bladsykode en uit URL’s, wat in logboeke beland. Om vanaf ’n blaaier te stroom, skep jou bediener ’n intydse sessie daarvoor; dit is ’n ander tutoriaal.
Elke oproep verifieer met die sleutel as ’n draerttoken:
Authorization: Bearer vs_key_...
Met die sleutel in jou omgewing is die volgende ding ’n kliënt om dit te stuur.
Daar is twee SDK’s. Elkeen het een kliënt, VocalStack, met sy metodes gegroepeer volgens hulpbron: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime en ’n paar meer. Die TypeScript-SDK loop op Node 20 of nuwer en in blaaiers; die Python-SDK is op asyncio gebou en het Python 3.10 of nuwer nodig.
Die SDK’s is nie op npm of PyPI nie. Die API bedien die bouwerkies aan elke rekening. Die installasie-opdragte, ingevul vir jou omgewing, is in die afdeling Installeer van die vinnige begin in die API-verwysing, en hulle stuur jou sleutel net na die API-gasheer. As jy liever niks wil installeer nie, benodig die HTTP-oproepe hieronder net curl en jq.
Watter een jy ook kies, die eerste oproep is die oplaai.
POST /v1/transcriptions neem die lêer as multipart/form-data: ’n file-deel, plus ’n opsionele options-deel wat JSON hou. Dit antwoord met 201 en die transkripsie. Sy job.recordingId is die id van die opname wat uit die lêer gemaak is, en elke oproep wat volg, word by daardie id aangespreek.
Twee instellings maak saak vir ’n eerste lopie, albei binne options.settings:
spokenLanguage: ’n kode soos en. Laat dit weg en die taal word opgespoor. Om dit te noem, verbeter die akkuraatheid op geraasrige opnames en sterk aksente.speakerLabels: auto, on of off. Met on dra elke sin ’n sprekernommer; met auto verskyn etikette eers wanneer ’n tweede spreker gehoor is.Die meeste oudio- en video-lêers word aanvaar: mp3, wav, flac, ogg, m4a, mp4 en ander. ’n Video hou net sy oudio. Die formaat maak amper nie saak vir akkuraatheid nie, want spraakherkenning werk op 16 kHz mono en alles word by aankoms na daardie tempo herbemonster. ’n 48 kHz stereo-WAV van een persoon wat praat, bestee die meeste van sy grepe om stilte in hoë getrouheid te beskryf. Dit transkribeer net so goed as ’n 64 kbps mono-MP3- of Opus-lêer van dieselfde praatjie, maar dit is ongeveer twintig keer groter, so dit neem twintig keer so lank om op te laai. As jy voor die oplaai omskakel, is mono en ’n matige bitsnelheid die twee instellings wat die moeite werd is om te verander.
Stuur ’n Idempotency-Key met elke oplaai, en verstaan waarom. ’n Netwerk-tydsoorskryding sê niks vir jou oor wat die bediener gedoen het nie: dit het dalk die versoek nooit gesien nie, of dit het dalk die lêer gestoor en die taak in die tou geplaas voordat die antwoord verlore gegaan het. ’n Blinde herprobeer loop dus die risiko van ’n tweede transkripsie en ’n tweede heffing. Die sleutel los dit op. Wanneer ’n versoek binne 24 uur met dieselfde sleutel en liggaam herhaal word, gee die bediener die eerste antwoord terug in plaas van iets te skep, wat "ten minste een keer" in "doeltreffend een keer" verander. Die reël is dus een sleutel per logiese oplaai, gegenereer voor die eerste poging en hergebruik by elke herprobeerpoging. ’n Nuwe sleutel per poging sou elke herprobeerpoging ’n nuwe oplaai maak, en die kopstuk sou niks beskerm nie.
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
Dieselfde oproep met 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)
Die 201 kom terug voordat ’n enkele woord getranskribeer is; wat jy hou, is ’n taak-id, so die volgende stap is om daarop te wag.
’n Taak beweeg van queued → transcribing → done, of eindig in failed met ’n error-boodskap en, wanneer die oorsaak masjien-leesbaar is, ’n errorCode. ’n Vierde status, cropping, beteken die taak is geparkeer totdat iemand kies watter stuk van die opname getranskribeer moet word, soos die dashboard se snoeistap doen. ’n Gewone oplaai betree dit nooit nie, al gee jobs.wait ook terug wanneer ’n taak dit bereik. Lees GET /v1/recordings/{id}/job totdat die status done of failed is.
Daar is geen webhooks nie, en dit is ’n doelbewuste ruil. ’n Webhook benodig ’n openbare HTTPS-eindpunt aan jou kant, ’n handtekeningkontrole sodat niemand anders dit kan roep nie, en ’n herprobeerbeleid vir wanneer jou eindpunt af is, wat baie werk is vir ’n taak wat ’n minuut neem. Nagaan het niks van dit nodig nie. Jou skrip sal elke twee sekondes vra "klaar al?", soos ’n kind op die agterste sitplek, en anders as die kind kry dit ’n persentasie terug.
’n Webblad wat reeds oop is, kan eerder die transkripsie se gebeurtenisse-sokkel volg (WS /v1/transcriptions/{id}/events, ’n job.progress-raam soos die taak vorder). Vir ’n skrip is nagaan die regte instrument.
Die SDK’s doen die nagaan: vs.jobs.wait lees die taak elke 2 sekondes en gee elke lesing aan onProgress (on_progress in Python). Die taak se progress dra pct (0 tot 100) en processedS, die mediasekondes wat tot dusver getranskribeer is. Die programme hieronder druk net ’n reël wanneer die status of die persentasie verander.
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}")
Let daarop dat wait nie by failed opduik nie; dit gee die taak terug en laat die kontrole aan jou. Twee sekondes pas by ’n praatjie. Vir ’n opname van drie uur, of ’n groep van honderde, maak die interval wyer (intervalMs, interval_s) of wag langer soos die taak loop: die eerste lesings vertel jou die meeste, en die latere bevestig meestal dat die bediener nog besig is. Sonder ’n SDK skryf jy dieselfde lus met die hand:
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
Wanneer die status done lees, is die transkripsie gereed.
GET /v1/recordings/{id}/transcript antwoord met die hele transkripsie in een dokument:
sentences: wat aan mense gewys moet word. Elk het text, speaker, en start en end in sekondes.segments: die enjin se reëls, elk met woordvlak-tydsberekeninge in words (elke woord het text, start en end).speakers: een inskrywing per sprekernommer, met sy name.Sprekers word vanaf 0 genommer. ’n speaker van -1 beteken sprekeretikette was af vir die opname. Voeg ?lang=es by om die transkripsie in Spaans vertaal te lees; die sinne kom terug in daardie taal.
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'
Die twee tydsvlakke dien twee take. Sinne is die eenheid van lees, so hulle is wat ’n onderskrif of ’n transkripsiebladsy wys. Woorde is die eenheid van wys, so ’n karaoke-styl-uitlig, ’n soektreffer of ’n knipsel loop eerder deur segments[].words. Die uitvoerformate word uit sinne gebou.
GET /v1/recordings/{id}/export gee ’n werklike lêer terug. format is een van srt, json, xlsx of pdf, en die lêer sluit enige regstellings in. Voeg lang= by vir ’n vertaalde lêer: die uitvoer lees dan in daardie taal, een reël per sin, en enige sin wat nog nie vertaal is nie, word eers vertaal.
Dit is die moeite werd om SRT aan die oog te ken, want dit is die onderskriftformaat wat byna elke speler aanvaar, en dit is gewone teks. ’n Lêer is ’n lys van kues wat deur leë reëls geskei word, en elke kue is ’n teller, ’n tydreeks en die teks. Die tydstempels gebruik ’n komma voor die millisekondes, ’n Franse gewoonte wat die formaat nooit afgeskud het nie:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Een sin per kue is die regte korrel: elke kue kan gelees word in die tyd wat dit op die skerm is, en niks breek midde-in ’n gedagte af nie. Wanneer die opname sprekeretikette gehad het, begin die teks van elke kue met die spreker se naam en ’n dubbelpunt.
In die TypeScript-SDK gee vs.recordings.export ’n Response terug; lees sy grepe met arrayBuffer(). In Python gee dit bytes terug.
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
Die transkripsie word gestoor, so dit verskyn onder Transcriptions in die dashboard, kan per skakel gedeel word, en kan enige tyd later weer gelees word. Dit is die kernlus: skep, wag, lees, voer uit. Die res van hierdie tutoriaal dek die variasies: media wat reeds aanlyn is, die foute wat jy sal teëkom, en wat ’n lêer kos.
As die media reeds aanlyn is, gee die skakel aan die API en dit haal die lêer self: ’n openbare http(s)-adres (’n mp3, ’n video, ’n podcast-episode) of ’n YouTube-video. Die oproep is dieselfde POST /v1/transcriptions, met ’n JSON-liggaam { "url": "..." } in plaas van die multipart-oplaai, en name en settings soos voorheen. In die SDK’s maak vs.transcriptions.createFromUrl(url) (TypeScript) en vs.transcriptions.create(url=...) (Python) dieselfde versoek.
Voordat jy iets skep, vertel GET /v1/transcriptions/probe?url= jou wat ’n skakel is. Sy kind is een van drie:
file: ’n opname met ’n einde; transkribeer dit hier.live: ’n YouTube-regstreekse stroom, ’n eindelose radiostroom of ’n HLS-speellys sonder ’n einde. ’n Regstreekse skakel eindig nooit nie, so dit kan nie as ’n lêer getranskribeer word nie; gebruik dit eerder as die bron van ’n regstreekse transkripsie.unknown: die bediener kon nie sê nie.Wanneer die skakel glad nie as media oopmaak nie (’n webblad, byvoorbeeld), is ok false en verduidelik problem waarom in een sin. Die toets is opsioneel, en beperk tot 60 oproepe per minuut.
’n Skakeltaak begin in fetching voordat dit na transcribing en done beweeg. Terwyl dit haal, sê progress.phase wat die bediener doen: fetching terwyl grepe aankom (met fetchedBytes en fetchTotalBytes wanneer die ander kant sê hoeveel), of extracting terwyl die oudio uit ’n video gehaal word. progress.phasePct sê hoe ver die taak deur daardie fase is. ’n Lang video neem ’n tydjie, so vra elke paar sekondes; vs.jobs.wait neem intervalMs (interval_s in Python) daarvoor.
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}%`);
},
});
Die verwysing se gids Transkribeer ’n skakel het die volledige program in albei tale en HTTP. Om van skakels af te haal, voeg meer maniere van misluk by, so hier is die foute wat jy werklik sal sien.
Elke misluk oproep antwoord met een JSON-koevert. code is die string waarop jy vertak en status die HTTP-status; message is vir mense en requestId vir ondersteuning. Onder data sê retryable of dieselfde versoek weer stuur kan help, en details hou wat die kode ook al nodig het. Die SDK’s gooi een foutsoort, VocalStackError, met dieselfde velde.
Drie kodes is die moeite werd om op dag een te hanteer.
402 insufficient_credit: die rekening se krediet is op, en niks is gehef nie. details.balance is die saldo in dollar en details.topUpUrl is waar krediet bygevoeg kan word. Wanneer die koste vooraf bekend was, soos dit is vir ’n lêer van bekende lengte, sê details.required wat dit sou gewees het.429 rate_limited: wag Retry-After sekondes. Wanneer details.metric creates_per_hour is, het jy die plan se skeppingstempo bereik (nuwe transkripsies per uur), nie ’n uitbarstlimiet nie; limit en resetsAt sê wat die plafon is en wanneer dit vrykom.503 unavailable: die diens is kortstondig onbeskikbaar. Stuur die versoek weer na Retry-After.Die SDK’s herprobeer ’n 429 en ’n 503 na Retry-After, standaard twee keer (maxRetries / max_retries op die kliënt). ’n 503 word egter net herprobeer op ’n oproep wat veilig is om twee keer te stuur: ’n GET, of ’n skepping wat ’n Idempotency-Key dra. ’n Skepping sonder een word aan jou oorgelaat, want die SDK kan nie weet of die eerste poging deurgekom het nie. Dit is die praktiese rede om op elke oplaai ’n sleutel te sit.
Elke antwoord dra ook RateLimit-Limit, RateLimit-Remaining en RateLimit-Reset, so ’n besige pyplyn kan vertraag voordat die 429 kom.
{
"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…"
}
Die volledige tabel van kodes is in Foute, herprobeerpogings en limiete. Daardie 402 is die een fout wat aan geld gekoppel is, so hier is wat ’n lêer kos.
Lêers kos $0.20 per uur oudio, per sekonde gefaktureer ($0.0033 per minuut) met ’n minimum van 5 minute: ’n lêer van 30 minute kos $0.10. Dit is tien sent, minder as die koffie wat jy sal drink terwyl die vorderingsbalk vul. Die tarief is dieselfde op elke plan, en dieselfde deur die API as in die dashboard. ’n Nuwe rekening begin met ’n eenmalige $5 registrasiekrediet, en jy hoef nie ’n kaart te verskaf nie.
Drie planlimiete raak aan hierdie tutoriaal.
GET /v1/realtime/capabilities as limits.maxUploadBytes rapporteer. Die kleinste van die twee geld, en ’n lêer daarbo word met ’n 413 geweier voordat iets getranskribeer word. Vir ’n lang opname is dit nog ’n rede om ’n gekomprimeerde mono-lêer op te laai eerder as ’n WAV.GET /v1/billing rapporteer jou plan, saldo en limiete op enige tyd; die volledige tabel is op die pryseblad.
Vanaf hier is die volgende stappe oudio regstreeks stroom wanneer jy nie kan wag vir ’n lêer om klaar te wees nie, of ’n KI-agent aan dieselfde rekening koppel deur die MCP-bediener, sodat dit vir jou transkribeer en soek sonder enige van die kode hierbo.