F6 enfoncé, je parle, je relâche, ça transcrit, ça colle où j'étais en train d'écrire. C'est tout ce que fait Tania Dictée, mon appli Windows de dictée vocale 100 % locale (aucun serveur, faster-whisper qui tourne sur ta machine, rien qui sort chez toi). Sauf qu'au début, ma sortie en français québécois plein d'anglicismes de dev sortait mal. Pas à cause du modèle. À cause de tout ce qu'il y a autour du modèle.
Je fais pas de benchmark ici, j'ai pas mesuré de taux d'erreur ni de latence, ça serait inventer des chiffres que j'ai pas. Ce que j'ai, c'est cinq réglages concrets qui ont changé ma sortie, dans l'ordre où je les ai découverts.
Le modèle, c'est un choix de matériel, pas un chiffre
Tania Dictée détecte le hardware au démarrage et choisit tout seul :
def auto_detect_device() -> str:
return "cuda" if HAS_CUDA else "cpu"
def auto_detect_compute_type(device: str) -> str:
return "float16" if device == "cuda" else "int8"
def auto_detect_model(device: str) -> str:
return "large-v3" if device == "cuda" else "medium"
GPU CUDA disponible : large-v3 en float16. Sinon, CPU : medium en int8. Le int8, c'est de la quantification (CTranslate2 sous le capot de faster-whisper compresse les poids du modèle), et c'est ça qui rend un modèle sérieux tenable sur un CPU ordinaire, là où le même modèle en pleine précision devient trop lent pour de la dictée au fil de la frappe.
La tentation, quand ça roule sur CPU, c'est de descendre à tiny pour que ça aille plus vite. Je le déconseille sur du français. C'est exactement là que tu perds les accents, les mots rares, les noms propres. medium est le compromis que j'ai gardé : assez de matière pour du français québécois avec du jargon, encore raisonnable sur un CPU.
Les paramètres de transcribe() qui comptent
L'appel, avec les valeurs par défaut de l'app (language et beam_size sortent en réalité de la config) :
segments, info = model.transcribe(
wav_path,
language="fr",
beam_size=5,
vad_filter=True,
condition_on_previous_text=True,
word_timestamps=False,
temperature=0.0,
initial_prompt=self.initial_prompt,
hotwords=self.hotwords,
)
language="fr" est forcé, pas laissé en auto-détection. Un clip push-to-talk, c'est souvent court, une phrase ou deux. Laisser Whisper deviner la langue sur un clip aussi court, c'est ouvrir la porte à ce qu'il bascule vers l'anglais en plein milieu. Forcer la langue élimine ce drift.
temperature=0.0 : je veux une transcription reproductible, pas de la créativité. Une température plus haute laisse le modèle halluciner du texte plausible mais faux, surtout sur du silence ou du bruit de fond. À 0, t'as le chemin le plus probable, stable d'un essai à l'autre. Le prix à payer, par contre : par défaut faster-whisper accepte une liste de températures et remonte l'échelle quand le décodage échoue à ses seuils. Passer un scalaire 0.0 désactive ce filet. Reproductible, mais plus de deuxième chance sur un clip pourri. C'est un arbitrage, pas un réglage gratuit.
word_timestamps=False : inutile pour de la dictée, et ça évite le passage d'alignement.
condition_on_previous_text=True : celui-là, je vais me le faire reprocher, fa que autant le sortir moi-même. Réinjecter le texte déjà décodé dans le contexte, c'est le classique amplificateur de boucles de répétition de Whisper. Ce qui le rend tenable ici, c'est le format : un clip push-to-talk, c'est un appel transcribe() isolé de quelques secondes, et rien n'est reporté d'un clip au suivant. Le contexte reste à l'intérieur d'une même dictée, où il aide plus qu'il nuit. Sur des fichiers longs, je le laisserais pas à True.
vad_filter=True : le VAD (détecteur d'activité vocale) coupe les segments de silence avant de les envoyer au modèle. Le silence, c'est justement là-dessus que Whisper hallucine le plus, des bouts de phrase qui sortent de nulle part. Filtrer le silence en amont règle le problème à la source plutôt que de patcher le résultat après coup.
hotwords, le réglage que tout le monde sous-estime
Tania Dictée embarque une liste de termes par défaut, en dur dans le code (DEFAULT_GLOSSARY_TERMS) : Tania Dictée, VS Code, Cursor, Claude Code, ChatGPT, GitHub, Whisper, Stripe, Notion, Python, Node.js, React, Tailwind, Docker, push-to-talk, Québec. Un fichier glossary.txt optionnel (une ligne par terme) vient s'ajouter à cette liste sans la remplacer : c'est là que tu mets tes noms de projets, de collègues, de produits maison.
Ce glossaire est passé DEUX fois au modèle :
glossary_terms = load_glossary_terms(cfg.glossary_path)
prompt = build_initial_prompt(glossary_terms)
hotwords_str = " ".join(glossary_terms) if glossary_terms else None
Une fois fondu dans l'initial_prompt, une fois tel quel dans hotwords. La différence est plus subtile que ce que je croyais au départ. Dans faster-whisper, hotwords ne biaise pas le décodeur à chaque token : la chaîne est encodée et posée en tête du contexte de conditionnement (le slot sot_prev, celui du texte précédent), avant les tokens de l'initial_prompt. C'est get_prompt() dans transcribe.py. Même canal que le prompt, mais tes termes se retrouvent en position privilégiée au lieu d'être noyés au milieu d'une longue consigne. Deux conséquences pratiques : ce slot est borné (autour de 224 tokens), donc un glossaire kilométrique se fait tronquer sans prévenir, et hotwords est ignoré si tu passes un prefix. Sur les noms propres rares, c'est le réglage qui m'a donné l'impression du plus gros gain. Impression, pas mesure : j'ai pas de banc d'essai à te montrer.
Attention à la version : hotwords n'existe pas dans les vieilles faster-whisper. Sur une version trop ancienne, tu vas manger un TypeError: transcribe() got an unexpected keyword argument 'hotwords'.
Le prompt qui dit au modèle de ne pas traduire
L'initial_prompt est en français, et une bonne partie de son contenu sert à une seule chose : empêcher Whisper de franciser poliment le jargon dev. Extrait réel :
Si un mot est dit en anglais, le GARDER EN ANGLAIS, ne jamais le traduire en français :
'build' reste 'build' (pas 'construire'), 'ship' reste 'ship' (pas 'expédier'),
'run' reste 'run', 'commit' reste 'commit', 'push' reste 'push', 'merge' reste 'merge'.
Laissé à lui-même, Whisper a tendance à "corriger" le franglais vers du français propre. Sauf que si tu dis "je vais ship-le", tu veux "ship", pas "expédier". Le prompt lui interdit aussi d'adoucir ou de reformuler, parce que ça, aussi, Whisper le fait naturellement.
Le filet regex, sale mais honnête
Même avec tout ça, il reste des ratés récurrents et prévisibles. Donc après la transcription, il y a une couche de remplacements regex, SOFT_REPLACEMENTS dans le code (une trentaine d'entrées, en voici six) :
SOFT_REPLACEMENTS = [
(r"\bpaillette\s*on\b", "Python"),
(r"\bno\s*jeasse\b", "Node.js"),
(r"\bdock\s*heure\b", "Docker"),
(r"\bouisp(er|eur)?\b", "Whisper"),
(r"\bvs codes?\b", "VS Code"),
(r"\bia\b", "IA"),
]
def apply_soft_replacements(text: str) -> str:
cleaned = (text or "").strip()
for pattern, replacement in SOFT_REPLACEMENTS:
cleaned = re.sub(pattern, replacement, cleaned, flags=re.IGNORECASE)
return re.sub(r"\s+", " ", cleaned).strip()
Le flags=re.IGNORECASE est pas décoratif : Whisper te ressort le même raté en majuscule ou en minuscule selon la ponctuation autour.
"paillette on" pour Python, "no jeasse" pour Node.js, "dock heure" pour Docker. C'est pas élégant, c'est du sparadrap. Mais quand un modèle se plante toujours de la même façon sur les mêmes mots, une regex déterministe bat un plus gros modèle, pour zéro coût de calcul. Je l'assume tel quel.
Le vrai risque de cette couche, c'est le faux positif : une regex trop large qui réécrit un mot légitime. J'en ai une qui transforme "équiper" en "ship", parce que Whisper me traduisait systématiquement "ship" par "équiper". Le jour où je vais dicter pour vrai le verbe équiper, elle va me mordre. C'est pour ça que chaque motif est ancré sur un raté que j'ai réellement observé, jamais sur un raté hypothétique, et que la couche entière se coupe d'un flag (--cleanup-mode raw rend la sortie brute de Whisper).
Si t'as une meilleure façon de gérer des ratés récurrents et prévisibles sans repartir sur un modèle plus lourd, dis-le en commentaire, je prends.
Un détail Windows qui fait perdre une soirée
Sur Windows, la console encode par défaut d'une façon qui transforme "Tania Dictée" en mojibake dans les logs et dans l'exe packagé. Le fix tient en quelques lignes, tout en haut du script :
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
try:
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
Les try sont pas de la paranoïa décorative : une fois l'app packagée en exe sans console (PyInstaller --noconsole), sys.stdout vaut None et le .reconfigure() nu te sort un AttributeError au démarrage.
Petit, bête, et ça sent le vécu d'avoir fixé des accents cassés à 23h un mardi.
C'est du build-in-public honnête : Tania Dictée est à 0 vente pour l'instant, et le code est ouvert en MIT sur GitHub si tu veux fouiller ou reprendre des bouts. T'as un pipeline speech-to-text local qui tourne, c'est quoi ton filet à toi pour les ratés récurrents ?
Top comments (0)