Η μεταφόρτωση επιστρέφει μια εργασία, όχι ένα απομαγνητοφωνημένο κείμενο. Βάλτε ένα Idempotency-Key σε κάθε δημιουργία, ώστε μια επανάληψη να μην δημιουργήσει ποτέ δεύτερη απομαγνητοφώνηση ούτε να τη χρεώσει· έπειτα ελέγξτε την εργασία έως ότου ολοκληρωθεί, και διαβάστε προτάσεις, λέξεις ή εξαγωγή από το ίδιο αναγνωριστικό.
Η απομαγνητοφώνηση ενός αρχείου είναι το απλούστερο πράγμα που κάνει το VocalStack API, και έχει το σχήμα κάθε ασύγχρονης εργασίας: μεταφόρτωση, αναμονή, ανάγνωση. Η μεταφόρτωση απαντά αμέσως με μια απομαγνητοφώνηση και την εργασία της, αλλά ακόμη χωρίς λέξεις. Όσο το πρόγραμμά σας ελέγχει την εργασία, ο διακομιστής απομαγνητοφωνεί το αρχείο πολλές φορές πιο γρήγορα από ό,τι παίζει ο ήχος. Όταν η κατάσταση γίνει done, διαβάζετε το κείμενο ως προτάσεις με ομιλητές και χρονισμούς, ή το εξάγετε ως αρχείο σε μία από τέσσερις μορφές, είτε στη γλώσσα της ομιλίας είτε μεταφρασμένο.
Όλα όσα περιγράφονται εδώ ισχύουν για κάθε API παρτίδας: μια δημιουργία που επιστρέφει αναγνωριστικό, μια κατάσταση που ελέγχετε, ένα αποτέλεσμα που ανακτάτε στη συνέχεια. Μόνο τα ονόματα των πεδίων είναι ειδικά για το VocalStack, και αυτό το μάθημα τα εξετάζει με τα SDK για TypeScript και Python, και όπου βοηθούν, με τις κλήσεις HTTP.
Ανοίξτε το Developers → API keys στο Dashboard και δημιουργήστε ένα κλειδί με τα δικαιώματα Read και Write. Το κλειδί εμφανίζεται μόνο μία φορά· αντιγράψτε το σε μια μεταβλητή περιβάλλοντος:
export VOCALSTACK_API_KEY="<your API key>"
Ένα κλειδί προορίζεται για διακομιστές και σενάρια. Ό,τι αποστέλλεται σε πρόγραμμα περιηγητή αποστέλλει μαζί του και το κλειδί, οπότε κρατήστε το εκτός κώδικα σελίδας και εκτός URL, καθώς τα 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 ή νεότερη έκδοση.
Τα SDK δεν βρίσκονται στο npm ή στο PyPI. Το API σερβίρει τις εκδόσεις σε κάθε λογαριασμό. Οι εντολές εγκατάστασης, συμπληρωμένες για το περιβάλλον σας, βρίσκονται στην ενότητα Install του γρήγορου οδηγού στην αναφορά του API, και στέλνουν το κλειδί σας μόνο στον host του 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 με έναν ομιλητή ξοδεύει τα περισσότερα bytes του για να περιγράψει σιωπή με υψηλή πιστότητα. Απομαγνητοφωνείται ακριβώς όπως ένα μονοφωνικό MP3 ή Opus των 64 kbps της ίδιας ομιλίας, αλλά είναι περίπου είκοσι φορές μεγαλύτερο, άρα χρειάζεται είκοσι φορές περισσότερο χρόνο για να ανέβει. Αν μετατρέψετε πριν από τη μεταφόρτωση, οι δύο ρυθμίσεις που αξίζει να αλλάξετε είναι η μονοφωνική λήψη και ένας μέτριος ρυθμός bit.
Στείλτε ένα Idempotency-Key με κάθε μεταφόρτωση, και κατανοήστε γιατί. Η λήξη χρονικού ορίου σε δίκτυο δεν σας λέει τίποτα για το τι έκανε ο διακομιστής: μπορεί να μην είδε ποτέ το αίτημα, ή μπορεί να αποθήκευσε το αρχείο και να έβαλε την εργασία στην ουρά πριν χαθεί η απάντηση. Μια τυφλή επανάληψη κινδυνεύει επομένως να δημιουργήσει δεύτερη απομαγνητοφώνηση και δεύτερη χρέωση. Το κλειδί το αποτρέπει. Όταν ένα αίτημα επαναλαμβάνεται με το ίδιο κλειδί και σώμα μέσα σε 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, σημαίνει ότι η εργασία είναι σε αναμονή μέχρι να επιλέξει κάποιος το τμήμα της ηχογράφησης που θα απομαγνητοφωνηθεί, όπως κάνει το βήμα περικοπής του Dashboard. Μια συνηθισμένη μεταφόρτωση δεν μπαίνει ποτέ σε αυτή την κατάσταση, αν και το jobs.wait επιστρέφει επίσης όταν η εργασία φτάσει εκεί. Διαβάζετε το GET /v1/recordings/{id}/job έως ότου η κατάσταση γίνει done ή failed.
Δεν υπάρχουν webhooks, και αυτό είναι σκόπιμη επιλογή. Ένα webhook χρειάζεται δημόσιο endpoint HTTPS στην πλευρά σας, έλεγχο υπογραφής ώστε να μην μπορεί να το καλέσει κανείς άλλος, και πολιτική επαναλήψεων για την περίπτωση που το endpoint σας είναι εκτός λειτουργίας· πολλή υποδομή για μια εργασία που διαρκεί ένα λεπτό. Η αναμονή με ερωτήματα δεν χρειάζεται τίποτα από αυτά. Το σενάριό σας θα ρωτά «έτοιμο;» κάθε δύο δευτερόλεπτα, σαν παιδί στο πίσω κάθισμα του αυτοκινήτου, με τη διαφορά ότι εκείνη παίρνει πίσω ένα ποσοστό.
Μια ήδη ανοιχτή ιστοσελίδα μπορεί αντί αυτού να ακολουθεί τη σύνδεση συμβάντων της απομαγνητοφώνησης (WS /v1/transcriptions/{id}/events, ένα πλαίσιο job.progress καθώς προχωρά η εργασία). Για ένα σενάριο, η αναμονή με ερωτήματα είναι το σωστό εργαλείο.
Τα SDK κάνουν τα ερωτήματα μόνα τους: το 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'
Τα δύο επίπεδα χρονισμού εξυπηρετούν δύο δουλειές. Οι προτάσεις είναι η μονάδα της ανάγνωσης, άρα είναι αυτές που δείχνει ένας υπότιτλος ή μια σελίδα κειμένου. Οι λέξεις είναι η μονάδα της επισήμανσης, άρα ένα highlight τύπου karaoke, ένα αποτέλεσμα αναζήτησης ή ένα κομμάτι κλιπ διατρέχουν τα segments[].words. Οι μορφές εξαγωγής χτίζονται από προτάσεις.
Το GET /v1/recordings/{id}/export επιστρέφει ένα πραγματικό αρχείο. Το format είναι ένα από τα srt, json, xlsx ή pdf, και το αρχείο περιλαμβάνει τυχόν διορθώσεις. Προσθέστε lang= για μεταφρασμένο αρχείο: τότε η εξαγωγή γράφεται σε αυτή τη γλώσσα, μία γραμμή ανά πρόταση, και όσες προτάσεις δεν έχουν ακόμη μεταφραστεί, μεταφράζονται πρώτα.
Αξίζει να αναγνωρίζετε με μια ματιά το SRT, γιατί είναι η μορφή υποτίτλων που δέχεται σχεδόν κάθε πρόγραμμα αναπαραγωγής και είναι απλό κείμενο. Ένα αρχείο είναι μια λίστα από cues χωρισμένα με κενές γραμμές, και καθένα αποτελείται από έναν αριθμό, ένα χρονικό διάστημα και το κείμενο. Οι χρονικές σημάνσεις χρησιμοποιούν κόμμα πριν από τα χιλιοστά, μια γαλλική συνήθεια που η μορφή δεν εγκατέλειψε ποτέ:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Μία πρόταση ανά cue είναι το σωστό μέγεθος: κάθε cue διαβάζεται μέσα στον χρόνο που μένει στην οθόνη, και κανένα δεν κόβεται στη μέση μιας σκέψης. Όταν η ηχογράφηση είχε ετικέτες ομιλητών, το κείμενο κάθε cue αρχίζει με το όνομα του ομιλητή και μια άνω τελεία.
Στο SDK για TypeScript, το vs.recordings.export επιστρέφει ένα Response· διαβάστε τα bytes του με 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 του Dashboard, μπορεί να κοινοποιηθεί με σύνδεσμο, και μπορεί να διαβαστεί ξανά οποτεδήποτε αργότερα. Αυτός είναι ο βασικός κύκλος: δημιουργία, αναμονή, ανάγνωση, εξαγωγή. Τα υπόλοιπα του μαθήματος καλύπτουν τις παραλλαγές: πολυμέσα που είναι ήδη online, τα σφάλματα που θα συναντήσετε, και το κόστος ενός αρχείου.
Αν τα πολυμέσα είναι ήδη online, δώστε στο API τον σύνδεσμο και εκείνο θα τα κατεβάσει μόνο του: μια δημόσια διεύθυνση http(s) (ένα mp3, ένα βίντεο, ένα επεισόδιο podcast) ή ένα βίντεο YouTube. Η κλήση είναι η ίδια POST /v1/transcriptions, με σώμα JSON { "url": "..." } στη θέση της μεταφόρτωσης πολλαπλών μερών, και τα name και settings όπως πριν. Στα SDK, το 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 όσο φτάνουν bytes (με 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}%`);
},
});
Ο οδηγός Transcribe a link της αναφοράς έχει το πλήρες πρόγραμμα και στις δύο γλώσσες και σε HTTP. Η λήψη από συνδέσμους προσθέτει περισσότερους τρόπους αποτυχίας, γι' αυτό ακολουθούν τα σφάλματα που πραγματικά θα δείτε.
Κάθε αποτυχημένη κλήση απαντά με έναν φάκελο JSON. Το code είναι το αλφαριθμητικό με βάση το οποίο διακλαδίζετε και το status είναι η κατάσταση HTTP· το message απευθύνεται σε ανθρώπους και το requestId στην υποστήριξη. Μέσα στο data, το retryable δηλώνει αν η επανάληψη του ίδιου αιτήματος μπορεί να βοηθήσει, και το details περιέχει ό,τι χρειάζεται ο κωδικός. Τα SDK πετούν έναν τύπο σφάλματος, το 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.Τα SDK επαναλαμβάνουν ένα 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…"
}
Ο πλήρης πίνακας κωδικών βρίσκεται στο Errors, retries and limits. Το 402 είναι το μόνο σφάλμα που συνδέεται με χρήματα, οπότε ορίστε πόσο κοστίζει ένα αρχείο.
Τα αρχεία κοστίζουν $0.20 την ώρα ήχου, χρεώνονται ανά δευτερόλεπτο ($0.0033 το λεπτό) με ελάχιστο 5 λεπτά: ένα αρχείο 30 λεπτών κοστίζει $0.10. Είναι δέκα σεντ, λιγότερα από τον καφέ που θα πιείτε όσο γεμίζει η γραμμή προόδου. Η τιμή είναι η ίδια σε κάθε πλάνο, και μέσω API όπως και στο Dashboard. Ένας νέος λογαριασμός ξεκινά με μια εφάπαξ πίστωση εγγραφής $5, και δεν απαιτείται κάρτα.
Τρία όρια πλάνου αγγίζουν αυτό το μάθημα.
GET /v1/realtime/capabilities αναφέρει ως limits.maxUploadBytes. Ισχύει το μικρότερο από τα δύο, και ένα αρχείο που το υπερβαίνει απορρίπτεται με 413 πριν απομαγνητοφωνηθεί οτιδήποτε. Για μια μεγάλη ηχογράφηση, αυτός είναι ένας ακόμη λόγος να μεταφορτώνετε ένα συμπιεσμένο μονοφωνικό αρχείο αντί για WAV.Το GET /v1/billing αναφέρει το πλάνο, το υπόλοιπο και τα όρια σας ανά πάσα στιγμή· ο πλήρης πίνακας βρίσκεται στη σελίδα τιμών.
Από εδώ, τα επόμενα βήματα είναι η ροή ήχου σε πραγματικό χρόνο, όταν δεν μπορείτε να περιμένετε να τελειώσει ένα αρχείο, ή η σύνδεση ενός AI agent στον ίδιο λογαριασμό μέσω του MCP server, ώστε να απομαγνητοφωνεί και να αναζητά για εσάς χωρίς κανέναν από τους κώδικες παραπάνω.