Guide · async
Transcribe a batch of files
Send many files or links as one batch, start it once, and read every transcript when it is done. Cheaper than one transcription per file.
A batch is several files transcribed together, for less than a transcription each: the batch counts once against the plan's transcriptions created per hour, and it is billed by the second of audio with one minimum charge for the whole batch, not one per file. Each file is still a transcription of its own (its batchId names the batch), with its job, transcript, audio and exports, and speaker labels when the batch asks for them. A batch's files never get a summary, a title, a share link or spoken translations.
Create the batch, add each file or link, start it, then wait for it to be done.
Create the batch
POST /v1/batchesanswers201with anopenbatch. The body is optional: aname(by default the batch is named for its date) andsettingsevery file runs with,spokenLanguage(absent: detected per file),speakerLabels(offby default,autooron) andlatencyMode.Add the files
POST /v1/batches/{id}/filestakes one file or link per call, the wayPOST /v1/transcriptionsdoes:multipart/form-datawith afilepart, or JSON with aurl. Of the options, onlyname,startS,endSanddurationSapply (the settings are the batch's). The answer is the file's transcription; its job waits for the batch's start. In the SDKs, adding a file takes either a file or aurl, and adding a URL is the link shorthand (addFile/addUrlin TypeScript and PHP,add_file/add_urlin Python).A batch holds up to the plan's files per batch (
limits.filesPerBatchinGET /v1/billing/plans); one more is refused withinvalid_state. Send the rest in another batch.Start it
POST /v1/batches/{id}/startseals the batch (no more files) and queues its files, longest first: as many run at once as the plan allows transcriptions at once. The account needs credit for more than one minimum charge; each file's length is checked against the balance as it starts. A batch that was never started can be deleted withDELETE /v1/batches/{id}(nothing was charged); a started one is stopped withPOST /v1/batches/{id}/stop.Wait for it
GET /v1/batches/{id}answers with the batch, itscounts(queued,running,done,failed,stopped) and itsfiles, each a transcription with itsjob. The batch isrunninguntil every file has finished or failed, thendone. The SDKs'batches.waitreads it every 5 s until the batch is closed (finishedAt: a stopped batch waits for a file it was still running); a failed file does not fail the wait, so checkcounts.failed.Read the transcripts
Each file's transcript is
GET /v1/recordings/{id}/transcriptwith itsjob.recordingId, as for a single file.GET /v1/batches/{id}/exportreturns every finished file's transcript in oneformat(srt,json,xlsxorpdf) as one zip.
TypeScript SDK
import { writeFile } from 'node:fs/promises';
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: 'https://api.vocalstack.com', apiKey: process.env.VOCALSTACK_API_KEY });
const batch = await vs.batches.create({ name: 'Interviews', settings: { spokenLanguage: 'en', speakerLabels: 'auto' } });
await vs.batches.addFile(batch.id, { file: './interview-1.mp3' });
await vs.batches.addFile(batch.id, { file: './interview-2.mp3' });
await vs.batches.addUrl(batch.id, 'https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac', { name: 'JFK' });
await vs.batches.start(batch.id);
const done = await vs.batches.wait(batch.id, {
onProgress: (b) => console.log(`${b.state}: ${b.counts.done} done, ${b.counts.running} running, ${b.counts.queued} queued`),
});
if (done.counts.failed > 0) console.log(`${done.counts.failed} file(s) failed`);
for (const tx of done.files ?? []) {
if (tx.job?.state !== 'done') continue;
const { sentences } = await vs.recordings.transcript(tx.job.recordingId);
console.log(`${tx.name}: ${sentences.map((s) => s.text).join(' ')}`);
}
const zip = await vs.batches.export(batch.id, 'srt');
await writeFile('interviews.zip', Buffer.from(await zip.arrayBuffer()));
Python SDK
import asyncio
import os
from vocalstack import VocalStack, models
def show(b: models.Batch) -> None:
print(f"{b.state}: {b.counts.done} done, {b.counts.running} running, {b.counts.queued} queued")
async def main() -> None:
async with VocalStack("https://api.vocalstack.com", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
batch = await vs.batches.create(name="Interviews", settings={"spokenLanguage": "en", "speakerLabels": "auto"})
await vs.batches.add_file(batch.id, file="./interview-1.mp3")
await vs.batches.add_file(batch.id, file="./interview-2.mp3")
await vs.batches.add_url(batch.id, "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac", name="JFK")
await vs.batches.start(batch.id)
done = await vs.batches.wait(batch.id, on_progress=show)
if done.counts.failed:
print(f"{done.counts.failed} file(s) failed")
for tx in done.files or []:
if tx.job is None or tx.job.state != "done":
continue
snapshot = await vs.recordings.transcript(tx.job.recording_id)
print(f"{tx.name}: " + " ".join(s.text for s in snapshot.sentences))
with open("interviews.zip", "wb") as f:
f.write(await vs.batches.export(batch.id, "srt"))
asyncio.run(main())
PHP SDK
<?php
require 'vendor/autoload.php';
use VocalStack\VocalStack;
$vs = new VocalStack(apiKey: getenv('VOCALSTACK_API_KEY'), baseUrl: 'https://api.vocalstack.com');
$batch = $vs->batches->create(['name' => 'Interviews', 'settings' => ['spokenLanguage' => 'en', 'speakerLabels' => 'auto']]);
$vs->batches->addFile($batch['id'], file: './interview-1.mp3');
$vs->batches->addFile($batch['id'], file: './interview-2.mp3');
$vs->batches->addUrl($batch['id'], 'https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac', ['name' => 'JFK']);
$vs->batches->start($batch['id']);
$done = $vs->batches->wait($batch['id'], onProgress: function (array $b): void {
$c = $b['counts'];
echo "{$b['state']}: {$c['done']} done, {$c['running']} running, {$c['queued']} queued\n";
});
if ($done['counts']['failed'] > 0) {
echo "{$done['counts']['failed']} file(s) failed\n";
}
foreach ($done['files'] ?? [] as $tx) {
if (($tx['job']['state'] ?? null) !== 'done') {
continue;
}
$snapshot = $vs->recordings->transcript($tx['job']['recordingId']);
echo $tx['name'], ': ', implode(' ', array_column($snapshot['sentences'], 'text')), "\n";
}
file_put_contents('interviews.zip', $vs->batches->export($batch['id'], 'srt')); // the zip's bytes
HTTP + WebSocket
#!/usr/bin/env bash
# curl and jq, no SDK.
set -euo pipefail
API=https://api.vocalstack.com
AUTH="Authorization: Bearer $VOCALSTACK_API_KEY"
BATCH=$(curl -sSf "$API/v1/batches" -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name":"Interviews","settings":{"spokenLanguage":"en","speakerLabels":"auto"}}' | jq -r .id)
curl -sSf "$API/v1/batches/$BATCH/files" -H "$AUTH" -F file=@interview-1.mp3 > /dev/null
curl -sSf "$API/v1/batches/$BATCH/files" -H "$AUTH" -F file=@interview-2.mp3 > /dev/null
curl -sSf "$API/v1/batches/$BATCH/files" -H "$AUTH" -H "Content-Type: application/json" \
-d '{"url":"https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac","name":"JFK"}' > /dev/null
curl -sSf -X POST "$API/v1/batches/$BATCH/start" -H "$AUTH" | jq -r .state
while :; do
B=$(curl -sSf "$API/v1/batches/$BATCH" -H "$AUTH")
jq -r '"\(.state): \(.counts.done) done, \(.counts.running) running, \(.counts.queued) queued"' <<<"$B"
case $(jq -r .state <<<"$B") in done|stopped) break ;; esac
sleep 5
done
for REC in $(jq -r '.files[] | select(.job.state == "done") | .job.recordingId' <<<"$B"); do
curl -sSf "$API/v1/recordings/$REC/transcript" -H "$AUTH" | jq -r '[.sentences[].text] | join(" ")'
done
curl -sSf "$API/v1/batches/$BATCH/export?format=srt" -H "$AUTH" -o interviews.zip