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 dźwięk podczas mówienia i zwraca słowa jako tekst w ciągu kilkuset milisekund. Utrzymanie niskich opóźnień STT to jednak równie mocno kwestia architektury, co modelu. Deweloperzy muszą uwzględnić transport, dzielenie na fragmenty, wykrywanie końca wypowiedzi i ścieżkę przechwytywania — każdy z tych elementów dodaje opóźnienie. Nieefektywność któregokolwiek z nich może przekroczyć budżet 200 ms.

Ten przewodnik przedstawia praktyczny system do budowy procesów zamiany mowy na tekst w czasie rzeczywistym — od warstwy transportu. Punktem odniesienia będzie Scribe v2 Realtime, który tworzy częściowe transkrypcje przy opóźnieniu modelu około 150 ms, obsługuje ponad 90 języków, przyjmuje dźwięk PCM (8–48 kHz) i mu-law oraz udostępnia Voice Activity Detection oraz ręczne zatwierdzanie do finalizacji segmentów.

Pokażemy, jak dźwięk dociera do serwera, jak hipotezy zmieniają się w zatwierdzony tekst, jaki jest koszt funkcji strumieniowych oraz jak poprawnie przechwytywać i przesyłać dźwięk.

Podsumowanie

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

WebSocket a WebRTC w zamianie mowy na tekst w czasie rzeczywistym

Zanim rozpocznie się transkrypcja, dźwięk musi dotrzeć ze źródła do rozpoznawania mowy. Wybrany kanał określa minimalne opóźnienie dla wszystkich kolejnych etapów. Dźwięk może dotrzeć do warstwy transkrypcji na dwa sposoby.

WebSocket to długotrwały, uporządkowany, niezawodny i dwukierunkowy kanał oparty na 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, które już przepuszczają HTTPS, a obsługują go wszystkie przeglądarki i środowiska serwerowe. 

Ograniczeniem WebSocket jest działanie na TCP. Gdy pakiet zginie, TCP wysył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 dźwięk się kumuluje, a potem dociera naraz.

WebRTC powstał z myślą o mediach w czasie rzeczywistym. Przesyła media przez UDP (za pośrednictwem SRTP), więc utrata pakietu nie zatrzymuje strumienia — proces działa dalej. Zawiera bufor jittera, który pochłania wahania czasu nadejścia pakietów, negocjuje przejście przez NAT za pomocą ICE/STUN/TURN, by urządzenia za routerami mogły się łączyć, oraz ma własne mechanizmy przechwytywania i kodowania dźwięku. 

Zwykle potrzebujesz serwerów TURN dla klientów, którzy nie mogą połączyć się bezpośrednio, a serwer musi kończyć strumień multimedialny 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żywaj go, gdy klienci mają dobre połączenie, a ty kontrolujesz ścieżkę przechwytywania: w procesach serwer-serwer, aplikacjach desktopowych, aplikacjach przeglądarkowych przy szerokopasmowym internecie oraz większości systemów contact center, gdzie dźwięk i tak trafia na serwer inną drogą. 

Wybierz WebRTC, gdy przechwytujesz dźwięk bezpośrednio z urządzeń konsumenckich w zawodnych sieciach mobilnych, gdy masz już stos WebRTC dla dwukierunkowego dźwięku (na przykład agent głosowy, który także odpowiada głosem) lub gdy zachowanie w czasie rzeczywistym odporne na straty jest ważniejsze niż prostota implementacji.

W dalszej części przewodnika używamy transportu WebSocket do połączenia z rozpoznawaniem mowy, ponieważ wyraźnie pokazuje wszystkie elementy i jest dobrym punktem wyjścia 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ć dźwięk do PCM na serwerze i przekazywać te same fragmenty do procesu.

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

System rozpoznawania mowy w czasie rzeczywistym nie czeka z wypowiedzią na całe zdanie. Zamiast tego emituje ciąg przypuszczeń, które stają się dokładniejsze wraz z kolejnymi porcjami dźwięku, a potem je zatwierdza. Zrozumienie różnicy między tymi stanami odróżnia transkrypcję, która wydaje się żywa, od takiej, która wygląda na wadliwą. 

Hipoteza częściowa (pośrednia) to najlepsze przypuszczenie modelu na podstawie dźwięku odebranego do tej pory. Wyniki częściowe są z założenia niestabilne. Gdy dociera więcej dźwięku, model poprawia wcześniejsze słowa: „Chcę” może zmienić się w „Chcę dwa bilety”, gdy dalszy kontekst rozstrzygnie niejednoznaczność. Pojawiają się szybko (do tego odnosi się opóźnienie około 150 ms) i mają być nadpisywane.

Hipoteza końcowa to zatwierdzony segment, który już się nie zmieni. Po sfinalizowaniu segmentu system rozpoznawania przechodzi dalej, a kolejne hipotezy dotyczą późniejszego dźwięku. Wyniki końcowe zapisujesz, wysyłasz do LLM lub przechowujesz jako transkrypcję.

Rozróżnienie między wynikami częściowymi a końcowymi wpływa na trzy kwestie, które pomylisz, jeśli je zatrzesz:

  • Doświadczenie użytkownika: Wyświetlanie wyników częściowych sprawia, że transkrypcja wydaje się działać na żywo: użytkownik widzi pojawiające się słowa podczas 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ą określić, kiedy mówca rzeczywiście skończył.
  • Czasowanie kolejnych etapów: W procesie agenta głosowego etapy to: dźwięk wejściowy, zamiana mowy na tekst, LLM, następnie zamiana tekstu na mowę, a na końcu dźwięk wyjściowy. Możesz rozpocząć spekulacyjne działania na wynikach częściowych i potwierdzić je po otrzymaniu wyników końcowych, skracając odczuwalny czas odpowiedzi kosztem sporadycznego odrzucania tej pracy.

Wyświetlaj wyniki częściowe i końcowe inaczej. Prostym i skutecznym wzorcem jest utrzymywanie jednej zmiennej „bieżącej linii”, powiązanej z najnowszym wynikiem częściowym, oraz dodawanie jej do transkrypcji tylko do dopisywania po nadejściu wyniku końcowego:

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 normalnie, a bieżący jaśniejszą czcionką 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 sukcesu. System rozpoznawania musi też wiedzieć, kiedy myśl się skończyła. Od tej decyzji zależy moment finalizacji segmentu, a w agencie — moment rozpoczęcia odpowiedzi przez system. 

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

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

  • Voice Activity Detection dzieli dźwięk na podstawie ciszy: System rozpoznawania wykrywa, kiedy mowa ustępuje dłuższej ciszy, i automatycznie używa tej granicy do finalizacji segmentu. VAD to dobry domyślny wybór 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, system rozpoznawania 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 puszczeniu przycisku push-to-talk, akcji „wyślij” lub zgodnie z zewnętrzną polityką zmiany tury.

Oba 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 obejście, dzięki czemu użytkownik, który robi pauzę na zastanowienie, nie zostanie przerwany, a użytkownik naciskający przycisk od razu wyznaczy granicę.

Próg ciszy to realny kompromis bez jednej uniwersalnie poprawnej wartości:

  • Krótki limit czasu po zakończeniu mowy (na przykład finalizacja po około 200–400 ms ciszy) sprawia, że system wydaje się responsywny. Przerywa jednak użytkownikom, którzy naturalnie robią pauzy między frazami, dzieląc jedną myśl na kilka segmentów i — w przypadku agenta — wywołując przedwczesną odpowiedź.
  • Długi limit czasu (na przykład około 800–1200 ms) toleruje naturalne pauzy i zachowuje wypowiedzi w całości, ale powoduje zauważalne opóźnienie, zanim system zareaguje.

Nie ma tu jednej globalnej stałej — dostosuj próg do interakcji:

  • Dyktowanie i robienie notatek tolerują dłuższe pauzy, ponieważ użytkownicy zastanawiają się w środku zdania. Ustaw dłuższe limity i polegaj na VAD.
  • Agenci do poleceń i zadań transakcyjnych agenci korzystają z krótszych limitów i ręcznego zatwierdzania, ponieważ tury są krótkie i konkretne.
  • Osoby wielojęzyczne lub mówiące w języku obcym robią więcej pauz, 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 strumieniowe: wykrywanie języka i diarizacja mówców

Rozpoznawanie strumieniowe potrafi więcej niż tylko tworzyć słowa. Każdy dodatkowy sygnał wpływa jednak na opóźnienie i stabilność. Zasada jest prosta: włączaj tylko to, czego wymaga działanie na żywo, a resztę odkładaj na przetwarzanie wsadowe.

Automatyczne rozpoznawanie języka pozwala Scribe v2 Realtime rozpoznać mówiony język spośród ponad 90 obsługiwanych, bez deklarowania go z góry. Model potrzebuje jednak krótkiego fragmentu dźwięku, by pewnie go określić, więc pierwsze wyniki częściowe strumienia mogą być mniej stabilne, zanim język zostanie ustalony. Jeśli znasz już język, jego wskazanie usuwa tę niejednoznaczność i zwykle daje stabilniejsze pierwsze wyniki częściowe.

Diarizacja mówców przypisuje mowę do różnych mówców, określając, kto co powiedział. W transkrypcji wsadowej jest to stosunkowo łatwe, bo model widzi cały plik. W strumieniowaniu jest trudniej: system rozpoznawania musi przypisać etykietę mówcy wyłącznie na podstawie dotychczasowego dźwięku, a etykieta nadana wczesnemu fragmentowi może wymagać poprawki po usłyszeniu większej części głosu tej osoby. Etykiety mówców w strumieniu traktuj tak jak tekst częściowy: jako tymczasowe aż do finalizacji segmentu.

Czasowanie na poziomie słów i kontekst encji działają według tej samej zasady. Im więcej metadanych na token żądasz, tym więcej musi przetworzyć model i przesłać połączenie. 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ć na wsadowe przetwarzanie po rozmowie w Scribe v2.

Formaty audio dla strumieniowania: PCM i mu-law

Transport i logika rozpoznawania przyciągają najwięcej uwagi, ale zaskakująco wiele błędów z rzeczywistych wdrożeń powstaje poziom niżej — przy kodowaniu i dzieleniu dźwięku na fragmenty. Poprawny format i rozmiar 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 używaj, gdy kontrolujesz przechwytywanie. Wyższe częstotliwości próbkowania zawierają 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 o wysokich częstotliwościach. Używaj częstotliwości zgodnej ze źródłem. Nie ma korzyści z próbkowania w górę dźwięku telefonicznego 8 kHz do 48 kHz, bo tych informacji nie da się odzyskać.

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

Rozmiar fragmentów najbardziej bezpośrednio wpływa na odczuwalne opóźnienie. Wysyłasz dźwięk w fragmentach, a system rozpoznawania tworzy wyniki częściowe, gdy one docierają. Mniejsze fragmenty oznaczają częstsze aktualizacje i krótsze opóźnienie do pierwszego wyniku częściowego; większe — mniej wiadomości i nieco więcej kontekstu dla każdej inferencji. Praktyczny zakres to 20–250 ms dźwięku na fragment. Dla przykładu: przy monofonicznym PCM 16-bit 16 kHz sekunda dźwięku ma 32 000 bajtów, więc fragment 100 ms ma około 3200 bajtów.

Przechwytywanie dźwięku z 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 dźwięk w małych ramkach i nie jest podatny na przycięcia głównego wątku, jak starszy ScriptProcessorNode. Jego zadaniem jest konwersja natywnych próbek zmiennoprzecinkowych przeglądarki do 16-bitowego PCM i przekazanie ich do głównego wątku, który wysyła je przez WebSocket.

Rdzeniem procesora worklet 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);

Proces w kodzie

Proces składa się z trzech części: klienta przeglądarkowego, który przechwytuje mikrofon i strumieniuje PCM do serwera, serwera Node przekazującego dźwięk do Scribe v2 Realtime i odsyłającego transkrypcje oraz skryptowalnego klienta strumieniującego PCM z pliku lub bramki telefonicznej.

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

Klient przeglądarkowy

Klient otwiera WebSocket do serwera, przechwytuje mikrofon za pomocą powyższego workletu i przekazuje każdą ramkę PCM zaraz po jej utworzeniu. Przychodzące zdarzenia (znormalizowane już przez serwer do postaci { 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 systemem rozpoznawania na klienta, przechowuje klucz API na serwerze, przekazuje binarny PCM bezpośrednio dalej i normalizuje zdarzenia rozpoznawania do stabilnej postaci { 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 konkretnego endpointu, znajduje się w dwóch poniższych funkcjach adaptera. Zastąp nazwy pól dokładnymi nazwami z dokumentacji Speech to Text; reszta procesu 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;
}

Skryptowalny klient backendowy

W procesach backendowych i w poniższym benchmarku to samo połączenie z systemem rozpoznawania działa bez przeglądarki: odczytaj PCM z dowolnego źródła, wysyłaj go 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 dźwięku, ścieżki 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 przekłada się automatycznie na środowisko produkcyjne w innym. Uruchamiaj narzędzie testowe z infrastruktury podobnej do produkcyjnej, na dźwięku podobnym do rzeczywistych danych wejściowych, 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 dźwięku z infrastruktury podobnej do produkcyjnej. Oto przewodnik po benchmarkowaniu opóźnienia zamiany mowy na tekst.

Co mierzyć w przypadku opóźnienia zamiany mowy na tekst

Oto główne metryki, które należy mierzyć podczas benchmarkowania opóźnienia zamiany mowy na tekst w czasie rzeczywistym:

  • Czas do pierwszego wyniku częściowego: Od wysłania pierwszego fragmentu dźwięku do otrzymania pierwszego niepustego wyniku częściowego.
  • Opóźnienie od wyniku częściowego do końcowego: Od ostatniego fragmentu dźwięku wypowiedzi do hipotezy końcowej.
  • Współczynnik błędów słów (WER): WER końcowej transkrypcji względem referencji przygotowanej przez człowieka, obliczony identycznie dla wszystkich systemów.
  • Zmiany stabilności: Ile wyników częściowych jest przepisywanych przed finalizacją. Ta miara wskazuje, jak bardzo zmienia się interfejs na żywo.

Kontrole

Aby uniknąć niewiarygodnych danych, zastosuj w eksperymencie kilka kontroli zapewniających spójność.

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

  • Identyczny dźwięk: Używaj tych samych plików, tej samej częstotliwości próbkowania i tego samego kodowania dla każdego systemu.
  • Identyczne tempo: Strumieniuj dane do każdego systemu w tym samym tempie fragmentów czasu rzeczywistego (na przykład po 100 ms).
  • Powtarzaj i raportuj rozkłady: Uruchamiaj każdy plik wielokrotnie w ciągu dnia; raportuj medianę i ogon rozkładu (p50/p95).
  • Identyczne referencje i ocena: Normalizuj tekst w ten sam sposób (wielkość liter, interpunkcję, liczby) przed obliczeniem WER.
  • Podaj region i sieć: Wskaż, gdzie uruchomiono narzędzie testowe i jaką ścieżką łączyło się z każdym dostawcą.

Jeśli zachowasz identyczne wszystkie te elementy, uzyskasz dokładniejsze metryki.

Szkielet narzędzia testowego

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 zamieniaj tekst na małe litery i usuwaj interpunkcję identycznie w referencji oraz hipotezie — inaczej będziesz mierzyć normalizator, a nie model. Uruchamiaj ten pomiar w pętli około 10 razy dla każdego pliku i dostawcy oraz raportuj medianę czasu do pierwszego wyniku częściowego i medianę WER (p50/p95), ponieważ pojedyncza próbka jest silnie zależna od zmienności sieci.

Aby to uruchomić, potrzebujesz dwóch rzeczy. Po pierwsze, napisz jeden adapter StreamFn dla każdego systemu. Powyższy skryptowalny klient już nim jest; adaptery dla pozostałych systemów powinny działać według tego samego kontraktu (audioPath, onEvent, result) i ustawiać result.lastChunkSentAt po wysłaniu ostatniego fragmentu dźwięku. Po drugie, wczytaj pliki audio i referencje, a następnie wywołaj pomiary dla nich wszystkich. Uruchom test z maszyny reprezentującej wdrożenie, na dźwięku reprezentującym użytkowników, a otrzymasz porównanie możliwe do odtworzenia.

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

W tym artykule omówiliśmy wiele zmian architektonicznych, które pozwolą ci 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 ze względu na prostotę i kontrolowane sieci, a WebRTC, gdy potrzebujesz odporności na utratę danych i przechwytujesz dźwięk 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 obejścia i dostosuj próg ciszy do interakcji, zamiast stosować stałą wartość.
  • Funkcje strumieniowe: Włączaj funkcje strumieniowe tylko tam, gdzie są potrzebne na żywo, a resztę odkładaj na przetwarzanie wsadowe w Scribe v2.
  • Format audio: Przechwytuj małe ramki PCM, wysyłaj fragmenty około 100 ms i dopasuj format źródłowy dla telefonii.
  • Benchmarkowanie: Empirycznie dostosuj parametry dokładności i opóźnienia na podstawie własnego dźwięku oraz docelowej metryki.
  • Bezpieczeństwo API: Przechowuj klucz API na serwerze lub twórz tokeny jednorazowe dla bezpośrednich połączeń klientów.

Jeśli chcesz zobaczyć, jak optymalizować opóźnienie w agencie głosowym, przygotowaliśmy też 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 około 150 ms. To, czy użytkownicy odczują właśnie taką wartość, czy większą, zależy od otaczającej go architektury — czyli części, którą kontrolujesz. Stosując strategie opisane w tym artykule, zbudujesz architekturę procesu, która zmniejsza opóźnienia i poprawia doświadczenie klienta. 

Aby dowiedzieć się więcej, poznaj omówienie 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 i Speech to Text w czasie rzeczywistym.

Gdy będziesz gotowy do tworzenia, załóż bezpłatne konto ElevenLabs i rozpocznij dziś strumieniowanie swojej pierwszej transkrypcji.

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

Autor

Tadas jest inżynierem ds. relacji z deweloperami i pomaga im tworzyć z pakietem produktów ElevenLabs. Wcześniej założył platformę edukacyjną dla deweloperów, był głównym rzecznikiem deweloperów w Agora, instruktorem w Zero To Mastery i zbudował społeczność na YouTube, ucząc deweloperów.

Podobne artykuły

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