Blog de développement

50 – Résolution automatique d'un CAPTCHA audio en français avec Whisper

Dernière modification : 2026-09-26

Ce billet raconte la construction d'un pipeline de résolution automatique d'un CAPTCHA audio en français, à base de Whisper et de Playwright. Ce n'est pas un billet théorique : les chiffres présentés sont mesurés sur une soixantaine de cas réels. Le billet est aussi l'occasion de mettre en perspective les résultats avec une étude académique récente sur le même sujet — et de noter quelques différences qui méritent discussion.

1. Le problème posé

Certains portails administratifs français utilisent des CAPTCHA de type « saisie de caractères » — une image distorc distordue accompagnée d'une piste audio. L'audio est synthétique, en français, et énonce des lettres et des chiffres un à un. L'objectif est d'automatiser complètement la résolution : extraire l'audio depuis la page, le transcrire, soumettre le code, et détecter si l'authentification a réussi.

La contrainte principale est que l'audio est en français synthétique — pas en anglais. Ce point, anodin en apparence, a des conséquences importantes sur le choix du modèle et sur les confusions phonétiques observées.

2. Architecture du pipeline

Le pipeline est entièrement en Python 3.11 sous Windows, orchestré par un script principal et lancé depuis un fichier .bat. Les grandes étapes sont :

  1. Chargement de la page via Playwright (Chromium headless=False), avec retry x5 sur domcontentloaded en cas d'absence du CAPTCHA dans le HTML.
  2. Extraction de l'audio : l'image et le fichier WAV sont encodés en base64 directement dans le HTML — pas d'appel réseau séparé, pas de cache à intercepter.
  3. Transcription Whisper : le WAV est décodé, écrit sur disque et passé à whisper.load_model("small") avec language='fr'.
  4. Nettoyage : suppression des espaces, conversion en majuscules, détection des hallucinations (caractère répété quatre fois ou plus).
  5. Soumission via press_sequentially() (le page.fill() standard étant incompatible avec ce formulaire).
  6. Détection du résultat : le serveur renvoie un 302 vers une URL invalide en cas de succès — bug de redirect connu et contourné par navigation manuelle vers la page cible.

Tous les résultats sont enregistrés dans un CSV horodaté et dans un rapport texte par tentative, ce qui permet une analyse post-hoc. Le navigateur Playwright reste ouvert sur la page de travail après un succès, ce qui permet une confirmation visuelle avant de passer à la suite.

3. Résultats mesurés

Sur 66 tests (dont une trentaine en « phase corrigée » après résolution des bugs techniques), les chiffres stabilisés sont :

  • Taux de transcription correcte (Whisper = code attendu) : 68 %
  • Taux de succès par tentative de soumission : 58 %
  • Taux cumulé sur 3 tentatives : 93 %
  • Taux cumulé sur 5 tentatives : 99 %
  • Durée moyenne de transcription Whisper small : 22 s
  • Durée totale par tentative (chargement + transcription + soumission) : 35 s
  • Hallucinations détectées : 1 sur 66 (caractère répété en boucle, bruit de fond)

L'écart entre 68 % de transcription correcte et 58 % de succès s'explique par les ambiguités phonétiques : Whisper transcrit fidèlement ce qu'il entend, mais ce qu'il entend est ambigu en français synthétique. Le code soumis est phonétiquement plausible mais pas lexicalement identique au code attendu.

4. Confusions phonétiques en français

C'est le point le plus instructif de l'expérience. En français, certaines lettres sont quasiment homophones lorsqu'elles sont énoncées isolément par une voix synthétique :

  • B / P / V : « bé », « pé », « vé » — triplet confirmé sur plusieurs occurrences.
  • D / V : « dé » / « vé » — confirmé.
  • L / F : « elle » / « effe » — confirmé (2 occurrences).
  • Z / 7 : « zède » / « sept » — confirmé (2 occurrences).
  • L / M, D / T : observés une fois chacun, à confirmer.

Ces confusions sont spécifiques au français. Un modèle entraîné majoritairement sur de l'anglais — comme Whisper tiny.en ou base.en utilisés dans l'étude académique — ne rencontrera pas les mêmes ambiguïtés, car les phonèmes concernés n'ont pas les mêmes voisinages sonores en anglais.

5. Discussion : l'étude de Grenoble INP et ses limites

L'étude d'Aubry et al. (EuroS&PW 2025) compare six modèles ASR sur le CAPTCHA audio de Google reCAPTCHA v2 en anglais. Les résultats sont impressionnants : les modèles cloud (Google Speech-to-Text, Azure, Deepgram) atteignent 99 % de précision, et Whisper tiny.en atteint 97 %. La résolution se fait en moins de 2 secondes pour les modèles cloud.

Ces chiffres sont corrects, mais leur contexte est très spécifique : le CAPTCHA de Google en anglais, où l'audio énonce des chiffres seulement (0–9), avec un vocabulaire de 10 tokens. Dans ce cas, même un modèle ASR médiocre obtient de bons résultats par élimination. La conclusion « les CAPTCHA audio sont trivialement cassables » est justifiée pour ce cas précis, mais elle ne se généralise pas sans précaution.

Le cas traité ici est différent sur plusieurs points :

  • L'audio est en français, pas en anglais.
  • Le code contient des lettres et des chiffres mélangés (6 à 9 caractères), contre des chiffres seuls chez Google.
  • Le vocabulaire effectif est de 36 tokens, ce qui rend le décodage statistiquement plus difficile.
  • Les modèles cloud ne sont pas utilisables ici : pas d'API publique, pas de coût variable acceptable, et contrainte de traitement local pour des raisons de confidentialité.

L'étude évalue Whisper uniquement dans ses versions tiny.en et base.en, c'est-à-dire les variantes monolingues anglais. En français, le modèle adapté est Whisper small (variante multilingue, 244 M paramètres). C'est ce qui est utilisé ici, avec language='fr' et fp16=False (CPU uniquement, sans GPU).

6. Ce que l'étude ne dit pas

L'étude est rigoureuse dans son périmètre, mais elle laisse de côté plusieurs aspects qui comptent en pratique.

Les bugs côté serveur. Dans le cas étudié ici, le principal obstacle n'était pas la précision ASR : c'était un bug de redirection HTTP côté serveur qui faisait interpréter tous les succès comme des échecs. L'étude suppose que la chaîne POST → valider → page suivante est correcte, ce qui n'est pas toujours le cas. L'analyse du HAR Playwright a été déterminante pour diagnostiquer ce problème.

La robustesse en boucle. L'étude mesure la précision par tentative individuelle. En pratique, le métrique pertinent est le taux de succès sur N tentatives. Avec 58 % par tentative, cinq essais donnent 99 % de réussite cumulée — largement suffisant pour un usage en production, sans qu'il soit nécessaire d'atteindre 97 %.

La performance CPU sans GPU. Les temps rapportés dans l'étude (1–2 s pour Whisper) supposent des ressources de type laptop récent. Sur un serveur domestique sans GPU dédié, Whisper small prend 20–25 s par transcription. C'est acceptable pour un usage occasionnel, mais il faut le mentionner.

7. Ce que l'étude confirme

Malgré ces nuances, les conclusions générales de l'étude sont confirmées par l'expérience pratique :

  • Whisper est le meilleur choix local, sans abonnement ni API externe.
  • Les CAPTCHA audio sont vulnérables aux ASR modernes, même sans GPU et même en français.
  • La combinaison Playwright + Whisper est déployable par un développeur seul, sur du matériel standard, en quelques jours de travail.
  • L'étude mentionne que Whisper tiny.en atteint 97 % sur reCAPTCHA anglais ; Whisper small atteint 68 % sur un CAPTCHA français alphanumériques — l'écart s'explique entièrement par le changement de langue et la complexité du vocabulaire cible.

8. Passage à faster-whisper : int8 ou float32 ?

Pour traiter en lot un corpus de plusieurs milliers d'enregistrements, Whisper small a été comparé à faster-whisper. Il ne s'agit pas d'un autre modèle : ce sont les mêmes poids que Whisper small multilingue, convertis pour CTranslate2, un moteur d'inférence optimisé pour les Transformers. PyTorch n'est plus nécessaire, ce qui allège l'installation d'environ 650 Mo. faster-whisper expose en outre, pour chaque segment, deux scores directement exploitables : avg_logprob (confiance moyenne du décodage, proche de 0 = sûr) et no_speech_prob (probabilité de silence, au-delà de 0,6 = hallucination probable). Le captcha étant en français, la langue est imposée à la transcription ; il n'existe pas de variante small monolingue française à télécharger.


from faster_whisper import WhisperModel

model = WhisperModel("small", device="cpu", compute_type="int8")
segments, info = model.transcribe(
    "captcha.wav", language="fr", temperature=0.0, beam_size=1,
    condition_on_previous_text=False,
    initial_prompt="A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9")
segments = list(segments)   # generateur : a consommer une seule fois
texte = " ".join(s.text for s in segments)
                    

Le paramètre décisif est compute_type, c'est-à-dire le format numérique dans lequel les poids sont chargés en mémoire :

  • float32 : nombres flottants sur 32 bits, précision complète. C'est le format de référence, celui qu'utilise Whisper sur CPU avec fp16=False.
  • int8 : poids quantifiés en entiers sur 8 bits. Chaque matrice est ramenée à une échelle d'entiers de −127 à +127 accompagnée d'un facteur d'échelle. Le modèle occupe environ quatre fois moins de mémoire, et les multiplications de matrices se font en arithmétique entière, beaucoup plus rapide … à condition que le processeur dispose des instructions vectorielles adaptées (AVX2, et idéalement AVX-512 VNNI). La perte de précision est théoriquement faible pour un vocabulaire aussi restreint que 36 caractères.

Les mesures ont été faites sur un mini-PC à processeur Celeron quadricœur basse consommation, sans AVX ni AVX2, avec 8 Go de mémoire — le genre de machine que l'on dédie volontiers à un traitement de fond. Durée moyenne de transcription par captcha, hors chargement du modèle :

  • Whisper small (PyTorch, float32) : 17,1 s
  • faster-whisper small int8 : 16,0 s
  • faster-whisper small float32 : 18,4 s

On est très loin du facteur 4 annoncé pour faster-whisper. Deux raisons se cumulent. D'abord, sans AVX2, CTranslate2 ne dispose pas de ses noyaux entiers optimisés et retombe sur des routines génériques : l'avantage de int8 se réduit à quelques pour cent, gagnés surtout sur la bande passante mémoire (des poids quatre fois plus légers à lire). Ensuite, Whisper traite toujours une fenêtre de 30 secondes, complétée par du silence : pour un audio de quelques secondes, ce coût fixe de l'encodeur domine et laisse peu de prise aux optimisations du décodage, là où faster-whisper brille sur de longs enregistrements. Enfin, float32 sous CTranslate2 est ici légèrement plus lent que Whisper sous PyTorch : sur ce processeur, la bibliothèque mathématique compte plus que le moteur.

Reste la question de la qualité. Sur les premiers échantillons, int8 et float32 ont produit des transcriptions strictement identiques : la quantification ne coûte rien ici, et float32 a été abandonné. Restait à comparer faster-whisper int8 à Whisper classique. Un test de concordance a porté sur 101 enregistrements répartis sur toute la période de collecte (un sur 186), pour ne pas tester qu'une seule série :

  • Accord entre les deux : 94 sur 101 (93 %).
  • Sur les 7 désaccords, vérifiés à l'oreille : 5 justes pour faster-whisper, 1 juste pour Whisper, 1 faux des deux côtés (faster-whisper à un caractère près, Whisper à deux).
  • Le seul échec propre à faster-whisper est un code tronqué à 5 caractères au lieu de 6 : une erreur détectable par la simple longueur, que la logique de décision écarte sans la soumettre.

Bilan

Ce projet illustre que la résolution automatique d'un CAPTCHA audio n'est pas un problème de modèle ASR : c'est un problème d'intégration. Le choix de Playwright pour la navigation, la gestion des bugs de redirection HTTP, l'analyse HAR, la détection fine des statuts (succès, échec de code, erreur technique), et le pilotage en boucle ont été aussi déterminants que la qualité de la transcription elle-même.

La prochaine étape est d'utiliser la session authentifiée obtenue pour automatiser des recherches dans l'interface — ce qui était l'objectif initial. Le CAPTCHA était l'obstacle, pas la destination.