# v2.18.1 — the backends config promised, and the privacy holes they opened

**Released 2026-08-13.** Upgrade:

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

## Two backends that config offered but no build could run (#70, #71)

`[voiceprint] backend = "resemblyzer"` and `[recimport]`/`[meeting] backend = "pyannote"`
were accepted by the config loader and then reported as *not implemented in this build*.
Both now ship an adapter, each behind its own extra — `voiceprint-resemblyzer` and
`diarization-pyannote`. Neither may advise the extra it sits beside: `voiceprint` is
speechbrain and `diarization` is sherpa-onnx, so naming those would send you after a package
that cannot supply the backend you picked. sherpa and ECAPA remain the defaults.

Resemblyzer does **not** simply call `embed_utterance`. That function slices audio into
1.6 s partials and zero-pads anything shorter, with no flag to disable it — so a 500 ms
Cocktail Filter window would be encoded as 0.5 s of speech plus 1.1 s of digital silence.
Below the partial length the adapter runs the encoder directly on the frame instead.
Measured same-speaker similarity against a full-utterance reference: **0.647 vs 0.509 at
500 ms** and **0.766 vs 0.605 at 1000 ms**, converging by 1500 ms. The Cocktail Filter's
default `target_threshold` is `0.5`, so the padding was dragging a speaker's *own* voice
onto the rejection line.

## Enabling pyannote diarization would have sent usage data off the machine

pyannote.audio 4.x ships OpenTelemetry tracking that is **on by default**: its bundled
`telemetry/config.yaml` sets `metrics_enabled: true` with an OTLP endpoint at
`otel.pyannote.ai`, and its `track_pipeline_apply` hook reports the **duration of the audio
being diarized** along with the requested speaker counts. `telemetry_log_level` is pinned to
`CRITICAL`, so it says nothing either way. This is not an optional extra —
`opentelemetry-exporter-otlp` and `pyannoteai-sdk` are hard requirements of the package.

No audio would have been transmitted, but the length of your meetings and your usage pattern
would have been, silently, from a tool whose entire promise is that nothing leaves the
machine ([ADR-011](https://github.com/MSKazemi/yazses/blob/main/design/adr/)). The backend
now sets `PYANNOTE_METRICS_ENABLED=false` **before** importing pyannote — the ordering *is*
the mechanism, because upstream defaults the variable from its config file only when it is
not already set, so disabling it afterwards would let the import-time tracker fire first. A
test pins the ordering, not just the value.

## Dictation cleanup could POST your transcribed text to any host

The optional LLM cleanup path took an endpoint from config and sent the transcript to it
with no restriction on where "it" was. A typo, a copied config, or a well-meaning suggestion
could route every dictated sentence to a third party. The endpoint is now guarded, and the
privacy statement — which had claimed cleanup made "no HTTP API call" — says what the code
actually does.

## Windows: hold-to-talk, command keys, and the liveness probe that killed the daemon

Eleven defects in `platform/windows/`, found by audit and each pinned by a regression test.
Three of them meant core features could never have worked on Windows:

- **`yazses status` terminated the daemon.** `is_running()` used `os.kill(pid, 0)` as a
  liveness probe. That is the POSIX idiom; on Windows CPython's `os.kill` has no signal
  semantics and falls through to `TerminateProcess(handle, sig)`, so signal 0 *kills the
  process* with exit code 0 ([bpo-14480](https://bugs.python.org/issue14480)). `status`,
  `doctor` and the tray's poll loop all reach it. Replaced with `OpenProcess` +
  `GetExitCodeProcess`.
- **Hold-to-talk never started.** The hold threshold was only re-checked when another key
  event arrived, which assumes typematic auto-repeat. Modifier keys — every supported hotkey
  except `space` — do not repeat, so with the default `right_ctrl` the single keydown landed
  at t=0 and the next event was the keyup.
- **Every named command key was a no-op.** The VK table was keyed capitalised (`"Return"`)
  but looked up lower-cased, so `Return`, `Tab`, `Escape`, `BackSpace` and the arrows all
  resolved to `vk=0` — a keystroke Windows accepts and discards.

Also fixed: 64-bit handle truncation, a named-pipe handle leak that left IPC dead after
eight failed accepts, an IPC client that stored a timeout and never applied it, and an
autostart value written unquoted that broke on any username containing a space.

## Also

- **`brew install --cask yazses`** works, via a real tap
- **Every macOS `.app` since v0.1.3 told the OS it was version 0.1.2**
- **DeepFilterNet noise suppression stays honestly unimplemented (#69)** — every release
  caps `numpy<2.0`, so there is deliberately no `denoise` extra rather than a config key
  nothing can honour
- **Windows packaging manifests no longer ship a release behind** — scoop, chocolatey and
  winget sat at 2.17.0 carrying the previous release's checksum

The full list for this release is in
[CHANGELOG.md](https://github.com/MSKazemi/yazses/blob/main/CHANGELOG.md).
