Įkėlimas grąžina užduotį, o ne nuorašą. Kiekviename sukūrimo kvietime įdėkite Idempotency-Key, kad pakartotinis bandymas niekada nesukurtų ir neapmokestintų antro transkribavimo; tada klausykite užduoties, kol ji bus atlikta, ir nuskaitykite sakinius, žodžius ar eksportą iš to paties id.
Failo transkribavimas – paprasčiausias dalykas, ką VocalStack API daro, ir jis turi įprastos asinchroninės užduoties formą: įkelti, laukti, skaityti. Įkėlimas iš karto atsako transkripcija ir jos užduotimi, bet dar be žodžių. Kol jūsų programa tikrina užduotį, serveris transkribuoja failą daug kartų greičiau, nei garsas grojamas. Kai būsena tampa done, nuorašą skaitote kaip sakinius su kalbėtojais ir laiko žymomis arba eksportuojate kaip failą vienu iš keturių formatų – tiek kalba, kuria kalbėta, tiek išverstą.
Viskas, kas turi šią schemą, tinka bet kuriam paketinio apdorojimo API: sukūrimas, grąžinantis id, būsena, kurią tikrinate, ir rezultatas, kurį gaunate vėliau. Tik laukų pavadinimai yra specifiniai VocalStack. Šis vadovas juos parodo su TypeScript ir Python SDK bei HTTP kvietimais, kur jie padeda.
Atidarykite valdymo skydelio skiltį Developers → API keys ir sukurkite raktą su leidimais Read ir Write. Raktas rodomas tik vieną kartą; nukopijuokite jį į aplinkos kintamąjį:
export VOCALSTACK_API_KEY="<your API key>"
Raktas skirtas serveriams ir scenarijams. Viskas, kas perduodama į naršyklę, kartu perduoda ir raktą, todėl laikykite jį už puslapio kodo ir URL ribų, nes URL patenka į žurnalus. Norint transliuoti iš naršyklės, jūsų serveris sukuria realaus laiko sesiją; tam skirtas kitas vadovas.
Kiekvienas kvietimas autentifikuojasi su raktu kaip bearer žetonu:
Authorization: Bearer vs_key_...
Kai raktas jau aplinkoje, kitas žingsnis – klientas, kuris jį siųs.
Yra du SDK. Kiekvienas turi vieną klientą, VocalStack, su metodais, sugrupuotais pagal išteklius: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime ir dar keletas. TypeScript SDK veikia Node 20 ar naujesne versija ir naršyklėse; Python SDK sukurtas ant asyncio ir reikalauja Python 3.10 ar naujesnės versijos.
SDK nėra npm ar PyPI. API pateikia versijas visoms paskyroms. Diegimo komandos, pritaikytos jūsų aplinkai, yra greitosios pradžios skyriuje Install API nuorodoje, ir jos siunčia jūsų raktą tik API serveriui. Jei nenorite nieko diegti, toliau pateiktiems HTTP kvietimams reikia tik curl ir jq.
Kad ir ką pasirinktumėte, pirmasis kvietimas – įkėlimas.
POST /v1/transcriptions priima failą kaip multipart/form-data: file dalį ir neprivalomą options dalį su JSON. Atsakymas 201 grąžina transkripciją. Jos job.recordingId yra iš failo sukurto įrašo id, ir visi tolesni kvietimai adresuojami šiuo id.
Pirmam paleidimui svarbu du nustatymai, abu options.settings viduje:
spokenLanguage: kodas, pavyzdžiui en. Jei jo nenurodysite, kalba bus aptikta automatiškai. Nurodžius kalbą, pagerėja tikslumas triukšminguose įrašuose ir stipriuose akcentuose.speakerLabels: auto, on arba off. Su on kiekvienas sakinys turi kalbėtojo numerį; su auto žymos atsiranda tik tada, kai pasigirsta antras kalbėtojas.Dauguma garso ir vaizdo failų priimami: mp3, wav, flac, ogg, m4a, mp4 ir kiti. Vaizdo įraše paliekamas tik garsas. Formatas beveik neturi įtakos tikslumui, nes kalbos atpažinimas dirba su 16 kHz mono signalu, o viskas atvykus perskaičiuojama į jį. 48 kHz stereo WAV įrašas, kuriame vienas žmogus kalba, daugiausia baitų skiria tylai aprašyti aukštos kokybės garsu. Jis transkribuojamas lygiai taip pat gerai kaip tos pačios kalbos 64 kbps mono MP3 ar Opus failas, bet yra apie dvidešimt kartų didesnis, todėl ir įkėlimas trunka dvidešimt kartų ilgiau. Jei konvertuojate prieš įkeldami, verta keisti du nustatymus: mono ir saikingą bitų srautą.
Kiekvienu įkėlimu siųskite Idempotency-Key ir suprantate, kodėl. Tinklo pasibaigęs laikas nieko nepasako apie tai, ką serveris padarė: jis galėjo niekada negauti užklausos arba išsaugoti failą ir įtraukti užduotį į eilę prieš tai, kai atsakymas dingo. Aklas pakartojimas todėl rizikuoja antru transkribavimu ir antru mokėjimu. Raktas tai išsprendžia. Kai užklausa pakartojama su tuo pačiu raktu ir turiniu per 24 valandas, serveris grąžina pirmąjį atsakymą, nieko nekurdamas, ir taip „bent kartą“ virsta „praktiškai vieną kartą“. Todėl taisyklė: vienas raktas vienam loginiam įkėlimui, sugeneruotas prieš pirmą bandymą ir naudojamas kiekvieno pakartojimo metu. Naujas raktas kiekvienam bandymui reikštų, kad kiekvienas pakartojimas yra naujas įkėlimas, ir antraštė nieko neapsaugotų.
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
Tas pats kvietimas su 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 ateina dar prieš transkribuojant nė vieno žodžio; ką gaunate, yra užduoties id, todėl kitas žingsnis – laukti jos.
Užduotis pereina queued → transcribing → done arba baigiasi failed su error pranešimu ir, kai priežastis mašinai suprantama, errorCode. Ketvirtoji būsena, cropping, reiškia, kad užduotis sustabdyta, kol kas nors pasirenka, kurią įrašo dalį transkribuoti, kaip tai daro valdymo skydelio apkirpimo žingsnis. Įprastas įkėlimas į ją niekada nepatenka, nors jobs.wait grąžina ir tada, kai užduotis į ją patenka. Skaitykite GET /v1/recordings/{id}/job, kol būsena bus done arba failed.
Webhook'ų nėra, ir tai sąmoningas kompromisas. Webhook'ui reikia viešo HTTPS galinio taško jūsų pusėje, parašo tikrinimo, kad niekas kitas negalėtų jo iškviesti, ir pakartojimo politikos, kai galinis taškas nepasiekiamas – tai daug sudėtingumo užduočiai, kuri trunka minutę. Apklausa to nereikalauja. Jūsų scenarijus kas dvi sekundes klaus „ar jau baigta?“ kaip vaikas užpakalinėje automobilio sėdynėje, tik skirtingai nei vaikas gaus procentų atsakymą.
Jau atidarytas žiniatinklio puslapis gali vietoj to sekti transkripcijos įvykių jungtį (WS /v1/transcriptions/{id}/events, job.progress kadras, kai užduotis progresuoja). Scenarijui geriausia įrankis – apklausa.
SDK pats atlieka apklausą: vs.jobs.wait skaito užduotį kas 2 sekundes ir kiekvieną skaitymą perduoda onProgress (Python – on_progress). Užduoties progress turi pct (nuo 0 iki 100) ir processedS, transkribuotas medijos sekundes. Toliau pateiktos programos išveda eilutę tik tada, kai pasikeičia būsena ar procentas.
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}")
Atkreipkite dėmesį, kad wait nemeta klaidos su failed; jis grąžina užduotį ir palieka patikrinimą jums. Dviejų sekundžių intervalas tinka pokalbiui. Trijų valandų įrašui arba šimtų failų paketui padidinkite intervalą (intervalMs, interval_s) arba mažinkite tikrinimo dažnį, kai užduotis vykdoma: pirmieji skaitymai pasako daugiausia, o vėlesni dažniausiai tik patvirtina, kad serveris dar užimtas. Be SDK tą pačią ciklą parašykite ranka:
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
Kai būsena rodo done, nuorašas paruoštas.
GET /v1/recordings/{id}/transcript atsako visu nuorašu vienu dokumentu:
sentences: tai, ką rodyti žmonėms. Kiekvienas turi text, speaker, o start ir end – sekundėmis.segments: variklio eilutės, kiekviena su žodžių lygio laikais words (kiekvienas žodis turi text, start ir end).speakers: po vieną įrašą kiekvienam kalbėtojo numeriui, su jo name.Kalbėtojai numeruojami nuo 0. speaker reikšmė -1 reiškia, kad įrašui kalbėtojų žymos buvo išjungtos. Pridėkite ?lang=es, kad gautumėte nuorašą išverstą į ispanų kalbą; sakiniai grįš ta kalba.
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'
Abu laiko lygiai tarnauja dviem darbams. Sakiniai yra skaitymo vienetas, todėl būtent juos rodo subtitrai ar nuorašo puslapis. Žodžiai yra rodymo vienetas, todėl paryškinimui stiliumi karaoke, paieškos rezultatui ar klipo iškirpimui naudokite segments[].words. Eksporto formatai sudaromi iš sakinių.
GET /v1/recordings/{id}/export grąžina tikrą failą. format yra vienas iš srt, json, xlsx ar pdf, o faile įtraukiami visi pataisymai. Pridėkite lang= išverstam failui: tada eksportas skaitomas ta kalba, po vieną eilutę kiekvienam sakiniui, o bet kuris dar neišverstas sakinys pirmiausia išverčiamas.
SRT verta pažinti iš akių, nes tai subtitrų formatas, kurį beveik visi grotuvai priima, ir jis yra paprastas tekstas. Failas – tai kvietimų (cue) sąrašas, atskirtas tuščiomis eilutėmis, ir kiekvienas kvietimas yra numeris, laiko intervalas ir tekstas. Laiko žymose prieš milisekundes naudojamas kablelis – prancūziškas įprotis, kurio šis formatas taip ir neatsisakė:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Vienas sakinys viename kvietime – tinkamiausias dydis: kiekvieną kvietimą galima perskaityti per laiką, kiek jis rodomas ekrane, ir nė vienas nenutrūksta minties viduryje. Kai įraše buvo kalbėtojų žymos, kiekvieno kvietimo tekstas prasideda kalbėtojo vardu ir dvitaškiu.
TypeScript SDK vs.recordings.export grąžina Response; jo baitus perskaitykite su arrayBuffer(). Python grąžina 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
Nuorašas saugomas, todėl jis rodomas valdymo skydelyje skiltyje Transcriptions, gali būti bendrinamas nuoroda ir perskaitomas bet kada vėliau. Tai pagrindinė grandinė: sukurti, laukti, skaityti, eksportuoti. Likusi šio vadovo dalis apima variantus: medija, kuri jau internete, klaidas, su kuriomis susidursite, ir failo kainą.
Jei medija jau internete, perduokite API nuorodą – jis pats ją atsisiųs: viešas http(s) adresas (mp3, vaizdo įrašas, podcast'o epizodas) arba YouTube vaizdo įrašas. Kvietimas toks pat – POST /v1/transcriptions, bet su JSON turiniu { "url": "..." } vietoj multipart įkėlimo, o name ir settings lieka kaip buvo. SDK'uose vs.transcriptions.createFromUrl(url) (TypeScript) ir vs.transcriptions.create(url=...) (Python) atlieka tą pačią užklausą.
Prieš ką nors kurdami, GET /v1/transcriptions/probe?url= pasako, kas yra nuoroda. Jos kind yra vienas iš trijų:
file: įrašas su pabaiga; transkribuokite jį čia.live: YouTube tiesioginė transliacija, begalinis radijo srautas ar HLS grojaraštis be pabaigos. Tiesioginė nuoroda niekada nesibaigia, todėl jos negalima transkribuoti kaip failo; vietoj to naudokite ją kaip tiesioginio transkribavimo šaltinį.unknown: serveris negalėjo nustatyti.Kai nuoroda neatsidaro kaip medija (pavyzdžiui, tinklalapis), ok yra false, o problem vienu sakiniu paaiškina priežastį. Probe yra neprivalomas ir ribojamas 60 kvietimų per minutę.
Nuorodos užduotis pradeda fetching, prieš pereidama į transcribing ir done. Kol ji atsisiunčia, progress.phase nurodo, ką serveris daro: fetching, kol ateina baitai (su fetchedBytes ir fetchTotalBytes, kai kita pusė nurodo, kiek jų iš viso), arba extracting, kol iš vaizdo įrašo išgaunamas garsas. progress.phasePct rodo, kiek toje fazėje nuveikta. Ilgas vaizdo įrašas užtrunka, todėl klauskite kas kelias sekundes; vs.jobs.wait tam priima 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}%`);
},
});
Pilnas programas abiem kalbomis ir HTTP rasite nuorodos transkribavimo vadove nuorodų skyriuje. Gaunant failus iš nuorodų atsiranda daugiau klaidų galimybių, todėl štai klaidos, kurias tikrai pamatysite.
Kiekviena nepavykusi užklausa atsako vienu JSON atsakymo apvalku. code yra eilutė, pagal kurią šakojate, o status – HTTP statusas; message skirtas žmonėms, requestId – palaikymo tarnybai. data viduje retryable nurodo, ar ta pati užklausa, pakartota, gali padėti, o details turi tai, ko reikia kodui. SDK meta vieną klaidos tipą, VocalStackError, su tais pačiais laukais.
Trys kodai verti dėmesio nuo pirmos dienos.
402 insufficient_credit: paskyros kreditas baigėsi, ir nieko neapmokestinta. details.balance – likutis doleriais, o details.topUpUrl – nuoroda kreditui papildyti. Kai kaina žinoma iš anksto, kaip failui, kurio ilgis žinomas, details.required parodo, kiek ji būtų buvusi.429 rate_limited: palaukite Retry-After sekundžių. Kai details.metric yra creates_per_hour, pasiekėte plano sukūrimo greitį (naujos transkripcijos per valandą), o ne trumpo antplūdžio ribą; limit ir resetsAt nurodo ribą ir tai, kada ji atsilaisvins.503 unavailable: paslauga trumpam nepasiekiama. Siųskite užklausą iš naujo po Retry-After.SDK pakartoja 429 ir 503 po Retry-After, pagal nutylėjimą du kartus (maxRetries / max_retries kliente). 503 tačiau pakartojama tik kvietimui, kurį saugu siųsti dukart: GET arba sukūrimui su Idempotency-Key. Sukūrimas be rakto paliekamas jums, nes SDK negali žinoti, ar pirmasis bandymas pasiekė tikslą. Tai praktinė priežastis, kodėl kiekvienas įkėlimas turi turėti raktą.
Kiekvienas atsakymas taip pat turi RateLimit-Limit, RateLimit-Remaining ir RateLimit-Reset, todėl užimtas srautas gali sulėtėti dar prieš gaunant 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…"
}
Visa kodų lentelė yra Klaidos, pakartojimai ir ribos skyriuje. Ta 402 – vienintelė su pinigais susijusi klaida, todėl štai kiek kainuoja failas.
Failai kainuoja $0.20 už valandą garso, apmokestinama pagal sekundę ($0.0033 už minutę) su 5 minučių minimumu: 30 minučių failas kainuoja $0.10. Tai dešimt centų, mažiau nei kava, kurią spėsite išgerti, kol pildosi progreso juosta. Kaina ta pati visuose planuose ir per API, ir valdymo skydelyje. Nauja paskyra pradeda nuo vienkartinio $5 registracijos kredito, be kortelės.
Trys plano ribos liečia šį vadovą.
GET /v1/realtime/capabilities rodo kaip limits.maxUploadBytes. Taikoma mažesnioji iš dviejų, o failas, viršijantis ją, atmetamas su 413 dar prieš transkribuojant. Ilgam įrašui tai dar viena priežastis įkelti suspaustą mono failą, o ne WAV.GET /v1/billing bet kada parodo jūsų planą, likutį ir ribas; visa lentelė – kainų puslapyje.
Toliau galimi tolesni žingsniai: garso srautas realiuoju laiku, kai negalite laukti, kol failas baigsis, arba AI agento prijungimas prie tos pačios paskyros per MCP serverį, kad jis transkribuotų ir ieškotų už jus, be jokio aukščiau pateikto kodo.