En upload returnerer et job, ikke en transskription. Sæt en Idempotency-Key på hvert create, så et genforsøg aldrig kan oprette eller faktureres for en ekstra transskription. Tjek derefter jobbet, til det er færdigt, og læs sætninger, ord eller en eksport ud fra det samme id.
En filtranskription er det enkleste, VocalStack-API'et gør, og den har formen af ethvert asynkront job: upload, vent, læs. Uploaden svarer med det samme med en transskription og dens job, men endnu ingen ord. Mens dit program tjekker jobbet, transskriberer serveren filen mange gange hurtigere, end lyden afspilles. Når tilstanden er done, læser du transskriptionen som sætninger med talere og tidspunkter, eller eksporterer den som en fil i et af fire formater, enten på det talte sprog eller oversat.
Alt i denne form gælder for ethvert batch-API: et create, der returnerer et id, en status du tjekker, og et resultat du henter bagefter. Kun feltnavnene er specifikke for VocalStack, og denne vejledning gennemgår dem med TypeScript- og Python-SDK'erne samt HTTP-kaldene, hvor de hjælper.
Åbn Developers → API keys i Dashboardet og opret en nøgle med tilladelserne Read og Write. Nøglen vises kun én gang; kopiér den til en miljøvariabel:
export VOCALSTACK_API_KEY="<your API key>"
En nøgle er til servere og scripts. Alt, der sendes til en browser, sender nøglen med sig, så hold den ude af sidekode og ude af URL'er, som ender i logs. Hvis du vil streame fra en browser, opretter din server en realtidssession til den; det er en anden vejledning.
Hvert kald godkendes med nøglen som bearer-token:
Authorization: Bearer vs_key_...
Med nøglen i dit miljø er næste skridt en klient til at sende den.
Der er to SDK'er. Hver har én klient, VocalStack, med metoderne grupperet efter ressource: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime og nogle få flere. TypeScript-SDK'et kører på Node 20 eller nyere og i browsere; Python-SDK'et er bygget på asyncio og kræver Python 3.10 eller nyere.
SDK'erne findes ikke på npm eller PyPI. API'et serverer builds til alle konti. Installationskommandoerne, udfyldt til dit miljø, står i installationsafsnittet i hurtigstarten i API-referencen, og de sender kun din nøgle til API-værten. Hvis du hellere vil installere intet, kræver HTTP-kaldene nedenfor kun curl og jq.
Uanset valget er det første kald uploaden.
POST /v1/transcriptions tager filen som multipart/form-data: en file-del plus en valgfri options-del med JSON. Det svarer 201 med transskriptionen. Dens job.recordingId er id'et for optagelsen, der er lavet af filen, og alle kald, der følger, adresseres med det id.
To indstillinger betyder noget ved en første kørsel, begge inde i options.settings:
spokenLanguage: en kode som en. Udelader du den, registreres sproget automatisk. Angiver du den, bliver nøjagtigheden bedre på støjende optagelser og kraftige accenter.speakerLabels: auto, on eller off. Med on får hver sætning et talernummer; med auto vises mærkater først, når en anden taler er blevet hørt.De fleste lyd- og videofiler accepteres: mp3, wav, flac, ogg, m4a, mp4 og flere. En video beholder kun sit lydspor. Formatet betyder næppe noget for nøjagtigheden, fordi talegenkendelse arbejder med 16 kHz mono, og alt omsamples til det ved modtagelse. En 48 kHz stereo-WAV med én person, der taler, bruger det meste af sine bytes på at beskrive stilhed i høj kvalitet. Den transskriberes præcis lige så godt som en 64 kbps mono-MP3- eller Opus-fil af den samme tale, men den er omkring tyve gange større og tager dermed tyve gange så lang tid at uploade. Hvis du konverterer før upload, er mono og en moderat bitrate de to indstillinger, der er værd at ændre.
Send en Idempotency-Key med hver upload, og forstå hvorfor. En netværkstimeout fortæller dig intet om, hvad serveren gjorde: den har måske aldrig set anmodningen, eller den har gemt filen og sat jobbet i kø, før svaret gik tabt. Et blindt genforsøg risikerer derfor en ekstra transskription og en ekstra opkrævning. Nøglen løser det. Når en anmodning gentages med samme nøgle og samme indhold inden for 24 timer, returnerer serveren det første svar i stedet for at oprette noget, hvilket gør »mindst én gang« til »effektivt én gang«. Reglen er derfor én nøgle pr. logisk upload, genereret før det første forsøg og genbrugt ved hvert genforsøg. En ny nøgle pr. forsøg ville gøre hvert genforsøg til en ny upload, og headeren ville ikke beskytte noget.
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 kald 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 ét eneste ord er transskriberet; det, du har, er et job-id, så næste skridt er at vente på det.
Et job går fra queued → transcribing → done, eller ender i failed med en error-besked og, når årsagen kan læses maskinelt, en errorCode. En fjerde tilstand, cropping, betyder, at jobbet står på pause, indtil nogen vælger, hvilken del af optagelsen der skal transskriberes, sådan som trinnet til beskæring i Dashboardet gør. En almindelig upload kommer aldrig ind i den tilstand, selvom jobs.wait også returnerer, når et job når dertil. Læs GET /v1/recordings/{id}/job, indtil tilstanden er done eller failed.
Der er ingen webhooks, og det er et bevidst valg. En webhook kræver et offentligt HTTPS-endpoint på din side, en signaturkontrol, så ingen andre kan kalde det, og en genforsøgspolitik til når dit endpoint er nede, hvilket er en hel del infrastruktur for et job, der tager et minut. Polling kræver ingen af det. Dit script spørger »færdig endnu?« hvert andet sekund, som et barn på bagsædet, og i modsætning til barnet får det en procentsats tilbage.
En webside, der allerede er åben, kan i stedet følge transskriptionens events-socket (WS /v1/transcriptions/{id}/events, en job.progress-ramme, mens jobbet skrider frem). Til et script er polling det rigtige værktøj.
SDK'erne står for pollingen: vs.jobs.wait læser jobbet hvert 2. sekund og giver hver læsning til onProgress (on_progress i Python). Jobbets progress indeholder pct (0 til 100) og processedS, de medie-sekunder, der er transskriberet indtil videre. Programmerne nedenfor udskriver kun en linje, når tilstanden eller procentsatsen ændrer sig.
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}")
Bemærk at wait ikke kaster en fejl ved failed; den returnerer jobbet og overlader kontrollen til dig. To sekunder passer til en tale. Ved en tre timer lang optagelse eller et batch på hundreder skal du forlænge intervallet (intervalMs, interval_s) eller lade det vokse, mens jobbet kører: de første læsninger fortæller mest, og de senere bekræfter mest, at serveren stadig er travl. Uden et SDK skriver du den samme løkke selv:
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 står done, er transskriptionen klar.
GET /v1/recordings/{id}/transcript svarer med hele transskriptionen i ét dokument:
sentences: det, man skal vise folk. Hver har text, speaker, og start og end i sekunder.segments: motorens linjer, hver med ordniveau-tidspunkter i words (hvert ord har text, start og end).speakers: én post pr. talernummer med dets name.Talere nummereres fra 0. En speaker på -1 betyder, at talermærkater var slået fra for optagelsen. Tilføj ?lang=es for at læse transskriptionen oversat til spansk; sætningerne kommer tilbage på det sprog.
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 tidsniveauer tjener to formål. Sætninger er enheden for læsning, så det er dem, en undertekst eller en transskriptionsside viser. Ord er enheden for at pege, så en karaoke-agtig fremhævning, et søgeresultat eller et klip går gennem segments[].words i stedet. Eksportformaterne er bygget af sætninger.
GET /v1/recordings/{id}/export returnerer en rigtig fil. format er en af srt, json, xlsx eller pdf, og filen inkluderer alle rettelser. Tilføj lang= for en oversat fil: eksporten læser da på det sprog, én linje pr. sætning, og enhver sætning, der endnu ikke er oversat, oversættes først.
SRT er værd at kende af udseende, fordi det er undertekstformatet, næsten alle afspillere accepterer, og det er ren tekst. En fil er en liste af cues adskilt af tomme linjer, og hver cue er et tal, et tidsinterval og teksten. Tidsstemplerne bruger et komma før millisekunderne, en fransk vane, som formatet aldrig har lagt af:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Én sætning pr. cue er den rigtige kornethed: hver cue kan læses på den tid, den er på skærmen, og ingen klipper midt i en tanke. Når optagelsen havde talermærkater, starter hver cues tekst med talerens navn og et kolon.
I TypeScript-SDK'et returnerer vs.recordings.export et Response; læs dens bytes 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
Transskriptionen er gemt, så den vises under Transcriptions i Dashboardet, kan deles via link og kan læses igen når som helst senere. Det er kerneløkken: opret, vent, læs, eksportér. Resten af denne vejledning dækker variationerne: medier, der allerede er online, de fejl, du vil støde på, og hvad en fil koster.
Hvis mediet allerede er online, giver du API'et linket, og det henter filen selv: en offentlig http(s)-adresse (en mp3, en video, et podcast-afsnit) eller en YouTube-video. Kaldet er det samme POST /v1/transcriptions med en JSON-body { "url": "..." } i stedet for den multipart-upload, og name og settings som før. I SDK'erne laver vs.transcriptions.createFromUrl(url) (TypeScript) og vs.transcriptions.create(url=...) (Python) det samme kald.
Før du opretter noget, fortæller GET /v1/transcriptions/probe?url= dig, hvad et link er. Dets kind er en af tre:
file: en optagelse med en ende; transskriber den her.live: en YouTube-livestream, en endeløs radiostream eller en HLS-playliste uden ende. Et live-link slutter aldrig, så det kan ikke transskriberes som en fil; brug det i stedet som kilde til en live-transskription.unknown: serveren kunne ikke afgøre det.Når linket slet ikke åbner som medie (en webside, for eksempel), er ok false, og problem forklarer hvorfor i én sætning. Proben er valgfri og begrænset til 60 kald i minuttet.
Et link-job starter i fetching, før det går til transcribing og done. Mens det henter, fortæller progress.phase, hvad serveren laver: fetching, mens bytes kommer ind (med fetchedBytes og fetchTotalBytes, når den anden ende oplyser, hvor mange der er), eller extracting, mens lyden tages ud af en video. progress.phasePct siger, hvor langt jobbet er i den fase. En lang video tager et stykke tid, så tjek hvert par sekund; vs.jobs.wait tager intervalMs (interval_s i Python) til 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}%`);
},
});
Referencens guide Transskriber et link har det fulde program på begge sprog og i HTTP. Hentning fra links giver flere måder at fejle på, så her er de fejl, du faktisk vil se.
Hvert fejlede kald svarer med én JSON-envelope. code er strengen, du forgrener på, og status er HTTP-statussen; message er til mennesker og requestId til support. Under data fortæller retryable, om det at sende den samme anmodning igen kan hjælpe, og details indeholder det, koden har brug for. SDK'erne kaster én fejltype, VocalStackError, med de samme felter.
Tre koder er værd at håndtere fra første dag.
402 insufficient_credit: kontoens kredit er brugt op, og intet er blevet opkrævet. details.balance er saldoen i dollar, og details.topUpUrl er stedet, hvor man tilføjer kredit. Når prisen var kendt på forhånd, som for en fil med kendt længde, siger details.required, hvad den ville have været.429 rate_limited: vent Retry-After sekunder. Når details.metric er creates_per_hour, har du ramt planens oprettelsesrate (nye transskriptioner i timen), ikke en burst-grænse; limit og resetsAt siger, hvad loftet er, og hvornår det frigives.503 unavailable: tjenesten er midlertidigt utilgængelig. Send anmodningen igen efter Retry-After.SDK'erne forsøger igen ved 429 og 503 efter Retry-After, to gange som standard (maxRetries / max_retries på klienten). Et 503 gentages dog kun ved et kald, der er sikkert at sende to gange: en GET, eller et create med en Idempotency-Key. Et create uden nøgle overlades til dig, fordi SDK'et ikke kan vide, om første forsøg gik igennem. Det er den praktiske grund til at sætte en nøgle på hver upload.
Hvert svar bærer også RateLimit-Limit, RateLimit-Remaining og RateLimit-Reset, så en travl pipeline kan sagtne farten, før den rammer 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 findes i Fejl, genforsøg og grænser. Den 402 er den ene fejl, der er knyttet til penge, så her er, hvad en fil koster.
Filer koster $0.20 pr. time lyd, faktureret pr. sekund ($0.0033 i minuttet) med et minimum på 5 minutter: en fil på 30 minutter koster $0.10. Det er ti cent, mindre end den kaffe, du drikker, mens fremskridtsbjælken fyldes. Prisen er den samme på alle planer og gælder lige meget via API'et som i Dashboardet. En ny konto starter med en engangskredit på $5 ved tilmelding, og der kræves intet kort.
Tre planbegrænsninger berører denne vejledning.
GET /v1/realtime/capabilities rapporterer som limits.maxUploadBytes. Den mindste af de to gælder, og en fil over den afvises med en 413, før noget transskriberes. Ved en lang optagelse er det endnu en grund til at uploade en komprimeret mono-fil frem for en WAV.GET /v1/billing rapporterer din plan, saldo og grænser når som helst; den fulde tabel står på prissiden.
Herfra er de næste skridt streaming af lyd i realtid, når du ikke kan vente på, at en fil er færdig, eller tilslutning af en AI-agent til den samme konto via MCP-serveren, så den transskriberer og søger for dig uden nogen af koden ovenfor.