En opplasting gir en jobb, ikke en transkripsjon. Legg en Idempotency-Key på hvert opprettelseskall, slik at et nytt forsøk aldri kan opprette eller fakturere en ekstra transkripsjon; deretter spør du jobben til den er ferdig, og leser setninger, ord eller en eksport fra samme id.
En filtranskripsjon er den enkleste tingen VocalStack-API-et gjør, og den har formen til enhver asynkron jobb: last opp, vent, les. Opplastingen svarer straks med en transkripsjon og dens jobb, men ingen ord ennå. Mens programmet ditt spør etter jobben, transkriberer serveren filen mange ganger raskere enn lyden spilles av. Når tilstanden er done, leser du transkripsjonen som setninger med talere og tidspunkter, eller eksporterer den som en fil i ett av fire formater, enten på det talte språket eller oversatt.
Alt i denne formen gjelder for ethvert batch-API: et opprettelseskall som gir en id, en status du spør etter, et resultat du henter etterpå. Bare feltnavnene er spesifikke for VocalStack, og denne veiledningen går gjennom dem med TypeScript- og Python-SDK-ene, og HTTP-kallene der de hjelper.
Åpne Developers → API keys i Dashboardet og opprett en nøkkel med tillatelsene Read og Write. Nøkkelen vises bare én gang; kopier den til en miljøvariabel:
export VOCALSTACK_API_KEY="<your API key>"
En nøkkel er for servere og skript. Alt som sendes til en nettleser, sender nøkkelen med seg, så hold den ute av sidekode og ut av URL-er, som ender opp i logger. Vil du strømme fra en nettleser, oppretter serveren din en sanntidsøkt for den; det er en annen veiledning.
Hvert kall autentiserer med nøkkelen som bærertoken:
Authorization: Bearer vs_key_...
Med nøkkelen i miljøet er neste steg en klient som sender den.
Det finnes to SDK-er. Hver har én klient, VocalStack, med metodene gruppert etter ressurs: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime og noen få til. TypeScript-SDK-en kjører på Node 20 eller nyere og i nettlesere; Python-SDK-en er bygget på asyncio og krever Python 3.10 eller nyere.
SDK-ene er ikke på npm eller PyPI. API-et serverer byggene til alle kontoer. Installasjonskommandoene, fylt ut for ditt miljø, står i Install-delen av hurtigstarten i API-referansen, og de sender nøkkelen din bare til API-verten. Vil du ikke installere noe, trenger HTTP-kallene under bare curl og jq.
Uansett hva du valgte, er det første kallet opplastingen.
POST /v1/transcriptions tar filen som multipart/form-data: en file-del, pluss en valgfri options-del med JSON. Den svarer 201 med transkripsjonen. Dens job.recordingId er id-en til opptaket laget av filen, og alle kall som følger, adresseres med den id-en.
To innstillinger betyr noe for en første kjøring, begge inne i options.settings:
spokenLanguage: en kode som en. Utelater du den, oppdages språket. Å oppgi den gir bedre nøyaktighet på støyende opptak og tung aksent.speakerLabels: auto, on eller off. Med on får hver setning et talernummer; med auto vises etiketter bare når en andre taler er hørt.De fleste lyd- og videofiler godtas: mp3, wav, flac, ogg, m4a, mp4 og flere. En video beholder bare lyden sin. Formatet betyr lite for nøyaktigheten, fordi talegjenkjenning arbeider på 16 kHz mono, og alt resamples til det ved ankomst. En 48 kHz stereo-WAV med én person som snakker, bruker det meste av bytene sine på å beskrive stillhet i høy kvalitet. Den transkriberes akkurat like godt som en 64 kbps mono-MP3 eller Opus-fil av samme tale, men den er omtrent tjue ganger større, så opplastingen tar tjue ganger så lang tid. Konverterer du før opplasting, er mono og en moderat bitrate de to innstillingene som er verdt å endre.
Send en Idempotency-Key med hver opplasting, og forstå hvorfor. En nettverkstidsavbrudd forteller deg ingenting om hva serveren gjorde: den kan ha aldri sett forespørselen, eller den kan ha lagret filen og satt jobben i kø før svaret gikk tapt. Et blindt nytt forsøk risikerer derfor en ny transkripsjon og en ny belastning. Nøkkelen løser det. Når en forespørsel gjentas med samme nøkkel og samme innhold innen 24 timer, returnerer serveren det første svaret i stedet for å opprette noe, noe som gjør «minst én gang» til «effektivt én gang». Regelen er altså én nøkkel per logisk opplasting, generert før første forsøk og gjenbrukt ved hvert nytt forsøk. En ny nøkkel per forsøk ville gjøre hvert nytt forsøk til en ny opplasting, og headeren ville ikke beskytte noe.
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
Det samme kallet med 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-svaret kommer før ett eneste ord er transkribert; det du har, er en jobb-id, så neste steg er å vente på den.
En jobb går fra queued → transcribing → done, eller ender i failed med en error-melding og, når årsaken er maskinlesbar, en errorCode. En fjerde tilstand, cropping, betyr at jobben står parkert til noen velger hvilket utsnitt av opptaket som skal transkriberes, slik Dashboardets trinn for klipping gjør. En vanlig opplasting kommer aldri inn i den, selv om jobs.wait også returnerer når en jobb når dit. Les GET /v1/recordings/{id}/job til tilstanden er done eller failed.
Det finnes ingen webhooks, og det er en bevisst avveining. En webhook krever et offentlig HTTPS-endepunkt på din side, en signatursjekk slik at ingen andre kan kalle den, og en retry-policy for når endepunktet ditt er nede, og det er mye rørlegging for en jobb som tar et minutt. Polling trenger ingenting av det. Skriptet ditt spør «ferdig nå?» hvert annet sekund, som et barn på baksetet, men i motsetning til barnet får det tilbake en prosentandel.
En nettside som allerede er åpen, kan i stedet følge transkripsjonens hendelsessocket (WS /v1/transcriptions/{id}/events, en job.progress-ramme etter hvert som jobben går fremover). For et skript er polling det riktige verktøyet.
SDK-ene gjør polling: vs.jobs.wait leser jobben hvert 2. sekund og gir hver lesing til onProgress (on_progress i Python). Jobbens progress har pct (0 til 100) og processedS, antall mediesekunder transkribert så langt. Programmene under skriver ut en linje bare når tilstanden eller prosenten endrer seg.
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}")
Merk at wait ikke kaster feil på failed; den returnerer jobben og overlater sjekken til deg. To sekunder passer for en samtale. For et tre timer langt opptak, eller en batch med hundrevis, øk intervallet (intervalMs, interval_s) eller la det vente lenger etter hvert som jobben går: de første lesningene forteller deg mest, og de senere bekrefter mest at serveren fortsatt er travel. Uten SDK skriver du den samme løkken for hånd:
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
Når tilstanden viser done, er transkripsjonen klar.
GET /v1/recordings/{id}/transcript svarer med hele transkripsjonen i ett dokument:
sentences: det man skal vise folk. Hver har text, speaker, og start og end i sekunder.segments: motorens linjer, hver med ordnivå-tidspunkter i words (hvert ord har text, start og end).speakers: én oppføring per talernummer, med dets name.Talerne er nummerert fra 0. En speaker på -1 betyr at talaretiketter var av for opptaket. Legg til ?lang=es for å lese transkripsjonen oversatt til spansk; setningene kommer tilbake på det språket.
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'
De to tidsnivåene tjener to oppgaver. Setninger er enheten for lesing, så det er dem en undertekst eller en transkripsjonsside viser. Ord er enheten for peking, så en karaoke-stil uthevning, et søketreff eller et klipp går gjennom segments[].words i stedet. Eksportformatene er bygd av setninger.
GET /v1/recordings/{id}/export returnerer en ekte fil. format er en av srt, json, xlsx eller pdf, og filen inkluderer eventuelle rettelser. Legg til lang= for en oversatt fil: eksporten leses da på det språket, én linje per setning, og enhver setning som ennå ikke er oversatt, oversettes først.
SRT er verdt å kjenne igjen, fordi det er undertekstformatet nesten alle avspillere godtar, og det er ren tekst. En fil er en liste med cues skilt av tomme linjer, og hver cue er en teller, et tidsrom og teksten. Tidsstemplene bruker komma før millisekundene, en fransk vane formatet aldri ble kvitt:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Én setning per cue er riktig størrelse: hver cue kan leses på den tiden den står på skjermen, og ingen kuttes midt i en tanke. Når opptaket hadde talaretiketter, begynner teksten i hver cue med talerens navn og et kolon.
I TypeScript-SDK-en returnerer vs.recordings.export en Response; les bytene med arrayBuffer(). I Python returnerer den 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
Transkripsjonen er lagret, så den vises under Transcriptions i Dashboardet, kan deles med lenke og kan leses igjen når som helst senere. Det er kjernen: opprett, vent, les, eksporter. Resten av denne veiledningen dekker variasjonene: medier som allerede er på nettet, feilene du vil møte, og hva en fil koster.
Er mediet allerede på nettet, gir du API-et lenken, og det henter filen selv: en offentlig http(s)-adresse (en mp3, en video, en podkastepisode) eller en YouTube-video. Kallet er det samme POST /v1/transcriptions, med en JSON-kropp { "url": "..." } i stedet for den flerdelte opplastingen, og name og settings som før. I SDK-ene gjør vs.transcriptions.createFromUrl(url) (TypeScript) og vs.transcriptions.create(url=...) (Python) den samme forespørselen.
Før du oppretter noe, forteller GET /v1/transcriptions/probe?url= deg hva en lenke er. Dens kind er en av tre:
file: et opptak med en slutt; transkriber det her.live: en YouTube-direktesending, en radiostrøm uten ende eller en HLS-spilleliste uten slutt. En direktelenke tar aldri slutt, så den kan ikke transkriberes som en fil; bruk den som kilde til en direkte transkripsjon i stedet.unknown: serveren kunne ikke avgjøre det.Når lenken ikke åpnes som media i det hele tatt (en nettside, for eksempel), er ok false, og problem forklarer hvorfor i én setning. Probe-kallet er valgfritt og begrenset til 60 kall i minuttet.
En lenkejobb starter i fetching før den går til transcribing og done. Mens den henter, sier progress.phase hva serveren gjør: fetching mens bytene kommer (med fetchedBytes og fetchTotalBytes når den andre enden sier hvor mange det er), eller extracting mens lyden tas ut av en video. progress.phasePct sier hvor langt jobben er i den fasen. En lang video tar en stund, så spør hvert par sekund; vs.jobs.wait tar intervalMs (interval_s i Python) for det.
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}%`);
},
});
Referansens Transkriber en lenke-guide har hele programmet på begge språk og i HTTP. Henting fra lenker gir flere måter å feile på, så her er feilene du faktisk vil se.
Hvert mislykket kall svarer med én JSON-envelope. code er strengen å forgrene på, og status er HTTP-statusen; message er for mennesker og requestId for støtte. Under data sier retryable om å sende den samme forespørselen igjen kan hjelpe, og details holder det koden trenger. SDK-ene kaster én feiltype, VocalStackError, med de samme feltene.
Tre koder er verdt å håndtere fra første dag.
402 insufficient_credit: kontoens kreditt er brukt opp, og ingenting ble belastet. details.balance er saldoen i dollar, og details.topUpUrl er stedet der du legger til kreditt. Når kostnaden var kjent på forhånd, slik den er for en fil med kjent lengde, sier details.required hva den ville vært.429 rate_limited: vent Retry-After sekunder. Når details.metric er creates_per_hour, har du nådd planens opprettelsesrate (nye transkripsjoner i timen), ikke en burst-grense; limit og resetsAt sier hva taket er og når det frigjøres.503 unavailable: tjenesten er midlertidig utilgjengelig. Send forespørselen på nytt etter Retry-After.SDK-ene prøver på nytt en 429 og en 503 etter Retry-After, to ganger som standard (maxRetries / max_retries på klienten). En 503 prøves likevel bare på nytt ved et kall som er trygt å sende to ganger: en GET, eller en opprettelse med en Idempotency-Key. En opprettelse uten en slik nøkkel overlates til deg, fordi SDK-en ikke kan vite om det første forsøket gikk gjennom. Det er den praktiske grunnen til å sette en nøkkel på hver opplasting.
Hvert svar har også RateLimit-Limit, RateLimit-Remaining og RateLimit-Reset, slik at en travel pipeline kan senke farten før 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…"
}
Hele tabellen over koder finnes i Feil, nye forsøk og grenser. Den 402-en er den eneste feilen knyttet til penger, så her er hva en fil koster.
Filer koster $0.20 per time lyd, faktureres per sekund ($0.0033 i minuttet) med et minimum på 5 minutter: en 30 minutter lang fil koster $0.10. Det er ti cent, mindre enn kaffen du drikker mens fremdriftslinjen fyller seg. Prisen er den samme på alle planer, og den samme gjennom API-et som i Dashboardet. En ny konto starter med en engangskreditt på $5 ved registrering, og det kreves ingen kort.
Tre planbegrensninger berører denne veiledningen.
GET /v1/realtime/capabilities rapporterer som limits.maxUploadBytes. Den minste av de to gjelder, og en fil over den avvises med en 413 før noe transkriberes. For et langt opptak er dette enda en grunn til å laste opp en komprimert mono-fil i stedet for en WAV.GET /v1/billing rapporterer planen din, saldo og grenser når som helst; hele tabellen står på prissiden.
Videre er neste steg streaming av lyd i sanntid når du ikke kan vente til en fil er ferdig, eller å koble en KI-agent til samme konto via MCP-serveren, slik at den transkriberer og søker for deg uten noe av koden over.