Retour au blog
19 octobre 2026Sergei Solod25 min de lecture

Comment ajouter une transcription vocale privée à un site web avec Whisper et WebAssembly

Créez une saisie vocale entièrement côté client avec Whisper, Transformers.js, ONNX Runtime Web, AudioWorklet et WebAssembly. Découvrez comment optimiser les performances sur mobile et résoudre les erreurs d’environnement d’exécution, de worker et de tokenizer qui empêchent la reconnaissance vocale de fonctionner.

Reconnaissance vocaleWhisperWebAssemblyAudioWorkletJavaScript

Ajouter un bouton de microphone à un site web semble simple : capter la voix de l’utilisateur, reconnaître ses paroles et les insérer dans un champ de texte. En pratique, obtenir un fonctionnement fiable sur les navigateurs d’ordinateur et de mobile est nettement plus difficile, surtout lorsque toute la reconnaissance vocale doit s’effectuer localement, sans envoyer l’enregistrement du microphone à un serveur.

La difficulté ne se limite pas au choix d’un modèle de reconnaissance vocale. Une implémentation fonctionnelle doit gérer correctement les autorisations d’accès au microphone, la capture audio, le rééchantillonnage, les workers du navigateur, WebAssembly, l’initialisation des modèles ONNX, la tokenisation, la mise en cache et l’intégration au champ de saisie de l’application.

Cet article explique comment mettre en place une transcription vocale pratique et respectueuse de la vie privée avec OpenAI Whisper, Transformers.js, ONNX Runtime Web, AudioWorklet et un Web Worker dédié. Il présente aussi plusieurs erreurs d’intégration réelles qui peuvent laisser le bouton du microphone bloqué sur un état de chargement, même lorsque les fichiers du modèle ont bien été téléchargés.

L’objectif est simple : l’internaute clique sur le bouton du microphone, parle naturellement et voit apparaître le texte reconnu dans un champ de saisie existant. Il peut relire ou corriger le résultat avant de l’envoyer. L’enregistrement audio n’a jamais besoin de quitter le navigateur.

Ce que signifie une reconnaissance vocale entièrement exécutée côté client

Deux approches fondamentalement différentes permettent d’ajouter la saisie vocale à un site web.

La première repose sur un service de reconnaissance vocale. Le navigateur capte l’audio, l’envoie à un service distant, puis reçoit le texte transcrit. Cette approche peut simplifier l’implémentation, mais elle crée une dépendance au réseau et soulève des questions de confidentialité, de disponibilité du fournisseur et, éventuellement, de coûts liés à l’utilisation.

La seconde exécute l’inférence directement dans le navigateur. Le site télécharge un modèle de reconnaissance vocale ainsi que l’environnement d’exécution nécessaire. Le navigateur capte le son du microphone, le convertit en échantillons numériques, exécute le modèle localement et produit la transcription sans serveur de reconnaissance vocale.

Cet article traite exclusivement de la seconde approche.

Il ne faut pas confondre la reconnaissance vocale ordinaire du navigateur avec une reconnaissance garantie comme locale. L’API Web Speech peut s’appuyer sur un moteur distant. Certaines implémentations récentes proposent une option processLocally , mais sa prise en charge varie selon les navigateurs et les systèmes d’exploitation. Si le traitement strictement local est indispensable, l’interface générique SpeechRecognition ne doit pas servir de solution de repli automatique.

L’application doit au contraire maîtriser toute la chaîne d’inférence.

Microphone
    |
    v
AudioWorklet
    |
    v
Audio PCM Float32
    |
    v
Web Worker dédié
    |
    v
Rééchantillonnage à 16 kHz
    |
    v
Whisper + ONNX Runtime Web
    |
    v
Tokenizer et décodage du texte
    |
    v
État du champ React

Le site fournit toujours le JavaScript, WebAssembly, les fichiers de configuration et les poids du modèle. Ce sont des téléchargements statiques ordinaires. La différence est que le son du microphone est traité localement plutôt qu’envoyé pour être transcrit. Si l’utilisateur soumet ensuite le texte obtenu au moyen d’un formulaire classique, il s’agit d’une action distincte de l’application.

Choisir un modèle adapté : Whisper Tiny ou Base

Whisper est une famille de modèles de reconnaissance vocale multilingues. Sa capacité à reconnaître de nombreuses langues avec un seul modèle le rend particulièrement intéressant pour les sites multilingues.

Pour les messages vocaux courts, deux modèles offrent un compromis intéressant entre précision de la transcription et consommation des ressources du navigateur : Whisper Tiny et Whisper Base.

ModèleTaille approximative des poids ONNX Q8 sélectionnésUsage recommandé
Whisper Tiny41 MBChoix prudent pour les mobiles et les appareils peu puissants
Whisper Base77 MBMeilleure qualité sur des appareils suffisamment puissants et testés

Ces tailles correspondent aux fichiers ONNX de l’encodeur et du décodeur sélectionnés dans les versions exportées examinées. Elles n’incluent pas tous les fichiers auxiliaires et ne doivent pas être confondues avec le pic de mémoire utilisé par le navigateur.

Whisper Tiny présente plusieurs avantages : son téléchargement est plus rapide, il nécessite généralement moins de ressources de calcul et convient mieux aux environnements mobiles dont les capacités sont incertaines. Whisper Base offre en général une meilleure qualité de reconnaissance, notamment dans certaines langues où les petits modèles commettent davantage d’erreurs.

Le choix du modèle ne doit pas reposer uniquement sur la quantité de RAM physique. Le navigateur peut disposer de bien moins de mémoire que ne le laissent penser les caractéristiques de l’appareil. Il faut aussi tenir compte de la mémoire déjà utilisée par la page, des allocations WebAssembly, des tenseurs temporaires et de la pression mémoire du système d’exploitation.

Une stratégie prudente consiste à commencer par Tiny sur les mobiles dont les capacités sont inconnues, à réserver Base aux catégories d’appareils qui remplissent les critères requis et à revenir à Tiny si l’initialisation de Base échoue.

Il faut libérer l’instance de Base qui a échoué avant de charger Tiny. Garder les deux modèles en mémoire annule l’intérêt de cette solution de repli.

Pourquoi WebAssembly est un bon choix par défaut en production

ONNX Runtime Web permet d’exécuter des réseaux de neurones dans le navigateur. Pour cette charge de travail, WebAssembly offre une solution pratique d’inférence sur CPU dans les principaux navigateurs, sur ordinateur comme sur mobile.

WebGPU peut accélérer les modèles compatibles sur du matériel adapté, mais sa disponibilité ne garantit pas le fonctionnement de toute la chaîne de reconnaissance vocale. Le fait qu’un navigateur expose navigator.gpu ne garantit ni la prise en charge de la plateforme par ONNX Runtime, ni l’initialisation correcte du graphe Whisper concerné, ni l’absence de problèmes de ressources lors d’inférences répétées.

Une implémentation prudente peut donc utiliser WASM comme moteur d’exécution en production et évaluer WebGPU séparément, après des tests de compatibilité, de précision, de mémoire et de sessions répétées.

Pour une première implémentation, WASM à un seul thread évite également de rendre tout le site dépendant de l’isolation entre origines et de SharedArrayBuffer.

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

Exécuter l’inférence dans un worker dédié écarte les calculs coûteux du thread JavaScript principal. Le champ de saisie, le clavier virtuel, le défilement et l’interface restent réactifs pendant le traitement audio.

Ne rien charger pour la reconnaissance vocale lors de l’ouverture initiale de la page

La présence d’un microphone ne doit pas imposer des téléchargements ni une consommation mémoire importants aux visiteurs qui ne l’utilisent jamais.

N’importez pas Transformers.js de manière anticipée dans le bundle initial de l’application. N’initialisez pas ONNX Runtime à l’ouverture de la page et ne téléchargez pas automatiquement les poids du modèle pendant les périodes d’inactivité.

Chargez plutôt les composants de reconnaissance seulement lorsque l’utilisateur clique explicitement sur le bouton du microphone.

Voici un enchaînement utile pour la première utilisation :

  1. Recevoir le clic sur le bouton du microphone.
  2. Demander l’autorisation d’utiliser le microphone via getUserMedia().
  3. Si l’autorisation est refusée, afficher une erreur sans télécharger le modèle.
  4. Si elle est accordée, libérer le flux audio temporaire pendant la préparation du modèle.
  5. Initialiser dynamiquement le worker dédié et charger le modèle local sélectionné.
  6. Attendre que le worker confirme que la chaîne de reconnaissance est prête.
  7. Démarrer automatiquement l’enregistrement si l’utilisateur n’a pas annulé l’opération.
  8. Afficher un état d’écoute compréhensible.

La demande d’autorisation doit être liée à une interaction de l’utilisateur. L’application doit également gérer les annulations et les restrictions de capture propres aux navigateurs, plutôt que de supposer qu’elle pourra toujours redémarrer l’enregistrement sans nouvelle interaction.

La préparation du modèle doit être signalée par un état visible, par exemple Préparation de la saisie vocale. Une animation de chargement qui n’évolue pas clairement peut donner l’impression que le microphone est défectueux, même si le téléchargement du modèle a réussi.

Une fois initialisé, le worker peut rester disponible quelque temps pour une nouvelle dictée. Un délai d’inactivité d’environ une minute constitue un point de départ raisonnable. Ensuite, mettez fin au worker et libérez le modèle chargé en mémoire. Les fichiers du modèle conservés dans le cache du navigateur peuvent être réutilisés lors de visites ultérieures.

Capturer le son brut avec AudioWorklet

Whisper a besoin d’échantillons audio numériques. MediaRecorder convient très bien à l’enregistrement de fichiers audio, mais son utilisation pour une reconnaissance locale peut imposer une étape inutile d’encodage suivie d’un décodage.

Le chemin de traitement le plus simple est le suivant :

getUserMedia()
    |
    v
AudioContext
    |
    v
AudioWorklet
    |
    v
Tampons PCM Float32
    |
    v
Worker dédié

AudioWorklet exécute le code de traitement audio en dehors du thread JavaScript principal de l’application. Il peut capturer des échantillons PCM, convertir une entrée multicanal en mono, calculer des mesures simples de volume et transmettre au worker des tampons regroupés.

Voici un exemple simplifié d’initialisation du microphone :

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

// Connecter le worklet à une sortie inaudible
// si nécessaire pour maintenir le graphe audio actif.

Les contraintes du microphone sont des préférences, pas des garanties. Les navigateurs peuvent appliquer des algorithmes différents de suppression d’écho, de réduction du bruit et de réglage automatique du gain.

Un point essentiel : la fréquence d’échantillonnage du microphone n’est pas nécessairement de 16 kHz. De nombreux contextes audio utilisent 44.1 kHz ou 48 kHz. Whisper attend une entrée à 16 kHz ; il faut donc rééchantillonner correctement les échantillons capturés.

Se contenter d’ignorer un échantillon sur deux ou sur trois n’est généralement pas une méthode correcte. Un rééchantillonneur passe-bas adapté, par exemple une implémentation à sinus cardinal fenêtré, réduit le repliement spectral et préserve les fréquences utiles de la parole lors de la diminution de la fréquence d’échantillonnage.

Effectuez le rééchantillonnage dans le worker dédié, et non dans la fonction de rappel AudioWorklet exécutée en temps réel.

Transférer efficacement l’audio entre les threads du navigateur

AudioWorklet peut produire de nombreux petits blocs audio. Transmettre chaque quantum de rendu audio dans un message distinct risque de créer une surcharge inutile.

Une stratégie pratique consiste à regrouper les échantillons PCM en petits lots et à transférer chacun sous forme d’ArrayBuffer.

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

La liste de transfert est importante : elle transfère la propriété de l’ArrayBuffer au lieu d’en créer inutilement une copie complète.

Le worker peut ajouter ces blocs dans un accumulateur audio, puis les réunir à l’arrêt de l’enregistrement.

Il est également utile de suivre la fréquence d’échantillonnage réelle de la capture, le nombre d’échantillons collectés et la durée de l’enregistrement. Ces métadonnées aident à diagnostiquer les enregistrements silencieux sans consigner de données audio sensibles.

Détecter les pauses sans ajouter un autre réseau de neurones

La dictée de messages courts ne nécessite pas forcément un détecteur neuronal d’activité vocale.

Un détecteur RMS adaptatif peut fournir une estimation légère de l’activité vocale. La valeur RMS mesure l’amplitude du signal ; elle permet à l’application d’estimer à quel moment une personne qui parlait vient de se taire.

L’interface doit toujours permettre d’arrêter l’enregistrement manuellement en cliquant une deuxième fois sur le microphone. La détection automatique du silence est une commodité, pas une condition indispensable à une transcription réussie.

Voici des valeurs initiales raisonnables pour une dictée courte :

ParamètreValeur initiale
Intervalle d’analyse20 ms
Étalonnage du bruit300 ms
Confirmation de la parole140 ms
Silence avant l’arrêt automatique1,4 seconde
Délai sans parole8 secondes
Durée maximale d’enregistrement30 secondes

Ces valeurs constituent des points de départ pour la configuration, et non des seuils universellement optimaux. Une voix faible, la musique de fond et le traitement du bruit par le microphone peuvent influencer les mesures RMS.

Mieux vaut permettre à l’utilisateur d’arrêter manuellement l’enregistrement que risquer de couper automatiquement les derniers mots d’une phrase.

Pour une saisie de texte courte, une transcription en une seule passe est généralement plus simple que des appels répétés à Whisper pour obtenir des résultats intermédiaires. Enregistrez la phrase, arrêtez la capture, exécutez une seule fois le modèle, puis insérez la transcription finale.

La chaîne complète de Whisper exige davantage qu’un modèle ONNX

C’est l’un des détails les plus importants de l’implémentation.

L’inférence Whisper ne se résume pas à appeler un encodeur ONNX puis un décodeur ONNX. Une chaîne complète de reconnaissance automatique de la parole nécessite aussi l’extraction des caractéristiques audio, la tokenisation, la configuration de la génération et le décodage des jetons prédits en texte lisible.

Voici la chaîne de traitement complète :

Signal PCM brut du microphone
        |
        v
Rééchantillonnage à 16 kHz
        |
        v
Extracteur de caractéristiques Whisper
        |
        v
Encodeur ONNX
        |
        v
Décodeur ONNX et génération de jetons
        |
        v
Tokenizer Whisper
        |
        v
Texte reconnu

La réussite de l’initialisation d’une session ONNX prouve uniquement que le graphe du modèle peut être chargé. Elle ne prouve pas que toute la chaîne de reconnaissance fonctionne.

Dans une intégration testée avec Transformers.js, les sessions du modèle pouvaient s’initialiser, mais la transcription échouait malgré tout : le pipeline construit localement ne disposait pas d’un processeur et d’un tokenizer correctement configurés.

L’implémentation compatible initialisait explicitement ces composants à partir des métadonnées locales du modèle.

L’exemple suivant illustre de manière simplifiée cette approche propre à une version donnée ; il ne constitue pas un worker autonome complet :

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

// Ces valeurs proviennent des fichiers de configuration locaux du modèle.
const tokenizer = new WhisperTokenizer(
  tokenizerJson,
  tokenizerConfig
);

const featureExtractor = new WhisperFeatureExtractor(
  preprocessorConfig
);

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

// Dans l’intégration concernée, propre à cette version :
recognizer.tokenizer = tokenizer;
recognizer.processor = processor;

Les signatures exactes des constructeurs et la méthode d’intégration doivent être vérifiées pour la version de Transformers.js installée. Certaines versions et certains exports du modèle peuvent créer automatiquement ces composants ; il ne faut pas considérer leur construction manuelle comme une obligation universelle.

Une fois la chaîne complète disponible, on peut lancer la transcription d’un court enregistrement en précisant la langue et la tâche :

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

Lorsque l’application connaît déjà la langue de l’utilisateur, la préciser évite une détection linguistique inutile et peut rendre le résultat plus prévisible pour les enregistrements courts.

Verrouiller des versions compatibles de Transformers.js et d’ONNX Runtime

Une deuxième catégorie importante d’échecs concerne la relation entre Transformers.js et ONNX Runtime Web.

Transformers.js fournit le pipeline de reconnaissance de haut niveau, tandis qu’ONNX Runtime Web exécute les graphes des réseaux de neurones. Ces packages doivent être compatibles entre eux et avec les exports ONNX précis qui sont utilisés.

Dans l’implémentation étudiée, une version de développement d’ONNX Runtime Web initialisait l’encodeur Whisper, mais échouait à la création d’une session du décodeur quantifié. L’erreur concernait une échelle de quantification manquante dans une opération DequantizeLinear .

Il ne s’agissait ni d’un problème d’autorisation du microphone, ni d’un problème de téléchargement, ni d’un problème de format audio. Le navigateur recevait les fichiers du modèle, mais l’environnement d’exécution ne parvenait pas à initialiser l’un des graphes nécessaires.

Tester directement l’encodeur et le décodeur avec une version stable et figée d’ONNX Runtime Web a résolu cette incompatibilité précise.

Cette vérification de compatibilité était particulièrement utile, car elle distinguait deux affirmations :

  • Le fichier du modèle a été téléchargé avec succès.
  • L’environnement d’exécution a réussi à créer une session d’inférence utilisable à partir du fichier du modèle.

Ces deux affirmations ne sont pas équivalentes.

Pour les exports de modèles testés, une combinaison compatible utilisait Transformers.js 4.2.0 avec ONNX Runtime Web 1.24.3. Une version de développement plus récente de l’environnement d’exécution échouait sur le graphe du décodeur quantifié. Il s’agit d’une observation propre à ces versions, et non d’une preuve que les anciens environnements d’exécution sont toujours meilleurs.

Si vous remplacez la version d’une dépendance transitive de l’environnement d’exécution, figez sa version exacte dans le fichier de verrouillage des dépendances et documentez la raison de ce remplacement. Toute mise à niveau doit donner lieu à un test de régression complet de l’inférence dans le navigateur, et pas seulement à une installation réussie du package.

Vérifier le format du bundle du worker avant de chercher un problème dans Whisper

Un Web Worker JavaScript peut être créé sous différents formats de modules.

Une erreur d’intégration s’est produite parce que l’application lançait un worker en tant que module ES, alors que le bundle généré pour celui-ci utilisait le mécanisme des workers classiques importScripts().

Ces deux modèles d’exécution ne sont pas interchangeables. Un worker de type module ne peut pas simplement exécuter du code réservé aux workers classiques.

La leçon pratique consiste à choisir le constructeur Worker en fonction du bundle réellement généré, plutôt qu’à reprendre une option de constructeur dans un tutoriel générique.

Par exemple, les options suivantes ne sont pas automatiquement équivalentes :

// Worker de type module : nécessite un bundle de modules compatible.
new Worker(workerUrl, { type: 'module' });

// Worker classique : nécessite un bundle classique compatible.
new Worker(workerUrl);

Le bon choix dépend de l’outil de bundling et du JavaScript produit.

Pour diagnostiquer une erreur d’initialisation du worker, examinez la console du navigateur, le fichier du worker généré et l’onglet Réseau. Déterminez si l’échec survient avant même le début de l’initialisation d’ONNX Runtime.

Certaines bibliothèques regroupées dans un bundle calculent également les URL par rapport à l’emplacement de leur propre module. Lorsqu’elles sont intégrées à des workers, testez les chemins obtenus pour les fichiers de l’environnement d’exécution, au lieu de supposer que les URL relatives d’origine fonctionneront encore.

Servir les modules WASM et JavaScript avec les bons types MIME

Même des fichiers JavaScript et ONNX parfaitement compatibles peuvent échouer si le serveur web renvoie des en-têtes de réponse incorrects.

Un échec concret concernait un module JavaScript portant l’extension .mjs et servi avec le type application/octet-stream. Le navigateur refusait d’exécuter le module, car le type de sa réponse ne convenait pas à l’exécution d’un module JavaScript.

Les types de réponse attendus sont généralement les suivants :

Extension du fichierContent-Type
.mjsapplication/javascript
.wasmapplication/wasm

Vérifiez la réponse réellement reçue en production, et pas seulement le fichier de configuration du serveur.

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

Les URL ci-dessus sont données à titre d’exemple. Utilisez les véritables noms de fichiers avec version correspondant à la distribution choisie de l’environnement d’exécution.

Un statut HTTP correct ne suffit pas. Le navigateur peut quand même refuser un fichier à cause de son type MIME, de la politique de sécurité du contenu (CSP), du mode d’exécution des modules ou d’une URL incorrecte.

N’oubliez pas qu’une même requête peut réussir lorsqu’on ouvre directement l’URL dans un onglet, mais échouer lorsque la ressource est chargée comme module exécutable ou comme dépendance WebAssembly.

Utiliser des URL de ressources immuables sans enfermer les utilisateurs dans des fichiers défectueux

Les fichiers des modèles Whisper sont assez volumineux pour que leur mise en cache soit importante.

Des chemins de ressources immuables et versionnés permettent au navigateur de réutiliser les fichiers du modèle et de l’environnement d’exécution qui n’ont pas changé, sans les télécharger à chaque dictée.

Voici une politique de cache typique :

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

Mais cela impose une contrainte au déploiement : dès qu’une URL est déclarée immuable, son contenu ne doit pas être remplacé silencieusement.

Si le binaire de l’environnement d’exécution doit changer, publiez-le sous une nouvelle URL versionnée.

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

Changer le chemin de l’environnement d’exécution n’est pas une question d’esthétique. Si l’application réutilise une URL immuable après avoir modifié son contenu, le navigateur risque de continuer à utiliser un ancien binaire WebAssembly incompatible, conservé dans son cache.

Il est également préférable de conserver les fichiers JavaScript de liaison et les fichiers WebAssembly provenant de la même distribution de l’environnement d’exécution. Mélanger des fichiers de différentes versions peut provoquer des erreurs d’initialisation difficiles à diagnostiquer.

Un manifeste généré lors de la compilation, indiquant les fichiers attendus, leurs tailles et leurs sommes de contrôle cryptographiques, permet de détecter des ressources manquantes ou incohérentes avant le déploiement.

Configurez la bibliothèque de reconnaissance pour qu’elle utilise des ressources hébergées sur votre propre domaine et désactivez le chargement des modèles distants. Ainsi, l’absence d’un modèle local produit une erreur explicite au lieu de déclencher un téléchargement imprévu depuis un hébergeur externe.

Pourquoi le microphone peut afficher un chargement sans jamais fonctionner

Pendant le développement, un symptôme courant est un bouton de microphone qui demande l’autorisation, affiche une animation de chargement, puis finit par revenir à un état d’erreur sans insérer de texte.

Le navigateur peut même indiquer que le microphone est utilisé.

Cela ne prouve pas que la reconnaissance vocale fonctionne. Cela prouve seulement que la capture du microphone a été autorisée ou active à un moment donné.

Plusieurs défaillances indépendantes peuvent produire des symptômes presque identiques :

  1. Type MIME incorrect pour un module JavaScript. Le navigateur télécharge un module de l’environnement d’exécution, mais refuse de l’exécuter.
  2. Incompatibilité du format d’exécution du worker. Le worker compilé utilise des API de worker classique alors qu’il est lancé comme worker de type module, ou inversement.
  3. Incompatibilité du décodeur ONNX. L’environnement d’exécution crée la session de l’encodeur, mais échoue sur le graphe du décodeur quantifié.
  4. Chaîne de reconnaissance incomplète. Les sessions ONNX existent, mais le tokenizer ou le processeur audio n’est pas correctement initialisé.

Ces problèmes doivent être diagnostiqués séparément. En résoudre un peut révéler le suivant.

Le principe de débogage essentiel est le suivant : une réponse HTTP 200 ne signifie pas que le modèle s’est correctement initialisé, et une initialisation réussie du modèle ne signifie pas que la transcription fonctionne.

Même une transcription réussie ne prouve pas que son résultat arrive dans le véritable champ de texte de l’application.

Chaque étape de la chaîne doit fournir un signal de diagnostic pertinent.

Ajouter de l’observabilité sans collecter les paroles privées

Il n’est pas nécessaire d’enregistrer les paroles de l’utilisateur pour disposer de journaux de diagnostic utiles.

L’application peut suivre des états techniques comme :

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

En cas d’échec, indiquez l’étape concernée, un code d’erreur technique stable et des métadonnées d’erreur nettoyées.

Une erreur d’initialisation du modèle doit pouvoir être distinguée d’un refus d’accès au microphone ou d’un enregistrement vide. Une défaillance du worker doit être visible pendant le développement, plutôt que masquée par un gestionnaire d’erreurs générique.

Ne consignez ni échantillons du microphone, ni phrases transcrites, ni séquences de jetons, ni autre contenu vocal. La télémétrie des performances peut enregistrer des plages de durées, la catégorie du modèle choisi, les temps d’initialisation et d’inférence, ainsi que les catégories d’erreurs.

Ces données facilitent considérablement le diagnostic d’un microphone défaillant sans affaiblir les exigences de confidentialité.

Insérer la transcription au moyen de l’état React

La reconnaissance vocale n’est utile que si la transcription finale arrive dans le champ du formulaire existant.

Dans une application React, ce champ est souvent piloté par l’état d’un composant. Affecter directement une valeur à input.value peut entrer en conflit avec le fonctionnement du rendu du framework.

Le texte reconnu doit plutôt passer par le même mécanisme de mise à jour de l’état que la saisie au clavier.

L’intégration doit aussi tenir compte de l’asynchronisme. La reconnaissance peut se terminer alors que l’utilisateur a déjà saisi d’autres caractères ou déplacé le curseur.

Utiliser une ancienne valeur de saisie, capturée au début de l’enregistrement, risque d’écraser les modifications récentes.

Voici un schéma d’intégration simplifié :

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

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

La fonction auxiliaire d’insertion doit préserver le texte actuel, éventuellement remplacer la plage sélectionnée, ajouter les espaces appropriés et respecter la longueur maximale autorisée par le champ.

Unicode mérite une attention particulière, car la longueur des chaînes JavaScript est calculée en unités de code UTF-16. Couper une chaîne à une position arbitraire peut scinder une paire de substitution.

Surtout, la reconnaissance vocale ne doit jamais soumettre automatiquement le formulaire. L’utilisateur doit pouvoir relire et corriger le texte avant de l’envoyer.

Gérer les échecs sans empêcher la saisie de texte ordinaire

Le sous-système de reconnaissance vocale doit rester facultatif.

Si l’accès au microphone est refusé, si le navigateur ne peut pas allouer la mémoire nécessaire au modèle, si un worker plante ou si un enregistrement ne contient aucune parole, le champ de saisie normal doit continuer à fonctionner.

Représentez le cycle de vie du microphone par des états explicites, par exemple :

idle
preparing
listening
recognizing
error

Utilisez des libellés accessibles et des indicateurs d’état visibles. Un microphone qui devient simplement rouge ou rose après le chargement n’explique presque rien à l’utilisateur.

Une annulation doit libérer les pistes audio, le contexte audio, les tampons en attente et la session d’enregistrement active. L’interface doit également empêcher qu’un résultat obsolète provenant d’un enregistrement annulé remplace un état de saisie plus récent.

Si un gros modèle échoue en raison d’un manque de mémoire ou de contraintes d’initialisation, libérez-le entièrement avant d’essayer un modèle plus petit. Limitez le nombre de tentatives pour éviter que le navigateur télécharge ou initialise à répétition des modèles qu’il ne peut pas exécuter.

Ne basculez pas silencieusement vers un service vocal dans le cloud en cas d’échec local. Si le traitement exclusivement local est une exigence stricte, un échec du moteur local doit rester un échec local.

Tester toute la chaîne dans le navigateur

Une vérification TypeScript réussie ou une compilation de production réussie ne suffit pas à prouver que la saisie vocale fonctionne.

De même, vérifier que le navigateur peut télécharger les fichiers ONNX ne prouve pas que les deux sessions du modèle s’initialisent correctement.

Le test de régression le plus utile doit faire fonctionner le même worker, les mêmes fichiers d’exécution, le même modèle, le même processeur et le même tokenizer qu’en production.

Fournissez un court enregistrement PCM connu à la véritable chaîne d’inférence du navigateur et vérifiez qu’une transcription non vide est produite.

Testez ensuite toute l’interaction avec le microphone dans un véritable navigateur.

  • Chargement initial de la page : vérifier qu’aucun modèle vocal ni fichier d’exécution n’est téléchargé avant l’utilisation du microphone.
  • Initialisation à froid : vérifier l’autorisation du microphone, les requêtes du modèle, les types MIME JavaScript et WASM, ainsi qu’une confirmation réelle de disponibilité du worker.
  • Initialisation ONNX : vérifier que les sessions de l’encodeur et du décodeur peuvent être créées.
  • Capture audio : confirmer qu’AudioWorklet produit des tampons PCM non vides.
  • Rééchantillonnage : confirmer que le worker reçoit la fréquence d’échantillonnage réelle et produit un signal audio valide à 16 kHz.
  • Arrêt manuel : vérifier qu’un deuxième clic sur le microphone arrête l’enregistrement de façon fiable.
  • Arrêt automatique : vérifier que les pauses naturelles terminent l’enregistrement sans couper une parole à faible volume.
  • Transcription : vérifier que le processeur, le modèle et le tokenizer réunis produisent un texte reconnaissable.
  • Intégration au champ de saisie : vérifier que le texte reconnu apparaît dans le véritable champ contrôlé sans écraser les modifications de l’utilisateur.
  • Récupération après erreur : tester les refus d’autorisation, les fichiers manquants, les erreurs de worker, l’échec d’initialisation du modèle, l’annulation et le repli vers un modèle plus petit.
  • Confidentialité : examiner les requêtes réseau et confirmer que l’audio du microphone n’est envoyé ni à un serveur ni à un service d’analyse.
  • Compatibilité mobile : réitérer les tests sur de vrais téléphones, avec démarrage à froid, démarrage à chaud et sessions de dictée répétées.

Les tests effectués à partir d’enregistrements PCM sont excellents pour valider la chaîne d’inférence, mais ils ne prouvent pas que la capture micro fonctionne. Le test d’acceptation final doit utiliser le véritable microphone et l’interface utilisateur réelle.

Une architecture pratique pour une saisie vocale fiable dans le navigateur

Pour un site web qui nécessite une saisie vocale courte, multilingue et respectueuse de la vie privée, l’architecture la plus pratique reste volontairement simple :

Bouton microphone React
        |
        v
Demande d’autorisation
        |
        v
Initialisation différée du modèle
        |
        v
Capture PCM avec AudioWorklet
        |
        v
RMS adaptatif / arrêt manuel
        |
        v
Worker dédié
        |
        v
Rééchantillonnage audio à 16 kHz
        |
        v
Whisper Tiny ou Base Q8
        |
        v
ONNX Runtime WASM compatible
        |
        v
Processeur Whisper + tokenizer
        |
        v
Transcription finale
        |
        v
Champ React contrôlé

L’application doit héberger elle-même les fichiers versionnés du modèle et de l’environnement d’exécution, effectuer l’inférence hors du thread principal, éviter les téléchargements inutiles de modèles, libérer les ressources après utilisation et ne jamais envoyer l’audio du microphone à un serveur pour le transcrire.

Des options plus avancées, comme l’accélération WebGPU, la détection neuronale de l’activité vocale, les transcriptions partielles continues ou des catégories de modèles supplémentaires, pourront être envisagées plus tard si les mesures justifient leur complexité.

La leçon la plus précieuse ne concerne ni une version de bibliothèque ni une taille de modèle en particulier. Elle consiste à valider chaque interface entre les étapes de la chaîne de reconnaissance vocale du navigateur.

Une demande d’autorisation du microphone qui fonctionne ne signifie pas que l’audio est transcrit. Le téléchargement d’un modèle ne signifie pas que son décodeur est initialisé. Une session ONNX créée avec succès ne signifie pas que le tokenizer est disponible. Et une transcription réussie ne signifie pas que l’application a inséré correctement le texte.

La fonctionnalité n’est terminée que lorsqu’un visiteur peut appuyer sur le microphone, parler naturellement, arrêter l’enregistrement et voir les mots reconnus apparaître dans le champ modifiable, sans envoyer l’enregistrement à un serveur de reconnaissance vocale.