Poznaj Eleven v4Poznaj Eleven v4, nasz najbardziej emocjonalny model.

Przejdź do treści

Transkrypcja mowy na tekst w czasie rzeczywistym poniżej 200 ms: przewodnik po architekturze

Opublikowano
Ostatnia aktualizacja

PosłuchajPosłuchaj tego artykułu

Zamiana mowy na tekst w czasie rzeczywistym (STT) aktywnie transkrybuje audio podczas wypowiedzi, zwracając słowa w formie tekstu w ciągu kilkuset milisekund. Utrzymanie niskiego opóźnienia STT to jednak problem zarówno architektury, jak i modelu. Deweloperzy muszą uwzględnić transport, porcjowanie danych, wykrywanie końca wypowiedzi i ścieżkę przechwytywania — każdy z tych elementów zwiększa opóźnienie. Niewydajność choćby jednego z nich może przekroczyć budżet 200 ms.

Ten przewodnik przedstawia praktyczny sposób budowania potoków zamiany mowy na tekst w czasie rzeczywistym — od warstwy transportu. Skupimy się na Scribe v2 Realtime, który tworzy częściowe transkrypcje przy opóźnieniu modelu wynoszącym około 150 ms, obsługuje ponad 90 języków, przyjmuje audio PCM (8–48 kHz) i mu-law oraz udostępnia Voice Activity Detection i ręczne zatwierdzanie finalizacji segmentów.

Pokażemy, jak audio trafia na serwer, jak hipotezy stają się zatwierdzonym tekstem, ile kosztują funkcje w strumieniu oraz jak poprawnie przechwytywać i przekazywać audio.

Podsumowanie

  • Tworzenie systemów zamiany mowy na tekst w czasie rzeczywistym wymaga dopracowania architektury, aby opóźnienie było niskie w całym potoku.
  • WebSocket to właściwy wybór domyślny dla większości potoków, choć WebRTC oferuje wiele korzyści kosztem większej złożoności.
  • Voice Activity Detection zapewnia segmentację bez użycia rąk, a ręczne zatwierdzanie pozwala aplikacji przejąć kontrolę, gdy wie, że tura dobiegła końca.
  • Częściowe wyniki są tymczasowe, a końcowe — zatwierdzone, dlatego należy wyświetlać je inaczej. 
  • Małe fragmenty PCM, około 100 ms, minimalizują opóźnienie do pierwszego częściowego wyniku.

WebSocket a WebRTC dla zamiany mowy na tekst w czasie rzeczywistym

Zanim rozpocznie się transkrypcja, audio musi dotrzeć ze źródła do rozpoznawacza. Wybrany kanał wyznacza minimalne opóźnienie dla wszystkich kolejnych etapów. Audio może dotrzeć do warstwy transkrypcji na dwa sensowne sposoby.

WebSocket to długotrwały, uporządkowany, niezawodny i dwukierunkowy kanał działający przez TCP. Otwierasz połączenie, wysyłasz binarne ramki audio i odbierasz zdarzenia transkrypcji. Jest prosty zarówno po stronie klienta, jak i serwera, przechodzi przez firmowe proxy i zapory sieciowe, które już zezwalają na HTTPS, i obsługuje go każda przeglądarka oraz środowisko serwerowe. 

Ograniczeniem WebSocket jest działanie przez TCP. Gdy pakiet zaginie, TCP przesyła go ponownie i wstrzymuje późniejsze dane, aż luka zostanie uzupełniona. W dobrych warunkach sieciowych nie jest to zauważalne. Przy utracie pakietów powoduje blokowanie head-of-line: krótkie zatrzymanie, podczas którego audio się gromadzi, a potem dociera gwałtownie.

WebRTC stworzono dla mediów w czasie rzeczywistym. Przesyła media przez UDP (za pośrednictwem SRTP), więc utracony pakiet nie zatrzymuje strumienia — potok działa dalej. Zawiera bufor jittera, który niweluje różnice w czasie nadejścia pakietów, negocjuje przejście przez NAT za pomocą ICE/STUN/TURN, aby peery za routerami mogły się połączyć, oraz ma własne mechanizmy przechwytywania i kodowania audio. 

Zwykle potrzebujesz serwerów TURN dla klientów, którzy nie mogą połączyć się bezpośrednio, a serwer musi kończyć strumień mediów zamiast odczytywać strumień bajtów.

Najważniejsze różnice w skrócie:

WebSocket
Transport
TCP (reliable, ordered)
Behavior under packet loss
Head-of-line blocking, bursty recovery
Jitter handling
Your responsibility
NAT traversal
Not needed (client-initiated)
Browser support
Universal, trivial
Server complexity
Low
WebRTC
Transport
UDP/SRTP (real-time, loss-tolerant)
Behavior under packet loss
Graceful degradation
Jitter handling
Built-in jitter buffer
NAT traversal
Requires ICE/STUN/TURN
Browser support
Universal, but more API surface
Server complexity
High (media server or SFU)

W większości przypadków właściwym wyborem jest WebSocket. Użyj go, gdy klienci mają dobre połączenie, a ty kontrolujesz ścieżkę przechwytywania: w potokach serwer-serwer, aplikacjach desktopowych, aplikacjach przeglądarkowych na łączach szerokopasmowych oraz w większości backendów contact center, gdzie audio już dociera na serwer w inny sposób. 

Wybierz WebRTC, gdy przechwytujesz audio bezpośrednio z urządzeń konsumenckich w zawodnych sieciach mobilnych, gdy masz już stos WebRTC do dwukierunkowego audio (na przykład agenta głosowego, który także odpowiada głosem) lub gdy odporność na utratę danych w czasie rzeczywistym jest ważniejsza niż prostota wdrożenia.

W dalszej części przewodnika używamy WebSocket do połączenia z rozpoznawaczem, ponieważ ułatwia to zrozumienie wszystkich elementów i jest dobrym początkiem dla większości zespołów. Nic z tego nie jest specyficzne dla WebSocket, więc później możesz dodać przed nim warstwę mediów WebRTC, zdekodować audio do PCM na serwerze i przekazywać te same fragmenty do potoku.

Częściowe i końcowe transkrypcje: wyjaśnienie wyników pośrednich

Rozpoznawacz działający w czasie rzeczywistym nie czeka z odpowiedzią na całe zdanie. Zamiast tego generuje ciąg przypuszczeń, które stają się dokładniejsze wraz z kolejnymi fragmentami audio, a następnie je zatwierdza. Zrozumienie różnicy między tymi stanami odróżnia transkrypcję, która wydaje się żywa, od takiej, która sprawia wrażenie zepsutej. 

Częściowa (pośrednia) hipoteza to najlepsze przypuszczenie modelu na podstawie dotychczas odebranego audio. Częściowe wyniki są z założenia niestabilne. Wraz z napływem audio model poprawia wcześniejsze słowa: „Chcę” może zmienić się w „Chcę dwa bilety”, gdy późniejszy kontekst rozwiąże niejednoznaczność. Pojawiają się szybko (do tego odnosi się opóźnienie ~150 ms) i mają być nadpisywane.

Końcowa hipoteza to zatwierdzony segment, który już się nie zmieni. Po finalizacji segmentu rozpoznawacz przechodzi dalej, a kolejne hipotezy opisują późniejsze audio. Wyniki końcowe zapisujesz, wysyłasz do LLM lub przechowujesz jako transkrypcję.

Rozróżnienie wyników częściowych i końcowych wpływa na trzy rzeczy, które pomylisz, jeśli je zatrzesz:

  • Doświadczenie użytkownika: Wyświetlanie wyników częściowych sprawia, że transkrypcja wydaje się aktywna: użytkownik widzi słowa w trakcie mówienia, co potwierdza, że mikrofon działa, a system słucha.
  • Wykrywanie końca wypowiedzi: Wyniki częściowe dają ciągły sygnał aktywności mowy. W połączeniu z VAD pozwalają ustalić, kiedy mówca faktycznie skończył.
  • Czas kolejnych etapów: W potoku agenta głosowego etapy to audio wejściowe, zamiana mowy na tekst, LLM, następnie zamiana tekstu na mowę i audio wyjściowe. Możesz rozpocząć wstępną pracę na wynikach częściowych, a potwierdzić ją wynikami końcowymi, skracając odczuwalny czas odpowiedzi kosztem okazjonalnego odrzucania tej pracy.

Wyświetlaj wyniki częściowe i końcowe inaczej. Prostym, skutecznym wzorcem jest utrzymywanie jednego zmiennego „bieżącego wiersza” powiązanego z najnowszym wynikiem częściowym i zatwierdzanie go w transkrypcji, do której tylko dopisujesz, gdy nadejdzie wynik końcowy:

type TranscriptState = {
  committed: string[]; // finalized segments, never rewritten
  current: string;     // latest partial, overwritten on each update
};

const onPartial = (s: TranscriptState, text: string): TranscriptState =>
  ({ ...s, current: text });

const onFinal = (s: TranscriptState, text: string): TranscriptState =>
  ({ committed: [...s.committed, text], current: "" });

Wizualnie wyświetlaj zatwierdzony tekst zwykłym stylem, a bieżący jaśniejszym lub kursywą, aby użytkownik wiedział, że może się jeszcze zmienić.

Wykrywanie końca wypowiedzi i wykrywanie aktywności głosowej (VAD)

Wiedza o tym, co zostało powiedziane, to tylko połowa zadania. Rozpoznawacz musi też wiedzieć, kiedy myśl się skończyła. Ta decyzja określa, kiedy finalizujesz segment, a w agencie — kiedy system zaczyna odpowiadać. 

Wykrywanie końca wypowiedzi to decyzja, że wypowiedź dobiegła końca. Zbyt wczesna finalizacja przerywa użytkownikom w połowie zdania. Zbyt późna sprawia, że agent milczy, mimo że użytkownik wyraźnie skończył.

Scribe v2 Realtime udostępnia dwa uzupełniające się mechanizmy:

  • Voice Activity Detection dzieli audio na podstawie ciszy: Rozpoznawacz wykrywa, gdy mowa ustępuje dłuższej ciszy, i automatycznie wykorzystuje tę granicę do finalizacji segmentu. VAD to dobry wybór domyślny dla interfejsów konwersacyjnych, ponieważ dostosowuje się do naturalnego rytmu mowy bez ręcznego śledzenia czasu.
  • Ręczne zatwierdzanie: Ręczne zatwierdzanie pozwala aplikacji zdecydować, kiedy sfinalizować bieżący segment, niezależnie od ciszy. Wysyłasz sygnał zatwierdzenia, rozpoznawacz zamyka bieżący segment i emituje wynik końcowy. To właściwe narzędzie, gdy aplikacja już wie, że tura się skończyła: po zwolnieniu przycisku push-to-talk, akcji „wyślij” lub zewnętrznej polityce zmian tury.

Te dwa mechanizmy dobrze ze sobą współpracują. Typowy agent głosowy używa VAD do obsługi bez użycia rąk i udostępnia ręczne zatwierdzanie jako opcję nadrzędną. Dzięki temu użytkownik, który robi przerwę na zastanowienie, nie zostanie przerwany, a użytkownik naciskający przycisk od razu wyznaczy granicę.

Próg ciszy to rzeczywisty kompromis — nie ma jednej uniwersalnie właściwej wartości:

  • Krótki limit czasu po zakończeniu mowy (na przykład finalizacja po ~200–400 ms ciszy) sprawia, że system wydaje się responsywny. Może też przerywać użytkownikom, którzy naturalnie robią pauzy między częściami zdania, dzieląc jedną myśl na kilka segmentów i w agencie wywołując przedwczesną odpowiedź.
  • Długi limit czasu (na przykład ~800–1200 ms) toleruje naturalne pauzy i zachowuje całe wypowiedzi, kosztem zauważalnego opóźnienia reakcji systemu.

Nie ma tu globalnej stałej — dopasuj próg do rodzaju interakcji:

  • Dyktowanie i robienie notatek tolerują dłuższe pauzy, ponieważ użytkownicy zastanawiają się w trakcie zdania. Wybierz dłuższe limity i oprzyj się na VAD.
  • Agenci do poleceń i zadań transakcyjnych korzystają na krótszych limitach oraz ręcznym zatwierdzaniu, ponieważ tury są krótkie i konkretne.
  • Osoby wielojęzyczne lub niemówiące w języku ojczystym częściej robią pauzy, więc uwzględnij więcej ciszy przed finalizacją.

Te wskazówki pomogą ci zbudować skuteczny system wykrywania końca wypowiedzi i zbliżyć się do zamiany mowy na tekst w czasie rzeczywistym.

Funkcje w strumieniu: wykrywanie języka i diarizacja mówców

Rozpoznawanie strumieniowe może robić więcej niż tylko generować słowa. Każdy dodatkowy sygnał wpływa jednak na opóźnienie i stabilność. Zasada jest prosta: włączaj tylko to, czego potrzebuje działanie na żywo, a resztę odłóż do przetwarzania wsadowego.

Automatyczne rozpoznawanie języka pozwala Scribe v2 Realtime określić język mówiony spośród ponad 90 obsługiwanych języków bez deklarowania go z góry. Koszt polega na tym, że model potrzebuje krótkiego fragmentu audio, aby pewnie go rozpoznać, więc pierwsze wyniki częściowe mogą być mniej stabilne, zanim język zostanie ustalony. Jeśli znasz język, jego wskazanie usuwa tę niejednoznaczność i zwykle zapewnia stabilniejsze początkowe wyniki.

Diarizacja mówców przypisuje wypowiedzi do poszczególnych mówców, wskazując, kto co powiedział. W transkrypcji wsadowej jest to stosunkowo łatwe, bo model widzi cały plik. W strumieniu jest trudniej: rozpoznawacz musi przypisać etykietę mówcy wyłącznie na podstawie dotychczasowego audio, a etykieta nadana na początku może wymagać korekty po usłyszeniu większej części głosu tej osoby. Traktuj etykiety mówców w strumieniu tak jak tekst częściowy: jako tymczasowe do finalizacji segmentu.

Czasy na poziomie słów i kontekst encji działają według tej samej zasady. Im więcej metadanych na token zamawiasz, tym więcej musi obsłużyć zarówno model, jak i łącze. W większości interfejsów czasu rzeczywistego potrzebujesz na żywo tylko tekstu i granic segmentów, a szczegółowe metadane możesz odłożyć do wsadowego przetwarzania po rozmowie w Scribe v2.

Formaty audio do przesyłania strumieniowego: PCM i mu-law

Transport i logika rozpoznawania przyciągają najwięcej uwagi, ale zaskakująco wiele rzeczywistych błędów powstaje poziom niżej — w sposobie kodowania i porcjowania audio. Dobranie formatu i rozmiaru fragmentu to najtańszy sposób na zmniejszenie opóźnienia zamiany mowy na tekst.

PCM (liniowy, 16-bitowy ze znakiem, little-endian) to format, którego należy używać, gdy kontrolujesz przechwytywanie. Wyższe częstotliwości próbkowania niosą więcej szczegółów akustycznych: 16 kHz to standardowe minimum dla rozpoznawania mowy i zwykle wystarcza; 8 kHz to jakość telefoniczna, która traci treści wysokoczęstotliwościowe. Użyj częstotliwości zgodnej ze źródłem. Zwiększanie próbkowania audio telefonicznego 8 kHz do 48 kHz nie daje korzyści, bo nie odzyska utraconych informacji.

Mu-law przy 8 kHz to format telefoniczny. Jeśli odbierasz rozmowy od dostawcy takiego jak Twilio, audio dociera jako mu-law 8 kHz i należy przekazać je w tym formacie zamiast transkodować je dwukrotnie. Dopasowanie formatu źródłowego pozwala uniknąć artefaktów resamplingu i zbędnego etapu konwersji.

Rozmiar fragmentów najbardziej bezpośrednio wpływa na odczuwalne opóźnienie. Wysyłasz audio we fragmentach, a rozpoznawacz tworzy wyniki częściowe wraz z ich nadejściem. Mniejsze fragmenty oznaczają częstsze aktualizacje i niższe opóźnienie do pierwszego wyniku częściowego; większe — mniej wiadomości i nieco więcej kontekstu na inferencję. Praktyczny zakres to 20–250 ms audio na fragment. Dla porównania: przy monofonicznym PCM 16-bit 16 kHz sekunda audio ma 32 000 bajtów, więc fragment 100 ms ma około 3200 bajtów.

Przechwytywanie mikrofonu w przeglądarce

W przeglądarce właściwym narzędziem jest Web Audio API z AudioWorklet. Worklet działa w wątku renderowania audio, odbiera audio w małych ramkach i nie jest narażony na zacięcia głównego wątku, jak starszy ScriptProcessorNode. Jego zadaniem jest konwersja natywnych próbek float przeglądarki do 16-bitowego PCM i przekazanie ich do głównego wątku, który wysyła je przez WebSocket.

Podstawą procesora workletu jest konwersja float do PCM:

// pcm-worklet.ts - registered via audioContext.audioWorklet.addModule()
class PCMWorklet extends AudioWorkletProcessor {
  process(inputs: Float32Array[][]) {
    const channel = inputs[0]?.[0]; // mono; Float32, range [-1, 1]
    if (!channel) return true;
    const pcm = new Int16Array(channel.length);
    for (let i = 0; i < channel.length; i++) {
      const s = Math.max(-1, Math.min(1, channel[i]));
      pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
    }
    // Transfer the buffer to the main thread without copying.
    this.port.postMessage(pcm.buffer, [pcm.buffer]);
    return true;
  }
}
registerProcessor("pcm-worklet", PCMWorklet);

Potok w kodzie

Potok składa się z trzech części: klienta przeglądarkowego, który przechwytuje mikrofon i przesyła PCM na twój serwer, serwera Node przekazującego audio do Scribe v2 Realtime i odsyłającego transkrypcje oraz klienta skryptowego przesyłającego PCM z pliku lub mostu telefonicznego.

Serwer pośredniczy zamiast udostępniać rozpoznawacz bezpośrednio przeglądarce z jednego ważnego powodu: twój klucz API ElevenLabs jest sekretem i nigdy nie może pojawić się w kodzie po stronie klienta. Klucz pozostaje na serwerze. Jeśli przeglądarka musi komunikować się z rozpoznawaczem bezpośrednio, utwórz po stronie serwera krótkotrwały token jednorazowy i przekaż klientowi jego zamiast klucza API.

Klient przeglądarkowy

Klient otwiera WebSocket do twojego serwera, przechwytuje mikrofon przez opisany wyżej worklet i przekazuje każdą ramkę PCM od razu po jej utworzeniu. Zdarzenia przychodzące (już znormalizowane przez serwer do { type, text }) sterują opisanym wcześniej stanem częściowym/końcowym:

// client.ts - runs in the browser. ws is an open WebSocket to your server.
const audioContext = new AudioContext({ sampleRate: 16000 });
await audioContext.audioWorklet.addModule("pcm-worklet.js");

const mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
});

const source = audioContext.createMediaStreamSource(mediaStream);
const worklet = new AudioWorkletNode(audioContext, "pcm-worklet");

// Forward each PCM frame to the server the moment it is produced.
worklet.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
  if (ws.readyState === WebSocket.OPEN) ws.send(e.data);
};
source.connect(worklet);

// Manual commit: tell the server to finalize the current segment.
const commit = () => ws.send(JSON.stringify({ type: "commit" }));

Przekaźnik serwerowy

Serwer otwiera jedno połączenie z rozpoznawaczem na klienta, zachowuje klucz API na serwerze, przekazuje binarne PCM bezpośrednio i normalizuje zdarzenia rozpoznawacza do stabilnej struktury { type, text }, z której korzysta klient:

// server.ts - Node, using the `ws` library. ELEVENLABS_API_KEY and the
// recognizer URL come from the environment; see the Speech to Text reference
// for the exact path and query parameters.
import { WebSocketServer, WebSocket } from "ws";

new WebSocketServer({ port: 8080 }).on("connection", (client) => {
  // The API key stays on the server, never on the wire to the browser.
  const recognizer = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
    headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
  });

  // Browser -> recognizer: forward binary PCM, translate control messages.
  client.on("message", (data, isBinary) => {
    if (recognizer.readyState !== WebSocket.OPEN) return;
    if (isBinary) recognizer.send(data); // raw PCM bytes
    else if (JSON.parse(data.toString()).type === "commit")
      recognizer.send(sendCommit());
  });

  // Recognizer -> browser: normalize events into a stable shape.
  recognizer.on("message", (raw) => {
    const event = parseRecognizerEvent(raw.toString());
    if (event && client.readyState === WebSocket.OPEN)
      client.send(JSON.stringify(event));
  });

  // ... open handshake, queueing pre-open audio, and teardown on close/error
});

Wszystko, co zależy od endpointu, ogranicza się do dwóch funkcji adaptera poniżej. Zastąp nazwy pól dokładnymi nazwami z dokumentacji Speech to Text; reszta potoku się nie zmienia:

// The single place that knows the recognizer's wire format.
const sendCommit = (): string => JSON.stringify({ type: "commit" });

type NormalizedEvent =
  | { type: "partial"; text: string }
  | { type: "final"; text: string }
  | { type: "vad"; speaking: boolean };

function parseRecognizerEvent(raw: string): NormalizedEvent | null {
  const msg = JSON.parse(raw);
  if (msg.is_final === true || msg.type === "final")
    return { type: "final", text: msg.text ?? "" };
  if (msg.type === "vad") return { type: "vad", speaking: !!msg.speaking };
  if (typeof msg.text === "string")
    return { type: "partial", text: msg.text };
  return null;
}

Skryptowy klient backendu

W potokach backendowych i w poniższym benchmarku to samo połączenie z rozpoznawaczem działa bez przeglądarki: odczytaj PCM z dowolnego źródła, wysyłaj je w tempie fragmentów czasu rzeczywistego i odbieraj zdarzenia. Klucz API i URL pochodzą ze środowiska, tak jak na serwerze.

// stream-stt.ts - pace ~100ms chunks at real time, then commit the tail.
const SAMPLE_RATE = 16000, CHUNK_MS = 100;
const CHUNK_BYTES = (SAMPLE_RATE * 2 * CHUNK_MS) / 1000; // 3200 bytes
const ws = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
  headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
});

// Send: walk the PCM buffer in 100ms chunks, sleeping between to mimic a
// live source. For audio that already arrives in real time, drop the sleep.
async function sendAudio(pcm: Buffer) {
  for (let off = 0; off < pcm.length; off += CHUNK_BYTES) {
    ws.send(pcm.subarray(off, off + CHUNK_BYTES));
    await new Promise((r) => setTimeout(r, CHUNK_MS));
  }
  ws.send(JSON.stringify({ type: "commit" })); // finalize the trailing segment
}

// Receive: print partials in place, append finals.
ws.on("message", (raw) => {
  const e = parseRecognizerEvent(raw.toString());
  if (e?.type === "final") console.log(`[final]   ${e.text}`);
  else if (e?.type === "partial") process.stdout.write(`[partial] ${e.text}\r`);
});

Benchmark opóźnienia zamiany mowy na tekst i współczynnika błędów słów

Opóźnienie i współczynnik błędów słów zależą od mówcy, języka, warunków akustycznych, długości audio, trasy sieciowej do najbliższego regionu każdego dostawcy oraz bieżącego obciążenia każdej usługi. 

Wynik zmierzony na laptopie w jednym mieście nie musi odpowiadać twojej infrastrukturze produkcyjnej w innym. Uruchamiaj testy z infrastruktury podobnej do produkcyjnej, na audio przypominającym rzeczywiste dane wejściowe, i raportuj zakresy oraz rozkłady zamiast pojedynczych wartości.

Liczą się tylko wartości opóźnienia i dokładności zmierzone na własnym audio z infrastruktury podobnej do produkcyjnej. Oto przewodnik po benchmarkowaniu opóźnienia zamiany mowy na tekst.

Co mierzyć w opóźnieniu zamiany mowy na tekst

Poniżej znajdziesz główne metryki do pomiaru w benchmarkach zamiany mowy na tekst w czasie rzeczywistym:

  • Czas do pierwszego wyniku częściowego: Od wysłania pierwszego fragmentu audio do otrzymania pierwszego niepustego wyniku częściowego.
  • Opóźnienie od wyniku częściowego do końcowego: Od ostatniego fragmentu audio wypowiedzi do końcowej hipotezy.
  • Współczynnik błędów słów (WER): WER końcowej transkrypcji względem referencji przygotowanej przez człowieka, obliczany identycznie we wszystkich systemach.
  • Zmiany stabilności: Ile wyników częściowych zostaje przepisanych przed finalizacją. To wskaźnik tego, jak bardzo będzie zmieniać się interfejs na żywo.

Kontrole

Aby uniknąć niewiarygodnych danych, zastosuj w eksperymencie kilka mechanizmów kontroli, które zapewnią spójność.

Oto główne kontrole stosowane w benchmarkach opóźnienia zamiany mowy na tekst:

  • Identyczne audio: Używaj tych samych plików, tej samej częstotliwości próbkowania i tego samego kodowania dla każdego systemu.
  • Identyczne tempo: Przesyłaj do każdego systemu fragmenty w tym samym rytmie czasu rzeczywistego (na przykład co 100 ms).
  • Powtarzaj i raportuj rozkłady: Uruchamiaj każdy plik wiele razy w ciągu dnia; raportuj medianę i ogon rozkładu (p50/p95).
  • Identyczne referencje i punktacja: Normalizuj tekst w ten sam sposób (wielkość liter, interpunkcja, liczby) przed obliczeniem WER.
  • Podaj region i sieć: Wskaż, gdzie uruchomiono test i jaka była trasa do każdego dostawcy.

Zachowanie tych samych warunków dla wszystkich elementów zapewni dokładniejsze metryki.

Szkielet testu

Rdzeń pomiaru przyjmuje adapter dostawcy i rejestruje czas do pierwszego wyniku częściowego, opóźnienie finalizacji oraz zmiany wyników częściowych:

// benchmark.ts - measurement core; one StreamFn adapter per provider.
type StreamFn = (
  audioPath: string,
  onEvent: (kind: "partial" | "final", text: string) => void,
  result: RunResult
) => Promise<void>; // adapter sets result.lastChunkSentAt on the final chunk

interface RunResult {
  firstPartialMs?: number;
  finalLagMs?: number;
  hypothesis: string;
  partialEdits: number;
  lastChunkSentAt: number;
  startedAt: number;
}

async function measure(streamFn: StreamFn, audioPath: string): Promise<RunResult> {
  const result: RunResult = {
    hypothesis: "", partialEdits: 0, lastChunkSentAt: 0,
    startedAt: performance.now(),
  };
  let prevPartial = "";

  await streamFn(audioPath, (kind, text) => {
    const now = performance.now();
    if (kind === "partial") {
      if (text && result.firstPartialMs === undefined)
        result.firstPartialMs = now - result.startedAt;
      if (text !== prevPartial) { result.partialEdits++; prevPartial = text; }
    } else { // final
      result.hypothesis = result.hypothesis ? `${result.hypothesis} ${text}` : text;
      if (result.lastChunkSentAt)
        result.finalLagMs = now - result.lastChunkSentAt;
    }
  }, result);

  return result;
}

Współczynnik błędów słów to standardowa odległość Levenshteina na poziomie tokenów dla znormalizowanego tekstu. Przed obliczeniem zamień wielkość liter i usuń interpunkcję identycznie w referencji oraz hipotezie, inaczej zmierzysz normalizator zamiast modelu. Umieść pomiar w pętli uruchamiającej każdy plik około 10 razy dla każdego dostawcy i raportuj medianę czasu do pierwszego wyniku częściowego oraz medianę WER (p50/p95), ponieważ pojedynczą próbkę silnie kształtuje zmienność sieci.

Aby go uruchomić, potrzebujesz dwóch rzeczy. Najpierw napisz po jednym adapterze StreamFn dla każdego systemu. Powyższy klient skryptowy już nim jest, a adaptery dla pozostałych powinny korzystać z tego samego kontraktu (audioPath, onEvent, result) i ustawiać result.lastChunkSentAt po wysłaniu ostatniego fragmentu audio. Następnie wczytaj pliki audio i referencje oraz wywołaj pomiary dla nich wszystkich. Uruchom to na maszynie reprezentującej twoje wdrożenie, na audio reprezentującym użytkowników, a uzyskasz porównanie, które da się odtworzyć.

Podsumowanie: jak osiągnąć zamianę mowy na tekst w czasie rzeczywistym 

Omówiliśmy wiele zmian architektonicznych, które pozwalają stopniowo ulepszać system i zbliżać się do zamiany mowy na tekst w czasie rzeczywistym.

Produkcyjny system STT w czasie rzeczywistym sprowadza się do kilku decyzji:

  • Transport: Wybierz WebSocket dla prostoty i kontrolowanych sieci, a WebRTC, gdy potrzebujesz odporności na utratę danych i przechwytujesz audio z urządzeń konsumenckich.
  • Wyniki częściowe i końcowe: Traktuj wyniki częściowe jako tymczasowe, a końcowe jako zatwierdzone, i wyświetlaj je inaczej, aby użytkownicy ufali tekstowi na żywo.
  • Wykrywanie końca wypowiedzi: Używaj VAD do segmentacji bez użycia rąk, ręcznego zatwierdzania jako opcji nadrzędnej i dopasuj próg ciszy do interakcji, zamiast stosować stałą wartość.
  • Funkcje w strumieniu: Włączaj funkcje w strumieniu tylko tam, gdzie są potrzebne na żywo, a resztę odłóż do przetwarzania wsadowego w Scribe v2.
  • Format audio: Przechwytuj małe ramki PCM, wysyłaj fragmenty ~100 ms i dopasuj format źródłowy dla telefonii.
  • Benchmarking: Empirycznie dobierz ustawienia dokładności i opóźnienia na podstawie własnego audio oraz docelowej metryki.
  • Bezpieczeństwo API: Przechowuj swój klucz API na serwerze albo twórz tokeny jednorazowe do bezpośrednich połączeń klientów.

Jeśli chcesz zobaczyć, jak optymalizować opóźnienie agenta głosowego, przygotowaliśmy też odpowiedni przewodnik.

Twórz systemy zamiany mowy na tekst w czasie rzeczywistym z Scribe v2 Realtime

Scribe v2 Realtime tworzy wyniki częściowe przy opóźnieniu modelu wynoszącym około 150 ms. To, czy użytkownicy odczują właśnie tyle, czy więcej, zależy od architektury wokół modelu — a tę część kontrolujesz ty. Korzystając ze strategii opisanych w tym artykule, zbudujesz lepszą architekturę potoku, która zmniejsza opóźnienia i poprawia doświadczenie klienta. 

Aby dowiedzieć się więcej, zobacz opis możliwości Speech to Text, przeczytaj naszą dokumentację modeli z pełną listą funkcji i języków oraz odwiedź strony produktów czasu rzeczywistego: API Speech to Text w czasie rzeczywistym oraz Speech to Text w czasie rzeczywistym.

Gdy zechcesz zacząć tworzyć, utwórz bezpłatne konto ElevenLabs i prześlij strumieniowo swoją pierwszą transkrypcję już dziś.

FAQ: opóźnienie zamiany mowy na tekst w czasie rzeczywistym

Autor

Tadas Petra to Developer Relations Engineer, który pomaga deweloperom korzystać z produktów ElevenLabs. Jest też założycielem Hungrimind, platformy edukacyjnej dla deweloperów. Wcześniej był Lead Developer Advocate w Agora i instruktorem w Zero To Mastery.

Podobne artykuły

Twórz z najwyższej jakości audio AI