v2.20.0 — the Windows release that actually works¶
Released 2026-08-14. Upgrade:
Most of this release comes from one thing that had not happened before: people running YazSes on Windows and telling us what they saw. Every fix below was reported from a live install, and each is proven on a Windows runner rather than asserted.
A blocked model download killed the daemon instead of explaining (#310)¶
Reported by @AtmanActive on Windows 10 behind the Fort firewall: on first run the daemon tried to fetch the Whisper model, the firewall refused the socket (WinError 10013), and the process died with a raw huggingface_hub traceback — which the PyInstaller bundle renders as a modal "Failed to execute script". Nothing on screen said what YazSes wanted, why, or what to do about it.
core/daemon.py::run() wrapped _build_pipeline() in try/finally with no except, so every startup failure escaped to the top. Notably stt/factory.py already got this right for the Parakeet and Moonshine engines ("dictation must still come up"); the default engine had no such guard.
Now ModelUnavailableError carries ready-to-print guidance — the cause, a firewall hint when the cause reads like one, and the three ways to get the model. The daemon holds itself in ERROR state with that text attached rather than exiting, so the tray turns red with the reason and yazses status can answer. Exiting would have taken the tray down too, leaving a vanished window as the only symptom.
yazses model download handles speech models, not just SLMs¶
The model can now be fetched as a deliberate, watchable step instead of a side effect of the first dictation — the only workable route on a firewalled or air-gapped machine. yazses model list gained a speech-to-text section showing every model, whether it is already present, and the cache path.
The name→repository mapping is not derivable: large resolves to large-v3, turbo comes from a different organisation, and the distil models use another prefix — so a URL built from a template would have sent three of them to a 404. It is mirrored from faster-whisper's private table and kept honest by a test that fails on drift.
Relatedly, the documented model-cache path was wrong on Windows: the docs said %LOCALAPPDATA%\huggingface\hub, but huggingface_hub uses ~/.cache/huggingface/hub on every platform — so anyone placing a model by hand was putting it where nothing would read it.
Windows shipped without its icon, and the tray showed a blank disc¶
Reported with screenshots. Two separate causes, both long-standing.
assets/yazses.ico never existed. The Windows spec had referenced it since the file was written, behind icon=str(ICON) if ICON.exists() else None — so every build silently passed icon=None and shipped PyInstaller's default artwork to the desktop shortcut, the Start menu, the taskbar and Add/Remove Programs. The macOS spec carried the identical dangling assets/yazses.icns. Both now fail the build rather than degrading in silence, and the containers are generated and committed.
The tray glyph is now the YazSes mark — a rounded badge with the white "Y", matching Linux — instead of a bare disc. Pillow anti-aliases nothing, which is where the jagged edge came from; the mark is supersampled and downsampled as coverage, so it has neither jagged edges nor the dark halo the naive fix introduces.
The Windows tray now obeys the shared colour policy. It had kept a private seven-entry table that bypassed tray/menu.py::icon_spec, so Windows showed different colours from Linux for the same state, had no command-mode purple and no "no text field focused" yellow, and rendered five of the twelve tray states — including Meeting Mode — as idle blue.
Every CLI command was unreachable on the Windows installer¶
yazses doctor on Windows printed nothing and then died in a message box with AttributeError: 'NoneType' object has no attribute 'isatty'. Two defects, stacked:
The console shim could never win. The bundle ships two binaries on purpose — a windowed one for the tray/daemon and yazses-cli.exe for the CLI — with a yazses.cmd shim putting the console one on PATH. But the windowed binary was named YazSes.exe, Windows resolves a bare yazses through PATHEXT (which lists .EXE before .CMD), and NTFS is case-insensitive. YazSes.exe therefore answered to yazses and shadowed the shim in the same directory. Every yazses <command> reached the windowed binary, which has no console — so yazses doctor and yazses -h printed nothing whatsoever. The windowed binary is now YazSesApp.exe, and the installer deletes a leftover YazSes.exe on upgrade.
A missing stdout was fatal rather than degrading. sys.stdout is None in a GUI-subsystem PyInstaller build, so sys.stdout.isatty() raised instead of answering "not a tty". New system/streams.py centralises that policy, and system/wincon.py adds a second line of defence: a CLI command that reaches the windowed binary anyway now borrows the launching terminal's console via AttachConsole.
Upgrading from 2.18.2 or earlier on Windows
The windowed binary was renamed from YazSes.exe to YazSesApp.exe. The installer removes the old file for you — an orphaned YazSes.exe would keep shadowing the CLI shim. See Install on Windows.
The full list for this release is in CHANGELOG.md.