Прикачувањето враќа задача, а не транскрипт. Ставете Idempotency-Key на секое создавање, за повторен обид никогаш да не создаде или наплати втора транскрипција; потоа проверувајте ја задачата додека не заврши и читајте реченици, зборови или извоз со истиот идентификатор.
Транскрипцијата на датотека е наједноставната работа што ја прави VocalStack API, и има облик на секоја асинхрона задача: прикачување, чекање, читање. Прикачувањето веднаш враќа транскрипција и нејзината задача, но уште без зборови. Додека вашата програма ја проверува задачата, серверот ја транскрибира датотеката многу пати побрзо од времето на пуштањето на аудиото. Кога состојбата ќе биде done, транскриптот го читате како реченици со говорници и временски ознаки, или го извезувате како датотека во еден од четири формати, на зборуваниот јазик или преведен.
Сè во оваа форма важи за секој API за групна обработка: создавање што враќа идентификатор, состојба што ја проверувате и резултат што го преземате подоцна. Само имињата на полињата се специфични за VocalStack, а во ова упатство ги разгледуваме со TypeScript и Python SDK-и, и со 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 и уште неколку. 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 моно, а сè се претворува на таа фреквенција при пристигнувањето. Стерео WAV од 48 kHz со еден човек што зборува троши најголем дел од бајтовите за опишување на тишината со висока верност. Транскрибира точно исто како моно MP3 или Opus датотека од 64 kbps од истото предавање, но е околу дваесет пати поголема, па и прикачувањето трае дваесет пати подолго. Ако претворите пред прикачувањето, моно и умерен битрејт се двете поставки што вреди да се променат.
Испраќајте 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
Транскрипцијата е зачувана, па се појавува под 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}%`);
},
});
Водичот Транскрибирање врска во референцата ја содржи целата програма на двата SDK-а и во 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 пред да се транскрибира што и да е. За долга снимка, ова е уште една причина да прикачите компресирана моно датотека наместо WAV.GET /v1/billing во секое време го пријавува вашиот план, салдо и лимити; целата табела е на страницата со цени.
Оттука, следните чекори се стримување аудио во реално време кога не можете да чекате датотеката да заврши, или поврзување AI агент со истата сметка преку MCP серверот, па тој транскрибира и пребарува за вас без ниту еден од кодовите погоре.