העלאה מחזירה משימה, לא תמליל. שימו Idempotency-Key בכל יצירה, כדי שניסיון חוזר לעולם לא ייצור או יחייב תמלול שני; אחר כך סקרו את המשימה עד שהיא מוכנה, וקראו משפטים, מילים או ייצוא מאותו מזהה.
קריאה של קובץ היא הפעולה הפשוטה ביותר ש-VocalStack API מבצע, ויש לה את המבנה של כל משימה אסינכרונית: מעלים, ממתינים, קוראים. ההעלאה עונה מיד עם תמלול והמשימה שלו, אבל עדיין בלי מילים. בזמן שהתוכנית שלכם שואלת את המשימה, השרת מתמלל את הקובץ פי כמה מהר מהזמן שבו השמע מתנגן. כשהמצב הוא done, קוראים את התמליל כמשפטים עם דוברים ותזמונים, או מייצאים אותו לקובץ באחד מארבעה פורמטים, בשפה המדוברת או מתורגם.
כל מה שיש לו את הצורה הזו מתאים לכל API של עיבוד באצוות: יצירה שמחזירה מזהה, סטטוס שמבררים שוב ושוב, ותוצאה שמושכים אחר כך. רק שמות השדות ספציפיים ל-VocalStack, והמדריך הזה עובר עליהם עם ה-SDK של TypeScript ו-Python, ובמקומות שזה עוזר גם עם קריאות ה-HTTP.
פתחו את Developers → API keys בלוח הבקרה וצרו מפתח עם ההרשאות Read ו-Write. המפתח מוצג פעם אחת בלבד; העתיקו אותו למשתנה סביבה:
export VOCALSTACK_API_KEY="<your API key>"
מפתח מיועד לשרתים ולסקריפטים. כל מה שנשלח לדפדפן נושא את המפתח איתו, ולכן הוציאו אותו מקוד העמוד ומכתובות URL, שמגיעות ליומנים. כדי להזרים מדפדפן, השרת שלכם יוצר עבורו סשן בזמן אמת; זה מדריך אחר.
כל קריאה מאמתת עם המפתח כ-bearer token:
Authorization: Bearer vs_key_...
כשהמפתח נמצא בסביבה, הצעד הבא הוא לקוח ששולח אותו.
יש שני SDK. לכל אחד לקוח אחד, VocalStack, והמתודות שלו מקובצות לפי משאב: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime ועוד כמה. ה-SDK של TypeScript רץ על Node 20 ומעלה ובדפדפנים; ה-SDK של Python בנוי על asyncio ודורש Python 3.10 ומעלה.
ה-SDKs לא מופיעים ב-npm או ב-PyPI. ה-API מגיש את הבניות לכל חשבון. פקודות ההתקנה, מותאמות לסביבה שלכם, נמצאות בסעיף Install של מדריך ההתחלה המהירה בתיעוד ה-API, והן שולחות את המפתח שלכם רק למארח ה-API. אם אתם מעדיפים לא להתקין כלום, קריאות ה-HTTP שלהלן צריכות רק curl ו-jq.
איזו מהדרכים תבחרו, הקריאה הראשונה היא ההעלאה.
POST /v1/transcriptions מקבל את הקובץ כ-multipart/form-data: חלק file, ועוד חלק options אופציונלי שמכיל JSON. התשובה היא 201 עם התמלול. ה-job.recordingId שלו הוא המזהה של ההקלטה שנוצרה מהקובץ, וכל קריאה שבאה אחריה מכוונת אליו.
שתי הגדרות חשובות בהרצה הראשונה, שתיהן בתוך options.settings:
spokenLanguage: קוד כמו en. אם משמיטים אותו, השפה מזוהה אוטומטית. ציון השפה משפר את הדיוק בהקלטות רועשות ובמבטאים כבדים.speakerLabels: auto, on או off. עם on, לכל משפט יש מספר דובר; עם auto, התוויות מופיעות רק אחרי ששמעו דובר שני.רוב קבצי השמע והווידאו מתקבלים: mp3, wav, flac, ogg, m4a, mp4 ועוד. בווידאו נשמר רק השמע. הפורמט כמעט לא משפיע על הדיוק, כי זיהוי הדיבור עובד על 16 kHz מונו, וכל קובץ מומר לזה עם ההגעה. קובץ WAV של 48 kHz סטריאו עם אדם אחד שמדבר מבזבז רוב הבייטים שלו על תיאור של שקט באיכות גבוהה. הוא מתומלל בדיוק כמו קובץ MP3 מונו של 64 kbps או Opus של אותו דיבור, אבל הוא גדול פי עשרים בערך, ולכן ההעלאה שלו לוקחת פי עשרים זמן. אם ממירים לפני ההעלאה, מונו ו-bitrate סביר הם שתי ההגדרות ששווה לשנות.
שלחו Idempotency-Key עם כל העלאה, והבינו למה. timeout ברשת לא אומר לכם מה השרת עשה: ייתכן שלא ראה את הבקשה בכלל, וייתכן שאחסן את הקובץ והכניס את המשימה לתור לפני שהתשובה אבדה. ניסיון חוזר בעיוור מסתכן לכן בתמלול שני וחיוב שני. המפתח פותר את זה. כשבקשה חוזרת עם אותו מפתח וגוף תוך 24 שעות, השרת מחזיר את התשובה הראשונה במקום ליצור משהו, וכך "לפחות פעם אחת" הופך ל"פעם אחת בפועל". לכן הכלל הוא מפתח אחד לכל העלאה לוגית, שנוצר לפני הניסיון הראשון ונשלח שוב בכל ניסיון חוזר. מפתח חדש לכל ניסיון היה הופך כל ניסיון חוזר להעלאה חדשה, והכותרת לא הייתה מגינה על כלום.
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
אותה קריאה עם 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 חוזרת לפני שמילה אחת תומללה; מה שבידיכם הוא מזהה משימה, ולכן הצעד הבא הוא להמתין לה.
משימה עוברת queued ← transcribing ← done, או מסתיימת ב-failed עם הודעת error ואם הסיבה ניתנת לקריאה במכונה גם errorCode. מצב רביעי, cropping, אומר שהמשימה ממתינה עד שמישהו יבחר איזה קטע מההקלטה לתמלל, כמו שלב החיתוך בלוח הבקרה. העלאה רגילה לעולם לא נכנסת אליו, אף ש-jobs.wait מחזיר גם כשמשימה מגיעה אליו. קראו את GET /v1/recordings/{id}/job עד שהמצב הוא done או failed.
אין webhooks, וזו החלטה מכוונת. webhook דורש endpoint ציבורי ב-HTTPS אצלכם, בדיקת חתימה כדי שאף אחד אחר לא יקרא לו, ומדיניות ניסיונות חוזרים למקרה שה-endpoint שלכם למטה. זו הרבה אינסטלציה למשימה שלוקחת דקה. polling לא צריך כלום מזה. הסקריפט שלכם ישאל "כבר?" כל שתי שניות, כמו ילד בכיסא האחורי, אבל בניגוד לילד הוא יקבל בחזרה אחוזים.
דף אינטרנט שכבר פתוח יכול לעקוב במקום זאת אחרי ה-socket של אירועי התמלול (WS /v1/transcriptions/{id}/events, פריים job.progress בכל התקדמות). לסקריפט, polling הוא הכלי הנכון.
ה-SDKs עושים את ה-polling: vs.jobs.wait קורא את המשימה כל 2 שניות ומעביר כל קריאה ל-onProgress (on_progress ב-Python). ה-progress של המשימה מכיל pct (0 עד 100) ו-processedS, שניות המדיה שתומללו עד כה. התוכניות שלהלן מדפיסות שורה רק כשהמצב או האחוזים משתנים.
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 לא זורק שגיאה על failed; הוא מחזיר את המשימה ומשאיר את הבדיקה לכם. שתי שניות מתאימות לשיחה. להקלטה של שלוש שעות, או לאצווה של מאות, הגדילו את המרווח (intervalMs, interval_s) או האטו ככל שהמשימה רצה: הקריאות הראשונות אומרות הכי הרבה, והמאוחרות בעיקר מאשרות שהשרת עדיין עסוק. בלי SDK, כותבים את אותה לולאה ביד:
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
כשהמצב קורא done, התמליל מוכן.
GET /v1/recordings/{id}/transcript מחזיר את כל התמליל במסמך אחד:
sentences: מה להציג לאנשים. לכל אחד יש text, speaker, ו-start ו-end בשניות.segments: השורות של המנוע, כל אחת עם תזמוני מילים ב-words (לכל מילה יש text, start ו-end).speakers: רשומה אחת לכל מספר דובר, עם ה-name שלו.הדוברים ממוספרים מ-0. speaker של -1 פירושו שתוויות הדוברים היו כבויות להקלטה. הוסיפו ?lang=es כדי לקרוא את התמליל מתורגם לספרדית; המשפטים חוזרים בשפה הזו.
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'
שתי רמות התזמון משרתות שני תפקידים. המשפטים הם יחידת הקריאה, ולכן הם מה שכתובית או דף תמליל מציגים. המילים הן יחידת ההצבעה, ולכן הדגשה בסגנון קריוקי, חיפוש או חיתוך קליפ עוברים על segments[].words. פורמטי הייצוא בנויים ממשפטים.
GET /v1/recordings/{id}/export מחזיר קובץ אמיתי. format הוא אחד מ-srt, json, xlsx או pdf, והקובץ כולל את כל התיקונים. הוסיפו lang= לקובץ מתורגם: הייצוא אז קורא בשפה הזו, שורה אחת לכל משפט, ומשפט שעדיין לא תורגם מתורגם קודם.
כדאי להכיר את SRT מראש, כי זה פורמט הכתוביות שכמעט כל נגן מקבל, והוא טקסט פשוט. קובץ הוא רשימה של cues מופרדים בשורות ריקות, וכל cue הוא מונה, טווח זמן וטקסט. החותמות משתמשות בפסיק לפני המילישניות, הרגל צרפתית שהפורמט מעולם לא נפטר ממנה:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
משפט אחד לכל cue הוא הגרעין הנכון: כל cue נקרא בזמן שהוא על המסך, ואף אחד לא נחתך באמצע מחשבה. כשלהקלטה היו תוויות דוברים, הטקסט של כל cue מתחיל בשם הדובר ובנקודתיים.
ב-SDK של TypeScript, vs.recordings.export מחזיר Response; קראו את הבייטים שלו עם arrayBuffer(). ב-Python הוא מחזיר 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
התמלול נשמר, ולכן הוא מופיע תחת Transcriptions בלוח הבקרה, אפשר לשתף אותו בקישור ולקרוא אותו שוב בכל זמן בהמשך. זו הלולאה הבסיסית: יוצרים, ממתינים, קוראים, מייצאים. שאר המדריך עוסק בווריאציות: מדיה שכבר באינטרנט, השגיאות שתפגשו, ומה קובץ עולה.
אם המדיה כבר נמצאת באינטרנט, נותנים ל-API את הקישור, והוא מוריד את הקובץ בעצמו: כתובת http(s) ציבורית (mp3, וידאו, פרק פודקאסט) או סרטון YouTube. הקריאה זהה: POST /v1/transcriptions, עם גוף JSON { "url": "..." } במקום ההעלאה בשני חלקים, ו-name ו-settings כרגיל. ב-SDKs, vs.transcriptions.createFromUrl(url) (TypeScript) ו-vs.transcriptions.create(url=...) (Python) שולחים את אותה בקשה.
לפני יצירת כל דבר, GET /v1/transcriptions/probe?url= אומר לכם מה הקישור. ה-kind שלו הוא אחד משלושה:
file: הקלטה עם סוף; תמללו אותה כאן.live: שידור חי ב-YouTube, שידור רדיו אינסופי או פלייליסט HLS ללא סוף. קישור חי לא נגמר לעולם, ולכן אי אפשר לתמלל אותו כקובץ; השתמשו בו כמקור לתמלול בשידור חי.unknown: השרת לא הצליח לדעת.כשהקישור לא נפתח כמדיה בכלל (דף אינטרנט, למשל), ok הוא false, ו-problem מסביר את הסיבה במשפט אחד. ה-probe אופציונלי, ומוגבל ל-60 קריאות בדקה.
משימת קישור מתחילה ב-fetching לפני שהיא עוברת ל-transcribing ול-done. בזמן ההורדה, progress.phase מראה מה השרת עושה: fetching בזמן שבייטים מגיעים (עם fetchedBytes ו-fetchTotalBytes כשהצד השני אומר כמה יש), או extracting בזמן שהשמע נלקח מווידאו. progress.phasePct מראה עד כמה המשימה התקדמה באותו שלב. סרטון ארוך לוקח זמן, לכן סקרו כל כמה שניות; vs.jobs.wait מקבל intervalMs (interval_s ב-Python) לכך.
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}%`);
},
});
המדריך המלא תמלול קישור נמצא בתיעוד, בשתי השפות וב-HTTP. הורדה מקישורים מוסיפה דרכים נוספות להיכשל, אז הנה השגיאות שאתם באמת תראו.
כל קריאה שנכשלה מחזירה מעטפת JSON אחת. code הוא המחרוזת שמפצלים לפיה, ו-status הוא קוד ה-HTTP; message מיועד לאנשים, ו-requestId לתמיכה. תחת data, retryable אומר אם שליחה חוזרת של אותה בקשה יכולה לעזור, ו-details מכיל את מה שהקוד צריך. ה-SDKs זורקים סוג שגיאה אחד, VocalStackError, עם אותם שדות.
שלושה קודים כדאי לטפל בהם מהיום הראשון.
402 insufficient_credit: הקרדיט של החשבון נגמר, ולא חויב דבר. details.balance הוא היתרה בדולרים, ו-details.topUpUrl הוא המקום להוסיף קרדיט. כשהעלות ידועה מראש, כמו לקובץ באורך ידוע, details.required אומר מה היא הייתה.429 rate_limited: המתינו Retry-After שניות. כש-details.metric הוא creates_per_hour, הגעתם לקצב היצירה של המסלול (תמלולים חדשים בשעה), לא למגבלת פרץ; limit ו-resetsAt אומרים מה התקרה ומתי היא משתחררת.503 unavailable: השירות לא זמין לרגע. שלחו את הבקשה שוב אחרי Retry-After.ה-SDKs מנסים שוב 429 ו-503 אחרי Retry-After, פעמיים כברירת מחדל (maxRetries / max_retries בלקוח). אבל 503 מנוסה שוב רק בקריאה שבטוח לשלוח פעמיים: GET, או יצירה עם Idempotency-Key. יצירה בלי מפתח נשארת באחריותכם, כי ה-SDK לא יכול לדעת אם הניסיון הראשון הצליח. זו הסיבה המעשית לשים מפתח בכל העלאה.
כל תשובה נושאת גם RateLimit-Limit, RateLimit-Remaining ו-RateLimit-Reset, כך שצנרת עמוסה יכולה להאט לפני שמגיע ה-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…"
}
הטבלה המלאה של הקודים נמצאת ב-שגיאות, ניסיונות חוזרים ומגבלות. ה-402 הזה הוא השגיאה היחידה שקשורה לכסף, אז הנה כמה עולה קובץ.
קבצים עולים $0.20 לשעת שמע, מחויבים לפי שנייה ($0.0033 לדקה) עם מינימום של 5 דקות: קובץ של 30 דקות עולה $0.10. זה עשרה סנט, פחות מהקפה שתשתו בזמן שפס ההתקדמות מתמלא. המחיר זהה בכל המסלולים, וזהה ב-API כמו בלוח הבקרה. חשבון חדש מתחיל עם קרדיט הרשמה חד-פעמי של $5, בלי צורך בכרטיס אשראי.
שלוש מגבלות מסלול נוגעות במדריך הזה.
GET /v1/realtime/capabilities מדווח עליה כ-limits.maxUploadBytes. התקרה הקטנה מביניהן חלה, וקובץ שחורג ממנה נדחה ב-413 לפני שמתומלל דבר. להקלטה ארוכה, זו עוד סיבה להעלות קובץ מונו דחוס ולא WAV.GET /v1/billing מדווח על המסלול, היתרה והמגבלות שלכם בכל רגע; הטבלה המלאה נמצאת ב-דף המחירים.
מכאן, הצעדים הבאים הם שידור שמע בזמן אמת כשאי אפשר לחכות לסיום קובץ, או חיבור סוכן AI לאותו חשבון דרך שרת ה-MCP, כך שהוא יתמלל ויחפש בשבילכם בלי אף אחד מהקוד שלמעלה.