Unha subida devolve unha tarefa, non unha transcrición. Pon un Idempotency-Key en cada creación para que un reintento nunca poida crear nin cobrar unha segunda transcrición; despois consulta a tarefa ata que remate e lee frases, palabras ou unha exportación desde o mesmo id.
Transcribir un ficheiro é a cousa máis sinxela que fai a API de VocalStack, e ten a forma de calquera tarefa asíncrona: subir, agardar, ler. A subida responde de inmediato cunha transcrición e a súa tarefa, pero aínda sen palabras. Mentres o programa consulta a tarefa, o servidor transcribe o ficheiro moitas veces máis rápido do que soa o audio. Cando o estado é done, lees a transcrición como frases con falantes e tempos, ou exportala como ficheiro nun dos catro formatos, na lingua falada ou traducida.
Todo o que ten esta forma transfírese a calquera API por lotes: unha creación que devolve un id, un estado que consultas, un resultado que recollés despois. Só os nomes dos campos son propios de VocalStack, e este tutorial recorre a eles cos SDK de TypeScript e Python, e cas chamadas HTTP onde axudan.
Abre Developers → API keys no panel e crea unha clave cos permisos Read e Write. A clave móstrase unha soa vez; cópiaa nunha variable de contorno:
export VOCALSTACK_API_KEY="<your API key>"
Unha clave é para servidores e scripts. Calquera cousa que se envíe a un navegador envía a clave con ela, así que mantena fóra do código da páxina e dos URL, que acaban nos rexistros. Para transmitir desde un navegador, o teu servidor crea unha sesión en tempo real para el; iso é outro tutorial diferente.
Cada chamada se autentica coa clave como token de portador:
Authorization: Bearer vs_key_...
Coa clave no teu contorno, o seguinte paso é un cliente que a envíe.
Hai dous SDK. Cada un ten un cliente, VocalStack, cos métodos agrupados por recurso: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime e algúns máis. O SDK de TypeScript funciona en Node 20 ou posterior e nos navegadores; o SDK de Python está construído sobre asyncio e necesita Python 3.10 ou posterior.
Os SDK non están en npm nin en PyPI. A API serve as compilacións a todas as contas. Os comandos de instalación, completados cos datos do teu contorno, están na sección Install do inicio rápido da referencia da API, e envían a clave só ao servidor da API. Se preferes non instalar nada, as chamadas HTTP de abaixo só necesitan curl e jq.
Sexa cal sexa o que escollas, a primeira chamada é a subida.
POST /v1/transcriptions recibe o ficheiro como multipart/form-data: unha parte file, máis unha parte options opcional con JSON. Responde 201 coa transcrición. O seu job.recordingId é o id da gravación feita a partir do ficheiro, e todas as chamadas seguintes se dirixen con ese id.
Dous axustes importan nunha primeira proba, ambos dentro de options.settings:
spokenLanguage: un código como en. Se o omites, a lingua detéctase. Nomeala mellora a precisión en gravacións con ruído e acentos fortes.speakerLabels: auto, on ou off. Con on, cada frase leva un número de falante; con auto, as etiquetas aparecen só cando se escoita un segundo falante.Acéptanse a maioría dos ficheiros de audio e vídeo: mp3, wav, flac, ogg, m4a, mp4 e outros. Un vídeo conserva só o seu audio. O formato case non afecta á precisión, porque o recoñecemento da fala traballa a 16 kHz mono e todo se remostrea a ese formato ao chegar. Un WAV estéreo de 48 kHz dunha soa persoa falando gasta a maior parte dos bytes describindo o silencio con alta fidelidade. Transcríbese exactamente igual que un ficheiro MP3 ou Opus mono de 64 kbps da mesma charla, pero é uns vinte veces máis grande, así que tarda vinte veces máis en subirse. Se convertes antes de subir, o mono e unha taxa de bits moderada son os dous axustes que vale a pena cambiar.
Envía un Idempotency-Key con cada subida, e entende por que. Un timeout de rede non che di nada sobre o que fixo o servidor: pode que nunca vise a petición, ou que gardase o ficheiro e puxese a tarefa en cola antes de perderse a resposta. Un reintento cego arrisca, polo tanto, unha segunda transcrición e un segundo cargo. A clave soluciona iso. Cando se repite unha petición coa mesma clave e o mesmo corpo dentro de 24 horas, o servidor devolve a primeira resposta sen crear nada, o que converte o «polo menos unha vez» en «efectivamente unha soa vez». Así que a regra é unha clave por cada subida lóxica, xerada antes do primeiro intento e reutilizada en cada reintento. Unha clave nova por intento faría que cada reintento fose unha subida nova, e a cabeceira non protexería nada.
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
A mesma chamada con 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)
O 201 chega antes de transcribirse unha soa palabra; o que tes na man é un id de tarefa, así que o seguinte paso é agardar por ela.
Unha tarefa pasa de queued → transcribing → done, ou termina en failed cunha mensaxe error e, cando a causa é legible por máquina, un errorCode. Un cuarto estado, cropping, significa que a tarefa queda en pausa ata que alguén escolla que tramo da gravación transcribir, como fai o paso de recorte do panel. Unha subida ordinaria nunca entra nel, aínda que jobs.wait tamén devolve cando unha tarefa o alcanza. Lee GET /v1/recordings/{id}/job ata que o estado sexa done ou failed.
Non hai webhooks, e é unha escolla deliberada. Un webhook necesita un endpoint HTTPS público no teu lado, unha comprobación de firma para que ninguén máis o poida chamar e unha política de reintentos para cando o teu endpoint estea caído, o que é moita canalización para unha tarefa que dura un minuto. Consultar non precisa nada disto. O teu script preguntará «xa?» cada dous segundos, coma un neno no asento de atrás, e ao contrario que o neno recibirá unha porcentaxe.
Unha páxina web que xa está aberta pode seguir o socket de eventos da transcrición (WS /v1/transcriptions/{id}/events, un marco job.progress mentres a tarefa avanza). Para un script, consultar é a ferramenta axeitada.
Os SDK fan as consultas: vs.jobs.wait lee a tarefa cada 2 segundos e pasa cada lectura a onProgress (on_progress en Python). O progress da tarefa trae pct (0 a 100) e processedS, os segundos de medios transcritos ata o momento. Os programas de abaixo imprimen unha liña só cando cambian o estado ou a porcentaxe.
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}")
Ten en conta que wait non lanza erro en failed; devolve a tarefa e deixa a comprobación en túa man. Dous segundos axustan para unha charla. Para unha gravación de tres horas, ou un lote de centos, amplía o intervalo (intervalMs, interval_s) ou retrasa as consultas a medida que a tarefa avanza: as primeiras lecturas dicen o máis, e as seguintes sobre todo confirman que o servidor segue ocupado. Sen SDK, escribes o mesmo bucle a man:
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
Cando o estado indique done, a transcrición está lista.
GET /v1/recordings/{id}/transcript responde coa transcrición completa nun único documento:
sentences: o que mostrar á xente. Cada un ten text, speaker, e start e end en segundos.segments: as liñas do motor, cada unha cos tempos por palabra en words (cada palabra ten text, start e end).speakers: unha entrada por número de falante, con o seu name.Os falantes numéranse desde o 0. Un speaker de -1 significa que as etiquetas de falante estaban desactivadas para a gravación. Engade ?lang=es para ler a transcrición traducida ao español; as frases chegan nesa lingua.
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'
Os dous niveis de tempo serven para dous fins. As frases son a unidade de lectura, así que son o que mostra unha subtítulo ou unha páxina de transcrición. As palabras son a unidade de sinalar, así que un realce tipo karaoke, un resultado de busca ou un corte de clip recorre segments[].words. Os formatos de exportación constrúense a partir das frases.
GET /v1/recordings/{id}/export devolve un ficheiro real. format é un de srt, json, xlsx ou pdf, e o ficheiro inclúe as correccións que haxa. Engade lang= para un ficheiro traducido: a exportación lese entón nesa lingua, unha liña por frase, e calquera frase que aínda non estea traducida tradúcese primeiro.
SRT paga a pena recoñecelo a primeira vista, porque é o formato de subtítulos que case todos os reprodutores aceptan e é texto plano. Un ficheiro é unha lista de bloques separados por liñas en branco, e cada bloque é un número, un intervalo de tempo e o texto. As marcas de tempo usan unha coma antes dos milisegundos, un hábito francés que o formato nunca perdeu:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Unha frase por bloque é o tamaño correcto: cada bloque pódese ler no tempo que está na pantalla, e ningún se corta a media idea. Cando a gravación tiña etiquetas de falante, o texto de cada bloque comeza co nome do falante e dous puntos.
No SDK de TypeScript, vs.recordings.export devolve un Response; lee os seus bytes con arrayBuffer(). En Python devolve 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
A transcrición gárdase, así que aparece en Transcriptions no panel, pode compartirse por ligazón e pódese volver ler en calquera momento posterior. Ese é o bucle central: crear, agardar, ler, exportar. O resto deste tutorial cobre as variacións: medios xa en liña, os erros que vas atopar e canto custa un ficheiro.
Se os medios xa están en liña, dálle á API a ligazón e ela descarga o ficheiro: un enderezo http(s) público (un mp3, un vídeo, un episodio de podcast) ou un vídeo de YouTube. A chamada é a mesma POST /v1/transcriptions, cun corpo JSON { "url": "..." } no lugar da subida multipart, e name e settings como antes. Nos SDK, vs.transcriptions.createFromUrl(url) (TypeScript) e vs.transcriptions.create(url=...) (Python) fan a mesma petición.
Antes de crear nada, GET /v1/transcriptions/probe?url= indícache o que é unha ligazón. O seu kind é un de tres:
file: unha gravación con final; transcríbea aquí.live: unha emisión de YouTube en directo, unha radio en fluxo sen fin ou unha lista HLS sen final. Unha ligazón en directo nunca termina, así que non se pode transcribir como ficheiro; úsaa como orixe dunha transcrición en directo.unknown: o servidor non o puido determinar.Cando a ligazón non se abre como medios (unha páxina web, por exemplo), ok é false e problem explica o motivo nunha frase. A sondaxe é opcional e está limitada a 60 chamadas por minuto.
Unha tarefa de ligazón comeza en fetching antes de pasar a transcribing e done. Mentres descarga, progress.phase indica o que fai o servidor: fetching mentres chegan bytes (con fetchedBytes e fetchTotalBytes cando o outro extremo indica cantos son), ou extracting mentres se extrae o audio dun vídeo. progress.phasePct indica por onde vai esa fase. Un vídeo longo tarda, así que consulta cada poucos segundos; vs.jobs.wait acepta intervalMs (interval_s en Python) para iso.
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 guía Transcribir unha ligazón da referencia ten o programa completo nas dúas linguas e en HTTP. Descargar desde ligazóns engade máis formas de fallar, así que aquí están os erros que de verdade vas ver.
Cada chamada fallida responde cun único sobre JSON. code é a cadea pola que se ramifica e status o estado HTTP; message é para as persoas e requestId para o soporte. Baixo data, retryable indica se reenviar a mesma petición pode axudar, e details recolle o que precise o código. Os SDK lanzan un único tipo de erro, VocalStackError, cos mesmos campos.
Tres códigos paga a pena xestionalos desde o primeiro día.
402 insufficient_credit: o crédito da conta esgotouse, e non se cobrou nada. details.balance é o saldo en dólares e details.topUpUrl indica onde engadir crédito. Cando o custo se coñecía de antemán, como ocorre cun ficheiro de duración coñecida, details.required indica cal sería.429 rate_limited: agarda os segundos de Retry-After. Cando details.metric é creates_per_hour, chegaches ao límite de creación do plan (novas transcricións por hora), non a un límite de ráfaga; limit e resetsAt indican cal é o tope e cando se libera.503 unavailable: o servizo está brevemente non dispoñible. Envía a petición de novo despois de Retry-After.Os SDK repiten un 429 e un 503 despois de Retry-After, dúas veces por defecto (maxRetries / max_retries no cliente). Un 503, con todo, só se repite nunha chamada que se pode enviar dúas veces sen problema: un GET, ou unha creación con Idempotency-Key. Unha creación sen clave déixase en túa man, porque o SDK non pode saber se o primeiro intento chegou. Esa é a razón práctica para poñer unha clave en cada subida.
Cada resposta tamén trae RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset, para que unha canalización ocupada poida frear antes do 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…"
}
A táboa completa de códigos está en Erros, reintentos e límites. Ese 402 é o único erro ligado aos cartos, así que aquí tes canto custa un ficheiro.
Os ficheiros cobran $0.20 por hora de audio, facturados por segundo ($0.0033 por minuto) cun mínimo de 5 minutos: un ficheiro de 30 minutos custa $0.10. Son dez céntimos, menos que o café que tomarás mentres avanza a barra de progreso. A tarifa é a mesma en todos os plans, e a mesma pola API que no panel. Unha conta nova comeza cun crédito de rexistro único de $5, e non se pide tarxeta.
Tres límites do plan afectan a este tutorial.
GET /v1/realtime/capabilities informa como limits.maxUploadBytes. Aplícase o menor dos dous, e un ficheiro por enriba del recházase cun 413 antes de transcribir nada. Para unha gravación longa, é unha razón máis para subir un ficheiro mono comprimido en lugar dun WAV.GET /v1/billing informa do teu plan, saldo e límites en calquera momento; a táboa completa está na páxina de prezos.
A partir de aquí, os seguintes pasos son transmitir audio en tempo real cando non podes esperar a que un ficheiro remate, ou conectar un axente de IA á mesma conta a través do servidor MCP, para que transcriba e busque por ti sen nada do código de arriba.