Skip to content

v2.25.0 — confident answers that were wrong

Released 2026-08-17. Upgrade:

sudo snap refresh yazses     # snap
pipx upgrade yazses          # or: uv tool upgrade yazses

Nothing here crashed. Every one of these ran to completion, printed something plausible, and was wrong — an empty transcript announced as success, a speaker count invented rather than capped, a privacy guard that answered differently depending on which Python you were running. A program that fails loudly gets fixed. These did not.

Two of them were found by fuzzing — the config loader and the MCP server, fifteen adversarial inputs each. Both held on the invariant they were built for and leaked on the half nobody had looked at. Two more were found by running the product rather than reading it, including a regression this project shipped in v2.24.0.

yazses transcribe told you it worked when it hadn't

This is the surface most people meet YazSes on. It needs no microphone, no hotkey and no re-login, so it is what the container and Codespace trials run — and it had four separate ways to mislead you.

An empty transcript was reported as success. Audio with nothing recognisable in it — music, silence, or speech in a language an English-only model cannot read — produced an empty file, Wrote transcript.txt, and the "if this was useful, a star helps" pointer. Success, by every signal the command gives.

It now says the transcript is empty and names causes you can act on, the useful one being the English-only model — which you can neither see nor guess from a blank file. The check reads result.utterances rather than the rendered text, because a VTT with no cues is still WEBVTT: a check on the string would have stayed silent for exactly one of the five formats.

--max-speakers invented speakers instead of capping them. --help called it "Upper bound on the auto-detected speaker count". On the shipped sherpa diarizer it becomes FastClusteringConfig(num_clusters=N) — an exact count. Measured against sherpa-onnx 1.13.5 on two well-separated synthetic speakers:

num_clusters clusters produced
-1 (auto) 2 ✓
2 2 ✓
4 4
6 6

So --max-speakers 6 on a three-person recording did not allow up to six, it manufactured six by splitting real speakers apart. The help text, the epilog and the config comments now say what it does. 0 still auto-detects and remains the default, so recordings and meetings are unaffected unless a bound was set deliberately. A real upper bound is not implemented — that needs a second clustering pass and the models to verify end to end. The measurement is recorded so whoever has them need not rediscover it.

--min-speakers did nothing at all. Only the pyannote adapter reads it, and that is one of the backends this build does not ship. Asking for at least three speakers got no error, no effect, and a transcript that ignored the floor. It now says so before transcription starts, names the backend actually in use, and points at --speakers.

An undecodable file showed you the ffmpeg command line. Pointing transcribe at a .docx, a truncated download or a renamed file printed a CalledProcessError with the full argv and exit status 183 — naming neither the problem nor anything you could do. load_audio already promised better; its docstring says it raises RuntimeError when no decoder can read the file, but the ffmpeg fallback was unguarded, so that promise held only when ffmpeg was absent. The function broke its own contract on its most likely failure. It now raises the documented error and lists formats that work.

A privacy guard answered differently on Python 3.11 and 3.12

is_loopback_endpoint trusted IPv6Address.is_loopback, and CPython only began counting IPv4-mapped addresses as loopback in 3.13. On 3.11 and 3.12 the same endpoint — http://[::ffff:127.0.0.1]:11434, which genuinely reaches your machine — was refused with a message about sending dictated text off-device.

This project supports 3.11 through 3.14, so a config that worked on one machine was rejected on another for no reason a user could see. It now resolves the mapping itself, so every supported interpreter agrees. The security direction was never wrong — the older interpreters were the stricter — but a guard whose answer depends on the interpreter cannot be reasoned about.

doctor told a running daemon to start

Its closing line contradicted its own warning three lines above:

[WARN] Daemon: running (PID 4054, state idle) — ... run `yazses restart` ...
▲ Good to go (3 optional warnings above) — run `yazses start`, then hold ...

The verdict decided "is it running?" from the check's tag (== "OK"), which held only while a running daemon was always OK. v2.24.0 introduced a running-but-WARN state for a stale daemon and did not update this line — so a demonstrably running daemon read as stopped, and the last thing you are told to do named the wrong command.

The regression was ours, shipped in v2.24.0 and found by running the command on a machine whose daemon predated the upgrade — not a state a fixture can manufacture. The verdict now reads the daemon check's own detail rather than re-asking the OS, so the bottom line cannot disagree with the lines above it.

vad_threshold = inf loaded cleanly and stopped dictation completely

Config loading is deliberately total: a bad file yields a working daemon, the documented default, and a problem doctor reports. nan and inf are floats, so the type check waved them through — no repair, no problem reported.

The silence gate is mean(|audio|) < threshold, so inf discards every burst — you hold the key, speak, and nothing is ever typed — while nan makes the comparison false forever and the gate stops gating. Either way doctor reported no config problem, because as far as the checker was concerned nothing was wrong.

Found by fuzzing the loader with fifteen adversarial files — binary, null bytes, unparseable TOML, 200 KB strings, deep nesting, RTL unicode. None made it raise, which is the invariant holding; these two were accepted too quietly.

Also

  • The MCP server handed agents Python internals. A missing argument returned TypeError: transcribe_tool.<locals>._run() missing 1 required positional argument. Arguments are now bound against the tool's signature first, and a mismatch is answered from the JSON Schema tools/list already publishes. Found by fuzzing over a real pipe: the protocol handling came back clean — correct -32700, -32600 and -32601, notifications unanswered, 200 KB payloads and 200-deep nesting survived. Only the half an agent reads was wrong.
  • A bare JSON array over IPC got no reply and a traceback in the log. [1,2,3], 42 and null are valid JSON and none has a .get, so that one class of malformed input raised AttributeError past a server that catches ValueError — a closed socket, no reply, and a traceback that reads like a crash to whoever later opens yazses logs about something real. Fixed in the parser, so the guarantee holds for every caller.
  • yazses transcribe --format json now has a documented shape, with a field table and the two things a parser could not have guessed: speaker is a cluster id, not a person, and does not carry across files; and the empty shapes differ. Nothing about the output changed — it was already more dependable than it was described.