Nahrání vrátí úlohu, nikoli přepis. U každého vytvoření pošlete Idempotency-Key, aby opakovaný pokus nikdy nevytvořil ani nezaúčtoval druhý přepis; pak dotazujte úlohu, dokud není hotová, a věty, slova nebo export čtěte ze stejného id.
Přepis souboru je nejjednodušší věc, kterou VocalStack API dělá, a má tvar každé asynchronní úlohy: nahrát, počkat, číst. Nahrání odpoví okamžitě přepisem a jeho úlohou, ale zatím bez slov. Zatímco váš program úlohu dotazuje, server přepisuje soubor mnohonásobně rychleji, než se zvuk přehrává. Až bude stav done, přečtete přepis jako věty s mluvčími a časy, nebo jej exportujete jako soubor v jednom ze čtyř formátů, a to v mluveném jazyce nebo přeložený.
Vše v tomto tvaru platí pro jakékoli dávkové API: vytvoření, které vrátí id, stav, na který se dotazujete, a výsledek, který si pak vyžádáte. Konkrétní jsou jen názvy polí VocalStack a tento návod je prochází v TypeScript a Python SDK a tam, kde pomohou, také v HTTP voláních.
Otevřete Developers → API keys v ovládacím panelu a vytvořte klíč s oprávněními Read a Write. Klíč se zobrazí jen jednou; zkopírujte jej do proměnné prostředí:
export VOCALSTACK_API_KEY="<your API key>"
Klíč je určen pro servery a skripty. Cokoli, co se nasazuje do prohlížeče, s sebou nese i klíč, takže jej držte mimo kód stránky a mimo URL, které končí v logech. Chcete-li streamovat z prohlížeče, váš server pro něj vytvoří relaci v reálném čase; to je jiný návod.
Každé volání se ověřuje klíčem jako bearer tokenem:
Authorization: Bearer vs_key_...
Jakmile máte klíč v prostředí, zbývá klient, který jej odešle.
Existují dvě SDK. Každé má jednoho klienta, VocalStack, jehož metody jsou seskupeny podle zdroje: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime a několik dalších. TypeScript SDK běží na Node 20 nebo novějším a v prohlížečích; Python SDK je postaveno na asyncio a vyžaduje Python 3.10 nebo novější.
SDK nejsou na npm ani PyPI. API sestavení servíruje každému účtu. Instalační příkazy s doplněnými údaji pro vaše prostředí jsou v sekci Install rychlého startu v referenci API a odesílají váš klíč jen na hostitele API. Pokud nechcete nic instalovat, HTTP volání níže potřebují jen curl a jq.
Ať zvolíte cokoli, prvním voláním je nahrání.
POST /v1/transcriptions přijímá soubor jako multipart/form-data: část file a volitelnou část options s JSONem. Odpoví 201 s přepisem. Jeho job.recordingId je id nahrávky vytvořené ze souboru a každé další volání se adresuje tímto id.
Pro první spuštění záleží na dvou nastaveních uvnitř options.settings:
spokenLanguage: kód, například en. Pokud jej vynecháte, jazyk se rozpozná automaticky. Jeho uvedení zlepší přesnost u hlučných nahrávek a silných přízvuků.speakerLabels: auto, on nebo off. S on nese každá věta číslo mluvčího; s auto se štítky objeví, až se ozve druhý mluvčí.Přijímá se většina zvukových a video souborů: mp3, wav, flac, ogg, m4a, mp4 a další. U videa se zachová jen zvuk. Formát má na přesnost malý vliv, protože rozpoznávání řeči pracuje na 16 kHz mono a vše se při příjmu převzorkuje na tuto hodnotu. 48kHz stereo WAV jednoho mluvčího utratí většinu bajtů na popis ticha ve vysoké kvalitě. Přepisuje se přesně stejně jako MP3 nebo Opus soubor stejné řeči o 64 kbps v monu, ale je zhruba dvacetkrát větší, takže se nahrává dvacetkrát déle. Pokud převádíte před nahráním, stojí za změnu dvě nastavení: mono a rozumný datový tok.
Pošlete s každým nahráním Idempotency-Key a rozumějte proč. Síťová časová prodleva vám neřekne, co server udělal: požadavek nemusel nikdy dorazit, nebo mohl soubor uložit a úlohu zařadit dřív, než se odpověď ztratila. Slepý opakovaný pokus proto riskuje druhý přepis a druhé zúčtování. Klíč to řeší. Když se požadavek zopakuje se stejným klíčem a tělem do 24 hodin, server vrátí první odpověď místo vytvoření něčeho nového, čímž se „alespoň jednou“ změní na „prakticky jednou“. Pravidlo tedy zní: jeden klíč na jedno logické nahrání, vytvořený před prvním pokusem a použitý znovu při každém opakování. Nový klíč pro každý pokus by z každého opakování udělal nové nahrání a hlavička by nechránila nic.
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
Stejné volání pomocí 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 přijde dřív, než je přepsáno jediné slovo; držíte id úlohy, takže dalším krokem je čekání na ni.
Úloha přechází z queued → transcribing → done, nebo končí ve stavu failed se zprávou error a, je-li příčina strojově čitelná, s errorCode. Čtvrtý stav, cropping, znamená, že úloha je pozastavena, dokud někdo nevybere úsek nahrávky k přepisu, jak to dělá krok ořezání v ovládacím panelu. Běžné nahrání do tohoto stavu nikdy nevstoupí, i když se jobs.wait vrátí, pokud úloha do něj dospěje. Čtěte GET /v1/recordings/{id}/job, dokud stav není done nebo failed.
Webhooky nejsou; je to záměrný kompromis. Webhook potřebuje veřejný HTTPS koncový bod na vaší straně, kontrolu podpisu, aby jej nemohl volat nikdo jiný, a politiku opakování pro případ, že je váš koncový bod nedostupný. To je hodně instalatérské práce pro úlohu, která trvá minutu. Dotazování nic z toho nepotřebuje. Váš skript se bude každé dva sekundy ptát „už hotovo?“ jako dítě na zadním sedadle auta, ale na rozdíl od dítěte dostane zpět procento.
Již otevřená webová stránka může místo toho sledovat socket událostí přepisu (WS /v1/transcriptions/{id}/events, rámec job.progress, když úloha postupuje). Pro skript je správným nástrojem dotazování.
SDK dotazování provádějí za vás: vs.jobs.wait čte úlohu každé 2 sekundy a každé čtení předá onProgress (on_progress v Pythonu). progress úlohy nese pct (0 až 100) a processedS, počet sekund médií přepsaných zatím. Programy níže vypíší řádek jen tehdy, když se změní stav nebo procento.
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}")
Všimněte si, že wait při failed nevyvolá chybu; vrátí úlohu a kontrolu nechá na vás. Dvě sekundy stačí pro přednášku. U třírodinové nahrávky nebo dávky stovek souborů interval rozšiřte (intervalMs, interval_s) nebo jej s během úlohy prodlužujte: první čtení řeknou nejvíc, pozdější hlavně potvrzují, že server je stále zaneprázdněn. Bez SDK napíšete stejnou smyčku ručně:
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
Když stav ukazuje done, přepis je připraven.
GET /v1/recordings/{id}/transcript odpoví celým přepisem v jediném dokumentu:
sentences: co ukázat lidem. Každá má text, speaker a start a end v sekundách.segments: řádky enginu, každý s časováním slov v words (každé slovo má text, start a end).speakers: jeden záznam na číslo mluvčího, s jeho name.Mluvčí se číslují od 0. speaker rovno -1 znamená, že štítky mluvčích byly pro nahrávku vypnuté. Přidáním ?lang=es přečtete přepis přeložený do španělštiny; věty se vrátí v tomto jazyce.
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'
Dvě úrovně časování slouží dvěma účelům. Věty jsou jednotkou čtení, takže je ukazuje titulek nebo stránka přepisu. Slova jsou jednotkou ukazování, takže zvýraznění ve stylu karaoke, hledání nebo střih klipu prochází místo toho segments[].words. Exportní formáty se staví z vět.
GET /v1/recordings/{id}/export vrátí skutečný soubor. format je jeden z srt, json, xlsx nebo pdf, a soubor obsahuje všechny opravy. Přidejte lang= pro přeložený soubor: export pak čte v tomto jazyce, jeden řádek na větu, a každá věta, která zatím přeložená není, se nejdřív přeloží.
SRT stojí za to znát ze vzhledu, protože je to titulkový formát, který přijímá téměř každý přehrávač, a je to prostý text. Soubor je seznam titulků oddělených prázdnými řádky a každý titulek je pořadové číslo, časový rozsah a text. Časové značky používají čárku před milisekundami, francouzský zvyk, který se formát nikdy nezbavil:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Jedna věta na titulek je správná zrnitost: každý titulek lze přečíst za dobu, po kterou je na obrazovce, a žádný nepřeruší myšlenku v půli. Když měla nahrávka štítky mluvčích, text každého titulku začíná jménem mluvčího a dvojtečkou.
V TypeScript SDK vs.recordings.export vrací Response; jeho bajty přečtete přes arrayBuffer(). V Pythonu vrací 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
Přepis je uložen, takže se objeví pod Transcriptions v ovládacím panelu, lze jej sdílet odkazem a lze jej později kdykoli znovu přečíst. To je základní smyčka: vytvořit, počkat, číst, exportovat. Zbytek tohoto návodu popisuje odchylky: média, která jsou už online, chyby, na které narazíte, a co soubor stojí.
Pokud je médium už online, předejte API odkaz a ono si soubor stáhne samo: veřejnou adresu http(s) (mp3, video, epizodu podcastu) nebo video na YouTube. Volání je stejné POST /v1/transcriptions, s tělem JSON { "url": "..." } místo nahrání multipart a s name a settings jako dřív. V SDK vs.transcriptions.createFromUrl(url) (TypeScript) a vs.transcriptions.create(url=...) (Python) odešlou stejný požadavek.
Než cokoli vytvoříte, GET /v1/transcriptions/probe?url= řekne, čím odkaz je. Jeho kind je jeden ze tří:
file: nahrávka s koncem; přepište ji zde.live: živý přenos na YouTube, nekonečný rozhlasový stream nebo playlist HLS bez konce. Živý odkaz nikdy neskončí, takže jej nelze přepsat jako soubor; místo toho jej použijte jako zdroj živého přepisu.unknown: server nedokázal rozhodnout.Když se odkaz vůbec neotevře jako médium (třeba webová stránka), je ok rovno false a problem vysvětlí důvod jednou větou. Probe je volitelný a je omezen na 60 volání za minutu.
Odkazová úloha začíná ve stavu fetching, než přejde do transcribing a done. Během stahování říká progress.phase, co server dělá: fetching, zatímco přicházejí bajty (s fetchedBytes a fetchTotalBytes, pokud druhá strana sdělí, kolik jich je), nebo extracting, zatímco se zvuk vytahuje z videa. progress.phasePct říká, jak daleko je úloha v této fázi. Dlouhé video chvíli trvá, takže dotazujte každých několik sekund; vs.jobs.wait na to přijímá intervalMs (interval_s v Pythonu).
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}%`);
},
});
Přepište odkaz v referenci obsahuje úplný program v obou jazycích i v HTTP. Stahování z odkazů přidává další způsoby selhání, takže zde jsou chyby, které skutečně uvidíte.
Každé neúspěšné volání odpoví jedním JSON obálkou. code je řetězec, podle kterého větvíte, a status je stav HTTP; message je pro lidi a requestId pro podporu. Pod data říká retryable, zda může pomoci odeslat stejný požadavek znovu, a details obsahuje, co daný kód potřebuje. SDK vyvolává jeden typ chyby, VocalStackError, se stejnými poli.
Tři kódy stojí za ošetření hned první den.
402 insufficient_credit: kredit účtu je vyčerpán a nic nebylo zúčtováno. details.balance je zůstatek v dolarech a details.topUpUrl je místo, kde kredit přidat. Pokud byla cena známa předem, jako u souboru se známou délkou, details.required říká, kolik by stál.429 rate_limited: počkejte Retry-After sekund. Pokud je details.metric rovno creates_per_hour, narazili jste na rychlost vytváření tarifu (nové přepisy za hodinu), ne na limit krátkodobé špičky; limit a resetsAt říkají, jaký je strop a kdy se uvolní.503 unavailable: služba je krátce nedostupná. Pošlete požadavek znovu po Retry-After.SDK opakují 429 a 503 po Retry-After, ve výchozím nastavení dvakrát (maxRetries / max_retries na klientovi). 503 se však opakuje jen u volání, které je bezpečné poslat dvakrát: GET, nebo vytvoření s Idempotency-Key. Vytvoření bez něj zůstává na vás, protože SDK nemůže vědět, zda první pokus prošel. To je praktický důvod, proč dát klíč na každé nahrání.
Každá odpověď nese také RateLimit-Limit, RateLimit-Remaining a RateLimit-Reset, takže zaneprázdněný proces může zpomalit ještě před 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…"
}
Celá tabulka kódů je v Chyby, opakování a limity. Ta 402 je jediná chyba spojená s penězi, takže se podívejme, kolik soubor stojí.
Soubory stojí $0.20 za hodinu zvuku, účtováno po sekundách ($0.0033 za minutu) s minimem 5 minut: 30minutový soubor stojí $0.10. To je deset centů, méně než káva, kterou si mezitím dáte, než se ukazatel průběhu naplní. Sazba je stejná u každého tarifu a stejná přes API jako v ovládacím panelu. Nový účet začíná s jednorázovým uvítacím kreditem $5 a karta se nevyžaduje.
Tři limity tarifu se dotýkají tohoto návodu.
GET /v1/realtime/capabilities uvádí jako limits.maxUploadBytes. Platí menší z obou a soubor přes něj je odmítnut s 413 dřív, než se cokoli přepíše. U dlouhé nahrávky je to další důvod nahrát komprimovaný mono soubor místo WAV.GET /v1/billing kdykoli hlásí váš tarif, zůstatek a limity; úplná tabulka je na ceníku.
Dál jsou dalšími kroky streamování zvuku v reálném čase, když nemůžete čekat, až soubor doběhne, nebo připojení AI agenta ke stejnému účtu přes MCP server, takže přepisuje a hledá za vás bez veškerého kódu výše.