Skip to content

Commit ec298ac

Browse files
authored
Merge pull request #3 from AkuchiS/fix/windows-support
Windows support: GPU→CPU fallback, bundled selftest clip, UTF-8 stdout, pipx-first README (v0.1.15)
2 parents 56d08cc + b5b7975 commit ec298ac

7 files changed

Lines changed: 104 additions & 35 deletions

File tree

README.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ What you get:
3131

3232
## Download — no terminal needed
3333

34-
Download, open the `.dmg`, drag **Yap** to Applications. That's the whole thing:
34+
Grab it for your platform — macOS is a signed, notarized app; Windows & Linux install in one line (see [Install](#install)):
3535

36-
| [macOS (.dmg)](https://github.com/AkuchiS/Yap/releases/latest) | [Windows (pipx)](#install) | [Linux (pipx)](#install) |
36+
| [![macOS](https://img.shields.io/badge/macOS-download_.dmg-000000?style=for-the-badge&logo=apple&logoColor=white)](https://github.com/AkuchiS/Yap/releases/latest) | [![Windows](https://img.shields.io/badge/Windows-install-0078D6?style=for-the-badge&logo=windows&logoColor=white)](#install) | [![Linux](https://img.shields.io/badge/Linux-install-FCC624?style=for-the-badge&logo=linux&logoColor=black)](#install) |
3737
|:---:|:---:|:---:|
3838

3939
> **macOS:** signed & notarized — just open it (no right-click dance). On first launch, **Allow** the Microphone, Accessibility, and Input Monitoring prompts; after allowing Accessibility, **quit and reopen Yap once** so the hotkey activates. Then hold your hotkey and talk. Comfortable with a terminal? See [Install](#install) below.
@@ -52,24 +52,28 @@ Download, open the `.dmg`, drag **Yap** to Applications. That's the whole thing:
5252
## Install
5353

5454
Requires **Python 3.9–3.13** (3.14 is too new — some native deps don't have
55-
wheels for it yet). The local engine downloads a small Whisper model on first use. Clone, run the installer, go:
55+
wheels for it yet). The local engine downloads a small Whisper model on first use.
56+
57+
**Any OS — one line** (recommended, and the way to install on **Windows**), via
58+
[pipx](https://pipx.pypa.io):
5659

5760
```bash
58-
git clone https://github.com/AkuchiS/yap.git
59-
cd yap
60-
./install.sh
61+
pipx install "yap-dictation[full] @ git+https://github.com/AkuchiS/yap"
6162
yap run
6263
```
6364

64-
`./install.sh` sets up an isolated [pipx](https://pipx.pypa.io) environment so
65-
nothing pollutes your system Python. Then hold **Right Option**, speak, and let
66-
go; your words land at the cursor.
65+
The `[full]` extra pulls the desktop bits (macOS menu-bar `rumps`); plain `git+…`
66+
installs the core CLI only, so `yap app` won't start without it.
67+
68+
**macOS / Linux — clone + installer** (sets up the pipx env for you):
69+
70+
```bash
71+
git clone https://github.com/AkuchiS/yap.git && cd yap && ./install.sh && yap run
72+
```
6773

68-
> Prefer a one-liner?
69-
> `pipx install "yap-dictation[full] @ git+https://github.com/AkuchiS/yap"`
70-
> does the same thing. The `[full]` extra pulls in the macOS menu-bar bits
71-
> (`rumps`); plain `git+…` installs the core CLI only, so `yap app` won't start
72-
> without it.
74+
> `./install.sh` is a **bash script — macOS/Linux only**. On **Windows**, use the
75+
> pipx one-liner above (from PowerShell or any terminal). Then hold **Right
76+
> Option**, speak, and let go; your words land at the cursor.
7377
7478
### Per-OS extras
7579

@@ -80,7 +84,10 @@ go; your words land at the cursor.
8084
`xclip` / `xsel`. On **Wayland**, install `wl-clipboard`; some compositors
8185
restrict synthetic keystrokes — use `--engine local` with
8286
`inject.method = "type"` if paste doesn't land.
83-
- **Windows** — no extra steps; run from a normal terminal.
87+
- **Windows** — no extra steps; run from a normal terminal. **Updating:** stop yap
88+
first (Ctrl-C the `yap run` window / quit the app) before `pipx upgrade
89+
yap-dictation` — a running worker holds `yap.exe` open, so an in-place upgrade
90+
otherwise hits `WinError 32` (file in use).
8491

8592
## Usage
8693

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "yap-dictation"
7-
version = "0.1.14"
7+
version = "0.1.15"
88
description = "Free, offline-first, cross-platform voice dictation — speak anywhere, get text at your cursor."
99
readme = "README.md"
1010
license = { text = "MIT" }
@@ -49,3 +49,7 @@ Issues = "https://github.com/yap-dictation/yap/issues"
4949

5050
[tool.setuptools.packages.find]
5151
include = ["yap*"]
52+
53+
[tool.setuptools.package-data]
54+
# ship the self-test clip inside the wheel so `yap selftest` works from a pip/pipx install
55+
yap = ["assets/*.wav"]

yap/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
cloud speed when you want it.
66
"""
77

8-
__version__ = "0.1.14"
8+
__version__ = "0.1.15"
99

1010
# Keep startup/shutdown quiet: HuggingFace's "set a HF_TOKEN" rate-limit notice
1111
# (we only ever download public Whisper models) and the benign leaked-semaphore

yap/assets/jfk.wav

344 KB
Binary file not shown.

yap/cli.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,16 @@ def _cmd_license(args) -> int:
210210

211211

212212
def _bundled_sample() -> str:
213-
"""Path to the built-in self-test clip (works frozen via _MEIPASS or in-repo)."""
213+
"""Path to the built-in self-test clip. Order: PyInstaller bundle → the packaged asset shipped
214+
inside the wheel (so `yap selftest` works from a pip/pipx install) → the in-repo tests dir."""
214215
base = getattr(sys, "_MEIPASS", None)
215216
if base:
216217
p = os.path.join(base, "jfk.wav")
217218
if os.path.exists(p):
218219
return p
220+
packaged = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "jfk.wav")
221+
if os.path.exists(packaged):
222+
return packaged
219223
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
220224
return os.path.join(here, "tests", "jfk.wav")
221225

@@ -458,7 +462,18 @@ def build_parser() -> argparse.ArgumentParser:
458462
return p
459463

460464

465+
def _force_utf8_stdio() -> None:
466+
"""Windows consoles default to cp1252 and crash on the ✓/✗/⚠ glyphs (`yap doctor` etc.).
467+
Make our output UTF-8 — a no-op where it already is. Fail-soft."""
468+
for stream in (sys.stdout, sys.stderr):
469+
try:
470+
stream.reconfigure(encoding="utf-8", errors="replace")
471+
except Exception:
472+
pass
473+
474+
461475
def main(argv: list[str] | None = None) -> int:
476+
_force_utf8_stdio()
462477
parser = build_parser()
463478
args = parser.parse_args(argv)
464479
from . import licensing

yap/doctor.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ def check_trust(prompt: bool = False):
5757
return trusted
5858

5959

60+
def _no_key_hint() -> None:
61+
"""Platform-appropriate advice when the hotkey listener saw no key events (non-Wayland)."""
62+
if sys.platform == "darwin":
63+
print(" → a trust/permission problem, not a key-choice problem. Add the app you ran")
64+
print(" this from to Accessibility AND Input Monitoring above, then Cmd+Q and reopen.")
65+
elif sys.platform.startswith("win"):
66+
print(" → on Windows there's no Accessibility gate. Usually another app is grabbing the")
67+
print(" key, or a fullscreen/elevated app is stealing input. Try a different hotkey")
68+
print(" (`yap config set hotkey.combo '\"<f9>\"'`), or run the terminal as Administrator.")
69+
else:
70+
print(" → on Linux/X11, global keys should work — check no other app has grabbed the")
71+
print(" key, and try a different combo. (On Wayland the listener can't fire at all.)")
72+
73+
6074
def keytest(cfg: dict[str, Any], seconds: int = 12) -> None:
6175
try:
6276
from pynput import keyboard
@@ -111,8 +125,7 @@ def on_release(k):
111125
print(" NOT a permission problem and no key choice fixes it. Bind a")
112126
print(" compositor key to `yap toggle` instead (see the note above).")
113127
else:
114-
print(" → a trust/permission problem, not a key-choice problem.")
115-
print(" Fix Accessibility + Input Monitoring above.")
128+
_no_key_hint()
116129
else:
117130
print(" (If you saw your hotkey above but no MATCH line, the combo is wrong —")
118131
print(" copy the 'canonical=' value into hotkey.combo.)")
@@ -177,7 +190,11 @@ def run(cfg: dict[str, Any], prompt: bool, seconds: int) -> int:
177190
_hr("4. Clipboard")
178191
cliptest()
179192

180-
print("\nDone. The #1 fix when section 2 saw no keys: add the app you ran this")
181-
print("from to Accessibility AND Input Monitoring, then Cmd+Q and reopen it.")
182-
print("Tip: `yap doctor --prompt` pops the macOS 'allow control' dialog for you.")
193+
if sys.platform == "darwin":
194+
print("\nDone. The #1 fix when section 2 saw no keys: add the app you ran this from to")
195+
print("Accessibility AND Input Monitoring, then Cmd+Q and reopen it.")
196+
print("Tip: `yap doctor --prompt` pops the macOS 'allow control' dialog for you.")
197+
else:
198+
print("\nDone. If section 2 saw no keys (and you're not on Wayland), another app is likely")
199+
print("grabbing the hotkey — pick a free one: `yap config set hotkey.combo '\"<f9>\"'`.")
183200
return 0

yap/stt/local_whisper.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,39 @@ def resolved_model(self) -> str:
4242
return recommend_model(prefer_english=prefer_en)
4343
return model
4444

45-
def _ensure_model(self):
45+
active_device = "cpu"
46+
47+
def _ensure_model(self, force_cpu: bool = False):
4648
if self._model is not None:
4749
return self._model
4850
from faster_whisper import WhisperModel
4951

50-
device, default_ct = _resolve_device(self.cfg.get("device", "auto"))
51-
compute_type = self.cfg.get("compute_type", "auto")
52-
if compute_type == "auto":
53-
compute_type = default_ct
52+
if force_cpu:
53+
device, compute_type = "cpu", "int8"
54+
else:
55+
device, default_ct = _resolve_device(self.cfg.get("device", "auto"))
56+
compute_type = self.cfg.get("compute_type", "auto")
57+
if compute_type == "auto":
58+
compute_type = default_ct
5459
self.active_model = self.resolved_model()
55-
self._model = WhisperModel(
56-
self.active_model,
57-
device=device,
58-
compute_type=compute_type,
59-
)
60+
try:
61+
self._model = WhisperModel(self.active_model, device=device, compute_type=compute_type)
62+
except Exception as e:
63+
# GPU was chosen but its CUDA runtime is missing/broken (e.g. `cublas64_12.dll not
64+
# found`) or out of memory — never crash: fall back to CPU int8, which always works.
65+
if device == "cpu":
66+
raise
67+
import sys
68+
print(f"yap: GPU init failed ({str(e)[:140]}); using CPU instead.", file=sys.stderr)
69+
self._model = WhisperModel(self.active_model, device="cpu", compute_type="int8")
70+
device = "cpu"
71+
self.active_device = device
6072
return self._model
6173

6274
def warmup(self) -> None:
6375
self._ensure_model()
6476

65-
def _decode(self, audio) -> str:
66-
model = self._ensure_model()
77+
def _run(self, model, audio) -> str:
6778
language: Optional[str] = self.cfg.get("language")
6879
segments, _info = model.transcribe(
6980
audio,
@@ -74,6 +85,21 @@ def _decode(self, audio) -> str:
7485
)
7586
return "".join(seg.text for seg in segments).strip()
7687

88+
def _decode(self, audio) -> str:
89+
model = self._ensure_model()
90+
try:
91+
return self._run(model, audio)
92+
except Exception as e:
93+
# Some CUDA breakage only surfaces on the FIRST transcription (lazy cuBLAS load). If
94+
# we're on the GPU, drop to CPU and retry once rather than crashing the app.
95+
if self.active_device == "cpu":
96+
raise
97+
import sys
98+
print(f"yap: GPU transcription failed ({str(e)[:140]}); retrying on CPU.", file=sys.stderr)
99+
self._model = None
100+
model = self._ensure_model(force_cpu=True)
101+
return self._run(model, audio)
102+
77103
def transcribe_file(self, wav_path: str) -> str:
78104
return self._decode(wav_path)
79105

0 commit comments

Comments
 (0)