Запампоўка вяртае заданне, а не транскрыпт. Ставце Idempotency-Key на кожнае стварэнне, каб паўторная спроба ніколі не стварыла другую транскрыпцыю і не спісала за яе грошы; потым апытвайце заданне, пакуль яно не будзе гатова, і чытайце сказы, словы або экспарт з таго ж ідэнтыфікатара.
Транскрыбаванне файла — самая простая задача, якую выконвае VocalStack API, і яна мае форму любога асінхроннага задання: запампаваць, чакаць, прачытаць. Запыт на запампоўку адказвае адразу транскрыпцыяй і яе заданнем, але слоў у ёй яшчэ няма. Пакуль ваша праграма апытвае заданне, сервер транскрыбуе файл у некалькі разоў хутчэй, чым гучыць аўдыё. Калі стан становіцца done, вы чытаеце транскрыпт як сказы з спікерамі і таймінгамі або экспартуеце яго ў выглядзе файла аднаго з чатырох фарматаў — на мове запісу або ў перакладзе.
Усё, што мае такую форму, пераносіцца на любы пакетны API: стварэнне, якое вяртае ідэнтыфікатар, статус, які вы апытваеце, і вынік, які запытваеце потым. Толькі назвы палёў уласцівыя VocalStack, і ў гэтым падручніку яны разбіраюцца з дапамогай SDK для TypeScript і Python, а там, дзе патрэбна, — з HTTP-выклікамі.
Адкрыйце ў панэлі кіравання Developers → API keys і стварыце ключ з дазволамі Read і Write. Ключ паказваецца адзін раз; скапіруйце яго ў зменную асяроддзя:
export VOCALSTACK_API_KEY="<your API key>"
Ключ прызначаны для сервераў і скрыптоў. Усё, што трапляе ў браўзер, трапляе туды разам з ключом, таму не змяшчайце яго ў коде старонкі і ў URL, якія ў выніку апынаюцца ў логах. Каб весці перадачу з браўзера, ваш сервер стварае для яго сесію рэальнага часу; гэта іншы падручнік.
Кожны выклік аўтэнтыфікуецца з ключом як bearer-токенам:
Authorization: Bearer vs_key_...
Калі ключ ужо ў асяроддзі, наступны крок — кліент, які яго адправіць.
Ёсць два SDK. У кожным адзін кліент, VocalStack, а яго метады згрупаваны па рэсурсах: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime і яшчэ некалькі. SDK для TypeScript працуе на Node 20 або навейшым і ў браўзерах; SDK для Python пабудаваны на 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 кГц, і ўсё пры паступленні перадыскрэтызуецца да гэтай частаты. Стэрэа WAV на 48 кГц, на якім адзін чалавек размаўляе, большую частку байтаў траціць на апісанне цішыні з высокай дакладнасцю. Транскрыбуецца ён гэтак жа добра, як манафанічны MP3 або Opus на 64 кбіт/с з той жа размовай, але ён у дваццаць разоў большы, таму запампоўваецца ў дваццаць разоў даўжэй. Калі перад запампоўкай вы канвертуеце файл, варта змяніць два налады: манафанічны гук і памяркоўны бітрэйт.
Дасылайце 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.
Вэбхукаў няма, і гэта свядомы кампраміс. Вэбхуку патрэбны публічны 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.
Адзін сказ на блок — правільны памер: кожны блок можна прачытаць за той час, пакуль ён на экране, і ніводзін не абрываецца сярод думкі. Калі ў запісе былі пазнакі спікераў, тэкст кожнага блока пачынаецца з імя спікера і двукроп’я.
У SDK для TypeScript 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
Транскрыпцыя захоўваецца, таму яна з’яўляецца ў Transcriptions у панэлі кіравання, можа быць падзелена спасылкай і прачытана ў любы час потым. Гэта асноўны цыкл: стварыць, чакаць, прачытаць, экспартаваць. Астатняя частка падручніка разглядае варыянты: медыя, якое ўжо анлайн, памылкі, з якімі вы сустрэнецеся, і кошт файла.
Калі медыя ўжо анлайн, перадайце API спасылку — і ён сам загрузіць файл: публічны адрас http(s) (mp3, відэа, эпізод подкаста) або відэа на YouTube. Выклік той самы — POST /v1/transcriptions, з JSON-целам { "url": "..." } замест multipart-запампоўкі, а 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}%`);
},
});
У даведніку ў Transcribe a link ёсць поўная праграма на абедзвюх мовах і ў 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…"
}
Уся табліца кодаў — у Errors, retries and limits. Гэта памылка 402 — адзіная, звязаная з грашыма, таму вось кошт файла.
Файлы каштуюць $0.20 за гадзіну гуку, спісваюцца па секундах ($0.0033 за хвіліну) з мінімумам 5 хвілін: 30-хвілінны файл каштуе $0.10. Гэта дзесяць цэнтаў — менш, чым кава, якую вы выпіце, пакуль запаўняецца паласа прагрэсу. Стаўка аднолькавая на кожным тарыфе і праз API, і ў панэлі кіравання. Новы ўліковы запіс пачынаецца з аднаразовага крэдыту пры рэгістрацыі $5, і картка не патрэбна.
Тры абмежаванні тарыфу тычацца гэтага падручніка.
GET /v1/realtime/capabilities паказвае як limits.maxUploadBytes. Дзейнічае меншы з двух, і файл, большы за яго, адхіляецца з 413, перш чым нешта транскрыбуецца. Для доўгага запісу гэта яшчэ адна прычына запампоўваць сцісненты манафанічны файл, а не WAV.GET /v1/billing у любы час паказвае ваш тарыф, баланс і абмежаванні; поўная табліца — на старонцы цэн.
Далей наступныя крокі — стрымінг аўдыё ў рэальным часе, калі не можаце чакаць, пакуль файл скончыцца, або падключэнне ШІ-агента да таго ж уліковага запісу праз MCP-сервер, каб ён транскрыбаваў і шукаў за вас без усяго кода вышэй.