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 (STT) w czasie rzeczywistym aktywnie transkrybuje audio podczas mówienia i zwraca słowa jako tekst w ciągu kilkuset milisekund. Utrzymanie niskich opóźnień STT to jednak w równym stopniu kwestia architektury, co modelu. Deweloperzy muszą zaplanować transport, wielkość chunków, wykrywanie końca wypowiedzi i ścieżkę przechwytywania — każdy z tych elementów zwiększa opóźnienie. Nieefektywność choćby jednego z nich może przekroczyć budżet 200 ms.

Ten przewodnik pokazuje praktyczny sposób budowania pipeline’ów zamiany mowy na tekst w czasie rzeczywistym — od warstwy transportu. Skupimy się na Scribe v2 Realtime, który generuje 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 oferuje Voice Activity Detection i ręczne zatwierdzanie finalizacji segmentów.

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

Podsumowanie

  • Budowa systemów zamiany mowy na tekst w czasie rzeczywistym wymaga dopracowania architektury, by opóźnienie pozostało niskie w całym pipeline’ie.
  • WebSocket to dobry domyślny wybór dla większości pipeline’ó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 pozwala aplikacji ją nadpisać, gdy wie, że wypowiedź się skończyła.
  • Częściowe wyniki są tymczasowe, a końcowe — zatwierdzone, więc należy wyświetlać je inaczej. 
  • Małe chunky PCM, około 100 ms, minimalizują opóźnienie do pierwszego częściowego wyniku.

WebSocket a WebRTC w zamianie mowy na tekst w czasie rzeczywistym

Zanim powstanie transkrypcja, audio musi dotrzeć ze źródła do rozpoznawacza. Wybrany kanał wyznacza minimalne opóźnienie dla wszystkiego, co następuje później. 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 po stronie klienta i serwera, przechodzi przez firmowe proxy i firewalle, które już zezwalają na HTTPS, a obsługuje go każda przeglądarka i środowisko serwerowe. 

Ograniczeniem WebSocket jest działanie przez TCP. Jeśli pakiet zginie, TCP przesyła go ponownie i wstrzymuje późniejsze dane, aż luka zostanie uzupełniona. W dobrych warunkach sieciowych jest to niezauważalne. Przy utracie pakietów powstaje blokowanie head-of-line: krótkie zatrzymanie, podczas którego audio się kumuluje, a potem dociera naraz.

WebRTC stworzono dla mediów w czasie rzeczywistym. Przesyła media przez UDP (za pomocą SRTP), więc utracony pakiet nie zatrzymuje strumienia — pipeline działa dalej. Obejmuje bufor jittera, który pochłania różnice w czasie docierania pakietów, negocjuje przechodzenie przez NAT z ICE/STUN/TURN, aby urządzenia 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 zakończyć strumień mediów zamiast odczytywać strumień bajtów.

Oto najważniejsze różnice:

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 i kontrolujesz ścieżkę przechwytywania: w pipeline’ach serwer-serwer, aplikacjach desktopowych, aplikacjach przeglądarkowych z dostępem szerokopasmowym oraz w większości backendów contact center, gdzie audio już trafia na serwer inną drogą. 

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 zachowanie w czasie rzeczywistym mimo strat jest ważniejsze niż prostota wdrożenia.

W dalszej części przewodnika używamy WebSocket do połączenia z rozpoznawaczem, ponieważ ułatwia to zobaczenie wszystkich elementów i stanowi dobry punkt wyjścia dla większości zespołów. Nic nie jest tu 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 chunky do pipeline’u.

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

Rozpoznawacz działający w czasie rzeczywistym nie czeka na całe zdanie. Zamiast tego wysyła ciąg bieżących przypuszczeń, które stają się dokładniejsze wraz z napływem 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 otrzymanego audio. Częściowe wyniki z założenia są niestabilne. Gdy dociera więcej audio, model poprawia wcześniejsze słowa: „Chcę” może zmienić się w „Chcę dwa bilety”, gdy późniejszy kontekst rozstrzygnie dwuznaczność. Pojawiają się szybko — to opisuje opóźnienie około 150 ms — i są przeznaczone do nadpisywania.

Końcowa hipoteza to zatwierdzony segment, który już się nie zmieni. Gdy segment zostanie sfinalizowany, 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 częściowych wyników sprawia, że transkrypcja wydaje się aktywna: użytkownik widzi słowa podczas mówienia, co potwierdza, że mikrofon działa, a system słucha.
  • Wykrywanie końca wypowiedzi: Częściowe wyniki dają ciągły sygnał aktywności mowy. W połączeniu z VAD pozwalają określić, kiedy rozmówca naprawdę skończył mówić.
  • Czas dalszego przetwarzania: W pipeline’ie agenta głosowego kolejność wygląda tak: audio wejściowe, zamiana mowy na tekst, LLM, zamiana tekstu na mowę, a następnie audio wyjściowe. Możesz rozpocząć spekulacyjne przetwarzanie na wynikach częściowych i potwierdzać je po wynikach końcowych, skracając odczuwalny czas odpowiedzi kosztem sporadycznego odrzucania takiej 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 można tylko dopisywać, 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świetl 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. Rozpoznawacz musi też wiedzieć, kiedy myśl się skończyła. Od tej decyzji zależy, kiedy finalizujesz segment i kiedy agent zaczyna odpowiadać. 

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, mimo że użytkownik wyraźnie skończył.

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

  • Voice Activity Detection segmentuje audio na podstawie ciszy: Rozpoznawacz wykrywa, kiedy mowa przechodzi w dłuższą ciszę, i automatycznie finalizuje segment na tej granicy. 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, 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 puszczeniu przycisku push-to-talk, akcji „wyślij” lub zgodnie z zewnętrzną polityką zmiany tury.

Oba mechanizmy dobrze działają razem. Typowy agent głosowy używa VAD do obsługi bez użycia rąk i udostępnia ręczne zatwierdzanie jako nadpisanie. Dzięki temu użytkownik, który robi pauzę, by pomyśleć, nie zostanie przerwany, a użytkownik naciskający przycisk od razu wyznaczy granicę.

Próg ciszy to realny kompromis bez jednej uniwersalnie właściwej wartości:

  • Krótki limit czasu po zakończeniu mowy — na przykład finalizacja po około 200–400 ms ciszy — sprawia, że system reaguje szybko. Może jednak przerywać użytkownikom, którzy naturalnie robią pauzy między zdaniami podrzędnymi, dzieląc jedną myśl na kilka segmentów i wywołując przedwczesną odpowiedź agenta.
  • Długi limit czasu — na przykład około 800–1200 ms — toleruje naturalne pauzy i zachowuje wypowiedzi w całości, kosztem zauważalnego opóźnienia reakcji systemu.

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

  • Dyktowanie i robienie notatek tolerują dłuższe pauzy, bo użytkownicy myślą w środku zdania. Ustaw dłuższy limit i polegaj na VAD.
  • Agenci do poleceń i transakcji korzystają z krótszych limitów oraz ręcznego zatwierdzania, ponieważ tury są krótkie i konkretne.
  • Osoby wielojęzyczne lub mówiące w obcym języku 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 potrafi więcej niż generować słowa. Każdy dodatkowy sygnał wpływa jednak na opóźnienie i stabilność. Włączaj tylko to, czego potrzebuje działanie na żywo, a resztę odłóż do przetwarzania wsadowego.

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

Diarizacja mówców przypisuje mowę różnym osobom, określając, kto co powiedział. W transkrypcji wsadowej jest to stosunkowo proste, ponieważ model widzi cały plik. W strumieniu jest trudniej: rozpoznawacz musi przypisać etykietę mówcy wyłącznie na podstawie dotychczasowego audio, a etykieta nadana wcześniejszemu audio może wymagać zmiany 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 aż do finalizacji segmentu.

Czasy na poziomie słów i kontekst encji działają na tej samej zasadzie. Im więcej metadanych na token zamawiasz, tym więcej musi przenieść zarówno model, jak i 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ć do wsadowego przetwarzania po rozmowie w Scribe v2.

Formaty audio do streamingu: PCM i mu-law

Transport i logika rozpoznawania przyciągają najwięcej uwagi, ale zaskakująco wiele błędów w praktyce powstaje poziom niżej — przy kodowaniu i dzieleniu audio na chunky. Poprawny format i rozmiar chunków 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żyj, 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 odpowiada jakości telefonicznej i traci treści o wysokich częstotliwościach. Użyj częstotliwości zgodnej ze źródłem. Zwiększanie próbkowania audio telefonicznego z 8 kHz do 48 kHz nic nie daje, ponieważ nie da się odzyskać brakujących informacji.

Mu-law przy 8 kHz to format telefonii. Jeśli odbierasz połączenia od dostawcy takiego jak Twilio, audio dociera jako mu-law 8 kHz i należy przekazywać je w tym formacie, zamiast dwukrotnie transkodować. Dopasowanie formatu źródłowego zapobiega artefaktom resamplingu i zbędnej konwersji.

Rozmiar chunków najsilniej wpływa na odczuwalne opóźnienie. Wysyłasz audio w chunkach, a rozpoznawacz tworzy wyniki częściowe, gdy one docierają. Mniejsze chunky oznaczają częstsze aktualizacje i mniejsze opóźnienie do pierwszego wyniku częściowego; większe oznaczają mniej wiadomości i nieco więcej kontekstu dla każdej inferencji. Praktyczny zakres to 20–250 ms audio na chunk. Dla porównania: przy mono PCM 16-bit i 16 kHz sekunda audio ma 32 000 bajtów, więc chunk 100 ms ma około 3200 bajtów.

Przechwytywanie sygnału z mikrofonu w przeglądarce

W przeglądarce najlepszym 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 podatny na zacięcia głównego wątku, jak starszy ScriptProcessorNode. Jego zadaniem jest przekształcenie 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.

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

Pipeline w kodzie

Pipeline ma trzy elementy: klienta przeglądarkowego, który przechwytuje mikrofon i streamuje PCM do serwera, serwer Node przekazujący audio do Scribe v2 Realtime i odsyłający transkrypcje oraz skryptowalnego klienta streamującego PCM z pliku lub mostka telefonicznego.

Serwer pośredniczy, zamiast wystawiać rozpoznawacz bezpośrednio przeglądarce, z jednego ważnego powodu: klucz API ElevenLabs jest sekretem i nigdy nie może pojawić się w kodzie po stronie klienta. Klucz przechowuje serwer. Jeśli przeglądarka musi komunikować się z rozpoznawaczem bezpośrednio, utwórz po stronie serwera krótko ważny token jednorazowy i przekaż go klientowi zamiast klucza API.

Klient przeglądarkowy

Klient otwiera WebSocket do serwera, przechwytuje mikrofon przez opisany wyżej worklet i przekazuje każdą ramkę PCM od razu po jej utworzeniu. Przychodzące zdarzenia — 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, przechowuje 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 pipeline’u 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 pipeline’ach backendowych i w poniższym benchmarku to samo połączenie z rozpoznawaczem działa bez przeglądarki: odczytuj PCM z dowolnego źródła, wysyłaj go w tempie chunkó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`);
});

Benchmarking 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, ś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ę na środowisko produkcyjne w innym. Uruchamiaj testy z infrastruktury podobnej do produkcyjnej, na audio podobnym do rzeczywistego wejścia, i raportuj zakresy oraz rozkłady zamiast pojedynczych liczb.

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

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

Poniżej znajdziesz główne metryki do pomiaru podczas benchmarkingu opóźnienia zamiany mowy na tekst w czasie rzeczywistym:

  • Czas do pierwszego wyniku częściowego: Od wysłania pierwszego chunka audio do otrzymania pierwszego niepustego wyniku częściowego.
  • Opóźnienie od częściowego do końcowego: Od ostatniego chunka 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, liczony identycznie we wszystkich systemach.
  • Zmiany stabilności: Ile wyników częściowych zostało nadpisanych przed finalizacją. To wskaźnik tego, jak bardzo będzie się zmieniać interfejs na żywo.

Kontrole

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

Oto najważniejsze kontrole w benchmarkingu 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 w każdym systemie.
  • Identyczne tempo: Streamuj do każdego systemu chunky w tym samym tempie czasu rzeczywistego, na przykład co 100 ms.
  • Powtarzaj i raportuj rozkłady: Uruchamiaj każdy plik wielokrotnie w ciągu dnia; raportuj medianę i wartości ogonowe (p50/p95).
  • Identyczne referencje i ocena: Normalizuj tekst w ten sam sposób — wielkość liter, interpunkcję i liczby — przed obliczeniem WER.
  • Podaj region i sieć: Wskaż, gdzie działał test i jaką ścieżkę miał do każdego dostawcy.

Zachowując wszystkie te elementy bez zmian, 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 identycznie zamień tekst referencyjny i hipotezę na małe litery oraz usuń interpunkcję, inaczej zmierzysz normalizator, a nie model. Uruchamiaj ten pomiar w pętli około 10 razy dla każdego pliku i dostawcy, raportując medianę czasu do pierwszego wyniku częściowego oraz medianę WER (p50/p95), ponieważ pojedynczą próbę silnie zaburzają wahania sieciowe.

Aby to uruchomić, potrzebujesz dwóch rzeczy. Po pierwsze, napisz jeden adapter StreamFn dla każdego systemu. Skryptowalny klient powyżej już nim jest; adaptery dla pozostałych powinny stosować ten sam kontrakt (audioPath, onEvent, result) i ustawiać result.lastChunkSentAt po wysłaniu ostatniego chunka audio. Po drugie, wczytaj pliki audio i referencje oraz wywołaj pomiary dla nich wszystkich. Uruchom to na maszynie reprezentującej twoje wdrożenie i na audio reprezentującym użytkowników, a otrzymasz porównanie, które da się odtworzyć.

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 dla prostoty i kontrolowanych sieci, a WebRTC, gdy potrzebujesz odporności na straty 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 nadpisania i dostrajaj próg ciszy do interakcji, zamiast używać stałej wartości.
  • Funkcje w strumieniu: Włączaj funkcje w strumieniu tylko tam, gdzie wymaga tego działanie na żywo, a resztę odłóż do przetwarzania wsadowego w Scribe v2.
  • Format audio: Przechwytuj małe ramki PCM, wysyłaj chunky około 100 ms i dopasuj format źródłowy dla telefonii.
  • Benchmarking: Empirycznie dobieraj parametry dokładności i opóźnienia na podstawie własnego audio oraz docelowej metryki.
  • Zabezpieczenia API: Trzymaj klucz API na serwerze, albo twórz tokeny jednorazowe dla bezpośrednich połączeń klientów.

Jeśli chcesz zobaczyć, jak zoptymalizować opóźnienie agenta głosowego, przygotowaliśmy też poradnik na ten temat.

Buduj systemy zamiany mowy na tekst w czasie rzeczywistym z Scribe v2 Realtime

Scribe v2 Realtime generuje wyniki częściowe przy opóźnieniu modelu wynoszącym około 150 ms. To, czy użytkownicy odczują właśnie taką wartość, czy większą, zależy od otaczającej model architektury — czyli części, którą kontrolujesz. Stosując strategie z tego artykułu, zbudujesz pipeline o niższym opóźnieniu i lepszym doświadczeniu klienta. 

Aby dowiedzieć się więcej, zobacz omówienie możliwości Speech to Text, przeczytaj nasze informacje o modelach 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 zechcesz zacząć tworzyć, utwórz bezpłatne konto ElevenLabs i rozpocznij streaming pierwszej transkrypcji jeszcze dziś.

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

Podobne artykuły

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