Wróć do bloga
19 października 2026Sergei Solod20 min czytania

Jak dodać prywatne rozpoznawanie mowy na stronie internetowej za pomocą Whisper i WebAssembly

Zbuduj w pełni działające po stronie przeglądarki wprowadzanie głosowe z użyciem Whisper, Transformers.js, ONNX Runtime Web, AudioWorklet i WebAssembly. Dowiedz się, jak zoptymalizować działanie na urządzeniach mobilnych i naprawić błędy środowiska uruchomieniowego, workerów oraz tokenizatora, które uniemożliwiają rozpoznawanie mowy.

Zamiana mowy na tekstWhisperWebAssemblyAudioWorkletJavaScript

Dodanie przycisku mikrofonu do strony internetowej wydaje się proste: wystarczy przechwycić głos użytkownika, rozpoznać słowa i wstawić je do pola tekstowego. W praktyce niezawodne działanie tej funkcji w przeglądarkach na komputerach i urządzeniach mobilnych jest znacznie trudniejsze, szczególnie gdy całe rozpoznawanie mowy musi odbywać się lokalnie, bez wysyłania dźwięku z mikrofonu na serwer.

Trudność nie sprowadza się do wyboru modelu rozpoznawania mowy. Działająca implementacja musi prawidłowo obsługiwać uprawnienia do mikrofonu, przechwytywanie dźwięku, zmianę częstotliwości próbkowania, workery przeglądarki, WebAssembly, inicjalizację modeli ONNX, tokenizację, pamięć podręczną oraz integrację z polem tekstowym aplikacji.

W artykule pokazano, jak zbudować praktyczną funkcję zamiany mowy na tekst, która chroni prywatność, wykorzystując OpenAI Whisper, Transformers.js, ONNX Runtime Web, AudioWorklet oraz osobny Web Worker. Omówiono też kilka rzeczywistych błędów integracyjnych, przez które przycisk mikrofonu może utknąć w stanie ładowania, mimo że pliki modelu zostały już pomyślnie pobrane.

Cel jest prosty: odwiedzający klika przycisk mikrofonu, mówi naturalnie i widzi rozpoznany tekst w istniejącym polu wprowadzania danych. Przed wysłaniem może go sprawdzić i poprawić. Samo nagranie z mikrofonu nie musi opuszczać przeglądarki.

Co oznacza rozpoznawanie mowy w całości po stronie przeglądarki

Istnieją dwa zasadniczo różne sposoby dodania wprowadzania głosowego do strony internetowej.

Pierwszy wykorzystuje usługę rozpoznawania mowy. Przeglądarka przechwytuje dźwięk, wysyła go do zdalnej usługi i otrzymuje rozpoznany tekst. Może to uprościć implementację, ale wprowadza zależność od sieci, kwestie prywatności i dostępności dostawcy, a także potencjalne opłaty zależne od wykorzystania.

Drugi sposób polega na wykonywaniu inferencji bezpośrednio w przeglądarce. Witryna pobiera model rozpoznawania mowy oraz środowisko potrzebne do jego uruchomienia. Przeglądarka przechwytuje dźwięk z mikrofonu, zamienia go na próbki liczbowe, uruchamia model lokalnie i tworzy transkrypcję bez serwera rozpoznawania mowy.

Ten artykuł dotyczy wyłącznie drugiego rozwiązania.

Nie należy mylić zwykłego rozpoznawania mowy w przeglądarce z rozpoznawaniem, które na pewno działa lokalnie. Web Speech API może korzystać z silnika rozpoznawania po stronie serwera. Niektóre nowsze implementacje udostępniają opcję processLocally, ale jej obsługa zależy od przeglądarki i systemu operacyjnego. Jeśli wymagane jest przetwarzanie wyłącznie lokalne, ogólny mechanizm SpeechRecognition nie może służyć jako automatyczne rozwiązanie zapasowe.

Zamiast tego aplikacja samodzielnie kontroluje cały proces inferencji.

Microphone
    |
    v
AudioWorklet
    |
    v
Float32 PCM audio
    |
    v
Dedicated Web Worker
    |
    v
16 kHz resampling
    |
    v
Whisper + ONNX Runtime Web
    |
    v
Tokenizer and text decoding
    |
    v
React input state

Witryna nadal udostępnia JavaScript, WebAssembly, pliki konfiguracyjne i wagi modelu. Są to zwykłe pobrania plików statycznych. Istotna różnica polega na tym, że dźwięk z mikrofonu jest przetwarzany lokalnie, zamiast trafiać na serwer w celu rozpoznania. Jeśli użytkownik później wyśle otrzymany tekst przez zwykły formularz, jest to osobne działanie aplikacji.

Wybór praktycznego modelu: Whisper Tiny czy Base

Whisper to rodzina wielojęzycznych modeli rozpoznawania mowy. Możliwość rozpoznawania wielu języków przez jeden model jest szczególnie przydatna w wielojęzycznych witrynach.

W przypadku krótkich wiadomości głosowych dwa modele zapewniają rozsądny kompromis między jakością rozpoznawania a zużyciem zasobów przeglądarki: Whisper Tiny i Whisper Base.

ModelPrzybliżony rozmiar wybranych wag ONNX Q8Zalecane zastosowanie
Whisper Tiny41 MBOstrożny wybór dla urządzeń mobilnych i o ograniczonych zasobach
Whisper Base77 MBWyższa jakość na sprawdzonych, odpowiednio wydajnych urządzeniach

Podane rozmiary odnoszą się do wybranych plików kodera i dekodera ONNX w analizowanych eksportach modeli. Nie obejmują wszystkich plików pomocniczych i nie należy ich utożsamiać ze szczytowym zużyciem pamięci przez przeglądarkę.

Whisper Tiny jest atrakcyjny, ponieważ pobiera się szybciej, zwykle wymaga mniej mocy obliczeniowej i lepiej nadaje się do nieprzewidywalnych warunków na urządzeniach mobilnych. Whisper Base na ogół zapewnia lepszą jakość rozpoznawania, zwłaszcza w niektórych językach, w których mniejsze modele popełniają więcej błędów.

Nie należy wybierać modelu wyłącznie na podstawie ilości fizycznej pamięci RAM. Przeglądarka może dysponować znacznie mniejszą ilością pamięci, niż sugeruje specyfikacja urządzenia. Liczą się też pamięć zajęta przez stronę, alokacje WebAssembly, tymczasowe tensory i presja na pamięć ze strony systemu operacyjnego.

Ostrożne podejście zakłada rozpoczęcie od Tiny na urządzeniach mobilnych o nieznanych możliwościach, zarezerwowanie Base dla odpowiednio wydajnych klas urządzeń i przełączenie z Base na Tiny, jeśli inicjalizacja modelu się nie powiedzie.

Przed załadowaniem Tiny trzeba zwolnić zasoby nieudanego egzemplarza Base. Pozostawienie obu modeli w pamięci pozbawia takie przełączenie sensu.

Dlaczego WebAssembly jest rozsądnym wyborem domyślnym w środowisku produkcyjnym

ONNX Runtime Web pozwala uruchamiać sieci neuronowe w przeglądarkach. W tym zastosowaniu WebAssembly zapewnia praktyczną ścieżkę inferencji na CPU w najważniejszych przeglądarkach desktopowych i mobilnych.

WebGPU może przyspieszyć obsługiwane modele na odpowiednim sprzęcie, ale sama jego dostępność nie oznacza, że rozpoznawanie mowy będzie działać. Udostępnienie przez przeglądarkę navigator.gpu nie gwarantuje ani obsługi danej platformy przez ONNX Runtime, ani prawidłowej inicjalizacji konkretnego grafu Whisper, ani braku problemów z zasobami podczas kolejnych inferencji.

Dlatego ostrożna implementacja może wykorzystywać WASM jako produkcyjny mechanizm wykonywania modeli, a WebGPU oceniać osobno po testach zgodności, dokładności, pamięci i powtarzanych sesji.

W początkowej implementacji jednowątkowy WASM pozwala też uniknąć uzależnienia całej witryny od izolacji między źródłami i SharedArrayBuffer.

env.backends.onnx.wasm.numThreads = 1;
env.backends.onnx.wasm.proxy = false;

Wykonywanie inferencji w osobnym workerze przenosi kosztowne obliczenia poza główny wątek JavaScript. Pole tekstowe, klawiatura ekranowa, przewijanie i interfejs aplikacji pozostają responsywne, gdy model przetwarza dźwięk.

Nie ładuj rozpoznawania mowy podczas pierwszego otwarcia strony

Funkcja mikrofonu nie powinna generować znacznych kosztów sieciowych ani pamięciowych u odwiedzających, którzy nigdy z niej nie korzystają.

Nie importuj Transformers.js z wyprzedzeniem do początkowego pakietu aplikacji. Nie inicjalizuj ONNX Runtime podczas otwierania strony i nie pobieraj automatycznie wag modelu w czasie bezczynności.

Zamiast tego ładuj komponenty rozpoznawania dopiero po wyraźnym kliknięciu przycisku mikrofonu przez użytkownika.

Przy pierwszym użyciu sprawdzi się następująca kolejność:

  1. Obsłuż kliknięcie przycisku mikrofonu.
  2. Poproś o dostęp do mikrofonu przez getUserMedia().
  3. Jeśli użytkownik odmówi, pokaż błąd bez pobierania modelu.
  4. Jeśli użytkownik zezwoli, zwolnij tymczasowy strumień mikrofonu na czas przygotowania modelu.
  5. Dynamicznie uruchom osobny worker i załaduj wybrany model lokalny.
  6. Poczekaj, aż worker potwierdzi gotowość całego procesu rozpoznawania.
  7. Automatycznie rozpocznij nagrywanie, jeśli użytkownik nie anulował operacji.
  8. Wyświetl zrozumiały stan nasłuchiwania.

Prośba o uprawnienie musi wynikać z interakcji użytkownika. Aplikacja powinna też obsługiwać anulowanie oraz ograniczenia przechwytywania dźwięku specyficzne dla przeglądarki, zamiast zakładać, że nagrywanie zawsze da się ponownie uruchomić bez dodatkowych działań.

Przygotowywanie modelu powinno mieć widoczny stan, na przykład Przygotowywanie wprowadzania głosowego. Sam wskaźnik ładowania bez wyraźnej zmiany stanu może sprawić, że nawet udane pobieranie modelu będzie wyglądało jak awaria mikrofonu.

Po inicjalizacji worker może przez krótki czas pozostać gotowy na kolejną wypowiedź. Limit bezczynności wynoszący około minuty to rozsądny punkt wyjścia. Po tym czasie należy zakończyć worker i zwolnić model z pamięci. Pliki modelu zapisane w pamięci podręcznej przeglądarki mogą pozostać dostępne przy kolejnych wizytach.

Przechwytywanie surowego dźwięku przez AudioWorklet

Whisper wymaga liczbowych próbek dźwięku. MediaRecorder świetnie nadaje się do nagrywania plików audio, ale w przypadku lokalnego rozpoznawania mowy może wprowadzać niepotrzebny etap kodowania i dekodowania.

Prostsza ścieżka przetwarzania wygląda tak:

getUserMedia()
    |
    v
AudioContext
    |
    v
AudioWorklet
    |
    v
Float32 PCM buffers
    |
    v
Dedicated Worker

AudioWorklet wykonuje kod przetwarzający dźwięk poza głównym wątkiem JavaScript aplikacji. Może przechwytywać próbki PCM, sprowadzać dźwięk wielokanałowy do mono, obliczać proste miary głośności i przekazywać zgromadzone bufory do workera.

Uproszczona inicjalizacja mikrofonu wygląda następująco:

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

const context = new AudioContext();

await context.audioWorklet.addModule(
  '/assets/speech/pcm-worklet.js'
);

const source = context.createMediaStreamSource(stream);
const processor = new AudioWorkletNode(
  context,
  'pcm-capture'
);

source.connect(processor);

// Connect the worklet through an inaudible output path
// if required to keep the audio processing graph active.

Ograniczenia podane w konfiguracji mikrofonu są preferencjami, a nie gwarancjami. Przeglądarki mogą używać różnych algorytmów usuwania echa, redukcji szumów i automatycznej regulacji wzmocnienia.

Ważny szczegół: częstotliwość próbkowania mikrofonu nie musi wynosić 16 kHz. Wiele kontekstów audio pracuje z częstotliwością 44,1 kHz lub 48 kHz. Whisper oczekuje sygnału 16 kHz, dlatego przechwycone próbki wymagają prawidłowej zmiany częstotliwości próbkowania.

Samo odrzucanie co drugiej lub trzeciej próbki nie jest ogólnie poprawnym rozwiązaniem. Odpowiedni resampler z filtrem dolnoprzepustowym, na przykład oparty na funkcji sinc z oknem, ogranicza aliasing i zachowuje istotne częstotliwości mowy podczas zmniejszania częstotliwości próbkowania.

Zmianę częstotliwości próbkowania należy wykonywać w osobnym workerze, a nie w działającym w czasie rzeczywistym callbacku AudioWorklet.

Wydajne przekazywanie dźwięku między wątkami przeglądarki

AudioWorklet może generować wiele małych fragmentów dźwięku. Przesyłanie każdego kwantu renderowania audio jako osobnej wiadomości może powodować zbędny narzut.

Praktyczna metoda polega na gromadzeniu krótkich partii PCM i przekazywaniu każdej z nich jako ArrayBuffer.

worker.postMessage(
  {
    type: 'audio-chunk',
    buffer,
  },
  [buffer]
);

Lista obiektów przekazywanych ma znaczenie: przenosi własność ArrayBuffer zamiast wykonywać niepotrzebną pełną kopię.

Worker może dopisywać te fragmenty do bufora zbiorczego i po zakończeniu nagrywania połączyć je w całość.

Warto również śledzić rzeczywistą częstotliwość próbkowania przy przechwytywaniu, liczbę zebranych próbek i czas trwania nagrania. Takie metadane pomagają diagnozować ciche nagrania bez zapisywania w dziennikach poufnych danych audio.

Wykrywanie pauz bez kolejnej sieci neuronowej

Dyktowanie krótkich wiadomości tekstowych nie musi wymagać neuronowego detektora aktywności głosowej.

Adaptacyjny detektor RMS może zapewnić lekkie, przybliżone wykrywanie aktywności mowy. RMS mierzy amplitudę sygnału, dzięki czemu aplikacja może ocenić, kiedy wcześniej aktywny mówca zamilkł.

Interfejs powinien zawsze umożliwiać ręczne zatrzymanie nagrywania przez ponowne kliknięcie przycisku mikrofonu. Automatyczne wykrywanie ciszy jest udogodnieniem, a nie warunkiem poprawnego rozpoznawania.

Rozsądne wartości początkowe dla krótkiego dyktowania to:

ParametrWartość początkowa
Odstęp analizy20 ms
Kalibracja szumu300 ms
Potwierdzenie mowy140 ms
Cisza przed automatycznym zatrzymaniem1,4 sekundy
Limit czasu bez mowy8 sekund
Maksymalny czas nagrywania30 sekund

To wartości wyjściowe do konfiguracji, a nie uniwersalne optymalne progi. Cicha mowa, muzyka w tle i przetwarzanie szumów przez mikrofon mogą wpływać na pomiary RMS.

Lepiej umożliwić użytkownikowi samodzielne zatrzymanie nagrywania, niż automatycznie ucinać ostatnie słowa zdania.

W przypadku zwykłego wprowadzania krótkich tekstów jednorazowa transkrypcja jest zazwyczaj prostsza niż wielokrotne uruchamianie Whisper w celu uzyskania wyników pośrednich. Nagraj wypowiedź, zatrzymaj nagrywanie, uruchom model jeden raz i wstaw końcowy tekst.

Pełny proces Whisper wymaga czegoś więcej niż model ONNX

To jeden z najważniejszych szczegółów implementacyjnych.

Inferencja Whisper nie polega wyłącznie na wywołaniu kodera ONNX, a następnie dekodera ONNX. Pełny proces automatycznego rozpoznawania mowy wymaga również ekstrakcji cech audio, tokenizacji, konfiguracji generowania i dekodowania przewidzianych tokenów do czytelnego tekstu.

Cały łańcuch przetwarzania wygląda tak:

Raw microphone PCM
        |
        v
Resampling to 16 kHz
        |
        v
Whisper feature extractor
        |
        v
ONNX encoder
        |
        v
ONNX decoder and token generation
        |
        v
Whisper tokenizer
        |
        v
Recognized text

Udana inicjalizacja sesji ONNX dowodzi jedynie, że graf modelu można załadować. Nie dowodzi, że cały proces rozpoznawania działa.

W jednej z testowanych integracji Transformers.js sesje modeli inicjalizowały się poprawnie, ale transkrypcja nadal nie działała, ponieważ lokalnie utworzony proces nie miał prawidłowo skonfigurowanego procesora i tokenizatora.

Zgodna implementacja jawnie inicjalizowała te komponenty na podstawie lokalnych metadanych modelu.

Poniższy skrócony przykład przedstawia to podejście zależne od wersji; nie jest kompletnym, samodzielnym workerem:

import {
  WhisperTokenizer,
  WhisperFeatureExtractor,
  WhisperProcessor,
} from '@huggingface/transformers';

// These values come from local model configuration files.
const tokenizer = new WhisperTokenizer(
  tokenizerJson,
  tokenizerConfig
);

const featureExtractor = new WhisperFeatureExtractor(
  preprocessorConfig
);

const processor = new WhisperProcessor(
  {},
  {
    tokenizer,
    feature_extractor: featureExtractor,
  },
  ''
);

// In the affected version-specific integration:
recognizer.tokenizer = tokenizer;
recognizer.processor = processor;

Dokładne sygnatury konstruktorów i sposób integracji należy sprawdzić dla zainstalowanej wersji Transformers.js. Niektóre wersje i eksporty modeli tworzą te komponenty automatycznie; ręcznej inicjalizacji nie należy traktować jako uniwersalnego wymogu.

Gdy cały proces jest gotowy, transkrypcję krótkiej wypowiedzi można uruchomić, jawnie podając język i zadanie:

const result = await recognizer(audio16k, {
  language: 'en',
  task: 'transcribe',
  return_timestamps: false,
});

Jeśli aplikacja zna już język użytkownika, jego jawne wskazanie pozwala uniknąć zbędnego wykrywania języka i może zwiększyć przewidywalność rozpoznawania krótkich nagrań.

Przypinaj zgodne wersje Transformers.js i ONNX Runtime

Drugim istotnym źródłem błędów jest współpraca Transformers.js z ONNX Runtime Web.

Transformers.js udostępnia wysokopoziomowy proces rozpoznawania, a ONNX Runtime Web wykonuje grafy sieci neuronowych. Pakiety te muszą być zgodne zarówno ze sobą, jak i z konkretnymi eksportami modeli ONNX.

W badanej implementacji rozwojowa wersja ONNX Runtime Web inicjalizowała koder Whisper, lecz podczas tworzenia sesji skwantyzowanego dekodera występował błąd. Dotyczył on brakującej skali kwantyzacji w operacji DequantizeLinear.

Nie był to problem z uprawnieniami mikrofonu, pobieraniem ani formatem audio. Przeglądarka otrzymywała pliki modelu, ale środowisko uruchomieniowe nie potrafiło zainicjalizować jednego z wymaganych grafów.

Bezpośrednie przetestowanie kodera i dekodera przy użyciu stabilnej, przypiętej wersji ONNX Runtime Web rozwiązało tę konkretną niezgodność.

Sprawdzenie zgodności było szczególnie cenne, ponieważ pozwoliło rozdzielić dwa różne stwierdzenia:

  • Plik modelu został pomyślnie pobrany.
  • Środowisko uruchomieniowe pomyślnie utworzyło na podstawie pliku modelu sesję nadającą się do inferencji.

To nie jest to samo.

Dla testowanych eksportów modeli zgodna okazała się para Transformers.js 4.2.0 i ONNX Runtime Web 1.24.3. Nowsza rozwojowa wersja środowiska kończyła się błędem na grafie skwantyzowanego dekodera. Jest to obserwacja dotycząca konkretnych wersji, a nie dowód, że starsze środowiska są zawsze lepsze.

Jeśli nadpisujesz wersję pośredniej zależności środowiska uruchomieniowego, przypnij dokładną wersję w pliku blokady zależności i udokumentuj powód. Aktualizacja środowiska powinna wymagać pełnego testu regresyjnego inferencji w przeglądarce, a nie wyłącznie udanej instalacji pakietu.

Przed debugowaniem Whisper sprawdź format pakietu workera

JavaScript Web Worker można utworzyć w różnych formatach modułów.

Jedna z usterek integracyjnych wynikała z tego, że aplikacja uruchamiała worker jako moduł ES, podczas gdy wygenerowany pakiet workera korzystał z mechanizmu klasycznych workerów importScripts().

Tych modeli wykonywania nie można stosować zamiennie. Worker modułowy nie może po prostu wykonać kodu przeznaczonego wyłącznie dla klasycznego workera.

Praktyczny wniosek: konstruktor Worker musi odpowiadać faktycznemu formatowi wygenerowanego pakietu, zamiast kopiować opcję konstruktora z ogólnego poradnika.

Na przykład poniższe warianty nie muszą być równoważne:

// Module worker: requires a compatible module bundle.
new Worker(workerUrl, { type: 'module' });

// Classic worker: requires a compatible classic bundle.
new Worker(workerUrl);

Właściwy wybór zależy od bundlera i wygenerowanego kodu JavaScript.

Diagnozując błąd inicjalizacji workera, sprawdź konsolę przeglądarki, wygenerowany plik workera i panel Network. Ustal, czy problem pojawia się jeszcze przed rozpoczęciem inicjalizacji ONNX Runtime.

Niektóre biblioteki przeglądarkowe po zbudowaniu pakietu wyznaczają też adresy URL względem lokalizacji własnego modułu. Gdy pakujesz je do workerów, sprawdzaj powstałe ścieżki do zasobów środowiska, zamiast zakładać, że pierwotne adresy względne pakietu nadal są poprawne.

Serwuj WASM i moduły JavaScript z właściwymi typami MIME

Nawet całkowicie zgodne pliki JavaScript i modele ONNX mogą nie działać, jeśli serwer WWW zwraca niewłaściwe nagłówki odpowiedzi.

Konkretny błąd dotyczył modułu JavaScript z rozszerzeniem .mjs, który był serwowany jako application/octet-stream. Przeglądarka odrzucała moduł, ponieważ typ odpowiedzi nie był odpowiedni do wykonywania modułów JavaScript.

Zwykle oczekiwane są następujące typy odpowiedzi:

Rozszerzenie plikuContent-Type
.mjsapplication/javascript
.wasmapplication/wasm

Sprawdzaj rzeczywistą odpowiedź serwera produkcyjnego, a nie tylko plik jego konfiguracji.

curl -I https://example.com/runtime/speech/v2/runtime.mjs
curl -I https://example.com/runtime/speech/v2/runtime.wasm

Podane adresy URL są przykładowe. Użyj rzeczywistych, wersjonowanych nazw plików zasobów wybranej wersji środowiska.

Sam prawidłowy status HTTP nie wystarczy. Przeglądarka może nadal odrzucić plik ze względu na typ MIME, politykę bezpieczeństwa treści (Content Security Policy), tryb wykonywania modułów lub błędny adres URL zasobu.

Pamiętaj, że to samo żądanie może działać po bezpośrednim otwarciu w karcie przeglądarki, ale nie działać przy ładowaniu jako wykonywalny moduł lub zależność WebAssembly.

Stosuj niezmienne adresy URL zasobów, nie blokując użytkowników na uszkodzonych plikach środowiska

Pliki modeli Whisper są na tyle duże, że buforowanie w przeglądarce ma duże znaczenie.

Wersjonowane, niezmienne ścieżki zasobów pozwalają przeglądarce ponownie używać niezmienionych plików modelu i środowiska bez pobierania ich za każdym razem, gdy użytkownik mówi.

Typowa polityka buforowania to:

Cache-Control: public, max-age=31536000, immutable

Wynika z tego jednak wymaganie wdrożeniowe: po oznaczeniu adresu URL jako niezmiennego nie należy po cichu podmieniać jego zawartości.

Jeśli plik binarny środowiska musi się zmienić, opublikuj go pod nowym, wersjonowanym adresem URL.

/models/speech/v1/
/runtime/speech/v1/
/runtime/speech/v2/

Zmiana ścieżki środowiska nie jest kosmetyczna. Po ponownym użyciu niezmiennego adresu URL ze zmienioną zawartością przeglądarka może nadal korzystać z wcześniej zbuforowanego, niezgodnego pliku binarnego WebAssembly.

Warto również zachować pliki pomocniczego kodu JavaScript i WebAssembly z tej samej dystrybucji środowiska. Mieszanie plików różnych wersji może powodować błędy inicjalizacji trudne do zdiagnozowania.

Manifest generowany podczas budowania, zawierający oczekiwane pliki, ich rozmiary i kryptograficzne sumy kontrolne, może wykryć brakujące lub niezgodne zasoby jeszcze przed wdrożeniem.

Skonfiguruj bibliotekę rozpoznawania tak, aby korzystała z zasobów hostowanych we własnej witrynie, i wyłącz zdalne pobieranie modeli. Brak lokalnego modelu spowoduje wtedy jawny błąd, zamiast uruchamiać nieplanowane pobieranie z zewnętrznego serwera modeli.

Dlaczego mikrofon pokazuje ładowanie, choć nic nie działa

Typowy objaw podczas programowania: przycisk mikrofonu prosi o uprawnienie, wyświetla animację ładowania, a ostatecznie przechodzi w stan błędu, nie wstawiając żadnego tekstu.

Przeglądarka może nawet pokazywać, że mikrofon jest używany.

Nie dowodzi to jednak, że rozpoznawanie mowy działa. Dowodzi tylko, że w pewnym momencie udzielono dostępu do mikrofonu lub przechwytywanie było aktywne.

Kilka niezależnych problemów może dawać niemal identyczne objawy:

  1. Nieprawidłowy typ MIME modułu JavaScript. Przeglądarka pobiera moduł środowiska, ale odmawia jego wykonania.
  2. Niezgodny format uruchamiania workera. Skompilowany worker używa interfejsów API klasycznego workera, choć uruchomiono go jako modułowy, lub odwrotnie.
  3. Niezgodność dekodera ONNX. Środowisko tworzy sesję kodera, ale nie radzi sobie z grafem skwantyzowanego dekodera.
  4. Niekompletny proces rozpoznawania. Sesje ONNX istnieją, lecz tokenizator lub procesor audio nie jest prawidłowo zainicjalizowany.

Każdy z tych problemów trzeba diagnozować osobno. Naprawienie jednego może ujawnić następny.

Najważniejsza zasada debugowania jest taka: HTTP 200 nie oznacza udanej inicjalizacji modelu, a udana inicjalizacja modelu nie oznacza pomyślnej transkrypcji.

Nawet pomyślna transkrypcja nie dowodzi, że wynik trafił do właściwego pola tekstowego aplikacji.

Każda granica między etapami wymaga czytelnego sygnału diagnostycznego.

Monitorowanie działania bez zbierania prywatnych nagrań

Przydatne logi diagnostyczne nie wymagają rejestrowania tego, co mówi użytkownik.

Aplikacja może śledzić następujące stany techniczne:

permission-granted
worker-created
runtime-loading
runtime-ready
encoder-ready
decoder-ready
recording-started
pcm-received
recording-stopped
inference-started
inference-completed
result-inserted

W razie błędu zapisuj etap, stabilny techniczny kod błędu i metadane oczyszczone z danych wrażliwych.

Błąd inicjalizacji modelu powinien dać się odróżnić od odmowy dostępu do mikrofonu i pustego nagrania. Awaria workera powinna być widoczna podczas programowania, a nie ukryta przez ogólny mechanizm obsługi błędów.

Nie zapisuj w logach próbek z mikrofonu, rozpoznanych zdań, sekwencji tokenów ani innej treści wypowiedzi. Telemetria wydajności może obejmować przedziały czasu trwania, wybraną klasę modelu, czas inicjalizacji, czas inferencji oraz kategorie błędów.

Dzięki temu znacznie łatwiej diagnozować niesprawny mikrofon bez osłabiania wymagań dotyczących prywatności.

Wstawianie transkrypcji przez stan React

Rozpoznawanie mowy ma sens tylko wtedy, gdy końcowy tekst trafia do istniejącego pola formularza.

W aplikacji React pole to często jest kontrolowane przez stan komponentu. Bezpośrednie przypisanie wartości do input.value może kolidować z mechanizmem renderowania frameworka.

Zamiast tego rozpoznany tekst powinien przechodzić przez tę samą ścieżkę aktualizacji stanu co zwykłe wpisywanie z klawiatury.

Integracja musi też uwzględniać asynchroniczność. Rozpoznawanie może się skończyć dopiero wtedy, gdy użytkownik dopisał już znaki lub przesunął kursor.

Użycie nieaktualnej wartości pola zapamiętanej w chwili rozpoczęcia nagrywania może przypadkowo nadpisać nowsze zmiany.

Uproszczony wzorzec integracji wygląda tak:

const insertion = insertTranscript({
  currentValue: latestValue,
  transcript: recognizedText,
  selection: currentSelection,
  maxLength: inputLimit,
});

setMessage(insertion.value);
restoreCaret(insertion.caret);

Funkcja wstawiająca tekst powinna zachowywać bieżącą zawartość, w razie potrzeby zastępować zaznaczony zakres, odpowiednio dobierać odstępy i respektować maksymalną długość pola.

Warto pamiętać o Unicode, ponieważ długość łańcucha w JavaScript jest liczona w jednostkach kodowych UTF-16. Przecięcie łańcucha w dowolnym miejscu może rozdzielić parę surogatów.

Co najważniejsze, rozpoznanie mowy nigdy nie powinno automatycznie wysyłać formularza. Użytkownik musi mieć możliwość sprawdzenia i poprawienia tekstu przed wysłaniem.

Obsługuj błędy bez zakłócania zwykłego wpisywania tekstu

Podsystem rozpoznawania mowy powinien być opcjonalny.

Jeśli odmówiono dostępu do mikrofonu, przeglądarka nie może przydzielić pamięci dla modelu, worker ulegnie awarii albo nagranie nie zawiera mowy, zwykłe pole tekstowe musi nadal działać.

Cykl życia mikrofonu warto przedstawić za pomocą wyraźnych stanów, takich jak:

idle
preparing
listening
recognizing
error

Stosuj etykiety dostępne dla technologii wspomagających i widoczne komunikaty o stanie. Mikrofon, który po ładowaniu po prostu zmienia kolor na czerwony lub różowy, niewiele wyjaśnia użytkownikowi.

Anulowanie powinno zwalniać ścieżki audio, kontekst audio, oczekujące bufory oraz aktywną sesję nagrywania. Interfejs musi też zapobiegać nadpisaniu nowszego stanu pola przez spóźniony wynik wcześniej anulowanego nagrania.

Jeśli duży model zawodzi z powodu ograniczeń pamięci lub inicjalizacji, przed próbą uruchomienia mniejszego modelu całkowicie zwolnij jego zasoby. Ogranicz liczbę ponowień, aby przeglądarka nie pobierała i nie inicjalizowała w kółko modeli, których nie może uruchomić.

Nie przełączaj po cichu na chmurową usługę rozpoznawania mowy, gdy rozpoznawanie lokalne zawiedzie. Jeśli przetwarzanie wyłącznie lokalne jest ścisłym wymogiem, awaria lokalnego rozpoznawania musi pozostać lokalna.

Testuj cały proces w prawdziwej przeglądarce

Pomyślna kontrola TypeScript ani produkcyjny build nie stanowią dowodu, że wprowadzanie głosowe działa.

Podobnie potwierdzenie, że przeglądarka może pobierać pliki ONNX, nie dowodzi pomyślnej inicjalizacji obu sesji modelu.

Najbardziej przydatny test regresyjny powinien uruchamiać ten sam worker, pliki środowiska, model, procesor i tokenizator, których używa wersja produkcyjna.

Przekaż znane krótkie nagranie PCM do rzeczywistej ścieżki inferencji w przeglądarce i sprawdź, czy zwracana jest niepusta transkrypcja.

Następnie sprawdź pełną interakcję z mikrofonem w rzeczywistej przeglądarce.

  • Początkowe ładowanie strony: sprawdź, czy przed użyciem mikrofonu nie są pobierane modele rozpoznawania ani pliki środowiska.
  • Pierwsza inicjalizacja: sprawdź uprawnienia mikrofonu, żądania pobrania modeli, typy MIME JavaScript i WASM oraz rzeczywiste potwierdzenie gotowości od workera.
  • Inicjalizacja ONNX: sprawdź, czy można utworzyć sesje kodera i dekodera.
  • Przechwytywanie audio: potwierdź, że AudioWorklet wytwarza niepuste bufory PCM.
  • Zmiana częstotliwości próbkowania: potwierdź, że worker otrzymuje rzeczywistą częstotliwość próbkowania i tworzy prawidłowy dźwięk 16 kHz.
  • Ręczne zatrzymanie: sprawdź, czy drugie kliknięcie mikrofonu niezawodnie zatrzymuje nagrywanie.
  • Automatyczne zatrzymanie: sprawdź, czy naturalne pauzy kończą nagrywanie bez ucinania cichej mowy.
  • Transkrypcja: sprawdź, czy kompletny proces obejmujący procesor, model i tokenizator zwraca rozpoznawalny tekst.
  • Integracja z polem wprowadzania: sprawdź, czy rozpoznany tekst pojawia się w rzeczywistym kontrolowanym polu, bez nadpisywania zmian użytkownika.
  • Odzyskiwanie po błędach: przetestuj odmowę uprawnień, brak plików, błędy workera, nieudaną inicjalizację modelu, anulowanie oraz przełączenie na mniejszy model.
  • Prywatność: przejrzyj żądania sieciowe i upewnij się, że dźwięk z mikrofonu nie jest wysyłany na serwer ani do usługi analitycznej.
  • Zgodność z urządzeniami mobilnymi: powtórz testy na prawdziwych telefonach, uwzględniając pierwsze i ponowne uruchomienia oraz wielokrotne sesje dyktowania.

Testy z nagranym PCM świetnie sprawdzają system inferencji, ale nie dowodzą, że przechwytywanie dźwięku z mikrofonu działa. Końcowy test akceptacyjny musi obejmować prawdziwy mikrofon i rzeczywisty interfejs użytkownika.

Praktyczna architektura niezawodnego wprowadzania głosowego w przeglądarce

W przypadku witryny wymagającej krótkiego, wielojęzycznego wprowadzania głosowego z ochroną prywatności najpraktyczniejsza architektura jest celowo prosta:

React microphone button
        |
        v
Permission request
        |
        v
Lazy model initialization
        |
        v
AudioWorklet PCM capture
        |
        v
Adaptive RMS / manual stop
        |
        v
Dedicated Worker
        |
        v
16 kHz audio resampling
        |
        v
Whisper Tiny or Base Q8
        |
        v
Compatible ONNX Runtime WASM
        |
        v
Whisper processor + tokenizer
        |
        v
Final transcript
        |
        v
Controlled React input

Aplikacja powinna samodzielnie hostować wersjonowane pliki modeli i środowiska, wykonywać inferencję poza głównym wątkiem, unikać zbędnego pobierania modeli, zwalniać zasoby po użyciu i nigdy nie wysyłać dźwięku z mikrofonu na serwer rozpoznawania mowy.

Bardziej zaawansowane opcje, takie jak przyspieszanie przez WebGPU, neuronowe wykrywanie aktywności głosowej, ciągła częściowa transkrypcja czy kolejne klasy modeli, można rozważyć później, gdy pomiary uzasadnią dodatkową złożoność.

Najważniejszy wniosek nie dotyczy konkretnej wersji biblioteki ani rozmiaru modelu. Chodzi o sprawdzanie każdej granicy między etapami przeglądarkowego procesu rozpoznawania mowy.

Działające okno uprawnień mikrofonu nie oznacza jeszcze, że dźwięk jest transkrybowany. Pobrany model nie oznacza, że jego dekoder się zainicjalizował. Udana sesja ONNX nie oznacza, że tokenizator jest dostępny. A nawet udana transkrypcja nie oznacza, że aplikacja prawidłowo wstawiła tekst.

Funkcja jest ukończona dopiero wtedy, gdy odwiedzający może nacisnąć przycisk mikrofonu, mówić naturalnie, zatrzymać nagrywanie i zobaczyć rozpoznane słowa w edytowalnym polu — bez wysyłania nagrania na serwer rozpoznawania mowy.