Unggahan mengembalikan sebuah job, bukan transkrip. Sertakan Idempotency-Key pada setiap permintaan create agar percobaan ulang tidak pernah membuat transkripsi kedua, lalu polling job sampai selesai dan baca kalimat, kata, atau ekspor dari id yang sama.
Pemanggilan file adalah hal paling sederhana yang dilakukan API VocalStack, dan bentuknya sama seperti job asinkron mana pun: unggah, tunggu, baca. Unggahan langsung menjawab dengan sebuah transkripsi beserta job-nya, tetapi belum ada kata. Selagi program Anda melakukan polling terhadap job, server mentranskripsi file beberapa kali lebih cepat daripada audionya diputar. Saat statusnya done, Anda membaca transkrip sebagai kalimat dengan pembicara dan waktunya, atau mengeksportnya sebagai file dalam salah satu dari empat format, baik dalam bahasa yang diucapkan maupun dalam terjemahannya.
Semua yang ada dalam bentuk ini berlaku untuk API batch mana pun: create yang mengembalikan id, status yang Anda poll, hasil yang Anda ambil setelahnya. Hanya nama field-nya yang spesifik untuk VocalStack, dan tutorial ini menelusurinya dengan SDK TypeScript dan Python serta panggilan HTTP jika memang membantu.
Buka Developers → API keys di Dasbor dan buat kunci dengan izin Read dan Write. Kunci hanya ditampilkan sekali; salin ke variabel lingkungan:
export VOCALSTACK_API_KEY="<your API key>"
Kunci untuk server dan skrip. Apa pun yang dikirim ke browser ikut membawa kuncinya, jadi jauhkan kunci dari kode halaman dan dari URL, karena URL berakhir di log. Untuk melakukan streaming dari browser, server Anda membuat sesi realtime untuk browser tersebut; itu tutorial yang berbeda.
Setiap panggilan melakukan autentikasi dengan kunci sebagai bearer token:
Authorization: Bearer vs_key_...
Dengan kunci di lingkungan Anda, langkah berikutnya adalah klien untuk mengirimkannya.
Ada dua SDK. Masing-masing punya satu klien, VocalStack, dengan metodenya dikelompokkan per resource: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime dan beberapa lainnya. SDK TypeScript berjalan di Node 20 atau lebih baru dan di browser; SDK Python dibangun di atas asyncio dan membutuhkan Python 3.10 atau lebih baru.
SDK tidak tersedia di npm atau PyPI. API menyajikan build-nya untuk setiap akun. Perintah instalasi, yang sudah diisi sesuai lingkungan Anda, ada di bagian Install dalam quickstart di referensi API, dan perintah itu hanya mengirim kunci Anda ke host API. Jika Anda lebih suka tidak memasang apa pun, panggilan HTTP di bawah hanya membutuhkan curl dan jq.
Apa pun yang Anda pilih, panggilan pertama adalah unggahan.
POST /v1/transcriptions menerima file sebagai multipart/form-data: sebuah bagian file, plus bagian options opsional berisi JSON. Ia menjawab 201 dengan transkripsi. job.recordingId-nya adalah id rekaman yang dibuat dari file, dan setiap panggilan berikutnya dialamatkan dengan id itu.
Dua pengaturan penting untuk percobaan pertama, keduanya berada di dalam options.settings:
spokenLanguage: kode seperti en. Jika dibiarkan kosong, bahasanya dideteksi otomatis. Menyebutkannya meningkatkan akurasi pada rekaman yang bising dan aksen yang kental.speakerLabels: auto, on atau off. Dengan on, setiap kalimat membawa nomor pembicara; dengan auto, label hanya muncul setelah pembicara kedua terdengar.Sebagian besar file audio dan video diterima: mp3, wav, flac, ogg, m4a, mp4 dan lainnya. Video hanya menyimpan audionya. Format hampir tidak memengaruhi akurasi, karena pengenalan ucapan bekerja pada 16 kHz mono dan semuanya di-resample ke format itu saat tiba. WAV stereo 48 kHz berisi satu orang yang berbicara menghabiskan sebagian besar byte-nya untuk mendeskripsikan keheningan dengan fidelitas tinggi. Transkripsinya sama persis dengan MP3 mono 64 kbps atau file Opus dari pembicaraan yang sama, tetapi ukurannya sekitar dua puluh kali lebih besar, sehingga unggahannya pun memakan waktu dua puluh kali lebih lama. Jika Anda mengonversi sebelum mengunggah, mono dan bitrate yang wajar adalah dua pengaturan yang layak diubah.
Kirim Idempotency-Key pada setiap unggahan, dan pahami alasannya. Timeout jaringan tidak memberi tahu apa yang dilakukan server: bisa jadi server tidak pernah menerima permintaan, atau sudah menyimpan file dan memasukkan job ke antrean sebelum jawabannya hilang. Mengulang tanpa berpikir karenanya berisiko menghasilkan transkripsi kedua dan tagihan kedua. Kunci ini mencegah hal itu. Ketika permintaan diulang dengan kunci dan isi yang sama dalam 24 jam, server mengembalikan jawaban pertama alih-alih membuat apa pun, yang mengubah "setidaknya sekali" menjadi "efektif sekali". Jadi aturannya adalah satu kunci per unggahan logis, dibuat sebelum percobaan pertama dan dipakai ulang di setiap percobaan ulang. Kunci baru untuk setiap percobaan membuat setiap retry menjadi unggahan baru, dan header itu tidak melindungi apa pun.
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
Pemanggilan yang sama dengan 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)
Respons 201 datang sebelum satu kata pun ditranskripsi; yang Anda pegang adalah id job, jadi langkah berikutnya adalah menunggu job itu.
Job bergerak dari queued → transcribing → done, atau berakhir di failed dengan pesan error dan, jika penyebabnya dapat dibaca mesin, errorCode. Status keempat, cropping, berarti job ditahan sampai seseorang memilih bagian rekaman mana yang akan ditranskripsi, seperti yang dilakukan langkah trim di Dasbor. Unggahan biasa tidak pernah masuk ke status ini, meski jobs.wait juga kembali saat job mencapainya. Baca GET /v1/recordings/{id}/job sampai statusnya done atau failed.
Tidak ada webhook, dan itu pilihan yang disengaja. Webhook membutuhkan endpoint HTTPS publik di sisi Anda, pemeriksaan tanda tangan agar tidak ada pihak lain yang bisa memanggilnya, dan kebijakan retry untuk saat endpoint Anda mati, yang merupakan banyak sekali instalasi untuk job yang hanya memakan waktu satu menit. Polling tidak membutuhkan semua itu. Skrip Anda akan bertanya "sudah selesai?" setiap dua detik, seperti anak di kursi belakang mobil, tetapi tidak seperti anak itu, ia mendapat persentase sebagai balasannya.
Halaman web yang sudah terbuka bisa mengikuti socket event transkripsi sebagai gantinya (WS /v1/transcriptions/{id}/events, sebuah frame job.progress setiap job maju). Untuk skrip, polling adalah alat yang tepat.
SDK yang melakukan polling: vs.jobs.wait membaca job setiap 2 detik dan menyerahkan setiap pembacaan ke onProgress (on_progress di Python). progress job memuat pct (0 sampai 100) dan processedS, detik media yang sudah ditranskripsi. Program di bawah hanya mencetak baris ketika status atau persentase berubah.
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}")
Perhatikan bahwa wait tidak melempar error pada failed; ia mengembalikan job dan menyerahkan pengecekannya kepada Anda. Dua detik cocok untuk pembicaraan. Untuk rekaman tiga jam, atau batch ratusan file, perlebar interval (intervalMs, interval_s) atau perlambat saat job berjalan: pembacaan pertama memberi tahu Anda paling banyak, dan yang berikutnya sebagian besar hanya memastikan server masih sibuk. Tanpa SDK, Anda menulis loop yang sama dengan tangan:
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
Ketika status terbaca done, transkrip sudah siap.
GET /v1/recordings/{id}/transcript menjawab dengan seluruh transkrip dalam satu dokumen:
sentences: apa yang ditampilkan kepada orang. Masing-masing memiliki text, speaker, serta start dan end dalam detik.segments: baris-baris dari mesin, masing-masing dengan waktu per kata di words (setiap kata memiliki text, start dan end).speakers: satu entri per nomor pembicara, dengan name-nya.Pembicara dinomori mulai dari 0. speaker bernilai -1 berarti label pembicara dimatikan untuk rekaman tersebut. Tambahkan ?lang=es untuk membaca transkrip yang diterjemahkan ke bahasa Spanyol; kalimatnya kembali dalam bahasa itu.
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'
Dua tingkat waktu ini melayani dua pekerjaan. Kalimat adalah unit untuk dibaca, jadi itulah yang ditampilkan subtitle atau halaman transkrip. Kata adalah unit untuk ditunjuk, jadi highlight gaya karaoke, hasil pencarian, atau pemotongan klip menelusuri segments[].words sebagai gantinya. Format ekspor dibangun dari kalimat.
GET /v1/recordings/{id}/export mengembalikan file sungguhan. format berupa salah satu dari srt, json, xlsx atau pdf, dan file tersebut menyertakan koreksi apa pun. Tambahkan lang= untuk file terjemahan: ekspor kemudian dibaca dalam bahasa itu, satu baris per kalimat, dan kalimat apa pun yang belum diterjemahkan akan diterjemahkan lebih dulu.
SRT perlu dikenali bentuknya, karena ini format subtitle yang hampir semua pemutar terima dan berupa teks biasa. Sebuah file adalah daftar cue yang dipisahkan baris kosong, dan setiap cue terdiri dari penghitung, rentang waktu, dan teksnya. Stempel waktunya memakai koma sebelum milidetik, kebiasaan Prancis yang tidak pernah ditinggalkan format ini:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Satu kalimat per cue adalah ukuran yang tepat: setiap cue bisa dibaca dalam waktu ia tampil di layar, dan tidak ada yang terpotong di tengah pikiran. Ketika rekaman memiliki label pembicara, teks setiap cue diawali dengan nama pembicara dan titik dua.
Di SDK TypeScript, vs.recordings.export mengembalikan Response; baca byte-nya dengan arrayBuffer(). Di Python, ia mengembalikan 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
Transkripsinya tersimpan, sehingga muncul di bawah Transcriptions di Dasbor, dapat dibagikan lewat tautan, dan dapat dibaca lagi kapan saja nanti. Itulah inti alurnya: buat, tunggu, baca, ekspor. Sisa tutorial ini membahas variasinya: media yang sudah online, galat yang akan Anda temui, dan biaya sebuah file.
Jika media sudah online, berikan tautannya ke API dan ia akan mengambil file sendiri: alamat publik http(s) (mp3, video, episode podcast) atau video YouTube. Panggilannya sama, POST /v1/transcriptions, dengan body JSON { "url": "..." } sebagai pengganti unggahan multipart, dan name serta settings seperti sebelumnya. Di SDK, vs.transcriptions.createFromUrl(url) (TypeScript) dan vs.transcriptions.create(url=...) (Python) membuat permintaan yang sama.
Sebelum membuat apa pun, GET /v1/transcriptions/probe?url= memberi tahu apa tautan itu. kind-nya salah satu dari tiga:
file: rekaman yang punya akhir; transkripsikan di sini.live: stream langsung YouTube, stream radio tanpa akhir, atau playlist HLS tanpa akhir. Tautan langsung tidak pernah berakhir, jadi tidak bisa ditranskripsikan sebagai file; gunakan sebagai sumber transkripsi langsung.unknown: server tidak dapat menentukannya.Ketika tautan tidak terbuka sebagai media sama sekali (misalnya halaman web), ok bernilai false dan problem menjelaskan alasannya dalam satu kalimat. Probe bersifat opsional, dan dibatasi 60 panggilan per menit.
Job tautan dimulai di fetching sebelum berpindah ke transcribing dan done. Selama pengambilan, progress.phase menjelaskan apa yang dikerjakan server: fetching selagi byte masuk (dengan fetchedBytes dan fetchTotalBytes jika pihak lain menyebutkan jumlahnya), atau extracting selagi audio diambil dari video. progress.phasePct menunjukkan seberapa jauh job telah berjalan dalam fase itu. Video panjang membutuhkan waktu, jadi lakukan polling setiap beberapa detik; vs.jobs.wait menerima intervalMs (interval_s di Python) untuk keperluan itu.
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}%`);
},
});
Panduan Transcribe a link di referensi memuat program lengkap dalam kedua bahasa dan HTTP. Mengambil dari tautan menambah cara-cara baru untuk gagal, jadi berikut galat yang benar-benar akan Anda lihat.
Setiap panggilan yang gagal menjawab dengan satu envelope JSON. code adalah string yang dijadikan dasar percabangan dan status adalah status HTTP-nya; message ditujukan untuk manusia dan requestId untuk dukungan. Di bawah data, retryable menyatakan apakah mengirim ulang permintaan yang sama bisa membantu, dan details berisi apa pun yang dibutuhkan kodenya. SDK melempar satu tipe error, VocalStackError, dengan field yang sama.
Tiga kode ini perlu ditangani sejak hari pertama.
402 insufficient_credit: kredit akun habis, dan tidak ada yang dikenakan biaya. details.balance adalah saldo dalam dolar dan details.topUpUrl adalah tempat menambah kredit. Ketika biayanya diketahui di muka, seperti untuk file dengan panjang yang sudah diketahui, details.required menyatakan berapa biayanya.429 rate_limited: tunggu Retry-After detik. Ketika details.metric bernilai creates_per_hour, Anda telah mencapai batas laju pembuatan paket (transkripsi baru per jam), bukan batas lonjakan; limit dan resetsAt menyatakan berapa batasnya dan kapan batas itu kembali tersedia.503 unavailable: layanan sedang tidak tersedia sebentar. Kirim ulang permintaan setelah Retry-After.SDK mengulang 429 dan 503 setelah Retry-After, dua kali secara default (maxRetries / max_retries pada klien). Namun, 503 hanya diulang pada panggilan yang aman dikirim dua kali: GET, atau create yang membawa Idempotency-Key. Create tanpa kunci diserahkan kepada Anda, karena SDK tidak tahu apakah percobaan pertama sudah berhasil. Itulah alasan praktis untuk memasang kunci pada setiap unggahan.
Setiap respons juga membawa RateLimit-Limit, RateLimit-Remaining dan RateLimit-Reset, sehingga pipeline yang sibuk dapat melambat sebelum terkena 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…"
}
Tabel lengkap kode ada di Errors, retries and limits. 402 itu adalah satu-satunya galat yang terkait uang, jadi berikut biaya sebuah file.
File dikenakan $0.20 per jam audio, ditagih per detik ($0.0033 per menit) dengan minimum 5 menit: file 30 menit berharga $0.10. Itu sepuluh sen, kurang dari kopi yang Anda minum sementara bilah progres terisi. Tarifnya sama di setiap paket, dan sama lewat API maupun di Dasbor. Akun baru mulai dengan kredit pendaftaran $5 sekali pakai, dan tidak perlu kartu.
Tiga batas paket menyentuh tutorial ini.
GET /v1/realtime/capabilities sebagai limits.maxUploadBytes. Yang lebih kecil dari keduanya yang berlaku, dan file yang melebihinya ditolak dengan 413 sebelum apa pun ditranskripsi. Untuk rekaman panjang, ini satu alasan lagi untuk mengunggah file mono yang sudah dikompresi, bukan WAV.GET /v1/billing melaporkan paket, saldo, dan batas Anda kapan saja; tabel lengkapnya ada di halaman harga.
Dari sini, langkah berikutnya adalah streaming audio secara real time jika Anda tidak bisa menunggu file selesai, atau menghubungkan agen AI ke akun yang sama lewat server MCP, sehingga agen itu mentranskripsi dan mencari untuk Anda tanpa kode di atas sama sekali.