# v2.16.0 — the snap becomes whole

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

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

## The bug: a snap can never install anything into itself

`yazses features enable <name>` installs a capability's optional Python packages on demand.
That works for every install channel except the one where it silently could not: a **snap**.

A snap's payload is a read-only squashfs, and the Python we stage is Debian's, which ships a
[PEP 668](https://peps.python.org/pep-0668/) `EXTERNALLY-MANAGED` marker. Two independent
blockers, so `pip` refuses no matter which flags it is given. YazSes did not know that. It ran
pip anyway, and handed the user Debian's error:

```
error: externally-managed-environment
... try apt install python3-xyz ...
... create a virtual environment using python3 -m venv ...
... it may be easiest to use pipx install xyz ...
```

Three suggestions, none of which a confined user can act on, and the last of which is the very
thing they chose not to do.

Worse, the config key had **already been written** by the time pip failed. So the capability
read as *enabled* while nothing could ever honour it — precisely the lie `features enable`
already refuses for unwired features.

Auditing the published snap showed the scale: of the 15 capabilities carrying optional Python
packages, only two had their libraries aboard. **Eight wired capabilities were unreachable for
the entire life of every revision — including Meeting Mode.**

## The fix, on both sides

### The snap bundles what fits

| Bundled | Unlocks |
|---|---|
| `sherpa-onnx` | `meeting`, `recimport`, `diarize` — Meeting Mode, diarized import, speaker labels |
| `kokoro-onnx` + `soundfile` | `read-back` — the spoken read-back voice |

Every bundled package must publish manylinux wheels for **x86_64 and aarch64**, because the
snap builds both and a single-arch dependency fails the `arm64` build outright.

### What cannot fit refuses honestly, before writing config

| Not bundled | Why |
|---|---|
| `speechbrain` (`cocktail`) | pulls PyTorch, ~1 GB — dwarfs the snap |
| `llama-cpp-python` (`llm-cleanup`) | publishes no PyPI wheels; needs a compiler |
| `mediapipe` + `opencv` (`gaze`) | ~110 MB for an experimental webcam feature |
| `praat-parselmouth` (`prosody`) | no `aarch64` wheel — would break the arm64 build |

Enabling one of those inside a snap now explains why, names the packages, and gives the one
instruction that works — while noting that config and models do **not** carry across to an
unconfined install. The gate is precise, not a blanket refusal: a capability whose libraries
*are* bundled still enables normally.

`yazses settings` had the same defect — it wrote the config keys and *then* reported the
missing packages, pointing at a `features enable` that refuses for the same reason. It refuses
before writing too. Turning a capability **off** is never blocked: that installs nothing, and a
locked environment must not trap you with a setting you cannot switch back.

The same gate also catches the general case of an unwritable site directory, such as a
root-owned install run as a normal user.

## Verified on the real artifact

Not on a build log — the published snap was downloaded, unpacked, and its **own** interpreter
run against its **own** `site-packages`:

- `sherpa_onnx`, `kokoro_onnx`, `soundfile`, `onnxruntime`, `onnx_asr`, `espeakng_loader` — all import
- `features enable meeting` and `read-back` → enable
- `features enable cocktail --force` → refuses, and writes **nothing** to `config.toml`

On `arm64`, the native binaries were confirmed to be genuinely `ARM aarch64`
(`libsndfile_arm64.so`, `libespeak-ng.so`), with the `espeak-ng-data` Read-Back needs.

Community testing is tracked in
[issue #142](https://github.com/MSKazemi/yazses/issues/142) — `arm64` at runtime is the one
surface nobody has exercised yet.

## Also in this release

- **`yazses settings`** — a settings window generated from the feature registry, so the GUI and
  the CLI cannot drift. Thanks to [@waterlemonnn](https://github.com/waterlemonnn) (PR #134).
- **The contract pins meaning, not only parity** (contract 5.1.0) — minimal pairs caught five
  filler-word bugs that byte-parity vectors could not see.
- **`man yazses`**, generated from the CLI so it cannot drift.
- **`[stt] language` is real.** It was documented but never implemented: every decode path
  hardcoded `language="en"`, so non-English dictation never worked.
- `install.sh` now checks the two hard prerequisites it previously assumed.

## Snap users: two interfaces still need connecting

Snap confinement does not auto-connect these, and without them the microphone and the
hold-to-talk key both stay dead:

```sh
sudo snap connect yazses:audio-record
sudo snap connect yazses:raw-input
```

See [issue #44](https://github.com/MSKazemi/yazses/issues/44) — joining the `input` group
cannot grant keyboard capture inside a snap, only the `raw-input` interface can.
