Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,30 @@ 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

- **Always write tests first (TDD)** — create failing tests before implementing features
- 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

- Single-file script, installed as console script via pyproject.toml
- 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

Expand All @@ -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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'

Expand Down
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "reedy"
version = "0.1.0"
Expand Down Expand Up @@ -25,6 +29,9 @@ dev = [
"pre-commit>=4.5.1",
]

[tool.uv]
default-groups = ["dev"]

[tool.setuptools]
py-modules = ["reed"]

Expand Down
11 changes: 11 additions & 0 deletions reed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down
35 changes: 34 additions & 1 deletion test_reed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading