<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: elboKazQC</title>
    <description>The latest articles on DEV Community by elboKazQC (@elbokazqc).</description>
    <link>https://dev.to/elbokazqc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3989535%2F6bc332b1-bfc3-40c5-832b-145b0624bb35.png</url>
      <title>DEV Community: elboKazQC</title>
      <link>https://dev.to/elbokazqc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elbokazqc"/>
    <language>en</language>
    <item>
      <title>Un raccourci clavier global sur Windows, c'est pas juste un if keydown</title>
      <dc:creator>elboKazQC</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:30:19 +0000</pubDate>
      <link>https://dev.to/elbokazqc/un-raccourci-clavier-global-sur-windows-cest-pas-juste-un-if-keydown-16em</link>
      <guid>https://dev.to/elbokazqc/un-raccourci-clavier-global-sur-windows-cest-pas-juste-un-if-keydown-16em</guid>
      <description>&lt;p&gt;La seule personne qui a commenté mes articles ici l'a fait sur exactement ça : « le coup du F6 enfoncé, je parle, je relâche, ça colle direct ». Cinq articles publiés, un seul commentateur, et il tombe pile sur le mécanisme que je pensais le plus trivial du produit. Fa qu'on va le creuser.&lt;/p&gt;

&lt;p&gt;Parce que c'est justement le morceau que j'ai le plus sous-estimé au début. Un raccourci global sur Windows, ça sonne comme un &lt;code&gt;if keydown: enregistre&lt;/code&gt; suivi d'un &lt;code&gt;if keyup: arrête&lt;/code&gt;. Dans les faits, entre le moment où tu tiens une touche et le moment où le texte atterrit au bon endroit, il y a une demi-douzaine de pièges que j'ai découverts un par un, en production, pas dans un tutoriel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toutes les touches ne font pas un bon raccourci global
&lt;/h2&gt;

&lt;p&gt;Le premier piège, c'est de choisir la touche. Une lettre seule (« j » par exemple) vole littéralement la frappe à toutes les applications ouvertes, parce que le raccourci est enregistré en mode suppression (&lt;code&gt;suppress=True&lt;/code&gt;) pour que la touche ne tape pas un caractère parasite dans ton éditeur : tape « j » n'importe où pendant que l'app tourne, et plus rien ne s'écrit à cet endroit.&lt;/p&gt;

&lt;p&gt;Les touches de fonction (F1 à F24) sont le compromis le moins pire : elles n'écrivent aucun caractère, donc elles ne détruisent pas la frappe. Libres pour autant, non : F1 ouvre l'aide, F5 rafraîchit, F11 passe en plein écran, F12 ouvre les outils de développement. Et comme le raccourci est enregistré en mode suppression, la touche choisie est réellement volée aux autres applications tant que l'app tourne. F6 est le défaut parce que c'est une des moins chargées, pas parce qu'elle est libre, et elle se change avec &lt;code&gt;--hotkey&lt;/code&gt;. Le code refuse une config bancale au démarrage plutôt que d'échouer en silence trois clics plus tard :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;single_function_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FUNCTION_KEYS&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;single_function_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hotkey must be a combo like ctrl+space or a function key like f6, got: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MODIFIER_KEYS&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hotkey combo must include a modifier key (ctrl/alt/shift/windows), got: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cancel key must not overlap the push-to-talk hotkey, got cancel=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; hotkey=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hotkey&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois règles, trois catégories de bug évitées avant même que l'app démarre : une touche seule qui n'est pas une touche de fonction, une combinaison sans modificateur (ctrl, alt, shift ou windows), et une touche d'annulation qui chevauche le raccourci lui-même. Ce dernier cas, je l'ai vécu en test : si la touche d'annulation partage une touche avec le hotkey, appuyer pour annuler peut redéclencher l'enregistrement au lieu de le couper. Mieux vaut planter au démarrage avec un message clair que planter en silence pendant que tu dictes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le relâchement de touche ment, dans les deux sens
&lt;/h2&gt;

&lt;p&gt;J'ai déjà raconté ici le bug qui m'a pris deux soirs : l'événement de relâchement de Windows qui ne se déclenche carrément jamais, à cause d'un changement de focus ou d'un autre hook clavier actif en même temps. Le fix, c'était de ne jamais faire confiance à l'événement et de revérifier l'état physique du clavier avec un timer.&lt;/p&gt;

&lt;p&gt;Ce que je n'avais pas encore raconté, c'est le problème inverse : il arrive qu'un relâchement parasite passe alors que la touche est encore enfoncée, typiquement pendant un changement de focus qui dure une fraction de seconde, ou quand un autre hook clavier réinjecte des événements. Sans anti-rebond, ce faux signal coupe l'enregistrement en plein milieu d'un mot, et la phrase ressort en deux clips séparés. Le même mécanisme règle les deux sens du problème : un timer qui attend un court délai, puis qui re-questionne le clavier avant de vraiment couper.&lt;/p&gt;

&lt;p&gt;Le timer, je l'ai déjà montré dans l'article sur ce bug. Ce qui compte ici, c'est ce qu'il va vérifier avant de couper :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_stop_if_hotkey_inactive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_pressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_recording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concrètement : le relâchement ne déclenche pas l'arrêt, il déclenche une question posée un peu plus tard. Si la touche est encore enfoncée, on continue. Sinon, on coupe. L'événement Windows n'est pas la vérité, juste une invitation à aller la vérifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'indicateur visuel peut voler ta fenêtre active
&lt;/h2&gt;

&lt;p&gt;Le piège le plus vicieux, c'est celui-là, parce qu'il ne casse rien de façon bruyante : il colle juste le texte au mauvais endroit. Quand Tania enregistre, une petite fenêtre d'état apparaît à l'écran. Si cette fenêtre devient la fenêtre active au moment du collage, le raccourci de collage part vers elle. Et comme elle n'a aucune zone de saisie, le texte ne va nulle part : ton éditeur n'a rien reçu et ta transcription est perdue sans un seul message d'erreur. Sur Windows, ça se règle au niveau du style de la fenêtre, pas dans la logique applicative :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;GWL_EXSTYLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="n"&gt;WS_EX_NOACTIVATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x08000000&lt;/span&gt;
&lt;span class="n"&gt;WS_EX_TOOLWINDOW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x00000080&lt;/span&gt;
&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetWindowLongW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hwnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GWL_EXSTYLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user32&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SetWindowLongW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hwnd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GWL_EXSTYLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;WS_EX_NOACTIVATE&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;WS_EX_TOOLWINDOW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WS_EX_NOACTIVATE&lt;/code&gt; dit à Windows que cette fenêtre n'a pas le droit de prendre le focus, jamais. &lt;code&gt;WS_EX_TOOLWINDOW&lt;/code&gt; la sort en plus de l'Alt+Tab et de la barre des tâches. Sans ces drapeaux, l'indicateur a l'air anodin à l'œil, mais plus rien ne l'empêche de prendre le focus quand il apparaît. Détail qui m'a coûté un moment de doute : le style étendu se pose sur une fenêtre déjà créée, alors je le réapplique à chaque réaffichage plutôt qu'une seule fois à l'initialisation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les modificateurs fantômes
&lt;/h2&gt;

&lt;p&gt;Dernier piège, plus discret : si tu tiens ctrl ou shift en parlant (un réflexe si t'étais en train de sélectionner du texte juste avant), ce modificateur peut encore être considéré comme enfoncé au moment où le code envoie ctrl+v pour coller. Résultat : le collage se transforme en autre chose selon l'application active. Le fix, c'est de relâcher explicitement les trois modificateurs avant d'envoyer le collage :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ctrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shift&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ctrl+v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois lignes qui coûtent rien, dans le chemin de collage par presse-papier. Ça ne défait pas la physique, si tu tiens vraiment la touche elle peut se ré-annoncer aussitôt, mais ça règle le cas courant du modificateur resté collé dans l'état du clavier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que le push-to-talk règle vraiment, et ce qu'il règle pas
&lt;/h2&gt;

&lt;p&gt;Il y a un gain qui n'est dans aucune des lignes ci-dessus, mais dans la forme même du geste. Un clip borné par un humain qui tient une touche n'a pas de longue traîne de silence au début ni à la fin. C'est un des terrains où les modèles de transcription hallucinent le plus, largement documenté sur Whisper, ces bouts de phrase (« merci », « sous-titres réalisés par... ») qui sortent de nulle part sur du silence pur. Un mode toggle ou une écoute continue avale forcément du silence et du bruit ambiant, donc ça a besoin d'un détecteur d'activité vocale pour compenser. Le push-to-talk transfère cette décision à l'utilisateur, qui la prend mieux qu'un seuil automatique.&lt;/p&gt;

&lt;p&gt;Ça reste un arbitrage, pas une victoire absolue. Tenir une touche pendant dix minutes de dictée continue, personne fait ça, fa que ce mode ne convient juste pas à la dictée longue. Et je vais le dire clairement parce que ça fait plusieurs articles que je le repousse : j'ai un protocole de banc d'essai pour mesurer un vrai taux d'erreur de transcription, et il n'a toujours pas tourné. Zéro chiffre de précision à montrer aujourd'hui, ici comme ailleurs dans la série. Le jour où ça tourne, ce sera un article à part, pas une ligne glissée dans celui-ci.&lt;/p&gt;

&lt;p&gt;Le code des pièges ci-dessus est dans &lt;code&gt;app.py&lt;/code&gt;, ouvert en MIT sur &lt;a href="https://github.com/elboKazQC/tania-dictee" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si vous avez déjà câblé un raccourci global sur Windows ou ailleurs, qu'est-ce qui a fini par piétiner un raccourci existant chez vous ? Je cherche encore les cas que j'ai pas prévus.&lt;/p&gt;

</description>
      <category>python</category>
      <category>windows</category>
      <category>buildinpublic</category>
      <category>opensource</category>
    </item>
    <item>
      <title>faster-whisper en local sur Windows, en français : les réglages qui ont vraiment changé ma sortie</title>
      <dc:creator>elboKazQC</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:31:58 +0000</pubDate>
      <link>https://dev.to/elbokazqc/faster-whisper-en-local-sur-windows-en-francais-les-reglages-qui-ont-vraiment-change-ma-sortie-4l34</link>
      <guid>https://dev.to/elbokazqc/faster-whisper-en-local-sur-windows-en-francais-les-reglages-qui-ont-vraiment-change-ma-sortie-4l34</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le modèle, c'est un choix de matériel, pas un chiffre
&lt;/h2&gt;

&lt;p&gt;Tania Dictée détecte le hardware au démarrage et choisit tout seul :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auto_detect_device&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;HAS_CUDA&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auto_detect_compute_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auto_detect_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;large-v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;medium&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GPU CUDA disponible : &lt;code&gt;large-v3&lt;/code&gt; en &lt;code&gt;float16&lt;/code&gt;. Sinon, CPU : &lt;code&gt;medium&lt;/code&gt; en &lt;code&gt;int8&lt;/code&gt;. Le &lt;code&gt;int8&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;La tentation, quand ça roule sur CPU, c'est de descendre à &lt;code&gt;tiny&lt;/code&gt; 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. &lt;code&gt;medium&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les paramètres de transcribe() qui comptent
&lt;/h2&gt;

&lt;p&gt;L'appel, avec les valeurs par défaut de l'app (&lt;code&gt;language&lt;/code&gt; et &lt;code&gt;beam_size&lt;/code&gt; sortent en réalité de la config) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;wav_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;beam_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vad_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;condition_on_previous_text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;word_timestamps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;initial_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initial_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hotwords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotwords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;language="fr"&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;temperature=0.0&lt;/code&gt; : 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 &lt;code&gt;0.0&lt;/code&gt; désactive ce filet. Reproductible, mais plus de deuxième chance sur un clip pourri. C'est un arbitrage, pas un réglage gratuit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;word_timestamps=False&lt;/code&gt; : inutile pour de la dictée, et ça évite le passage d'alignement.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;condition_on_previous_text=True&lt;/code&gt; : 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 &lt;code&gt;transcribe()&lt;/code&gt; 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 à &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vad_filter=True&lt;/code&gt; : 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  hotwords, le réglage que tout le monde sous-estime
&lt;/h2&gt;

&lt;p&gt;Tania Dictée embarque une liste de termes par défaut, en dur dans le code (&lt;code&gt;DEFAULT_GLOSSARY_TERMS&lt;/code&gt;) : 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 &lt;code&gt;glossary.txt&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Ce glossaire est passé DEUX fois au modèle :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;glossary_terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_glossary_terms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;glossary_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_initial_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glossary_terms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;hotwords_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glossary_terms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;glossary_terms&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Une fois fondu dans l'&lt;code&gt;initial_prompt&lt;/code&gt;, une fois tel quel dans &lt;code&gt;hotwords&lt;/code&gt;. La différence est plus subtile que ce que je croyais au départ. Dans faster-whisper, &lt;code&gt;hotwords&lt;/code&gt; 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 &lt;code&gt;sot_prev&lt;/code&gt;, celui du texte précédent), avant les tokens de l'&lt;code&gt;initial_prompt&lt;/code&gt;. C'est &lt;code&gt;get_prompt()&lt;/code&gt; dans &lt;code&gt;transcribe.py&lt;/code&gt;. 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 &lt;code&gt;hotwords&lt;/code&gt; est ignoré si tu passes un &lt;code&gt;prefix&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;Attention à la version : &lt;code&gt;hotwords&lt;/code&gt; n'existe pas dans les vieilles faster-whisper. Sur une version trop ancienne, tu vas manger un &lt;code&gt;TypeError: transcribe() got an unexpected keyword argument 'hotwords'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le prompt qui dit au modèle de ne pas traduire
&lt;/h2&gt;

&lt;p&gt;L'&lt;code&gt;initial_prompt&lt;/code&gt; 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 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le filet regex, sale mais honnête
&lt;/h2&gt;

&lt;p&gt;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, &lt;code&gt;SOFT_REPLACEMENTS&lt;/code&gt; dans le code (une trentaine d'entrées, en voici six) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SOFT_REPLACEMENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bpaillette\s*on\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bno\s*jeasse\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Node.js&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bdock\s*heure\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bouisp(er|eur)?\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Whisper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bvs codes?\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VS Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bia\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_soft_replacements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SOFT_REPLACEMENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;flags=re.IGNORECASE&lt;/code&gt; est pas décoratif : Whisper te ressort le même raté en majuscule ou en minuscule selon la ponctuation autour.&lt;/p&gt;

&lt;p&gt;"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.&lt;/p&gt;

&lt;p&gt;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 (&lt;code&gt;--cleanup-mode raw&lt;/code&gt; rend la sortie brute de Whisper).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un détail Windows qui fait perdre une soirée
&lt;/h2&gt;

&lt;p&gt;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 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les &lt;code&gt;try&lt;/code&gt; sont pas de la paranoïa décorative : une fois l'app packagée en exe sans console (PyInstaller &lt;code&gt;--noconsole&lt;/code&gt;), &lt;code&gt;sys.stdout&lt;/code&gt; vaut &lt;code&gt;None&lt;/code&gt; et le &lt;code&gt;.reconfigure()&lt;/code&gt; nu te sort un &lt;code&gt;AttributeError&lt;/code&gt; au démarrage.&lt;/p&gt;

&lt;p&gt;Petit, bête, et ça sent le vécu d'avoir fixé des accents cassés à 23h un mardi.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://github.com/elboKazQC/tania-dictee" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; 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 ?&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>opensource</category>
    </item>
    <item>
      <title>3 semaines de build-in-public plus tard : 4 abonnés, 0 vente, et le bug de hotkey qui m'a pris 2 soirs</title>
      <dc:creator>elboKazQC</dc:creator>
      <pubDate>Mon, 06 Jul 2026 16:20:40 +0000</pubDate>
      <link>https://dev.to/elbokazqc/3-semaines-de-build-in-public-plus-tard-4-abonnes-0-vente-et-le-bug-de-hotkey-qui-ma-pris-2-jfh</link>
      <guid>https://dev.to/elbokazqc/3-semaines-de-build-in-public-plus-tard-4-abonnes-0-vente-et-le-bug-de-hotkey-qui-ma-pris-2-jfh</guid>
      <description>&lt;p&gt;Ça fait trois semaines que je publie ici en mode transparent sur Tania Dictée, mon outil de dictée vocale FR-Québec. Le premier article avait un titre franc : 0 inscrit, 3 jours pour valider l'idée. Il a eu une réaction. Les deux suivants, sur le code-switching et la vie privée, zéro.&lt;/p&gt;

&lt;p&gt;Alors on continue avec les vrais chiffres, plus un bug qui m'a fait perdre deux soirées et qui vaut la peine d'être raconté.&lt;/p&gt;

&lt;h2&gt;
  
  
  Les chiffres, sans les habiller
&lt;/h2&gt;

&lt;p&gt;Sur dev.to : 3 articles publiés, 4 abonnés au blog. Les 4 sont arrivés en silence, aucun commentaire, aucune réaction associée. Moins de 25 vues par article. Une seule réaction au total, sur le tout premier post.&lt;/p&gt;

&lt;p&gt;Sur X (@TaniaDictee) : 1 abonné. Sur Hacker News, mon karma est à 1. Le repo GitHub a 0 star.&lt;/p&gt;

&lt;p&gt;Zéro vente. Le produit est à 9$ CAD sur Gumroad depuis des semaines, personne n'a encore acheté.&lt;/p&gt;

&lt;p&gt;Je pourrais dramatiser ou je pourrais dire que c'est normal pour un projet solo à budget zéro. La vérité est entre les deux : c'est petit, mais dev.to est le seul endroit où quelque chose bouge, même faiblement. Alors j'y mets plus d'énergie plutôt que de disperser sur cinq canaux qui ne répondent pas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le bug qui m'a pris deux soirs
&lt;/h2&gt;

&lt;p&gt;Tania Dictée fonctionne en push-to-talk : tu tiens F6, tu parles, tu relâches, ça transcrit et ça colle le texte où t'as le curseur. Simple sur papier. Sauf que la détection du relâchement de touche sur Windows n'est pas aussi fiable qu'on pense.&lt;/p&gt;

&lt;p&gt;Le module &lt;code&gt;keyboard&lt;/code&gt; en Python expose deux événements : &lt;code&gt;add_hotkey&lt;/code&gt; pour la pression, &lt;code&gt;on_release_key&lt;/code&gt; pour le relâchement. Mon premier réflexe a été de démarrer l'enregistrement sur la pression et de l'arrêter sur le relâchement. Ça marchait... la plupart du temps.&lt;/p&gt;

&lt;p&gt;Le problème : si tu relâches F6 pendant qu'une autre fenêtre a le focus, ou si Windows avale l'événement à cause d'un conflit avec un autre hook clavier actif (j'en avais un autre qui tournait pour une raison complètement différente), l'événement de relâchement ne se déclenche jamais. Résultat : Tania reste convaincue que tu parles encore. Elle enregistre en boucle jusqu'à ce que tu réappuies sur F6, ce qui bascule l'état dans le mauvais sens et fige l'app dans un état bizarre.&lt;/p&gt;

&lt;p&gt;La vraie source du bug, ce n'était pas mon code d'enregistrement, c'était une hypothèse fausse : je pensais qu'un événement clavier Windows me serait toujours livré. Dans mon cas il ne l'était pas, surtout avec plusieurs hooks actifs en parallèle.&lt;/p&gt;

&lt;p&gt;La solution qui a fini par tenir : au lieu de dépendre uniquement de l'événement &lt;code&gt;on_release_key&lt;/code&gt;, j'ai ajouté un &lt;code&gt;threading.Timer&lt;/code&gt; qui revérifie activement l'état des touches à intervalle court avec &lt;code&gt;keyboard.is_pressed()&lt;/code&gt;. Si aucune des touches du hotkey n'est physiquement pressée, on arrête l'enregistrement, peu importe si l'événement de relâchement est arrivé ou pas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_schedule_stop_if_hotkey_released&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# debounce configurable, pas un chiffre magique code en dur
&lt;/span&gt;    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;release_debounce_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stop_if_hotkey_inactive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daemon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_stop_if_hotkey_inactive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# on ne fait pas confiance a l'evenement de relachement :
&lt;/span&gt;    &lt;span class="c1"&gt;# on verifie l'etat physique reel des touches
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_pressed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hotkey_keys&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_recording&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En gros : ne fais pas confiance à l'événement, vérifie l'état réel. Ça semble évident écrit comme ça, mais sur le coup j'ai passé deux soirées à ajouter des logs, suspecter le driver du micro, suspecter faster-whisper, avant de réaliser que le vrai coupable était une hypothèse sur le comportement du clavier que je n'avais jamais vérifiée.&lt;/p&gt;

&lt;p&gt;Il y a un deuxième détail qui a mordu en même temps : &lt;code&gt;add_hotkey&lt;/code&gt; avec &lt;code&gt;suppress=True&lt;/code&gt; empêche la touche d'atteindre les autres apps (utile pour que F6 ne tape pas un caractère parasite dans ton éditeur), mais ça change aussi comment Windows route certains événements de relâchement selon le contexte de focus. Ce n'est documenté nulle part de façon limpide, il a fallu tester à la main pour comprendre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que ça change concrètement
&lt;/h2&gt;

&lt;p&gt;Depuis ce fix, Tania tourne chez moi tous les jours sans ce comportement fantôme. C'est le genre de bug qu'un utilisateur externe aurait rencontré dans sa première semaine et qui aurait tué la confiance direct. Bonne nouvelle : c'est réglé avant que qui que ce soit d'autre que moi ne l'ait vécu, parce que pour l'instant je suis mon seul utilisateur.&lt;/p&gt;

&lt;p&gt;C'est aussi une leçon plus large sur le solo dev : quand t'es le seul testeur, les bugs les plus vicieux sont ceux qui dépendent du contexte système (autres apps ouvertes, autres hooks actifs, focus de fenêtre) et que tu ne reproduis pas facilement en isolation. Le fix a fini par être plus robuste que l'approche initiale précisément parce qu'il ne fait plus confiance à un seul signal.&lt;/p&gt;

&lt;p&gt;Au fond, c'est pour ça que je bâtis ce truc : si tu dictes en français québécois une bonne partie de ta journée et que l'idée d'envoyer ta voix dans le cloud te dérange, c'est exactement l'outil que j'aurais voulu trouver au lieu de le coder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Où on en est
&lt;/h2&gt;

&lt;p&gt;Le code reste ouvert sous licence MIT sur &lt;a href="https://github.com/elboKazQC/tania-dictee" rel="noopener noreferrer"&gt;github.com/elboKazQC/tania-dictee&lt;/a&gt; si le bug ou le hotkey handling vous intéresse en détail. Toujours 0 star, ça ne me dérange pas de le dire.&lt;/p&gt;

&lt;p&gt;La suite : je continue de nourrir dev.to en priorité puisque c'est le seul canal où il se passe quelque chose, même petit. Prochain post, probablement le premier vrai benchmark de précision (WER) que j'ai promis dans les articles précédents et que je n'ai toujours pas publié. Il est temps.&lt;/p&gt;

&lt;p&gt;Si vous avez déjà chassé un bug qui se cachait derrière une hypothèse fausse sur un événement système (clavier, réseau, fichier), ça m'intéresse de l'entendre en commentaire.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>opensource</category>
      <category>python</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Dicter du code confidentiel : pourquoi j'ai arrêté d'envoyer ma voix dans le cloud</title>
      <dc:creator>elboKazQC</dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:22:35 +0000</pubDate>
      <link>https://dev.to/elbokazqc/dicter-du-code-confidentiel-pourquoi-jai-arrete-denvoyer-ma-voix-dans-le-cloud-1e9d</link>
      <guid>https://dev.to/elbokazqc/dicter-du-code-confidentiel-pourquoi-jai-arrete-denvoyer-ma-voix-dans-le-cloud-1e9d</guid>
      <description>&lt;p&gt;Je suis consultant IA. La moitié de mes journées, je pilote des modèles de langage pour des clients. Je dicte beaucoup : des prompts à Claude Code, des notes de conception, des noms de variables, des bouts de logique métier qui ne sont pas encore publics.&lt;/p&gt;

&lt;p&gt;Pendant un bon moment, j'ai utilisé une solution de dictée cloud. Commode. Ça marche bien. Et puis un jour j'ai réalisé ce qui part réellement sur leurs serveurs quand j'appuie sur le bouton d'enregistrement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que ta voix contient vraiment
&lt;/h2&gt;

&lt;p&gt;Quand tu dictes un prompt à un outil de code ou à un agent IA, t'as pas l'impression de transmettre quelque chose de sensible. T'es dans ta bulle, t'es chez toi, c'est juste toi qui parles à un micro.&lt;/p&gt;

&lt;p&gt;Mais dans ce flux audio, il y a le nom du produit client qui n'est pas encore annoncé. Il y a la logique d'un flux de données propriétaire. Il y a les noms des tables, des endpoints, des règles d'affaires. Il y a les acronymes internes qui permettent à n'importe qui de chercher sur Google et de comprendre dans quelle industrie tu travailles.&lt;/p&gt;

&lt;p&gt;Pour les outils de dictée cloud comme Wispr Flow, Otter ou les fonctions vocales intégrées à divers services, cet audio transite par des serveurs distants. Ce qui arrive ensuite, c'est leur affaire : rétention du contenu pour amélioration du modèle, accès interne potentiel, politiques qui changent silencieusement d'une mise à jour des CGU à l'autre.&lt;/p&gt;

&lt;p&gt;Sous NDA, t'es responsable de ça. Pas eux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que "100 % local" veut dire en pratique
&lt;/h2&gt;

&lt;p&gt;La promesse d'une solution locale, c'est que l'audio ne quitte jamais ton disque. Point. Pas de requête réseau, pas de token envoyé quelque part, pas de métadonnée. Le modèle Whisper tourne directement sur ta machine.&lt;/p&gt;

&lt;p&gt;Il y a un test simple pour le vérifier : tu coupes ta connexion Wi-Fi, tu dictes, tu lâches le bouton. Si la transcription arrive, c'est 100 % local. Sinon, quelque chose part quelque part.&lt;/p&gt;

&lt;p&gt;Tu peux aussi ouvrir un moniteur réseau (Little Snitch, Glasswire, ou simplement Wireshark si t'as le goût de souffrir un peu) pendant une session de dictée, et regarder si des connexions sortantes s'ouvrent. Avec une solution vraiment locale, t'as rien. C'est le silence réseau qui prouve la confidentialité, pas la politique de vie privée du vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le comment technique, sans faire semblant que c'est compliqué
&lt;/h2&gt;

&lt;p&gt;Le moteur sous le capot, c'est faster-whisper, une implémentation optimisée de Whisper par Systran. Plus rapide que l'originale d'OpenAI sur CPU, et elle tourne proprement sur Windows sans configuration ésotérique.&lt;/p&gt;

&lt;p&gt;La première fois que tu l'actives, le modèle (quelques centaines de mégaoctets selon la taille choisie) se télécharge une fois. Après ça, plus besoin de connexion. Si tu veux voir à quoi ressemble un appel de base :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;faster_whisper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WhisperModel&lt;/span&gt;

&lt;span class="n"&gt;modele&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WhisperModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compute_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;int8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modele&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audio.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;initial_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;useState, Stripe, fa que, pis, API REST, webhook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le paramètre &lt;code&gt;initial_prompt&lt;/code&gt; est la pièce souvent oubliée. Il force le modèle à s'attendre à certains mots, ce qui améliore la transcription des termes techniques et des expressions québécoises. Tu peux y mettre des noms de variables, des mots de ton domaine, des expressions typiques de ton projet. La limite est d'environ 224 tokens, pis ça se remplit vite, fa que sois sélectif.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le compromis honnête
&lt;/h2&gt;

&lt;p&gt;Je vais pas te vendre ça comme une solution parfaite, parce que c'est pas le cas.&lt;/p&gt;

&lt;p&gt;L'accent québécois demeure plus difficile pour Whisper que le français de France standard. Sur un texte technique avec du vocabulaire précis, le glossaire dans &lt;code&gt;initial_prompt&lt;/code&gt; aide vraiment. Sur une phrase purement conversationnelle avec des expressions régionales denses, attends-toi à plus de corrections manuelles qu'en FR standard. C'est la réalité. Je publie pas de chiffres de taux d'erreur parce que j'ai pas encore fait le benchmark formel, pis je vais pas inventer un nombre.&lt;/p&gt;

&lt;p&gt;C'est aussi Windows uniquement pour l'instant. Mac et Linux, c'est pas là. Et l'interface reste en phase alpha : fonctionnelle, mais elle a pas encore le polish d'un produit mature.&lt;/p&gt;

&lt;p&gt;Le setup de base demande quelques étapes techniques si tu pars de zéro. C'est précisément pourquoi j'ai fini par empaqueter tout ça.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi j'ai packagé ça
&lt;/h2&gt;

&lt;p&gt;J'avais déjà monté ce pipeline pour moi-même. Faster-whisper installé, un petit script Python, push-to-talk sur F6 (maintiens pour parler, relâche pour transcrire), intégration au presse-papier pour coller dans n'importe quelle app. Ça marchait.&lt;/p&gt;

&lt;p&gt;J'ai montré ça à quelques personnes. La réaction habituelle : "c'est cool, mais je vais pas m'asseoir pour installer tout ça."&lt;/p&gt;

&lt;p&gt;Fa que j'ai fait l'installeur. 63 MB. Tu lance, t'installes, t'appuies sur F6. Le code est ouvert sous licence MIT sur &lt;a href="https://github.com/elboKazQC/tania-dictee" rel="noopener noreferrer"&gt;github.com/elboKazQC/tania-dictee&lt;/a&gt; si tu veux voir exactement ce qui tourne sur ta machine (pis c'est là que la transparence privacy commence vraiment : open source, pas juste une promesse dans une FAQ).&lt;/p&gt;

&lt;p&gt;La version packagée est disponible à 9 $ CAD, achat unique, sur Gumroad. Pas d'abonnement.&lt;/p&gt;

&lt;p&gt;Zéro utilisateur externe à date. Je suis en mode build-in-public, pis je te parle depuis le début du tunnel, pas depuis l'autre bout.&lt;/p&gt;

&lt;p&gt;Si le sujet t'intéresse, la waitlist est ici : &lt;a href="https://elbokazqc.github.io/tania-dictee" rel="noopener noreferrer"&gt;elbokazqc.github.io/tania-dictee&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>privacy</category>
      <category>ai</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>La dictée vocale en français québécois, c'est pas un gadget : c'est un problème de code-switching</title>
      <dc:creator>elboKazQC</dc:creator>
      <pubDate>Fri, 26 Jun 2026 18:17:19 +0000</pubDate>
      <link>https://dev.to/elbokazqc/la-dictee-vocale-en-francais-quebecois-cest-pas-un-gadget-cest-un-probleme-de-code-switching-1fjg</link>
      <guid>https://dev.to/elbokazqc/la-dictee-vocale-en-francais-quebecois-cest-pas-un-gadget-cest-un-probleme-de-code-switching-1fjg</guid>
      <description>&lt;p&gt;J'utilise la dictée vocale tous les jours depuis six mois. Pas pour taper moins vite. Pour penser plus vite quand je vibe-code avec Claude Code et Cursor.&lt;/p&gt;

&lt;p&gt;Pis j'ai fini par construire mon propre outil parce que les outils existants me tapaient sur les nerfs d'une façon très précise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le problème réel
&lt;/h2&gt;

&lt;p&gt;Quand tu travailles en tech au Québec, tes phrases ressemblent à ça :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"OK fa que je fais un useState pour le component pis je passe le handler en props"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ça, c'est une phrase normale. Personne en tech QC ne parle autrement. Pas parce qu'on est négligents avec la langue. Parce que le vocabulaire technique vient de l'anglais et qu'on le soude naturellement au français au fil de la pensée.&lt;/p&gt;

&lt;p&gt;Ça s'appelle le code-switching. Et c'est là que la plupart des outils de dictée craquent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que les outils mainstream font mal
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dragon NaturallySpeaking
&lt;/h3&gt;

&lt;p&gt;Dragon, c'est le vieux standard. Médical, juridique, corporate. Ça coûte environ 500$ en une shot. C'est lourd à installer et à entraîner. Et sa gestion du français québécois avec des termes tech intercalés... c'est en gros zéro.&lt;/p&gt;

&lt;p&gt;"useState" devient "usé état". "Fa que" devient "faque" parfois, "fake" d'autres fois. C'est aléatoire. T'as intérêt à corriger après chaque phrase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wispr Flow
&lt;/h3&gt;

&lt;p&gt;Wispr Flow est plus moderne. UX propre, cross-platform, et leur gestion du français s'est améliorée. Leur plan Pro coûte 15$/mois, soit environ 144$/an.&lt;/p&gt;

&lt;p&gt;Mais il y a un problème structurel que leur propre doc admet : la détection de langue se fait par session, pas par mot.&lt;/p&gt;

&lt;p&gt;Autrement dit : Wispr détecte la langue une fois au début de la session. Si tu commences en français, il reste en mode français jusqu'à la fin. Les mots anglais qui arrivent dans la phrase, il tente de les translittérer en français. "Handler" peut devenir "andler" ou "ender", "props" survit parfois, parfois pas. C'est variable.&lt;/p&gt;

&lt;p&gt;Pour une phrase de temps en temps avec un mot anglais, ça passe. Pour un vibe-coder québécois qui switch constamment dans la même phrase, ça ne passe pas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi c'est un vrai problème, pas un détail
&lt;/h2&gt;

&lt;p&gt;La dictée vocale utile, c'est quand tu peux parler aussi vite que tu penses et que la transcription suit. Le moment où tu dois ralentir pour articuler différemment, ou corriger constamment après, tu perds exactement l'avantage pour lequel t'as adopté l'outil.&lt;/p&gt;

&lt;p&gt;J'ai fini par faire le calcul : le temps de correction de franglais = le temps que je sauvais à dicter. Retour à la case départ.&lt;/p&gt;

&lt;p&gt;Fa que j'ai décidé de régler ça directement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que j'ai construit
&lt;/h2&gt;

&lt;p&gt;Tania Dictée est un outil de dictée vocale Windows, 100% local. Tu maintiens F6, tu parles, tu relâches. Le texte se colle dans n'importe quelle app active : VS Code, Cursor, Claude Code, Gmail, peu importe.&lt;/p&gt;

&lt;p&gt;La différence principale par rapport aux outils existants :&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traitement par phrase, pas par session.&lt;/strong&gt; Chaque phrase passe à travers Whisper (via faster-whisper) avec un glossaire FR-QC intégré d'office. "useState", "handler", "fa que", "pis", "Supabase", "Cursor", tout ça est dans le glossaire. Le modèle sait que c'est du franglais québécois. Il n'essaie pas de choisir une langue et de tout forcer dedans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100% local, zéro cloud.&lt;/strong&gt; Ce que tu dictes reste sur ton PC. Pas de compte à créer. Pas de tokens qui partent sur un serveur externe. Pour un consultant sous NDA ou un dev avec du code proprio, ça compte.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows-first, sans friction.&lt;/strong&gt; Installeur d'environ 63 MB. Pas de Python à installer, pas de droits admin. Tu dézippe, tu lances, ça marche.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prix : 9$ CAD une seule fois.&lt;/strong&gt; Pas d'abonnement. Un achat, c'est fini.&lt;/p&gt;

&lt;p&gt;Le code est open source MIT sur GitHub (github.com/elboKazQC/tania-dictee).&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que je ne prétends pas (build-in-public oblige)
&lt;/h2&gt;

&lt;p&gt;Je construis en transparent, alors voici ce qui est vrai à date :&lt;/p&gt;

&lt;p&gt;Zéro utilisateur externe. C'est moi qui l'utilise tous les jours. La première vraie validation externe, c'est ce que je cherche en ce moment.&lt;/p&gt;

&lt;p&gt;Pas de version Mac ou Linux. Windows only, point.&lt;/p&gt;

&lt;p&gt;L'UX est en alpha. Il n'y a pas encore de paramètres graphiques élaborés, pas d'onboarding fancy.&lt;/p&gt;

&lt;p&gt;Le benchmark de précision (protocole WER contre Whisper vanilla et Dragon) n'est pas encore publié. Le protocole est prêt et les mesures arrivent. Je ne vais pas inventer des chiffres d'ici là.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi ça peut quand même marcher
&lt;/h2&gt;

&lt;p&gt;La niche est petite mais réelle : développeurs et consultants qui travaillent en franglais québécois, qui vibe-codent, qui ont du code proprio à protéger. Les outils qui existent ne sont pas conçus pour eux.&lt;/p&gt;

&lt;p&gt;La preuve du concept existe : moi. Six mois d'usage quotidien, ça m'a économisé du temps réel. Si ça marche pour moi, il y a probablement d'autres personnes avec le même problème.&lt;/p&gt;

&lt;p&gt;Si t'as déjà pesté contre Dragon ou Wispr Flow quand tu dictes du franglais tech, j'aimerais t'entendre. Qu'est-ce qui t'a le plus agacé ? Est-ce que c'était le code-switching, la précision globale, ou autre chose ?&lt;/p&gt;

&lt;p&gt;La waitlist est à &lt;a href="https://elbokazqc.github.io/tania-dictee" rel="noopener noreferrer"&gt;https://elbokazqc.github.io/tania-dictee&lt;/a&gt;. 9$ CAD, achat unique, si jamais tu veux soutenir le projet et être parmi les premiers testeurs externes.&lt;/p&gt;

&lt;p&gt;-- Vincent&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>productivity</category>
      <category>ai</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>Build-in-public : 0 inscrit, 3 jours pour valider mon outil de dictée vocale FR/QC</title>
      <dc:creator>elboKazQC</dc:creator>
      <pubDate>Wed, 17 Jun 2026 17:12:50 +0000</pubDate>
      <link>https://dev.to/elbokazqc/build-in-public-0-inscrit-3-jours-pour-valider-mon-outil-de-dictee-vocale-frqc-54ma</link>
      <guid>https://dev.to/elbokazqc/build-in-public-0-inscrit-3-jours-pour-valider-mon-outil-de-dictee-vocale-frqc-54ma</guid>
      <description>&lt;p&gt;Je lance un compte à rebours public.&lt;/p&gt;

&lt;p&gt;Point de contrôle : 20 juin 2026.&lt;br&gt;
Cible : 25 inscrits à la waitlist.&lt;br&gt;
En-dessous, je repense le plan.&lt;/p&gt;

&lt;p&gt;Voilà où on en est.&lt;/p&gt;

&lt;h2&gt;
  
  
  C'est quoi Tania Dictée
&lt;/h2&gt;

&lt;p&gt;Un outil de dictée vocale Windows, 100 % local (Whisper via faster-whisper : modèle small sur CPU, large-v3 sur GPU), optimisé pour le français québécois et le franglais. Tu maintiens F6, tu parles, tu relâches, ton texte apparaît dans n'importe quelle app active.&lt;/p&gt;

&lt;p&gt;Pourquoi j'ai bâti ça : j'ai essayé Dragon NaturallySpeaking et Wispr Flow. Aucun des deux ne gère le code-switching FR/EN dans la même phrase. Et Wispr Flow à 144 $/an pour transcrire mon code à moitié de travers, c'est non.&lt;/p&gt;

&lt;p&gt;Résultat : j'ai bâti le mien. Ça fait 6 mois que je l'utilise tous les jours pour vibe coder avec Claude Code et Cursor.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'état du produit (honnête)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Installeur Windows (63 Mo, pas de Python, pas de droits admin requis)&lt;/li&gt;
&lt;li&gt;Zéro cloud, zéro compte, zéro abonnement&lt;/li&gt;
&lt;li&gt;Glossaire FR-QC inclus (Cursor, Supabase, useState, "pis", "fa que"...)&lt;/li&gt;
&lt;li&gt;Prix : 9 $ CAD sur Gumroad&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ce qui manque encore : version Mac, UX plus polie, benchmark WER public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi je publie ça maintenant
&lt;/h2&gt;

&lt;p&gt;Le produit marche pour moi, mais j'ai 0 donnée sur s'il aide quelqu'un d'autre.&lt;/p&gt;

&lt;p&gt;Le seul moyen de le savoir : aller chercher des early adopters, leur faire tester, voir si ça colle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le plan des 3 prochains jours
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Posts X @TaniaDictee quotidiens (build-in-public transparent)&lt;/li&gt;
&lt;li&gt;Découvrabilité GitHub (annuaires d'outils Whisper)&lt;/li&gt;
&lt;li&gt;Point de contrôle public le 20 juin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si tu es dev québécois, consultant qui dicte beaucoup, ou juste curieux d'un outil Whisper local sur Windows : &lt;a href="https://elbokazqc.github.io/tania-dictee" rel="noopener noreferrer"&gt;waitlist ici&lt;/a&gt; ou achat direct 9 $ CAD.&lt;/p&gt;

&lt;p&gt;Pas de spam. Juste les updates concrètes.&lt;/p&gt;

&lt;p&gt;-- Vincent&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>indiehackers</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
