<?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: Ahmed Ibrahim</title>
    <description>The latest articles on DEV Community by Ahmed Ibrahim (@ahmed_ibrahim_1994).</description>
    <link>https://dev.to/ahmed_ibrahim_1994</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%2F4160387%2Fc642d598-563b-4132-b18b-5fb929505b25.jpeg</url>
      <title>DEV Community: Ahmed Ibrahim</title>
      <link>https://dev.to/ahmed_ibrahim_1994</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_ibrahim_1994"/>
    <language>en</language>
    <item>
      <title>Your voice typing app will type your password into a login box. Here is how to stop it</title>
      <dc:creator>Ahmed Ibrahim</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:42:18 +0000</pubDate>
      <link>https://dev.to/ahmed_ibrahim_1994/your-voice-typing-app-will-type-your-password-into-a-login-box-here-is-how-to-stop-it-b9f</link>
      <guid>https://dev.to/ahmed_ibrahim_1994/your-voice-typing-app-will-type-your-password-into-a-login-box-here-is-how-to-stop-it-b9f</guid>
      <description>&lt;p&gt;A dictation app has a simple job: you hold a key, say a sentence, and the text is typed into whatever window has focus. That is also its worst failure mode. Click into a login page, hold the key out of habit, say a few words, and they land in the password box. You cannot see them, a wrong word is invisible, and a copy of what you said has just been saved to the app's history.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ahmedhmam1994/voxscribe-ai-voice-dictation" rel="noopener noreferrer"&gt;VoxScribe&lt;/a&gt;, a free and open source dictation app for Windows and Android, has to make sure that never happens. Here is how the Windows side works, including the one bug that made the check look broken for browsers.&lt;/p&gt;

&lt;p&gt;The rules were strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It must never raise an exception.&lt;/li&gt;
&lt;li&gt;It must never make the hotkey wait.&lt;/li&gt;
&lt;li&gt;It must not add a dependency to the installer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check 1: the classic Win32 password style
&lt;/h2&gt;

&lt;p&gt;Old desktop apps and many Windows dialogs use a plain edit control with the &lt;code&gt;ES_PASSWORD&lt;/code&gt; style. That check is cheap and instant, using only &lt;code&gt;user32&lt;/code&gt;:&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;_ES_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0020&lt;/span&gt;
&lt;span class="n"&gt;_GWL_STYLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_win32_focus_is_password&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user32&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="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_GUITHREADINFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cbSize&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="nf"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_GUITHREADINFO&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;GetGUIThreadInfo&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="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hwndFocus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;class_name&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="nf"&gt;create_unicode_buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&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;GetClassNameW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hwndFocus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&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;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hwndFocus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_GWL_STYLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_ES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where most tutorials stop, and it is why most implementations are wrong. A browser does not use Windows edit controls. Chrome, Edge and Electron draw their own text boxes, so &lt;code&gt;GetClassNameW&lt;/code&gt; returns something like &lt;code&gt;Chrome_RenderWidgetHostHWND&lt;/code&gt; and the style check sees nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: UI Automation knows
&lt;/h2&gt;

&lt;p&gt;UI Automation exposes an &lt;code&gt;IsPassword&lt;/code&gt; property on the focused element, and browsers set it for &lt;code&gt;&amp;lt;input type="password"&amp;gt;&lt;/code&gt;. The question is how to ask it from Python without bundling a COM library. The answer is raw vtable calls with &lt;code&gt;ctypes&lt;/code&gt;. Two methods are all you need:&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="c1"&gt;# IUIAutomation: IUnknown (0-2), CompareElements, CompareRuntimeIds,
# GetRootElement, ElementFromHandle, ElementFromPoint, GetFocusedElement (8).
&lt;/span&gt;&lt;span class="n"&gt;_IUIAUTOMATION_GET_FOCUSED_ELEMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="c1"&gt;# IUIAutomationElement: ..., GetCurrentPropertyValue (10).
&lt;/span&gt;&lt;span class="n"&gt;_ELEMENT_GET_CURRENT_PROPERTY_VALUE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;_UIA_IS_PASSWORD_PROPERTY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30019&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_com_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;argtypes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vtable&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="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&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="nc"&gt;POINTER&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="nc"&gt;POINTER&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;c_void_p&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="n"&gt;contents&lt;/span&gt;
    &lt;span class="n"&gt;prototype&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="nc"&gt;WINFUNCTYPE&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;HRESULT&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;c_void_p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;argtypes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vtable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One trap in the result: &lt;code&gt;IsPassword&lt;/code&gt; comes back in a &lt;code&gt;VARIANT&lt;/code&gt;, and a &lt;code&gt;VARIANT_BOOL&lt;/code&gt; uses &lt;code&gt;-1&lt;/code&gt; (&lt;code&gt;0xFFFF&lt;/code&gt;) for true, not &lt;code&gt;1&lt;/code&gt;. Compare against zero after masking, or every password field reads as false:&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;is_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;_VT_BOOL&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bug that looked like a failed check
&lt;/h2&gt;

&lt;p&gt;In Edge, the check said "not a password field" on a login page, even though the password box was right there.&lt;/p&gt;

&lt;p&gt;The cause: Chromium builds its accessibility tree lazily. The very first time a UI Automation client asks, the focused element is just the bare window &lt;code&gt;Pane&lt;/code&gt;, not the text box. The second question, a moment later, returns the real element. That first answer looked exactly like "this is not a password field".&lt;/p&gt;

&lt;p&gt;The fix is small: if the first answer is not a password but the element is a bare pane, wait 150 ms and ask once more.&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;is_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_pane&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_ask_focused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;automation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_password&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;is_pane&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_CHROMIUM_WAKE_SEC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 0.15
&lt;/span&gt;    &lt;span class="n"&gt;is_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_ask_focused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;automation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later questions are answered immediately, so the retry only costs time once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making sure it never blocks
&lt;/h2&gt;

&lt;p&gt;UI Automation calls can stall for seconds when the target app is hung. A dictation hotkey cannot wait on that. Three decisions keep it safe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Its own thread with its own COM apartment.&lt;/strong&gt; Every question goes to a worker thread and waits at most 0.5 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A busy flag.&lt;/strong&gt; If the worker is still stuck on an earlier question, a new one returns immediately instead of queueing behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip our own window.&lt;/strong&gt; Asking UI Automation about the app's own window from inside the app waits on the app's own busy UI thread. So if the foreground window belongs to this process, the check is skipped.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What happens when it says "password"
&lt;/h2&gt;

&lt;p&gt;The first check runs as soon as the text comes back from the speech model, before anything is shown or saved. If it reports a password field, the text is not typed, and it is not written to the transcript or the history either. A spoken password never reaches the disk. A second check runs just before typing, after the target window is refocused, and it stops the typing if focus moved into a password box in the meantime. At that point the text is already in the history, so that second check protects the box but not the log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off I would like your opinion on
&lt;/h2&gt;

&lt;p&gt;A timeout counts as "not a password field". That is fail-open: a slow or hung app loses this protection for that moment, and dictation still works. The alternative is fail-closed: if the check cannot answer in time, refuse to type. That is safer for passwords and much worse for everyone dictating into a slow app.&lt;/p&gt;

&lt;p&gt;I chose fail-open because a dictation tool that randomly refuses to type gets uninstalled, and the check answers in time in nearly every normal case. But I am not sure it is right. Would you fail closed?&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not cover
&lt;/h2&gt;

&lt;p&gt;It trusts what apps report. A custom-drawn control that never sets &lt;code&gt;IsPassword&lt;/code&gt;, and anything inside a remote desktop or virtual machine window, looks like an ordinary field from the outside. On Android the equivalent is the accessibility service reporting that a node is a password field, and the floating button simply hides there.&lt;/p&gt;

&lt;p&gt;The code is in &lt;code&gt;core/password_field.py&lt;/code&gt; if you want to read it in full or send a pull request, especially from an app where it gets the answer wrong. This article and the code were written with Claude Code as an assistant, and the Edge behavior was found and fixed by testing in the browser itself.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>python</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What broke when I shipped a local Whisper dictation app to real Windows hardware</title>
      <dc:creator>Ahmed Ibrahim</dc:creator>
      <pubDate>Sat, 03 Oct 2026 19:32:49 +0000</pubDate>
      <link>https://dev.to/ahmed_ibrahim_1994/what-broke-when-i-shipped-a-local-whisper-dictation-app-to-real-windows-hardware-11kg</link>
      <guid>https://dev.to/ahmed_ibrahim_1994/what-broke-when-i-shipped-a-local-whisper-dictation-app-to-real-windows-hardware-11kg</guid>
      <description>&lt;p&gt;I built VoxScribe, a free, open source voice dictation app for Windows. You hold a hotkey anywhere, talk, release, and the text is typed into whatever app has focus. Speech recognition runs locally with faster-whisper, so the audio never leaves the machine.&lt;/p&gt;

&lt;p&gt;The Whisper part was the easy part. Almost every real bug came from audio hardware and drivers. Here are the ones that cost me the most time.&lt;/p&gt;

&lt;p&gt;The first was a Bluetooth headset that recorded near silence. On my machine the sounddevice library picked a default input device that landed on an MME driver, and for my headset that produced audio so quiet that Whisper transcribed almost nothing. The same physical microphone shows up several times, once for each Windows audio host API (MME, WASAPI and DirectSound), and they are not interchangeable. The fix was to ask for the WASAPI default input device explicitly instead of trusting the generic default. Same headset, usable level right away.&lt;/p&gt;

&lt;p&gt;The second was a crash when forcing 16 kHz. Whisper wants 16 kHz audio, so my first version opened the microphone at 16 kHz. That worked on my headset, then failed with an invalid sample rate error on a laptop's internal mic running at 48 kHz. Some drivers just refuse a rate they do not support natively. The fix was to record at the device's own native rate and resample to 16 kHz in software afterward. Plain linear interpolation is not audiophile quality, but for speech going into Whisper it was good enough, and it has worked on every device I have tried so far.&lt;/p&gt;

&lt;p&gt;The third was dropping voice activity detection. My original design was fully automatic: run a voice activity detector (Silero VAD) on the live stream, start a segment when it hears speech and stop when it hears silence. On quiet or variable input, like a compressed Bluetooth headset, the detector's confidence was inconsistent. Worse, false triggers did not just miss words, they sent background noise to Whisper, and Whisper does not fail quietly on noise. It makes up plausible sentences. I switched to hold-to-talk, so the user decides when speech starts and stops and there is nothing to guess. I kept the VAD code in the repo but it is no longer used in the live path. Letting faster-whisper's built-in VAD trim silence inside a finished clip turned out to be enough.&lt;/p&gt;

&lt;p&gt;There is also a known weakness in any design that opens the mic when you press a key: the stream takes a moment to start, especially on Bluetooth, so the first word can get clipped. I added an opt-in mode that keeps the stream open and holds the last half second in a small in-memory buffer, so a recording starts with audio that is already there. It is off by default, because an always-open mic means Windows shows the mic as in use all the time. Nothing is saved or transcribed unless you press the hotkey.&lt;/p&gt;

&lt;p&gt;If I had to give advice to anyone building something like this: test on at least three different microphones before you trust anything, never assume a sample rate, record at the native rate and resample after, and expect the AI part to be the smallest source of bugs.&lt;/p&gt;

&lt;p&gt;The project is MIT licensed if you want to read the capture code or try it: &lt;a href="https://github.com/ahmedhmam1994/voxscribe-ai-voice-dictation" rel="noopener noreferrer"&gt;https://github.com/ahmedhmam1994/voxscribe-ai-voice-dictation&lt;/a&gt;. It is Windows only for now, since that is the machine I can test on. I used AI coding assistance while building it, and I tested every fix on real hardware myself. Issues and feedback are welcome, especially from people with unusual microphones.&lt;/p&gt;

</description>
      <category>python</category>
      <category>whisper</category>
      <category>windows</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
