Отпремање враћа посао, а не транскрипт. Ставите Idempotency-Key на свако креирање да поновни покушај никад не створи нити наплати другу транскрипцију; затим проверавајте посао док не буде готов, па читајте реченице, речи или извоз под истим идентификатором.
Транскрипција фајла је најједноставнија ствар коју VocalStack API ради, и има облик сваког асинхроног посла: отпремање, чекање, читање. Отпремање одмах враћа транскрипцију и њен посао, али без речи за сада. Док ваш програм проверава посао, сервер транскрибује фајл много пута брже него што се звук пушта. Када стање буде done, транскрипт читате као реченице са говорницима и временима, или га извозите као фајл у једном од четири формата, било на језику говора или преведен.
Све у овом облику важи за било који batch API: креирање које враћа идентификатор, стање које проверавате, резултат који касније преузимате. Само су имена поља специфична за VocalStack, а овај водич их објашњава уз TypeScript и Python SDK и, где помажу, HTTP позиве.
Отворите Developers → API keys на контролној табли и направите кључ са дозволама Read и Write. Кључ се приказује само једном; копирајте га у променљиву окружења:
export VOCALSTACK_API_KEY="<your API key>"
Кључ је за сервере и скриптове. Све што се шаље у прегледач шаље и кључ, па га држите ван кода странице и ван URL-ова, који завршавају у логовима. Да бисте стримовали из прегледача, ваш сервер прави realtime сесију за њега; то је посебан водич.
Сваки позив се аутентификује кључем као bearer токеном:
Authorization: Bearer vs_key_...
Кад је кључ у окружењу, следећа ствар је клијент који ће га слати.
Постоје два SDK-а. Сваки има један клијент, VocalStack, са методама груписаним по ресурсима: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime и још неколико. TypeScript SDK ради на Node 20 или новијем и у прегледачима; Python SDK је изграђен на asyncio и захтева Python 3.10 или новији.
SDK-ови нису на npm-у ни PyPI-ју. API служи верзије сваком налогу. Команде за инсталацију, попуњене за ваше окружење, налазе се у одељку Install водича за брзи почетак у API референци, и шаљу ваш кључ само на API хост. Ако не желите ништа да инсталирате, HTTP позиви испод требају само curl и jq.
Шта год изаберете, први позив је отпремање.
POST /v1/transcriptions прима фајл као multipart/form-data: део file, и опционални део options са JSON-ом. Одговара са 201 и транскрипцијом. Његов job.recordingId је идентификатор снимка направљеног из фајла, и сваки следећи позив се упућује тим идентификатором.
Два подешавања важе за први пробни рад, оба унутар options.settings:
spokenLanguage: код, на пример en. Ако га изоставите, језик се детектује. Његово навођење побољшава тачност на бучним снимцима и јаким акцентима.speakerLabels: auto, on или off. Са on, свака реченица носи број говорника; са auto, ознаке се појављују тек кад се чује други говорник.Већина аудио и видео фајлова се прихвата: mp3, wav, flac, ogg, m4a, mp4 и други. Видео задржава само свој звук. Формат једва утиче на тачност, јер препознавање говора ради на 16 kHz мono и све се при пријему претвара на то. WAV од 48 kHz стерео једне особе која говори троши већину бајтова на опис тишине у високој верности. Транскрибује се потпуно исто као MP3 или Opus фајл моно 64 kbps истог говора, али је око двадесет пута већи, па се и отпремање одужи двадесет пута. Ако конвертујете пре отпремања, вредно је променити два подешавања: мono и умерен битрејт.
Пошаљите Idempotency-Key уз свако отпремање, и разумите зашто. Истек мреже вам не говори ништа о томе шта је сервер урадио: можда никад није видео захтев, или је можда сачувао фајл и ставио посао у ред пре него што је одговор изгубљен. Слепи поновни покушај зато ризикује другу транскрипцију и другу наплату. Кључ то спречава. Кад се захтев понови са истим кључем и истим телом у року од 24 сата, сервер враћа први одговор уместо да нешто створи, што претвара „бар једном” у „практично једном”. Правило је зато један кључ по логичком отпремању, генерисан пре првог покушаја и поново коришћен при сваком покушају. Нов кључ по покушају би сваки поновни покушај учинио новим отпремањем, и заглавље не би ништа штитило.
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
Исти позив са 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 стиже пре него што је и једна реч транскрибована; оно што држите је идентификатор посла, па је следећи корак чекање на њега.
Посао прелази из queued → transcribing → done, или се завршава као failed са поруком error и, кад је узрок читљив машини, errorCode. Четврто стање, cropping, значи да је посао паркиран док неко не изабере део снимка који ће се транскрибовати, као што то ради корак сечења на контролној табли. Обично отпремање никад не улази у њега, мада jobs.wait враћа и кад посао дође до њега. Читајте GET /v1/recordings/{id}/job док стање не буде done или failed.
Нема webhook-ова, и то је намерна размена. Webhook тражи јавни HTTPS крај на вашој страни, проверу потписа да га нико други не позове и политику поновних покушаја за случај да је ваш крај недоступан, што је доста посла за посао који траје минут. Провера стања то не тражи. Ваша скрипта ће сваке две секунде питати „је ли готово?”, као дете на задњем седишту, а за разлику од детета, добија проценат.
Веб страница која је већ отворена може уместо тога пратити сокет догађаја транскрипције (WS /v1/transcriptions/{id}/events, оквир job.progress како посао напредује). За скрипту је провера стања прави алат.
SDK-ови сами проверавају стање: vs.jobs.wait чита посао сваке 2 секунде и свако читање прослеђује у onProgress (on_progress у Python-у). progress посла садржи pct (0 до 100) и processedS, број медијских секунди до сада транскрибованих. Програми испод исписују ред само кад се стање или проценат промени.
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}")
Имајте на уму да wait не баца грешку при failed; враћа посао, а проверу препушта вама. Две секунде одговарају предавању. За снимак од три сата, или серију од стотина, проширите интервал (intervalMs, interval_s) или успоравајте како посао одмиче: прва читања највише говоре, а каснија углавном потврђују да је сервер још заузет. Без SDK-а, исту петљу пишете ручно:
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
Кад стање гласи done, транскрипт је спреман.
GET /v1/recordings/{id}/transcript враћа цели транскрипт у једном документу:
sentences: оно што треба приказати људима. Свака има text, speaker, и start и end у секундама.segments: линије мотора, свака са временима по речима у words (свака реч има text, start и end).speakers: по један унос за сваки број говорника, са његовим name.Говорници се броје од 0. speaker од -1 значи да су ознаке говорника биле искључене за снимак. Додајте ?lang=es да бисте прочитали транскрипт преведен на шпански; реченице стижу на том језику.
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'
Два нивоа временских ознака служе двама пословима. Реченице су јединица читања, па их приказује титл или страница транскрипта. Речи су јединица показивања, па истакнуто подвлачење у стилу караоке, погодак претраге или исецање клипа иду кроз segments[].words. Формати извоза су састављени од реченица.
GET /v1/recordings/{id}/export враћа прави фајл. format је један од srt, json, xlsx или pdf, а фајл укључује све исправке. Додајте lang= за преведен фајл: извоз тада чита на том језику, по једна линија за реченицу, и свака реченица која још није преведена прво се преводи.
SRT вреди препознати по изгледу, јер је то формат титлова који скоро сваки плејер прихвата и он је чист текст. Фајл је списак блокова раздвојених празним редовима, а сваки блок чине редни број, временски опсег и текст. Временске ознаке користе зарез пре милисекунди, француска навика коју формат никад није одбацио:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Једна реченица по блоку је право зрно: сваки блок може да се прочита за време док је на екрану, и ниједан не прекида мисао усред реченице. Кад снимак има ознаке говорника, текст сваког блока почиње именом говорника и двотачком.
У TypeScript SDK-у, vs.recordings.export враћа Response; његове бајтове читате преко arrayBuffer(). У Python-у враћа 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
Транскрипција се чува, па се појављује под Транскрипције на контролној табли, може се делити линком и може се поново читати било када касније. То је основна петља: направите, сачекајте, прочитајте, извезите. Остатак овог водича покрива варијације: медије који су већ на мрежи, грешке које ћете сретати и шта фајл кошта.
Ако су медији већ на мрежи, дајте API-ју линк и он сам преузима фајл: јавну http(s) адресу (mp3, видео, епизоду подкаста) или YouTube видео. Позив је исти POST /v1/transcriptions, са JSON телом { "url": "..." } уместо отпремања у више делова, а name и settings као и пре. У SDK-овима, vs.transcriptions.createFromUrl(url) (TypeScript) и vs.transcriptions.create(url=...) (Python) шаљу исти захтев.
Пре креирања било чега, GET /v1/transcriptions/probe?url= казује шта је линк. Његов kind је један од три:
file: снимак са крајем; транскрибујте га овде.live: YouTube текући пренос, бескрајни радио стрим или HLS плејлиста без краја. Живи линк никад не завршава, па се не може транскрибовати као фајл; уместо тога користите га као извор за транскрипцију уживо.unknown: сервер није могао да утврди.Кад линк уопште не отвара медиј (на пример веб страница), ok је false, а problem једном реченицом објашњава зашто. Провера је опциона и ограничена на 60 позива у минути.
Посао по линку почиње у fetching пре него што пређе у transcribing и done. Док преузима, progress.phase казује шта сервер ради: fetching док стижу бајтови (са fetchedBytes и fetchTotalBytes кад друга страна каже колико их има), или extracting док се звук вади из видеа. progress.phasePct казује колико је фаза одмакла. Дуг видео траје, па проверавајте на сваких неколико секунди; vs.jobs.wait прима intervalMs (interval_s у Python-у) за то.
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}%`);
},
});
Пун програм на оба језика и преко HTTP-а налази се у водичу Транскрибујте линк у референци. Преузимање са линкова доноси још начина да пропадне, па ево грешака које ћете заиста видети.
Сваки неуспели позив враћа један JSON омотач. code је стринг по коме се грана, а status HTTP статус; message је за људе, а requestId за подршку. Испод data, retryable казује да ли може да помогне слање истог захтева поново, а details садржи оно што код захтева. SDK-ови дижу један тип грешке, VocalStackError, са истим пољима.
Три кода вреди обрадити одмах.
402 insufficient_credit: кредит налога је потрошен и ништа није наплаћено. details.balance је стање у доларима, а details.topUpUrl је место где се кредит допуњује. Кад је трошак био познат унапред, као за фајл познате дужине, details.required казује колико би износио.429 rate_limited: сачекајте Retry-After секунди. Кад је details.metric creates_per_hour, достигли сте стопу креирања пакета (нове транскрипције на сат), а не ограничење за налет; limit и resetsAt казују колика је граница и када се ослобађа.503 unavailable: услуга је накратко недоступна. Пошаљите захтев поново после Retry-After.SDK-ови поново покушавају 429 и 503 након Retry-After, подразумевано два пута (maxRetries / max_retries на клијенту). 503, међутим, поново се шаље само за позив који је безбедно послати два пута: GET, или креирање са Idempotency-Key. Креирање без њега препуштено је вама, јер SDK не може знати да ли је први покушај прошао. То је практичан разлог да ставите кључ на свако отпремање.
Сваки одговор носи и RateLimit-Limit, RateLimit-Remaining и RateLimit-Reset, па се заузети цевовод може успорити пре 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…"
}
Цела табела кодова налази се у Грешке, поновни покушаји и ограничења. Та 402 је једина грешка везана за новац, па ево шта фајл кошта.
Фајлови коштају $0.20 по сату аудија, наплаћено по секунди ($0.0033 у минути) са минималних 5 минута: фајл од 30 минута кошта $0.10. То је десет центи, мање од кафе коју ћете попити док се трака напретка пуни. Цена је иста на сваком пакету и исте су преко API-ја и на контролној табли. Нови налог започиње са једнократним кредитом од $5 при регистрацији, а картица није потребна.
Три ограничења пакета дотичу овај водич.
GET /v1/realtime/capabilities приказује као limits.maxUploadBytes. Важи мања од две, а фајл преко ње се одбија са 413 пре него што се ишта транскрибује. За дуг снимак то је још један разлог да отпремите сажет mono фајл, а не WAV.GET /v1/billing у сваком тренутку враћа ваш пакет, стање и ограничења; пуна табела је на страници са ценама.
Даљи кораци су стримовање звука у реалном времену кад не можете да чекате да фајл заврши, или повезивање AI агента на исти налог преко MCP сервера, тако да транскрибује и претражује уместо вас, без свега овог кода.