Μια απομαγνητοφώνηση σε πραγματικό χρόνο φτάνει δύο φορές: πρώτα ως προσχέδιο που συνεχώς αλλάζει, και μετά ως τελική γραμμή που δεν αλλάζει. Αποδώστε το προσχέδιο ως ζωντανό τέλος που αντικαθίσταται και την τελική γραμμή ως ιστορικό, και η υπόλοιπη υποδομή μιας προβολής ζωντανών υποτίτλων είναι απλή δουλειά.
Μια συνεδρία σε πραγματικό χρόνο είναι το μικρότερο πράγμα που κάνει το API του VocalStack. Ο διακομιστής σας κάνει μία κλήση HTTP και παίρνει πίσω ένα υπογεγραμμένο streamUrl. Ο πελάτης σας ανοίγει ένα WebSocket προς αυτό το URL, στέλνει ακατέργαστο ήχο καθώς εγγράφεται, και διαβάζει το κείμενο πίσω ως πλαίσια κειμένου JSON, λίγο πίσω από τον ομιλητή. Όταν τελειώσει ο ήχος, στέλνετε eof· ο διακομιστής αδειάζει ό,τι του έχει απομείνει, αναφέρει τα μετρημένα στοιχεία της συνεδρίας και κλείνει.
Τίποτα δεν αποθηκεύεται. Το κείμενο υπάρχει μόνο κατά τη μεταφορά, κάτι που ταιριάζει σε μια προβολή υποτίτλων, σε επικάλυψη συσκέψεων ή σε φωνητική λειτουργία μέσα στη δική σας εφαρμογή. Αν θέλετε να διατηρηθούν οι λέξεις, να μοιραστούν και να εξαχθούν στη συνέχεια, χρειάζεστε μια ζωντανή απομαγνητοφώνηση και μια μετάδοση· η τελευταία ενότητα δείχνει προς τα εκεί.
Η μορφή είναι αυτή που μοιράζονται οι περισσότερες API ομιλίας σε ροή: μια ανταλλαγή διαπιστευτηρίων μέσω HTTP, ένα μακρόβιο socket με ήχο να μπαίνει και JSON να βγαίνει, και κάποιος τρόπος να ξεχωρίζει το προσωρινό κείμενο από το τελικό. Τα ονόματα διαφέρουν από API σε API· τα κινούμενα μέρη σχεδόν ποτέ. Εδώ, με τη σειρά τους:
POST /v1/realtime/sessions με το κλειδί API σας, στον διακομιστή σας. Η απάντηση είναι { sessionId, streamUrl, expiresAt }.streamUrl.session.loading έως ότου έρθει το session.ready.transcript.draft (αντικαθιστά το ζωντανό τέλος) και το transcript.committed (προσθέτει μια τελική γραμμή).{"type":"eof"} και διαβάζετε το session.done.Μόνο το βήμα 1 χρειάζεται το κλειδί σας, γι' αυτό γίνεται στον διακομιστή σας.
Δημιουργήστε ένα κλειδί στο Developers, API keys με το δικαίωμα Write. Το κλειδί προορίζεται για διακομιστές και σενάρια: το API απορρίπτει ένα κλειδί που σταλεί από σελίδα ιστού ή μέσα σε URL, οπότε δημιουργείτε τη συνεδρία πίσω από μια δική σας διαδρομή, και η σελίδα βλέπει μόνο το streamUrl.
Το URL φέρει το δικό του διακριτικό λόγω ενός περιορισμού του προγράμματος περιήγησης που αξίζει να γνωρίζετε γενικά. Ο κατασκευαστής WebSocket του προγράμματος περιήγησης δεν μπορεί να ορίσει κεφαλίδες αιτήματος, άρα δεν υπάρχει κεφαλίδα Authorization όπου να μπει ένα κλειδί· κάθε API ομιλίας που απευθύνεται σε πρόγραμμα περιήγησης πιστοποιεί το socket της με ένα βραχύβιο διακριτικό μέσα στο URL (ή περασμένο σε όνομα subprotocol). Το βραχύβιο είναι το ζητούμενο: τα URL καταλήγουν σε αρχεία καταγραφής και στο ιστορικό, οπότε ό,τι μεταφέρουν πρέπει να λήγει. Συνδεθείτε πριν από το expiresAt.
Τα SDK κατεβάζονται από την περιοχή Developers, όχι από μητρώο πακέτων· η ενότητα Install του οδηγού γρήγορης εκκίνησης δείχνει τις εντολές για αυτό το περιβάλλον. Με το SDK της TypeScript, μια διαδρομή στον διακομιστή μοιάζει με αυτό:
// On your server, behind a route of yours (POST /api/transcribe, say):
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: '{{api}}', apiKey: process.env.VOCALSTACK_API_KEY });
export async function createStream(): Promise<{ sessionId: string; streamUrl: string }> {
const { sessionId, streamUrl } = await vs.realtime.createSession({ spokenLanguage: 'en' });
return { sessionId, streamUrl };
}
Χωρίς SDK είναι ένα fetch. Αυτό ζητά επίσης ισπανικά και απενεργοποιεί τη συναρμολόγηση προτάσεων, ώστε να ταιριάζει με το δείγμα του πρωτοκόλλου που ακολουθεί:
const res = await fetch('{{api}}/v1/realtime/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOCALSTACK_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ spokenLanguage: 'en', translate: ['es'], sentences: { enabled: false } }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // the error envelope: { code, message, … }
const { streamUrl } = (await res.json()) as { sessionId: string; streamUrl: string; expiresAt: string };
Κάθε επιλογή έχει προεπιλεγμένη τιμή. Αυτές που αξίζει να γνωρίζετε από την αρχή είναι το spokenLanguage (αν το παραλείψετε, η γλώσσα ανιχνεύεται), το translate (λίστα γλωσσών-στόχων), το speakerLabels και το latencyMode· τα υπόλοιπα εμφανίζονται όταν χρειαστούν παρακάτω.
Δώστε το streamUrl σε ό,τι καταγράφει τον ήχο· από εδώ και πέρα δεν εμπλέκεται κανένα κλειδί.
Συνδεθείτε στο streamUrl όπως είναι, χωρίς κεφαλίδα. Όσο ξεκινά η συνεδρία, ο διακομιστής στέλνει μηνύματα session.loading ως σήματα ζωής. Έπειτα έρχεται το session.ready. Μην στέλνετε ήχο πριν από αυτό. Ήχος που στέλνεται νωρίς δεν κρατιέται για αργότερα· ο διακομιστής το θεωρεί σφάλμα, στέλνει ένα πλαίσιο error και κλείνει το socket.
Μόλις ανοίξει το socket, κάθε σφάλμα έχει τη μορφή αυτού του πλαισίου: {"type":"error","code":"…","message":"…","retryable":true|false}. Το code είναι κλειστός κατάλογος (invalid_audio, invalid_options, invalid_token, capacity_exceeded, session_expired, slow_consumer, engine_unavailable, internal), το message προορίζεται για τα αρχεία καταγραφής σας, και το retryable δείχνει αν αξίζει να δοκιμάσετε νέα συνεδρία. Χειριστείτε το από το πρώτο πλαίσιο:
const ws = new WebSocket(streamUrl); // the token is in the URL: no header
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', (m: MessageEvent) => {
if (typeof m.data !== 'string') return;
const f = JSON.parse(m.data);
switch (f.type) {
case 'session.loading':
console.error('starting…');
break;
case 'session.ready':
startMicrophone(); // send audio only after session.ready
break;
case 'error':
console.error(`${f.code}: ${f.message}`);
if (f.retryable) scheduleReconnect(); // a new session, with backoff; see "Stop"
break;
}
});
Ένας πελάτης χωρίς SDK πρέπει να χειριστεί μόνος του δύο ακόμη περιπτώσεις· τα SDK το κάνουν ήδη. Πρώτον, μια απορριφθείσα αναβάθμιση φέρει τον συνηθισμένο φάκελο σφάλματος HTTP του API: 401 invalid_token, 410 session_expired, ή 503 unavailable με κεφαλίδα Retry-After που πρέπει να τηρηθεί.
Δεύτερον, η κορεσμένη χωρητικότητα μοιάζει με σιωπή, όχι με σφάλμα. Αν δεν φτάσει κανένα πλαίσιο μέσα σε 30 δευτερόλεπτα, αντιμετωπίστε το ως capacity_exceeded και συνδεθείτε ξανά· μια νέα συνεδρία συνήθως περνάει. Αν τα μηνύματα φόρτωσης συνεχίζουν να έρχονται αλλά το session.ready δεν έρχεται ποτέ, αντιμετωπίστε το ως engine_unavailable, που επίσης επιτρέπει επανάληψη. Στα SDK, το RealtimeSession.connect ολοκληρώνεται όταν έρθει το session.ready και αποτυγχάνει μετά από readyDeadlineMs με τον σωστό κωδικό.
Μόλις φτάσει το session.ready, ο διακομιστής περιμένει ήχο με τον ρυθμό με τον οποίο ομιλείται.
Ο ήχος μπαίνει ως δυαδικά πλαίσια ακατέργαστου PCM: 32-bit float, little-endian, 16 kHz, μονοφωνικό (pcm_f32le, η προεπιλεγμένη μορφή audio της συνεδρίας). Στείλτε τον καθώς καταγράφεται, σε κομμάτια περίπου 100 έως 250 ms, χωρίς να περιμένετε το κείμενο ανάμεσα στα κομμάτια.
Αξίζει να κάνετε τον υπολογισμό μία φορά. 16.000 δείγματα το δευτερόλεπτο με 4 byte το καθένα είναι 64.000 byte το δευτερόλεπτο, άρα τα 100 ms είναι 6.400 byte και τα 250 ms είναι 16.000. Τα μοντέλα ομιλίας εκπαιδεύονται συχνά σε μονοφωνικό 16 kHz, γι' αυτό τόσα πολλά API σε πραγματικό χρόνο το ζητούν, ενώ τα μικρόφωνα και τα προγράμματα περιήγησης καταγράφουν σε 44,1 ή 48 kHz και συχνά σε στέρεο. Κάτι πρέπει να μετατρέψει τον ρυθμό δειγματοληψίας και να συμπτύξει τα κανάλια σε ένα πριν σταλούν τα byte: εδώ αυτό είναι το ffmpeg, και στο πρόγραμμα περιήγησης είναι το βοηθητικό του SDK για το μικρόφωνο. Αν στείλετε ήχο 48 kHz και δηλώσετε 16 kHz, δεν παρουσιάζεται σφάλμα· η ομιλία μεταγράφεται σαν αργή κίνηση, και άσχημα.
Στο Node, το ffmpeg είναι η απλούστερη συσκευή καταγραφής:
import { spawn } from 'node:child_process';
function startMicrophone(): void {
// Raw PCM: f32le, 16 kHz, mono, as binary frames. macOS: -f avfoundation -i :0, Linux: -f pulse -i default.
const mic = spawn('ffmpeg', ['-loglevel', 'error', '-f', 'avfoundation', '-i', ':0', '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
mic.stdout.on('data', (chunk: Buffer) => {
// Backpressure: bytes the socket has accepted but not yet put on the wire.
// Two seconds of audio (128,000 bytes) queued means the link, not the server, is behind.
if (ws.bufferedAmount > 128_000) console.error(`link behind by ${ws.bufferedAmount} bytes`);
ws.send(new Uint8Array(chunk));
});
mic.on('close', () => ws.send(JSON.stringify({ type: 'eof' })));
process.once('SIGINT', () => mic.kill());
}
ws.send δεν μπλοκάρει· βάζει τα δεδομένα σε ουρά. Το bufferedAmount είναι το μέγεθος αυτής της ουράς, και σε υγιή σύνδεση στέκεται κοντά στο μηδέν. Ο ζωντανός ήχος δεν μπορεί να σταματήσει, οπότε η χρήσιμη απάντηση σε μια μεγαλώνουσα ουρά είναι να την προσέξετε, όχι να ρίξετε πλαίσια. Ο διακομιστής εφαρμόζει επίσης backpressure. Δέχεται ήχο λίγα δευτερόλεπτα μπροστά από τον πραγματικό χρόνο (limits.audioBufferS), και μετά σταματά να διαβάζει μέχρι να τον φτάσει. Στην αντίστροφη πορεία, τα οριστικά τμήματα μπαίνουν σε ουρά χωρίς απώλεια, ενώ τα προσχέδια συγχωνεύονται, οπότε ένας αργός αναγνώστης βλέπει λιγότερα, πιο φρέσκα προσχέδια. Ένας πελάτης που δεν μπορεί να συμβαδίσει ούτε με τα οριστικά τμήματα λαμβάνει πλαίσιο error με slow_consumer και retryable: false, και αποσυνδέεται· αν το δείτε, ο χειριστής μηνυμάτων σας κάνει πάρα πολλή δουλειά.
Στο πρόγραμμα περιήγησης, το streamMicrophone από το @vocalstack/sdk/mic ζητά το μικρόφωνο, μετατρέπει σε 16 kHz και στέλνει τα κομμάτια μαζί με πλαίσια clock, που επιτρέπουν στον διακομιστή να σημειώνει κενά όταν το πρόγραμμα περιήγησης περιορίζει την καρτέλα. Η σελίδα παίρνει τη συνεδρία από τη δική σας διαδρομή και δεν βλέπει ποτέ το κλειδί:
import { RealtimeSession } from '@vocalstack/sdk';
import { streamMicrophone } from '@vocalstack/sdk/mic';
export async function transcribeMicrophone(
stream: { sessionId: string; streamUrl: string },
render: (committed: string, tail: string) => void,
): Promise<void> {
const session = await RealtimeSession.connect(stream);
const mic = await streamMicrophone(session); // asks for the microphone, sends 16 kHz PCM
document.querySelector('#stop')?.addEventListener('click', () => {
void mic.stop();
session.eof();
});
let committed = '';
for await (const ev of session) {
if (ev.type === 'transcript.draft') render(committed, ev.segments.map((s) => s.text).join(' '));
if (ev.type === 'transcript.committed') {
committed += `${ev.segment.text} `;
render(committed, '');
}
}
}
Αυτή η υπογραφή render(committed, tail) είναι όλο το μοτίβο διεπαφής· η επόμενη ενότητα εξηγεί γιατί.
Δύο τύποι πλαισίων χτίζουν την απομαγνητοφώνηση, και η σύγχυση μεταξύ τους είναι το πιο συνηθισμένο λάθος.
Πρώτα, γιατί υπάρχουν δύο. Ένα μοντέλο ομιλίας αποφασίζει τι ήταν μια λέξη εν μέρει από τις λέξεις που ακολουθούν: το «I scream» και το «ice cream» ακούγονται το ίδιο μέχρι να συνεχιστεί η πρόταση. Έτσι ένας αναγνωριστής σε πραγματικό χρόνο εκπέμπει μια πρώιμη εικασία και μετά την αναθεωρεί καθώς φτάνει περισσότερος ήχος, και σταθεροποιείται μόνο όταν έχει ακούσει αρκετά. Κάθε API αναγνώρισης ομιλίας σε ροή το εκθέτει με κάποιον τρόπο. Μερικά ονομάζουν τις εικασίες «ενδιάμεσα» ή «μερικά» αποτελέσματα και σημαδεύουν τα σταθερά με is_final· το λεξιλόγιο διαφέρει, η πειθαρχία όχι. Εδώ η εικασία είναι ένα προσχέδιο και το σταθερό κείμενο είναι ένα οριστικό τμήμα.
Το transcript.draft είναι το ζωντανό τέλος: οι λέξεις που ακούστηκαν από το τελευταίο οριστικό τμήμα, και εξακολουθούν να αλλάζουν. Κάθε προσχέδιο αντικαθιστά πλήρως το προηγούμενο. Ξανασχεδιάστε το ζωντανό τέλος με τα segments του· ποτέ μην το προσθέτετε. Τα προσχέδια δεν φέρουν seq σκόπιμα, οπότε μην προσπαθείτε να τα αντιστοιχίσετε με τα οριστικά τμήματα στα οποία καταλήγουν.
Το transcript.committed είναι οριστικό: ένα segment με seq (0, 1, 2, χωρίς κενά), start, end, text, speaker και words με χρονισμούς ανά λέξη. Προσθέστε το και αφαιρέστε το προσχέδιο που καλύπτει· το επόμενο προσχέδιο αρχίζει εκεί που τελείωσε αυτό το οριστικό τμήμα.
committed: [seq 0] [seq 1]
draft: "how are" ← frame 1
draft: "how are you do" ← frame 2 REPLACES frame 1
committed: [seq 0] [seq 1] [seq 2 "How are you doing?"]
← the commit consumes the tail; drop the draft
Η διεπαφή που προσθέτει τα προσχέδια αντί να τα αντικαθιστά δείχνει διπλασιασμένο, τρεμοπαίζον κείμενο. Η προβολή των υποτίτλων σας αποκτά ένα τρεμόπαιγμα που ο ομιλητής δεν είχε ποτέ. Σχεδιάστε όλα τα οριστικά τμήματα και, μετά από αυτά, το τελευταίο προσχέδιο, και καθαρίζετε το προσχέδιο όταν έρθει οριστικό τμήμα. Ένα οριστικό τμήμα μπορεί να περιέχει λέξεις που δεν εμφανίστηκαν σε κανένα προσχέδιο.
Ακολουθούν λίγα δευτερόλεπτα ομιλίας στη σύνδεση, από το παράδειγμα fetch παραπάνω (translate: ["es"], συναρμολόγηση προτάσεων απενεργοποιημένη):
{"type":"session.loading"}
{"type":"session.ready"}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good","speaker":0,"tentative":true}]}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good morning every","speaker":0,"tentative":true}]}
{"type":"transcript.committed","segment":{"seq":0,"start":0.0,"end":1.4,"text":"Good morning, everyone.","speaker":0,"words":[{"start":0.0,"end":0.3,"text":"Good"},{"start":0.3,"end":0.8,"text":"morning,"},{"start":0.8,"end":1.4,"text":"everyone."}]}}
{"type":"transcript.translated","seq":0,"translations":{"es":"Buenos días a todos."}}
{"type":"transcript.draft","segments":[{"start":1.6,"text":"Thanks for","speaker":0,"tentative":true}]}
Δύο ακόμη πλαίσια βρίσκονται γύρω από αυτά. Το transcript.amended διορθώνει ένα οριστικό τμήμα επιτόπου (το ίδιο seq, νέο text ή speaker) όταν έχει διορθωθεί μια λέξη του· εφαρμόστε το στη γραμμή που έχετε ήδη σχεδιάσει. Το transcript.words στέλνει χρονισμούς λέξεων για το ζωντανό τέλος λίγο πριν γίνει οριστικό. Χρειάζεται το events.words, που είναι ενεργό από προεπιλογή· ένα οριστικό τμήμα περιέχει πάντα τα words του, ό,τι κι αν λένε οι επιλογές συμβάντων. Τέλος, αγνοήστε τους τύπους πλαισίων που δεν γνωρίζετε· νέοι προστίθενται χωρίς προειδοποίηση.
Ακολουθεί η επαναληπτική ανάγνωση του SDK της Python, με το τερματικό ως προβολή υποτίτλων. Η συναρμολόγηση προτάσεων είναι εδώ επίσης απενεργοποιημένη, άρα οι μεταφράσεις φτάνουν ως transcript.translated, και το πλαίσιο error εμφανίζεται ως VocalStackError που πετιέται έξω από τον βρόχο:
import asyncio
import os
from vocalstack import RealtimeSession, VocalStack, VocalStackError
MIC = ["ffmpeg", "-loglevel", "error", "-f", "avfoundation", "-i", ":0",
"-f", "f32le", "-ac", "1", "-ar", "16000", "-"]
async def main() -> None:
async with VocalStack("{{api}}", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
created = await vs.realtime.create_session(
spoken_language="en", translate=["es"], sentences={"enabled": False}
)
session = await RealtimeSession.connect(created.session_id, created.stream_url)
mic = await asyncio.create_subprocess_exec(*MIC, stdout=asyncio.subprocess.PIPE)
async def feed() -> None:
assert mic.stdout is not None
while chunk := await mic.stdout.read(8000): # 125 ms of audio
await session.send_audio(chunk)
await session.eof() # the server flushes, then sends session.done
feeder = asyncio.create_task(feed())
try:
async with session:
async for event in session:
if event.type == "transcript.draft":
# The live tail: replace it, never append it.
tail = " ".join(s.text for s in event.segments)
print(f"\r\x1b[K\x1b[2m{tail}\x1b[0m", end="", flush=True)
elif event.type == "transcript.committed":
# Final: append it (seq 0, 1, 2...), and the tail it covers is gone.
print(f"\r\x1b[K{event.segment.text}", flush=True)
elif event.type == "transcript.translated":
print(f" es: {event.translations.get('es')}")
await feeder
print(f"done ({session.done_reason})", session.stats)
except VocalStackError as e:
# An `error` frame mid-stream, or a refused connect.
print(f"\nerror {e.code}: {e.message} (retryable: {e.retryable})")
finally:
if mic.returncode is None:
mic.kill()
asyncio.run(main())
Η μετάφραση ανά τμήμα, όπως παραπάνω, είναι η απλή μορφή. Η μορφή που προτιμούν οι αναγνώστες παίρνει τη δική της ενότητα.
Το translate: ["es", "fr"] ζητά μεταφράσεις καθώς προχωρά η συνεδρία. Το πώς φτάνουν εξαρτάται από τη συναρμολόγηση προτάσεων (sentences: { enabled: true }), που είναι ενεργή από προεπιλογή.
Με απενεργοποιημένες τις προτάσεις, κάθε οριστικό τμήμα ακολουθείται λίγο αργότερα από ένα πλαίσιο transcript.translated που αναφέρει το seq του και φέρει τα translations με κλειδί τη γλώσσα· τα συνδέετε με εκείνο το τμήμα. Αυτό χρησιμοποιούν τα παραδείγματα που είδαμε ως τώρα. Είναι απλό και μοιάζει με αρχείο καταγραφής: ένα τμήμα, μία μετάφραση, και το όριο της πρότασης εκεί όπου ο ομιλητής πήρε ανάσα.
Με ενεργές τις προτάσεις, ο διακομιστής συναρμολογεί τα οριστικά τμήματα σε ολόκληρες προτάσεις και μεταφράζει αυτές, κάτι που διαβάζεται πολύ καλύτερα. Κάθε πλαίσιο transcript.sentence αντικαθιστά πλήρως μία πρόταση, με κλειδί το sentenceId. Το status της πηγαίνει από open σε closed (ή dropped, που σημαίνει να την απορρίψετε), και τα translations της είναι προσωρινά όσο είναι ανοιχτή και τελικά μόλις κλείσει. Κάντε upsert με βάση το sentenceId, ποτέ μην προσθέτετε. Το SDK της TypeScript παρέχει το applySentence ακριβώς γι' αυτό:
import { applySentence, type SentenceSegment } from '@vocalstack/sdk';
let sentences: SentenceSegment[] = [];
for await (const ev of session) {
if (ev.type !== 'transcript.sentence') continue;
sentences = applySentence(sentences, ev.sentence); // upsert by sentenceId
if (ev.sentence.status === 'closed') console.log(`${ev.sentence.text}\n es: ${ev.sentence.translations?.es}`);
}
Οι μεταφράσεις ακολουθούν την απομαγνητοφώνηση με διαφορά ενός-δύο δευτερολέπτων. Αν θέλετε να ακούγονται κι αυτές, προσθέστε voice: ["es"] (χρειάζεται προτάσεις). Τα ισπανικά για κάθε κλειστή πρόταση φτάνουν τότε ως πλαίσιο transcript.voice, που φέρει το κλιπ ως base64 Ogg/Opus σε 24 kHz μονοφωνικό. Μια μεγάλη πρόταση έρχεται σε κομμάτια (part από τα parts) που παίζονται το ένα μετά το άλλο. Το προσωρινό κείμενο δεν μιλιέται ποτέ. Ο οδηγός ζωντανής μετάφρασης καλύπτει λεπτομερώς τις φωνές, και το πού χάνονται τα δευτερόλεπτα εξηγεί την αλυσίδα καθυστέρησης.
Οι γλώσσες-στόχοι δεν είναι σταθερές κατά τη δημιουργία.
Ένα πλαίσιο κειμένου session.update αλλάζει τα spokenLanguage, translate, speakerLabels, latencyMode, voice ή voiceByLang από εκείνο το σημείο και μετά· τα πεδία που παραλείπετε μένουν ως έχουν.
{"type":"session.update","translate":["es","fr"],"latencyMode":"fast"}
Τα SDK το εκθέτουν ως session.update(…). Το translate αντικαθιστά ολόκληρη τη λίστα, και το [] απενεργοποιεί τη μετάφραση. Τα τμήματα που έχουν ήδη παραδοθεί δεν ξαναμεταγράφονται ούτε ξαναμεταφράζονται, και αν ενεργοποιήσετε τις ετικέτες ομιλητών, οι ομιλητές επισημαίνονται μόνο από εκείνο το σημείο και μετά. Όσα ειπώθηκαν έχουν ειπωθεί· οι ρυθμίσεις ισχύουν μόνο για ό,τι έρχεται στη συνέχεια.
Όταν τελειώσει ο ήχος, στείλτε {"type":"eof"} (session.eof() στα SDK). Ο διακομιστής απομαγνητοφωνεί ό,τι του έχει απομείνει, στέλνει τα τελευταία οριστικά τμήματα, έπειτα session.done και κλείνει. Αν στείλετε cancel, σταματά αμέσως και πετάει τα υπόλοιπα.
{"type":"session.done","reason":"eof","stats":{"audioS":30.0,"processingS":30.6,"realtimeFactor":0.98}}
Το reason είναι eof, max_duration (η συνεδρία έφτασε το μέγιστο μήκος του προγράμματος και εκτέλεσε την ίδια εκκαθάριση) ή cancelled. Το stats μετρά ο διακομιστής: το audioS είναι ο ήχος που στείλατε, το processingS είναι ο χρόνος του ρολογιού κατά τον οποίο έτρεξε η συνεδρία, και το realtimeFactor είναι ο λόγος τους. Μια ζωντανή συνεδρία καταλήγει λίγο κάτω από το 1, επειδή ο χρόνος του ρολογιού περιλαμβάνει την τελική εκκαθάριση μετά το eof σας. Ένας αριθμός αισθητά χαμηλότερος σημαίνει ότι η συνεδρία κράτησε εμφανώς περισσότερο από τη διάρκεια του ήχου· το πρώτο που πρέπει να ελέγξετε είναι πόσο γρήγορα έφτασε ο ήχος. Για ένα αρχείο, ο ίδιος αριθμός δείχνει πόσες φορές πιο γρήγορα από την αναπαραγωγή επέστρεψε η απομαγνητοφώνηση.
Μια συνεδρία διαρκεί το πολύ limits.maxDurationS από το GET /v1/realtime/capabilities, και ο ήχος της καταναλώνει την πίστωσή σας ανά δευτερόλεπτο με τη ζωντανή τιμή του προγράμματός σας, με το ίδιο ελάχιστο 5 λεπτών που ισχύει για κάθε απομαγνητοφώνηση: κάθε συνεδρία που λαμβάνει ήχο χρεώνεται τουλάχιστον 5 λεπτά (τιμές). Το ίδιο socket δέχεται και ένα ολόκληρο αρχείο: δημιουργήστε τη συνεδρία με input: { kind: "file" }, στείλτε τα ίδια τα byte του αρχείου (mp3, wav, flac, ogg, m4a) και eof, και η απομαγνητοφώνηση επιστρέφει πολλές φορές γρηγορότερα από το μήκος του ήχου, με πλαίσια transcript.progress στο ενδιάμεσο.
Δεν υπάρχει συνέχιση. Μια διακοπή της σύνδεσης τερματίζει τη συνεδρία· η επανασύνδεση σημαίνει δημιουργία νέας συνεδρίας, που χρεώνεται με το δικό της ελάχιστο, και νέα μετάδοση από τη ζωντανή πηγή σας, με το seq πάλι από το 0. Η ίδια η επανασύνδεση είναι το τυπικό μοτίβο για κάθε API με socket, και αξίζει να το κάνετε σωστά μία φορά:
retryable: true σε πλαίσιο error, ένα 503 με το Retry-After του, ή 30 δευτερόλεπτα χωρίς πλαίσιο, που σημαίνει capacity_exceeded. Τα invalid_token και session_expired σημαίνουν νέο streamUrl από τον διακομιστή σας, όχι άλλη προσπάθεια με το παλιό.Κρατήστε τη διαδρομή που δημιουργεί συνεδρίες φθηνή στην κλήση της και αντιμετωπίζετε το κλείσιμο ως νέα αρχή. Η απουσία συνέχισης είναι το τίμημα του να μην αποθηκεύεται τίποτα· αν η απομαγνητοφώνηση πρέπει να επιζήσει του socket, αποθηκεύστε την.
Όταν θέλετε την ίδια ροή αποθηκευμένη ως εγγραφή, διορθωμένη αργότερα, εξαγόμενη και διαβαζόμενη από κοινό στις δικές του γλώσσες, δημοσιεύστε την σε μια ζωντανή απομαγνητοφώνηση. Ο οδηγός ζωντανής μετάδοσης περνά το ίδιο PCM socket μέσα από μια απομαγνητοφώνηση και έναν σύνδεσμο μετάδοσης, και ο οδηγός ακρόασης δείχνει την πλευρά του συνδρομητή, όπου κάθε ακροατής επιλέγει γλώσσα και ένας αργοπορημένος συμμετέχων καλύπτει το χαμένο κείμενο από έναν δείκτη lastSeq. Το πώς μοιάζει αυτό για μια αίθουσα είναι το θέμα του ένας σύνδεσμος, κάθε γλώσσα.
Για ένα αρχείο που θέλετε να κρατήσετε, η αποθηκευμένη διαδρομή είναι η απομαγνητοφώνηση αρχείου με το API. Ο πλήρης οδηγός πλαισίων, με κάθε πεδίο, βρίσκεται στον οδηγό Ροή μικροφώνου της αναφοράς API.