A feltöltés egy feladatot ad vissza, nem átiratot. Minden létrehozó kéréshez adj Idempotency-Key fejlécet, hogy az újrapróbálkozás soha ne hozzon létre vagy számlázzon második átírást; utána kérdezd le a feladatot, amíg kész nem lesz, és az átiratot mondatokként, szavakként vagy exportfájlként ugyanarról az azonosítóról olvasd ki.
A fájlátírás a legegyszerűbb dolog, amit a VocalStack API tud, és ugyanolyan aszinkron feladat, mint bármelyik: feltöltés, várakozás, olvasás. A feltöltésre a válasz azonnal érkezik, egy átírással és annak feladatával, de még szavak nélkül. Amíg a programod lekérdezi a feladatot, a szerver sokszorosan gyorsabban írja át a fájlt, mint ahogy a hang lejátszódik. Ha az állapot done, az átiratot mondatokként olvasod ki beszélőkkel és időzítésekkel, vagy exportálod fájlként az négy formátum egyikében, akár a beszélt nyelven, akár lefordítva.
Ez a forma bármely kötegelt API-ra igaz: egy létrehozás, amely azonosítót ad vissza, egy állapot, amelyet lekérdezel, és egy eredmény, amelyet utólag lekérsz. Csak a mezőnevek jellemzőek a VocalStackre. Ez a bemutató a TypeScript- és Python-SDK-val halad végig, és ahol segít, a HTTP-hívásokat is megmutatja.
Nyisd meg a Dashboardon a Developers → API keys oldalt, és hozz létre egy kulcsot a Read és Write jogosultságokkal. A kulcs csak egyszer látszik; másold be egy környezeti változóba:
export VOCALSTACK_API_KEY="<your API key>"
A kulcs szervereknek és szkripteknek való. Minden, ami böngészőbe kerül, a kulcsot is magával viszi, ezért tartsd távol a kulcsot az oldal kódjától és az URL-ektől, mert azok a naplókba is bekerülnek. Ha böngészőből szeretnél streamelni, a szervered készít hozzá egy valós idejű munkamenetet; erről egy másik bemutató szól.
Minden hívás a kulccsal hitelesít, bearer tokenként:
Authorization: Bearer vs_key_...
A kulcs a környezetedben van, a következő lépés egy kliens, amely elküldi.
Két SDK érhető el. Mindkettőnek egy kliense van, VocalStack néven, a metódusai erőforrásonként csoportosítva: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime és még néhány. A TypeScript SDK Node 20-as vagy újabb verzión és böngészőkben fut; a Python SDK asyncio-ra épül, és Python 3.10-es vagy újabb verziót igényel.
Az SDK-k nincsenek npm-en vagy PyPI-n: az API minden fióknak kiszolgálja a buildeket. A telepítő parancsok a környezetedhez igazítva az API-referencia gyorsindításának Telepítés szakaszában találhatók, és a kulcsodat csak az API-hosztnak küldik el. Ha semmit sem akarsz telepíteni, a lenti HTTP-hívásokhoz csak a curl és a jq kell.
Bármelyiket választottad, az első hívás a feltöltés.
A POST /v1/transcriptions a fájlt multipart/form-data formában várja: egy file résszel, és egy opcionális options résszel, amely JSON-t tartalmaz. 201-es válasszal tér vissza az átírással. A job.recordingId a fájlból készült felvétel azonosítója, a további hívások mind ezzel az azonosítóval címzendők.
Két beállítás fontos egy első futtatásnál, mindkettő az options.settings alatt:
spokenLanguage: nyelvkód, például en. Ha kihagyod, a nyelvet a rendszer felismeri. Ha megadod, zajos felvételeknél és erős akcentusnál javul a pontosság.speakerLabels: auto, on vagy off. on esetén minden mondat kap egy beszélőszámot; auto esetén a címkék csak akkor jelennek meg, ha egy második beszélő is megszólalt.A legtöbb hang- és videofájl elfogadott: mp3, wav, flac, ogg, m4a, mp4 és más formátumok. A videóból csak a hang marad meg. A formátum alig befolyásolja a pontosságot, mert a beszédfelismerés 16 kHz-es, monó hangon dolgozik, és minden fájl érkezéskor erre mintavételeződik át. Egy 48 kHz-es, sztereó WAV, amelyen egy ember beszél, a bájtjainak nagy részét a csend nagy hűségű leírására költi. Ugyanolyan jól íródik át, mint ugyanannak az előadásnak 64 kbps-es mono MP3 vagy Opus változata, de körülbelül húszszor akkora, így a feltöltése is húszszor tovább tart. Ha feltöltés előtt konvertálsz, a mono és a mérsékelt bitráta az a két beállítás, amelyet érdemes megváltoztatni.
Minden feltöltéssel küldj Idempotency-Key fejlécet, és értsd meg, miért. Hálózati időtúllépésből nem derül ki, mit tett a szerver: lehet, hogy sosem kapta meg a kérést, de az is lehet, hogy a fájlt már tárolta, és a feladatot sorba állította, mielőtt a válasz elveszett. A vakon megismételt kérés ezért kockáztatja a második átírást és a második számlázást. A kulcs ezt megakadályozza. Ha a kérés ugyanazzal a kulccsal és törzzsel 24 órán belül megismétlődik, a szerver az első választ adja vissza, és nem hoz létre semmit, így az „legalább egyszer” elvből „gyakorlatilag pontosan egyszer” lesz. Ezért a szabály: egy logikai feltöltéshez egy kulcs, amelyet az első kísérlet előtt generálsz, és minden újrapróbálkozáskor újrahasználsz. Ha minden kísérlethez új kulcsot adnál, minden újrapróbálkozás új feltöltés lenne, és a fejléc semmit sem védene.
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
Ugyanez a hívás curl-lel:
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)
A 201 már azelőtt megérkezik, hogy egyetlen szó is átíródna; amit kézben tartasz, az egy feladatazonosító, ezért a következő lépés a feladatra való várakozás.
A feladat queued → transcribing → done állapotokon halad, vagy failed állapotban végződik, error üzenettel és, ha az ok géppel olvasható, errorCode mezővel. Egy negyedik állapot, a cropping, azt jelenti, hogy a feladat várakozik, amíg valaki ki nem választja a felvétel átírandó szakaszát, ahogy a Dashboard vágási lépése teszi. Egy szokásos feltöltés soha nem kerül ebbe az állapotba, bár a jobs.wait ilyenkor is visszatér. Olvasd a GET /v1/recordings/{id}/job végpontot, amíg az állapot done vagy failed nem lesz.
Nincsenek webhookok, és ez tudatos döntés. Egy webhookhoz nyilvános HTTPS-végpont kell nálad, aláírás-ellenőrzés, hogy más ne hívhassa meg, és újrapróbálkozási szabály arra az esetre, ha a végpontod leáll. Ez sok bekötés egy olyan feladathoz, amely egy percig tart. A lekérdezéshez mindez nem kell. A szkripted kétmásodpercenként megkérdezi, hogy kész-e már, mint a hátsó ülésen ülő gyerek, de a gyerekkel ellentétben százalékos értéket kap vissza.
Egy már megnyitott weboldal inkább az átírás eseményeinek socketjét követheti (WS /v1/transcriptions/{id}/events, a job.progress keretekkel, ahogy a feladat halad). Szkripthez a lekérdezés a helyes eszköz.
Az SDK-k elvégzik a lekérdezést: a vs.jobs.wait 2 másodpercenként olvassa a feladatot, és minden olvasást átad az onProgress függvénynek (Pythonban on_progress). A feladat progress mezője tartalmazza a pct (0-tól 100-ig) és a processedS értéket, vagyis az eddig átírt médiamásodperceket. Az alábbi programok csak akkor írnak ki egy sort, ha az állapot vagy a százalék megváltozik.
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}")
Figyelj arra, hogy a wait failed állapotnál nem dob hibát; visszaadja a feladatot, és az ellenőrzés rád marad. Két másodperc egy előadásnak megfelel. Háromórás felvételnél vagy több száz fájlból álló kötegnél szélesítsd az időközt (intervalMs, interval_s), vagy a feladat előrehaladtával ritkítsd a kérdezést: az első olvasások mondják el a legtöbbet, a későbbiek főleg azt erősítik meg, hogy a szerver még dolgozik. SDK nélkül ugyanezt a ciklust kézzel írod meg:
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
Amikor az állapot done, az átirat kész.
A GET /v1/recordings/{id}/transcript az egész átiratot egyetlen dokumentumban adja vissza:
sentences: amit a felhasználóknak meg kell mutatni. Mindegyiknek van text, speaker, valamint start és end mezője másodpercben.segments: a motor sorai, mindegyik szintű időzítéssel a words mezőben (minden szónak van text, start és end mezője).speakers: beszélőszámonként egy bejegyzés, a name mezővel.A beszélők 0-tól számozódnak. A speaker értéke -1, ha a felvételnél ki voltak kapcsolva a beszélőcímkék. Ha az átiratot spanyolra fordítva szeretnéd olvasni, tedd hozzá: ?lang=es; ekkor a mondatok azon a nyelven érkeznek.
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'
A két időzítési szint két feladatot szolgál ki. A mondatok az olvasás egységei, ezért ezeket mutatja a felirat vagy az átiratoldal. A szavak a mutatás egységei, ezért a karaoke-stílusú kiemelés, a keresési találat vagy a klipvágás a segments[].words mezőn halad végig. Az exportformátumok mondatokból épülnek fel.
A GET /v1/recordings/{id}/export valódi fájlt ad vissza. A format értéke srt, json, xlsx vagy pdf lehet, és a fájl tartalmazza a javításokat is. Az lang= paraméterrel lefordított fájlt kapsz: az export ekkor az adott nyelven íródik, soronként egy mondattal, és a még le nem fordított mondatok előbb lefordításra kerülnek.
Érdemes az SRT-t első látásra is felismerni, mert ez az a felirat-formátum, amelyet szinte minden lejátszó elfogad, és egyszerű szöveg. A fájl üres sorokkal elválasztott feliratblokkok (cue) listája; minden blokk egy sorszámból, egy időtartományból és a szövegből áll. Az időbélyegek ezredmásodperc előtt vesszőt használnak, ezt a francia szokást a formátum sosem vetette le:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Egy mondat egy blokk: ekkora egységet a képernyőn töltött idő alatt el lehet olvasni, és egyik sem vágódik el félbe a gondolatban. Ha a felvételnél voltak beszélőcímkék, minden blokk szövege a beszélő nevével és kettősponttal kezdődik.
A TypeScript SDK-ban a vs.recordings.export egy Response-t ad vissza; bájtjait az arrayBuffer() metódussal olvasd ki. Pythonban bytes-t ad vissza.
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
Az átirat tárolódik, ezért megjelenik a Dashboardon a Transcriptions alatt, linkkel megosztható, és bármikor később újra olvasható. Ez az alapciklus: létrehozás, várakozás, olvasás, exportálás. A bemutató többi része a változatokat tárgyalja: az már online lévő médiát, a hibákat, amelyekbe belefutsz, és egy fájl költségét.
Ha a média már online van, add meg az API-nak a linket, és az maga tölti le a fájlt: egy nyilvános http(s) címet (mp3, videó, podcast-epizód) vagy egy YouTube-videót. A hívás ugyanaz a POST /v1/transcriptions, a multipart feltöltés helyett { "url": "..." } JSON-törzzsel, a name és a settings mezőkkel együtt, ahogy eddig. Az SDK-kban a vs.transcriptions.createFromUrl(url) (TypeScript) és a vs.transcriptions.create(url=...) (Python) ugyanazt a kérést küldi.
Mielőtt bármit létrehoznál, a GET /v1/transcriptions/probe?url= megmondja, mi a link. A kind mezője három érték egyike lehet:
file: véges felvétel; itt átírható.live: YouTube-élő adás, végtelen rádióadás vagy véget nem érő HLS-lejátszólista. Az élő link soha nem ér véget, ezért fájlként nem írható át; helyette egy élő átírás forrásaként használd.unknown: a szerver nem tudta eldönteni.Ha a link egyáltalán nem nyílik meg médiaként (például egy weboldal), az ok értéke false, a problem mező pedig egy mondatban megmagyarázza, miért. A probe nem kötelező lépés, és percenként 60 hívásra korlátozott.
Egy linkfeladat a fetching állapotban indul, mielőtt transcribing, majd done állapotba lépne. Letöltés közben a progress.phase mutatja, mit csinál a szerver: fetching, amíg bájtok érkeznek (a fetchedBytes és fetchTotalBytes mezőkkel, ha a másik fél megmondja, mennyi lesz), vagy extracting, amíg a hangot kinyerik a videóból. A progress.phasePct mutatja, hogy az adott fázisban hol tart a feladat. Egy hosszú videó eltart egy darabig, ezért néhány másodpercenként kérdezz le; a vs.jobs.wait erre az intervalMs (Pythonban interval_s) paramétert fogadja el.
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}%`);
},
});
A referencia Átírás linkből útmutatója mindkét nyelven és HTTP-ben teljes programot tartalmaz. A linkes letöltés több hibalehetőséget hoz, ezért íme azok a hibák, amelyekkel valóban találkozni fogsz.
Minden sikertelen hívás egyetlen JSON-borítékot ad vissza. A code az a szöveg, amelyre ágazni kell, a status a HTTP-állapot; a message embereknek szól, a requestId pedig a támogatásnak. A data alatt a retryable mező azt mondja meg, segíthet-e ugyanazt a kérést újraküldeni, a details pedig mindazt tartalmazza, amire a kódnak szüksége van. Az SDK-k egyetlen hibatípust dobnak, a VocalStackError-t, ugyanezekkel a mezőkkel.
Három kódot érdemes első napon kezelni.
402 insufficient_credit: a fiók kreditje elfogyott, és nem számlázott semmit. A details.balance az egyenleg dollárban, a details.topUpUrl pedig azt mutatja, hol lehet krediteket feltölteni. Ha a költség előre ismert volt, ahogy egy ismert hosszúságú fájlnál, a details.required megmondja, mennyi lett volna.429 rate_limited: várj Retry-After másodpercet. Ha a details.metric értéke creates_per_hour, a csomagod létrehozási sebességét érted el (óránkénti új átírás), nem egy rövid lökésű korlátot; a limit és a resetsAt megmondja a felső határt és azt, mikor szabadul fel.503 unavailable: a szolgáltatás átmenetileg nem elérhető. Küldd újra a kérést a Retry-After után.Az SDK-k a 429-et és a 503-at a Retry-After után újrapróbálják, alapértelmezetten kétszer (a kliensen a maxRetries / max_retries beállítással). A 503-at azonban csak olyan hívásnál próbálják újra, amelyet biztonságosan lehet kétszer elküldeni: GET kérésnél, vagy Idempotency-Key-es létrehozásnál. Kulcs nélküli létrehozásnál ez rád marad, mert az SDK nem tudhatja, átment-e az első kísérlet. Ez a gyakorlati oka annak, hogy minden feltöltésre tegyél kulcsot.
Minden válasz tartalmazza a RateLimit-Limit, RateLimit-Remaining és RateLimit-Reset fejléceket, így a leterhelt folyamat a 429 előtt lassíthat.
{
"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…"
}
A kódok teljes táblázata a Hibák, újrapróbálkozások és korlátok oldalon van. Az a 402 az egyetlen pénzhez kötött hiba, ezért nézzük meg, mibe kerül egy fájl.
A fájlok óránként $0.20 áron szerepelnek, másodpercenként számlázva ($0.0033 percenként), 5 perces minimummal: egy 30 perces fájl $0.10-ba kerül. Ez tíz cent, kevesebb, mint az a kávé, amelyet a folyamatjelző töltése közben megiszol. Az ár minden csomagban ugyanaz, és az API-n keresztül ugyanaz, mint a Dashboardon. Új fiók egyszeri $5-ös regisztrációs kredittel indul, bankkártya nélkül.
Három csomagkorlát érinti ezt a bemutatót.
GET /v1/realtime/capabilities ad vissza limits.maxUploadBytes néven. A kisebb érték számít, és az ennél nagyobb fájlt 413-as válasszal visszautasítják, mielőtt bármi átíródna. Hosszú felvételnél ez egy újabb ok arra, hogy tömörített mono fájlt tölts fel WAV helyett.A GET /v1/billing bármikor megmutatja a csomagodat, az egyenlegedet és a korlátaidat; a teljes táblázat az Árak oldalon van.
Innen a következő lépés a hang valós idejű streamelése, ha nem akarsz egy fájl befejezésére várni, vagy MI-ügynök csatlakoztatása ugyanahhoz a fiókhoz az MCP-szerveren keresztül, hogy az átírja és keresse helyetted, a fenti kód nélkül.