The install instructions had been tested. By me, on the machine where every dependency was already present, the key already in place and the program already running. That is not a test of the instructions. It is a test of my memory.
Finding 1
Without an API key, the first start opens a small window and waits for someone to paste one in. At a desk that is exactly right — better than telling people to copy a file and edit it by hand.
From a script, a remote session, or an AI agent doing the installation, it is useless in a specific and nasty way: the process starts, prints nothing, and never exits. No error, no timeout, no exit code. Measured: alive after twelve seconds, no output, no indication of what it was waiting for. Whoever automated the install would see a hung command and no reason.
Finding 2
The changelog states the recording high-pass filter attenuates mains hum at 50 Hz by about 14 dB while leaving speech untouched. That number was measured and it is correct.
It also did not apply to a single person who followed the install instructions.
The filter needs scipy, and scipy was missing from
requirements.txt. The import sits inside a try block, so
nothing crashed — the code fell back to removing the DC offset and logged one line at
debug level, which nobody reads.
Measured on a fresh install, 16 kHz sine input, with and without the package:
| Input | With scipy | Without | Documented |
|---|---|---|---|
| 30 Hz | −31.0 dB | 0.0 dB | −32 dB |
| 50 Hz | −14.2 dB | 0.0 dB | −14.3 dB |
| 110 Hz | −0.2 dB | 0.0 dB | −0.2 dB |
The same applied to pycaw, which mutes playback during a recording.
Both are genuinely optional in the sense that the program runs without them. Neither
is optional in the sense that the program behaves as described.
try around an import converts a
loud failure into a quiet difference in behaviour. That is usually what you want — but
it means "it runs" and "it works as documented" stop being the same statement, and
only the first one gets tested. Both packages are now listed, with a comment saying
what is lost without them.Finding 3
The program keeps a hidden window to receive power events, so it can recover after
standby. Its message handler ended by passing anything it did not handle to
DefWindowProcW — called through ctypes without declared argument
types.
LPARAM is pointer-wide. On 64-bit Windows, ctypes without
argtypes tries to squeeze it into a 32-bit int and raises
OverflowError: int too long to convert. Every window message. The reason
nobody noticed: the exception is raised inside a ctypes callback, where Python cannot
propagate it. It gets printed and discarded. Nothing crashes, nothing is reported —
the message simply goes unanswered.
Finding 4
The first line of the install block was git clone. Windows on the test
machine has no git — the very first command fails for anyone who has not
installed it separately. It is an unremarkable prerequisite among developers and an
invisible wall for everyone else.
The second line was pip install -r requirements.txt. On that machine,
python resolves to 3.14 and pip to the scripts folder of a
different, 32-bit 3.11 installation. Following the instructions literally installs
every package where nothing will ever look for it, and the resulting error names a
missing module rather than the real cause.
git and offer the ZIP download
as an alternative, and use py -m pip throughout, which is guaranteed to
match the interpreter that py starts.None of the four came from complicated code. Three came from the same blind spot: the author's machine already had everything, so the paths a newcomer walks were never walked.
The checks that existed did their job on what they covered. A legal-and-links linter runs on every push and caught a missing liability notice on the new page the same afternoon. It cannot catch a missing dependency, because a missing dependency is not visible in the source — only in the gap between the source and the machine it lands on.
The cheap fix is not a bigger test suite. It is one command that reports what is actually present:
py pushdictate.py --check
It names packages, key and write access, exits non-zero when the program would not start, and speaks JSON when something other than a human is asking. Three of the four findings above would have shown up in its output. The fourth — the ctypes overflow — needed the program to actually run with its output captured, which is the other half of the exercise and cannot be replaced by a check.