From 5e36aecd7dde46d5f1258647709307f750338b24 Mon Sep 17 00:00:00 2001 From: Azis Date: Mon, 16 Feb 2026 22:23:31 +0100 Subject: [PATCH] Add play sound support for Windows --- AGENTS.md | 15 ++++++--------- README.md | 11 +++++++++-- pyproject.toml | 7 +++++++ reed.py | 11 +++++++++++ test_reed.py | 35 ++++++++++++++++++++++++++++++++++- uv.lock | 6 +++--- 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 557cb91..2d34cf4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,13 +20,14 @@ This is `reed`, a convenient CLI for text-to-speech using piper-tts. ## Commands -- Run: `reed "text"` +- Run: `reed 'text'` - Interactive mode: `reed` (launches automatically when no input provided) - List voices: `reed voices` - Download voice: `reed download en_US-amy-medium` - Typecheck: `mypy reed.py --ignore-missing-imports` -- Test (unit): `pytest -v` -- Test (smoke): `echo "test" | reed -o /dev/null` +- Sync deps: `uv sync` +- Test (unit): `uv run pytest` +- Test (smoke): `echo 'test' | reed -o /dev/null` ## Testing @@ -34,7 +35,7 @@ This is `reed`, a convenient CLI for text-to-speech using piper-tts. - Test file: `test_reed.py` using `pytest` - Tests use dependency injection (fake `run`, `stdin`, `print_fn`) to avoid real subprocess calls - The `reed` module is imported directly (`import reed as _reed`) -- Run full test suite before and after every change: `pytest -v` +- Run full test suite before and after every change: `uv run pytest -v` ## Conventions @@ -42,7 +43,7 @@ This is `reed`, a convenient CLI for text-to-speech using piper-tts. - Use `argparse` for CLI argument parsing - Use `subprocess` to invoke piper and platform audio player - Default model auto-downloaded to `_data_dir()` on first run -- `ReedConfig` dataclass for core configuration (not `argparse.Namespace`) +- `ReedConfig` dataclass for core configuration ## UI Development @@ -52,7 +53,3 @@ This is `reed`, a convenient CLI for text-to-speech using piper-tts. - Commands available in interactive mode: `/quit`, `/exit`, `/help`, `/clear`, `/replay` - Tab autocomplete available for commands - Include `print_fn` parameter for testability with dependency injection - -## Git - -- Do NOT add `Co-authored-by` or `Amp-Thread-ID` trailers to commits diff --git a/README.md b/README.md index c0c3894..6991c9d 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,10 @@ A CLI that reads text aloud using [piper-tts](https://github.com/rhasspy/piper). ## Requirements - Python 3.14+ -- macOS or Linux +- macOS, Linux, or Windows - **macOS**: `afplay` (audio), `pbpaste` (clipboard) — included with the OS - **Linux**: one of `paplay`, `aplay`, or `ffplay` (audio); one of `wl-paste`, `xclip`, or `xsel` (clipboard) + - **Windows**: `powershell` (audio playback) or `ffplay` fallback - [uv](https://docs.astral.sh/uv/) (for dependency management) - Rich library for beautiful terminal UI @@ -110,9 +111,15 @@ reed --silence 1 # Save to WAV file instead of playing reed -o output.wav 'Save this' -# Play a saved WAV file +# Play a saved WAV file (macOS) afplay output.wav +# Play a saved WAV file (Linux) +paplay output.wav + +# Play a saved WAV file (Windows PowerShell) +powershell -NoProfile -NonInteractive -c "(New-Object System.Media.SoundPlayer 'output.wav').PlaySync()" + # Adjust speed (lower = slower) and volume reed -s 0.8 -v 1.5 'Slower and louder' diff --git a/pyproject.toml b/pyproject.toml index 3c475ec..7f826df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + [project] name = "reedy" version = "0.1.0" @@ -25,6 +29,9 @@ dev = [ "pre-commit>=4.5.1", ] +[tool.uv] +default-groups = ["dev"] + [tool.setuptools] py-modules = ["reed"] diff --git a/reed.py b/reed.py index bf450b9..fe46625 100755 --- a/reed.py +++ b/reed.py @@ -112,6 +112,17 @@ def _default_play_cmd() -> list[str]: ]: if shutil.which(cmd): return [cmd, *args] + if platform.system() == "Windows": + if shutil.which("powershell"): + return [ + "powershell", + "-NoProfile", + "-NonInteractive", + "-c", + "(New-Object System.Media.SoundPlayer $args[0]).PlaySync()", + ] + if shutil.which("ffplay"): + return ["ffplay", "-nodisp", "-autoexit", "-hide_banner"] raise ReedError("No supported audio player found") diff --git a/test_reed.py b/test_reed.py index 89a4625..6b62872 100644 --- a/test_reed.py +++ b/test_reed.py @@ -453,10 +453,43 @@ def test_linux_no_player_raises(self, monkeypatch): with pytest.raises(ReedError, match="No supported audio player found"): _default_play_cmd() - def test_unknown_platform_raises(self, monkeypatch): + def test_windows_powershell(self, monkeypatch): + from reed import _default_play_cmd + + monkeypatch.setattr("reed.platform.system", lambda: "Windows") + monkeypatch.setattr( + "reed.shutil.which", + lambda cmd: r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + if cmd == "powershell" + else None, + ) + result = _default_play_cmd() + assert result[0] == "powershell" + assert "-c" in result + assert "System.Media.SoundPlayer" in " ".join(result) + + def test_windows_ffplay_fallback(self, monkeypatch): + from reed import _default_play_cmd + + monkeypatch.setattr("reed.platform.system", lambda: "Windows") + monkeypatch.setattr( + "reed.shutil.which", + lambda cmd: r"C:\ffmpeg\bin\ffplay.exe" if cmd == "ffplay" else None, + ) + assert _default_play_cmd() == ["ffplay", "-nodisp", "-autoexit", "-hide_banner"] + + def test_windows_no_player_raises(self, monkeypatch): from reed import ReedError, _default_play_cmd monkeypatch.setattr("reed.platform.system", lambda: "Windows") + monkeypatch.setattr("reed.shutil.which", lambda cmd: None) + with pytest.raises(ReedError, match="No supported audio player found"): + _default_play_cmd() + + def test_unknown_platform_raises(self, monkeypatch): + from reed import ReedError, _default_play_cmd + + monkeypatch.setattr("reed.platform.system", lambda: "FreeBSD") with pytest.raises(ReedError, match="No supported audio player found"): _default_play_cmd() diff --git a/uv.lock b/uv.lock index 33b6215..b026121 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.14" [[package]] @@ -368,9 +368,9 @@ wheels = [ ] [[package]] -name = "reed" +name = "reedy" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "pathvalidate" }, { name = "piper-tts" },