Muat naik memulangkan tugas, bukan transkrip. Letakkan Idempotency-Key pada setiap permintaan cipta supaya percubaan semula tidak pernah mencipta atau mengenakan bayaran untuk transkripsi kedua; kemudian undi tugas itu sehingga siap, dan baca ayat, perkataan atau eksport daripada id yang sama.
Transkripsi fail ialah perkara paling mudah yang dilakukan oleh API VocalStack, dan ia mempunyai bentuk mana-mana tugas tak segerak: muat naik, tunggu, baca. Muat naik memberikan jawapan serta-merta dengan transkripsi dan tugasnya, tetapi belum ada perkataan. Semasa program anda mengundi tugas itu, pelayan mentranskripsi fail beberapa kali lebih pantas daripada audio dimainkan. Apabila keadaannya done, anda membaca transkrip sebagai ayat dengan penutur dan masa, atau mengeksportnya sebagai fail dalam salah satu daripada empat format, sama ada dalam bahasa asal atau diterjemah.
Setiap perkara dalam bentuk ini boleh dipakai pada mana-mana API kelompok: cipta yang memulangkan id, status yang anda undi, dan hasil yang anda ambil kemudian. Hanya nama medan yang khusus untuk VocalStack, dan tutorial ini membimbing anda melaluinya dengan SDK TypeScript dan Python, serta panggilan HTTP di tempat yang membantu.
Buka Developers → API keys dalam Papan Pemuka dan cipta kunci dengan kebenaran Read dan Write. Kunci itu hanya dipaparkan sekali; salinnya ke dalam pembolehubah persekitaran:
export VOCALSTACK_API_KEY="<your API key>"
Kunci ialah untuk pelayan dan skrip. Apa-apa yang dihantar ke pelayar turut menghantar kunci itu bersama, jadi simpanlah ia di luar kod halaman dan di luar URL, kerana URL berakhir dalam log. Untuk strim daripada pelayar, pelayan anda mencipta sesi masa nyata untuknya; itu ialah tutorial yang berbeza.
Setiap panggilan disahkan dengan kunci itu sebagai token pembawa:
Authorization: Bearer vs_key_...
Dengan kunci dalam persekitaran anda, langkah seterusnya ialah klien untuk menghantarnya.
Terdapat dua SDK. Setiap satu mempunyai satu klien, VocalStack, dengan kaedahnya dikumpulkan mengikut sumber: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime dan beberapa lagi. SDK TypeScript berjalan pada Node 20 atau lebih baharu serta dalam pelayar; SDK Python dibina atas asyncio dan memerlukan Python 3.10 atau lebih baharu.
SDK tidak terdapat di npm atau PyPI. API menghidangkan binaan kepada setiap akaun. Arahan pemasangan, yang diisi untuk persekitaran anda, terdapat dalam bahagian "Install" pada kuick mula dalam rujukan API, dan ia hanya menghantar kunci anda ke hos API. Jika anda lebih suka tidak memasang apa-apa, panggilan HTTP di bawah hanya memerlukan curl dan jq.
Apa pun pilihan anda, panggilan pertama ialah muat naik.
POST /v1/transcriptions menerima fail sebagai multipart/form-data: bahagian file, serta bahagian options pilihan yang memegang JSON. Ia menjawab 201 dengan transkripsi. job.recordingId ialah id rakaman yang dibuat daripada fail itu, dan setiap panggilan selepas ini dialamatkan dengan id tersebut.
Dua tetapan penting untuk larian pertama, kedua-duanya dalam options.settings:
spokenLanguage: kod seperti en. Jika anda tinggalkannya, bahasa dikesan secara automatik. Menyatakannya meningkatkan ketepatan pada rakaman bising dan loghat yang berat.speakerLabels: auto, on atau off. Dengan on, setiap ayat membawa nombor penutur; dengan auto, label hanya muncul apabila penutur kedua sudah didengar.Kebanyakan fail audio dan video diterima: mp3, wav, flac, ogg, m4a, mp4 dan lain-lain. Video hanya mengekalkan audionya. Format hampir tidak menjejaskan ketepatan, kerana pengecaman pertuturan berfungsi pada 16 kHz mono dan semuanya disampel semula kepada kadar itu apabila tiba. Fail WAV stereo 48 kHz bagi seorang yang bercakap menghabiskan kebanyakan bainya untuk menggambarkan kesunyian dengan kesetiaan tinggi. Ia ditranskripsi sama baiknya seperti fail MP3 mono 64 kbps atau Opus bagi ucapan yang sama, tetapi saiznya kira-kira dua puluh kali ganda, jadi muat naiknya mengambil masa dua puluh kali lebih lama. Jika anda menukar fail sebelum memuat naik, mono dan kadar bit yang sederhana ialah dua tetapan yang berbaloi untuk diubah.
Hantar Idempotency-Key bersama setiap muat naik, dan fahami sebabnya. Tamat masa rangkaian tidak memberitahu anda apa yang dilakukan oleh pelayan: ia mungkin tidak pernah menerima permintaan itu, atau ia mungkin telah menyimpan fail dan menggilirkan tugas sebelum jawapan hilang. Percubaan semula secara membuta tuli oleh itu berisiko menghasilkan transkripsi kedua dan bayaran kedua. Kunci ini menyelesaikan perkara itu. Apabila permintaan diulang dengan kunci dan badan yang sama dalam tempoh 24 jam, pelayan memulangkan jawapan pertama tanpa mencipta apa-apa, yang menukarkan "sekurang-kurangnya sekali" kepada "berkesan sekali sahaja". Maka peraturannya ialah satu kunci bagi setiap muat naik logik, dijana sebelum percubaan pertama dan digunakan semula pada setiap percubaan semula. Kunci baharu bagi setiap percubaan akan menjadikan setiap percubaan semula muat naik yang baharu, dan header itu tidak akan melindungi apa-apa.
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
Panggilan 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)
201 datang sebelum satu perkataan pun ditranskripsi; apa yang anda pegang ialah id tugas, jadi langkah seterusnya ialah menunggu tugas itu.
Tugas bergerak daripada queued → transcribing → done, atau berakhir di failed dengan mesej error dan, apabila sebabnya boleh dibaca mesin, errorCode. Keadaan keempat, cropping, bermaksud tugas diletakkan untuk seketika sehingga seseorang memilih bahagian rakaman yang hendak ditranskripsi, seperti langkah pemangkasan dalam Papan Pemuka. Muat naik biasa tidak pernah memasuki keadaan ini, namun jobs.wait juga memulangkan apabila tugas sampai ke sana. Baca GET /v1/recordings/{id}/job sehingga keadaannya done atau failed.
Tiada webhook, dan itu ialah pertukaran yang disengajakan. Webhook memerlukan titik akhir HTTPS awam di pihak anda, semakan tandatangan supaya tiada orang lain boleh memanggilnya, dan dasar cuba semula apabila titik akhir anda tidak berfungsi, yang merupakan banyak kerja tambahan untuk tugas yang hanya mengambil masa seminit. Undian tidak memerlukan semua itu. Skrip anda akan bertanya "dah siap ke?" setiap dua saat, seperti kanak-kanak di tempat duduk belakang kereta, dan tidak seperti kanak-kanak itu, ia mendapat peratusan sebagai jawapan.
Halaman web yang sudah terbuka boleh mengikuti soket peristiwa transkripsi sebaliknya (WS /v1/transcriptions/{id}/events, dengan bingkai job.progress apabila tugas maju). Untuk skrip, undian ialah alat yang tepat.
SDK melakukan undian: vs.jobs.wait membaca tugas setiap 2 saat dan menyerahkan setiap bacaan kepada onProgress (on_progress dalam Python). progress tugas membawa pct (0 hingga 100) dan processedS, iaitu saat media yang sudah ditranskripsi setakat ini. Program di bawah mencetak satu baris hanya apabila keadaan atau peratusan 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}")
Ambil perhatian bahawa wait tidak membuang ralat pada failed; ia memulangkan tugas dan membiarkan semakan kepada anda. Dua saat sesuai untuk ceramah. Bagi rakaman tiga jam, atau kumpulan ratusan, lebarkan selang (intervalMs, interval_s) atau perlahankan semasa tugas berjalan: bacaan awal memberitahu anda paling banyak, dan bacaan kemudian kebanyakannya mengesahkan bahawa pelayan masih sibuk. Tanpa SDK, anda menulis gelung 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
Apabila keadaan menunjukkan done, transkrip sudah sedia.
GET /v1/recordings/{id}/transcript menjawab dengan keseluruhan transkrip dalam satu dokumen:
sentences: apa yang hendak ditunjukkan kepada orang. Setiap satu mempunyai text, speaker, dan start serta end dalam saat.segments: baris enjin, setiap satu dengan masa peringkat perkataan dalam words (setiap perkataan mempunyai text, start dan end).speakers: satu entri bagi setiap nombor penutur, dengan namenya.Penutur dinomborkan dari 0. speaker bernilai -1 bermaksud label penutur dimatikan untuk rakaman itu. Tambahkan ?lang=es untuk membaca transkrip yang diterjemahkan ke bahasa Sepanyol; ayat-ayatnya akan dipulangkan 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'
Kedua-dua tahap masa itu melayani dua tugas. Ayat ialah unit bacaan, jadi itulah yang ditunjukkan oleh sari kata atau halaman transkrip. Perkataan ialah unit penunjukan, jadi sorotan gaya karaoke, hasil carian atau pemotongan klip akan melalui segments[].words. Format eksport dibina daripada ayat.
GET /v1/recordings/{id}/export memulangkan fail sebenar. format ialah salah satu daripada srt, json, xlsx atau pdf, dan fail itu termasuk sebarang pembetulan. Tambahkan lang= untuk fail yang diterjemahkan: eksport kemudian dibaca dalam bahasa itu, satu baris bagi setiap ayat, dan mana-mana ayat yang belum diterjemahkan akan diterjemahkan dahulu.
SRT berbaloi untuk dikenali kerana ia format sari kata yang hampir setiap pemain terima dan ia berbentuk teks biasa. Sebuah fail ialah senarai cue yang dipisahkan oleh baris kosong, dan setiap cue ialah pembilang, julat masa dan teks. Cap masa menggunakan koma sebelum milisaat, tabiat Perancis yang format ini tidak pernah tinggalkan:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Satu ayat bagi setiap cue ialah saiz yang betul: setiap cue boleh dibaca dalam masa ia berada di skrin, dan tiada satu pun yang terpotong di tengah-tengah fikiran. Apabila rakaman mempunyai label penutur, teks setiap cue bermula dengan nama penutur dan titik bertindih.
Dalam SDK TypeScript, vs.recordings.export memulangkan Response; baca bait-baitnya dengan arrayBuffer(). Dalam Python ia memulangkan 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
Transkripsi itu disimpan, jadi ia muncul di bawah Transcriptions dalam Papan Pemuka, boleh dikongsi melalui pautan, dan boleh dibaca semula bila-bila masa kemudian. Itulah gelung terasnya: cipta, tunggu, baca, eksport. Baki tutorial ini meliputi variasinya: media yang sudah dalam talian, ralat yang akan anda temui, dan kos sebuah fail.
Jika media sudah dalam talian, berikan pautannya kepada API dan ia akan mengambil fail itu sendiri: alamat http(s) awam (fail mp3, video, episod podcast) atau video YouTube. Panggilannya ialah POST /v1/transcriptions yang sama, dengan badan JSON { "url": "..." } menggantikan muat naik multipart, dan name serta settings seperti sebelumnya. Dalam SDK, vs.transcriptions.createFromUrl(url) (TypeScript) dan vs.transcriptions.create(url=...) (Python) membuat permintaan yang sama.
Sebelum mencipta apa-apa, GET /v1/transcriptions/probe?url= memberitahu anda apakah pautan itu. kindnya ialah salah satu daripada tiga:
file: rakaman yang ada penghujung; transkripsikannya di sini.live: strim langsung YouTube, strim radio tanpa henti atau senarai main HLS tanpa penghujung. Pautan langsung tidak pernah tamat, jadi ia tidak boleh ditranskripsi sebagai fail; gunakannya sebagai sumber bagi transkripsi langsung sebaliknya.unknown: pelayan tidak dapat menentukannya.Apabila pautan tidak dibuka sebagai media sama sekali (contohnya halaman web), ok ialah false dan problem menerangkan sebabnya dalam satu ayat. Probe adalah pilihan, dan dihadkan kepada 60 panggilan seminit.
Tugas pautan bermula dalam fetching sebelum beralih kepada transcribing dan done. Semasa ia mengambil fail, progress.phase memberitahu apa yang dilakukan oleh pelayan: fetching semasa bait tiba (dengan fetchedBytes dan fetchTotalBytes apabila pihak lain menyatakan jumlahnya), atau extracting semasa audio diekstrak daripada video. progress.phasePct menunjukkan sejauh mana fasa itu telah berjalan. Video panjang mengambil masa, jadi undilah setiap beberapa saat; vs.jobs.wait menerima intervalMs (interval_s dalam Python) untuk 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 Transkripsi pautan dalam rujukan mempunyai program penuh dalam kedua-dua bahasa dan HTTP. Pengambilan daripada pautan menambah lebih banyak cara untuk gagal, jadi inilah ralat yang benar-benar akan anda lihat.
Setiap panggilan yang gagal memberikan satu sampul JSON. code ialah rentetan untuk dibuat cabang, dan status ialah status HTTP; message untuk manusia dan requestId untuk sokongan. Di bawah data, retryable memberitahu sama ada menghantar permintaan yang sama semula boleh membantu, dan details memegang apa sahaja yang diperlukan oleh kod itu. SDK menimbulkan satu jenis ralat, VocalStackError, dengan medan yang sama.
Tiga kod patut ditangani pada hari pertama.
402 insufficient_credit: kredit akaun sudah habis, dan tiada apa-apa dikenakan. details.balance ialah baki dalam dolar dan details.topUpUrl ialah tempat untuk menambah kredit. Apabila kos diketahui lebih awal, seperti untuk fail yang panjangnya diketahui, details.required menyatakan berapa jumlahnya.429 rate_limited: tunggu Retry-After saat. Apabila details.metric ialah creates_per_hour, anda telah mencapai kadar cipta pelan itu (transkripsi baharu sejam), bukan had letupan; limit dan resetsAt menyatakan had itu dan bila ia dibebaskan.503 unavailable: perkhidmatan tidak tersedia buat sementara. Hantar permintaan semula selepas Retry-After.SDK mencuba semula 429 dan 503 selepas Retry-After, dua kali secara lalai (maxRetries / max_retries pada klien). Tetapi 503 hanya dicuba semula pada panggilan yang selamat untuk dihantar dua kali: GET, atau cipta yang membawa Idempotency-Key. Cipta tanpa kunci itu diserahkan kepada anda, kerana SDK tidak tahu sama ada percubaan pertama telah berjaya. Itulah sebab praktikal untuk meletakkan kunci pada setiap muat naik.
Setiap respons juga membawa RateLimit-Limit, RateLimit-Remaining dan RateLimit-Reset, supaya saluran paip yang sibuk boleh memperlahankan diri sebelum menerima 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…"
}
Jadual lengkap kod ada dalam Ralat, percubaan semula dan had. 402 itu ialah satu-satunya ralat yang berkaitan dengan wang, jadi inilah kos sebuah fail.
Fail dikenakan $0.20 bagi setiap jam audio, dibilkan mengikut saat ($0.0033 seminit) dengan minimum 5 minit: fail 30 minit berharga $0.10. Itu sepuluh sen, kurang daripada kopi yang anda minum ketika bar kemajuan dipenuhi. Kadarnya sama pada setiap pelan, dan sama melalui API seperti dalam Papan Pemuka. Akaun baharu bermula dengan kredit pendaftaran sekali sahaja sebanyak $5, dan tiada kad diperlukan.
Tiga had pelan menyentuh tutorial ini.
GET /v1/realtime/capabilities sebagai limits.maxUploadBytes. Yang lebih kecil antara keduanya dikenakan, dan fail yang melebihinya ditolak dengan 413 sebelum apa-apa ditranskripsi. Untuk rakaman panjang, ini satu lagi sebab untuk memuat naik fail mono yang dimampatkan dan bukannya WAV.GET /v1/billing melaporkan pelan, baki dan had anda pada bila-bila masa; jadual penuh ada di halaman harga.
Dari sini, langkah seterusnya ialah penstriman audio secara masa nyata apabila anda tidak dapat menunggu fail siap, atau menyambungkan ejen AI kepada akaun yang sama melalui pelayan MCP, supaya ia mentranskripsi dan mencari untuk anda tanpa sebarang kod di atas.