Yükleme bir transkript değil, bir iş döndürür. Bir yeniden deneme asla ikinci bir transkripsiyon oluşturmasın ya da ücretlendirmesin diye her oluşturma isteğine bir Idempotency-Key ekleyin; ardından işin bitmesini yoklayın ve cümleleri, kelimeleri ya da bir dışa aktarmayı aynı kimlikten okuyun.
Dosya transkripsiyonu VocalStack API'nin en basit işidir ve her asenkron işin biçimine sahiptir: yükleyin, bekleyin, okuyun. Yükleme hemen bir transkripsiyon ve onun işiyle yanıt verir, ama henüz hiçbir kelime yoktur. Programınız işi yoklarken sunucu dosyayı, ses çalınan süreden çok daha hızlı transkripsiyon eder. Durum done olduğunda transkripti konuşmacılar ve zamanlamalarla birlikte cümleler hâlinde okursunuz ya da dört biçimden birinde dosya olarak dışa aktarırsınız; konuşulan dilde ya da çevrilmiş olarak.
Bu biçimdeki her şey herhangi bir toplu iş API'sine uyar: bir kimlik döndüren bir oluşturma, yoklanan bir durum ve sonra alınan bir sonuç. Yalnızca alan adları VocalStack'e özgüdür; bu öğretici, TypeScript ve Python SDK'larını ve yardımcı olduğu yerlerde HTTP çağrılarını adım adım gösterir.
Panel'de Geliştiriciler → API anahtarları sayfasını açın ve Read ve Write izinleriyle bir anahtar oluşturun. Anahtar yalnızca bir kez gösterilir; onu bir ortam değişkenine kopyalayın:
export VOCALSTACK_API_KEY="<your API key>"
Anahtar sunucular ve betikler içindir. Bir tarayıcıya gönderilen her şey anahtarı da beraberinde taşır; bu yüzden anahtarı sayfa kodunun dışında ve günlüklere düşen URL'lerin dışında tutun. Bir tarayıcıdan akış yapmak için sunucunuz onun adına bir gerçek zamanlı oturum oluşturur; bu ayrı bir öğreticidir.
Her çağrı, anahtarı bir taşıyıcı belirteci olarak doğrular:
Authorization: Bearer vs_key_...
Anahtar ortamınızdayken, sıradaki adım onu gönderecek bir istemcidir.
İki SDK vardır. Her birinde VocalStack adında tek bir istemci bulunur ve yöntemleri kaynağa göre gruplanmıştır: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime ve birkaç tane daha. TypeScript SDK'sı Node 20 ve üzerinde ile tarayıcılarda çalışır; Python SDK'sı asyncio üzerine kuruludur ve Python 3.10 veya üzerini gerektirir.
SDK'lar npm ya da PyPI'da yer almaz. API, derlemeleri her hesaba sunar. Kurulum komutları, ortamınıza göre doldurulmuş hâlde API başvurusundaki hızlı başlangıcın Kurulum bölümünde bulunur ve anahtarınızı yalnızca API sunucusuna gönderir. Hiçbir şey kurmak istemezseniz, aşağıdaki HTTP çağrıları yalnızca curl ve jq gerektirir.
Hangisini seçerseniz seçin, ilk çağrı yüklemedir.
POST /v1/transcriptions dosyayı multipart/form-data olarak alır: bir file parçası ve JSON içeren isteğe bağlı bir options parçası. 201 yanıtıyla birlikte transkripsiyonu döndürür. Onun job.recordingId değeri, dosyadan oluşturulan kaydın kimliğidir ve bundan sonraki her çağrı bu kimlikle adreslenir.
İlk çalıştırma için iki ayar önemlidir; ikisi de options.settings içindedir:
spokenLanguage: en gibi bir kod. Bunu atlarsanız dil algılanır. Adını vermek, gürültülü kayıtlarda ve ağır aksanlarda doğruluğu artırır.speakerLabels: auto, on veya off. on ile her cümle bir konuşmacı numarası taşır; auto ile etiketler yalnızca ikinci bir konuşmacı duyulduktan sonra görünür.Çoğu ses ve video dosyası kabul edilir: mp3, wav, flac, ogg, m4a, mp4 ve diğerleri. Video yalnızca sesini tutar. Biçim doğruluğu pek etkilemez, çünkü konuşma tanıma 16 kHz mono üzerinde çalışır ve gelen her şey buna yeniden örneklenir. Tek bir kişinin konuştuğu 48 kHz stereo bir WAV dosyası, baytlarının çoğunu yüksek kalitede sessizliği tanımlamaya harcar. Aynı konuşmanın 64 kbps mono bir MP3 ya da Opus dosyası kadar iyi transkripsiyon edilir, ama yirmi kat büyüktür ve bu yüzden yüklenmesi de yirmi kat uzun sürer. Yüklemeden önce dönüştürüyorsanız, değiştirmeye değer iki ayar mono ve makul bir bit hızıdır.
Her yüklemeye bir Idempotency-Key gönderin ve nedenini anlayın. Ağ zaman aşımı, sunucunun ne yaptığı hakkında hiçbir şey söylemez: isteği hiç görmemiş olabilir ya da yanıt kaybolmadan önce dosyayı saklayıp işi kuyruğa almış olabilir. Körü körüne bir yeniden deneme, bu yüzden ikinci bir transkripsiyon ve ikinci bir ücret riskini taşır. Anahtar bunu çözer. Aynı anahtar ve gövdeyle 24 saat içinde tekrarlanan bir istekte sunucu bir şey oluşturmak yerine ilk yanıtı döndürür; böylece "en az bir kez", "fiilen bir kez"e dönüşür. Kural şudur: mantıksal bir yükleme başına bir anahtar; ilk denemeden önce üretilir ve her yeniden denemede yeniden kullanılır. Her deneme için yeni bir anahtar, her yeniden denemeyi yeni bir yükleme yapardı ve başlık hiçbir şeyi korumazdı.
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
Aynı çağrının curl ile yazılmış hâli:
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 yanıtı tek bir kelime transkripsiyon edilmeden gelir; elinizde bir iş kimliği vardır, bu yüzden sıradaki adım ona bağlanmaktır.
Bir iş queued → transcribing → done sırasıyla ilerler ya da failed ile biter; failed durumunda bir error mesajı ve neden makine tarafından okunabiliyorsa bir errorCode bulunur. Dördüncü bir durum olan cropping, işin bir sonraki adım için beklediği anlamına gelir: Panel'deki kırpma adımında olduğu gibi, transkripsiyon edilecek kayıt aralığını biri seçene kadar iş beklemededir. Sıradan bir yükleme hiçbir zaman bu duruma girmez, ancak jobs.wait iş buraya ulaştığında da döner. Durum done ya da failed olana kadar GET /v1/recordings/{id}/job adresini okuyun.
Webhook yoktur ve bu bilinçli bir takastır. Bir webhook, sizin tarafınızda herkese açık bir HTTPS uç noktası, başka birinin çağıramaması için bir imza denetimi ve uç noktanız çalışmadığında bir yeniden deneme politikası gerektirir; bir dakika süren bir iş için bu çok fazla altyapıdır. Yoklama bunların hiçbirine ihtiyaç duymaz. Betiğiniz her iki saniyede bir "bitti mi?" diye soracak; arka koltuktaki bir çocuk gibi, ama çocuktan farklı olarak bir yüzde de geri alacak.
Halihazırda açık olan bir web sayfası, bunun yerine transkripsiyonun olay soketini izleyebilir (WS /v1/transcriptions/{id}/events; iş ilerledikçe bir job.progress çerçevesi gelir). Bir betik için yoklama doğru araçtır.
SDK'lar yoklamayı kendileri yapar: vs.jobs.wait işi her 2 saniyede bir okur ve her okumayı onProgress (Python'da on_progress) işlevine verir. İşin progress alanı pct (0 ile 100 arası) ve processedS değerlerini taşır; processedS, şimdiye kadar transkripsiyon edilen medya saniyesidir. Aşağıdaki programlar yalnızca durum ya da yüzde değiştiğinde bir satır yazdırır.
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 işin failed durumunda hata fırlatmadığını unutmayın; işi döndürür ve kontrolü size bırakır. İki saniye bir konuşma için uygundur. Üç saatlik bir kayıt ya da yüzlerce işten oluşan bir toplu iş için aralığı genişletin (intervalMs, interval_s) ya da iş ilerledikçe bekleme süresini artırın: ilk okumalar en çok bilgiyi verir, sonraki okumalar çoğunlukla sunucunun hâlâ meşgul olduğunu doğrular. SDK olmadan aynı döngüyü elle yazarsınız:
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
Durum done yazdığında transkript hazırdır.
GET /v1/recordings/{id}/transcript tüm transkripti tek bir belgede yanıtlar:
sentences: insanlara gösterilecek olan. Her birinde text, speaker ve saniye cinsinden start ile end bulunur.segments: motorun satırları; her birinde words içinde kelime düzeyinde zamanlamalar bulunur (her kelimenin text, start ve end değeri vardır).speakers: konuşmacı numarası başına bir giriş, name alanıyla birlikte.Konuşmacılar 0'dan numaralandırılır. -1 olan bir speaker, kayıt için konuşmacı etiketlerinin kapalı olduğu anlamına gelir. Transkripti İspanyolcaya çevrilmiş olarak okumak için ?lang=es ekleyin; cümleler o dilde döner.
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'
İki zamanlama düzeyi iki ayrı işe hizmet eder. Cümleler okuma birimidir; bu yüzden bir altyazının ya da transkript sayfasının gösterdiği şey onlardır. Kelimeler işaretleme birimidir; bu yüzden karaoke tarzı bir vurgulama, bir arama sonucu ya da bir klip kesimi segments[].words listesinde ilerler. Dışa aktarma biçimleri cümlelerden oluşturulur.
GET /v1/recordings/{id}/export gerçek bir dosya döndürür. format değeri srt, json, xlsx ya da pdf olabilir; dosya uygulanan tüm düzeltmeleri içerir. Çevrilmiş bir dosya için lang= ekleyin: dışa aktarma o dilde, cümle başına bir satır olarak okunur ve henüz çevrilmemiş her cümle önce çevrilir.
SRT'yi görerek tanımak faydalıdır, çünkü neredeyse her oynatıcının kabul ettiği altyazı biçimidir ve düz metindir. Dosya, boş satırlarla ayrılmış altyazı bloklarından oluşur; her blok bir sayaç, bir zaman aralığı ve metinden oluşur. Zaman damgaları milisaniyeden önce virgül kullanır; biçimin hiç bırakmadığı bir Fransız alışkanlığı:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Cümle başına bir altyazı bloğu doğru ölçüdür: her blok ekranda kaldığı sürede okunabilir ve hiçbiri bir düşüncenin ortasında kesilmez. Kayıtta konuşmacı etiketleri varsa, her bloğun metni konuşmacının adı ve iki nokta üst üste ile başlar.
TypeScript SDK'sında vs.recordings.export bir Response döndürür; baytlarını arrayBuffer() ile okuyun. Python'da bytes döndürür.
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
Transkripsiyon kaydedilir; bu yüzden Panel'de Transkripsiyonlar altında görünür, bağlantıyla paylaşılabilir ve istediğiniz zaman sonra yeniden okunabilir. Temel döngü budur: oluştur, bekle, oku, dışa aktar. Bu öğreticinin geri kalanı varyasyonları kapsar: çevrimiçi hâlihazırda bulunan medya, karşılaşacağınız hatalar ve bir dosyanın maliyeti.
Medya zaten çevrimiçiyse, API'ye bağlantıyı verin; dosyayı kendisi getirir: herkese açık bir http(s) adresi (bir mp3, bir video, bir podcast bölümü) ya da bir YouTube videosu. Çağrı aynı POST /v1/transcriptions çağrısıdır; çok parçalı yüklemenin yerine { "url": "..." } JSON gövdesiyle, name ve settings ise öncekiyle aynıdır. SDK'larda vs.transcriptions.createFromUrl(url) (TypeScript) ve vs.transcriptions.create(url=...) (Python) aynı isteği yapar.
Bir şey oluşturmadan önce GET /v1/transcriptions/probe?url= bağlantının ne olduğunu söyler. Onun kind değeri üç türden biridir:
file: sonu olan bir kayıt; burada transkripsiyon edin.live: bir YouTube canlı yayını, sonu olmayan bir radyo akışı ya da bir HLS oynatma listesi. Canlı bir bağlantı hiç bitmez, bu yüzden dosya olarak transkripsiyon edilemez; onu bunun yerine canlı transkripsiyonun kaynağı olarak kullanın.unknown: sunucu ayırt edemedi.Bağlantı hiç medya olarak açılmıyorsa (örneğin bir web sayfasıysa), ok değeri false olur ve problem nedenini tek cümlede açıklar. Yoklama isteğe bağlıdır ve dakikada 60 çağrıyla sınırlıdır.
Bir bağlantı işi fetching durumunda başlar, sonra transcribing ve done'a geçer. Getirme sırasında progress.phase sunucunun ne yaptığını söyler: bayt gelirken fetching (karşı taraf kaç bayt olduğunu söylüyorsa fetchedBytes ve fetchTotalBytes ile birlikte) ya da sesin bir videodan çıkarıldığını gösteren extracting. progress.phasePct işin o aşamada ne kadar ilerlediğini gösterir. Uzun bir video zaman alır; bu yüzden birkaç saniyede bir yoklayın. Bunun için vs.jobs.wait, intervalMs (Python'da interval_s) alır.
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}%`);
},
});
Tam program, her iki dilde ve HTTP olarak, Bir bağlantıyı transkripsiyon edin kılavuzunda yer alır. Bağlantılardan getirmenin de başarısız olma yolları eklenir; bu yüzden gerçekten karşılaşacağınız hatalar şunlardır.
Başarısız her çağrı tek bir JSON zarfıyla yanıt verir. code, koda göre dallanmak için kullanacağınız dizedir ve status HTTP durumudur; message insanlar içindir, requestId ise destek içindir. data altında retryable, aynı isteğin yeniden gönderilmesinin yardımcı olup olmayacağını söyler; details ise kodun ihtiyaç duyduğu her şeyi tutar. SDK'lar tek bir hata türü, VocalStackError, fırlatır ve alanları aynıdır.
İlk günden ele alınmaya değer üç kod vardır.
402 insufficient_credit: hesabın kredisi bitmiştir ve hiçbir ücret alınmamıştır. details.balance bakiyeyi dolar olarak, details.topUpUrl ise kredi ekleyeceğiniz adresi verir. Maliyet önceden biliniyorsa, örneğin bilinen uzunluktaki bir dosyada olduğu gibi, details.required tutarın ne olacağını söyler.429 rate_limited: Retry-After saniye kadar bekleyin. details.metric creates_per_hour ise, bir patlama sınırına değil, planın oluşturma hızına (saatte yeni transkripsiyon) takılmışsınızdır; limit ve resetsAt sınırın ne olduğunu ve ne zaman açılacağını söyler.503 unavailable: hizmet kısa süreliğine kullanılamıyor. İsteği Retry-After süresinden sonra yeniden gönderin.SDK'lar 429 ve 503 yanıtlarını Retry-After süresinden sonra, varsayılan olarak iki kez yeniden dener (istemcideki maxRetries / max_retries). Ancak bir 503 yalnızca iki kez gönderilmesi güvenli olan bir çağrıda yeniden denenir: bir GET ya da bir Idempotency-Key taşıyan bir oluşturma. Anahtarı olmayan bir oluşturma sizin sorumluluğunuzdadır, çünkü SDK ilk denemenin geçip geçmediğini bilemez. Her yüklemede bir anahtar kullanmanın pratik nedeni budur.
Her yanıt ayrıca RateLimit-Limit, RateLimit-Remaining ve RateLimit-Reset başlıklarını taşır; böylece yoğun bir boru hattı 429'a gelmeden yavaşlayabilir.
{
"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…"
}
Kodların tam tablosu Hatalar, yeniden denemeler ve sınırlar sayfasındadır. Bu 402, parayla bağlantılı tek hatadır; bu yüzden bir dosyanın ne kadara mal olduğuna da bakalım.
Dosyalar ses saati başına $0.20 üzerinden, saniye başına faturalandırılır ($0.0033 dakika başına) ve 5 dakikalık bir asgari süreyle: 30 dakikalık bir dosya $0.10 tutar. Bu, ilerleme çubuğu dolarken içeceğiniz kahveden daha az. Oran her planda aynıdır ve API üzerinden de Panel'deki ile aynıdır. Yeni bir hesap, tek seferlik $5'lik bir kayıt kredisiyle başlar ve kart gerekmez.
Bu öğreticiyi etkileyen üç plan sınırı vardır.
GET /v1/realtime/capabilities bunu limits.maxUploadBytes olarak bildirir. İkisinden küçük olanı geçerlidir ve bu sınırı aşan bir dosya, hiçbir şey transkripsiyon edilmeden 413 ile reddedilir. Uzun bir kayıt için bu, bir WAV yerine sıkıştırılmış mono bir dosya yüklemek için bir sebep daha.GET /v1/billing planınızı, bakiyenizi ve sınırlarınızı istediğiniz zaman bildirir; tam tablo fiyatlandırma sayfasındadır.
Buradan sonraki adımlar, bir dosyanın bitmesini bekleyemediğinizde ses akışını gerçek zamanlı yapmak ya da yukarıdaki kodların hiçbirine ihtiyaç duymadan aynı hesaba MCP sunucusu üzerinden bir AI ajanı bağlamak olabilir; ajan sizin yerinize transkripsiyon eder ve arar.