diff --git a/README.md b/README.md index 7521b85..e3e89a0 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,11 @@ Progress auto-saves on every completed set and on Ctrl-C. Re-run the same workou ## Voice -Uses macOS `say` for coaching cues. Silently skipped on other platforms. +Uses macOS `say` or Linux `espeak`/`spd-say` for coaching cues. Silently skipped if no TTS is available. + +On Linux, install a TTS engine: + +``` +sudo apt install espeak # Debian/Ubuntu +sudo dnf install espeak-ng # Fedora +``` diff --git a/coach.py b/coach.py index 21e652d..46cf959 100755 --- a/coach.py +++ b/coach.py @@ -144,34 +144,70 @@ def exercises_match(a: list[Exercise], b: list[Exercise]) -> bool: # --------------------------------------------------------------------------- -# Voice coaching (non-blocking macOS `say`) +# Voice coaching (macOS `say`, Linux `espeak`/`spd-say`) # --------------------------------------------------------------------------- _say_proc: subprocess.Popen | None = None +def _tts_cmd(text: str) -> list[str]: + """Return the TTS command + args for the current platform.""" + if sys.platform == "darwin": + return ["say", text] + # Linux: prefer espeak, fall back to spd-say + for cmd in ("espeak", "spd-say"): + if _which(cmd): + return [cmd, text] + return [] + + +def _which(cmd: str) -> bool: + """Check whether *cmd* is on PATH (cached).""" + if cmd not in _which_cache: + _which_cache[cmd] = ( + subprocess.call( + ["which", cmd], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + == 0 + ) + return _which_cache[cmd] + + +_which_cache: dict[str, bool] = {} + + def say(text: str) -> None: global _say_proc + cmd = _tts_cmd(text) + if not cmd: + return try: if _say_proc and _say_proc.poll() is None: _say_proc.terminate() _say_proc = subprocess.Popen( - ["say", text], + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except FileNotFoundError: - pass # not on macOS + pass def say_sync(text: str, wait: float = 0) -> None: """Say something and optionally wait after it finishes.""" global _say_proc + cmd = _tts_cmd(text) + if not cmd: + if wait > 0: + time.sleep(wait) + return try: if _say_proc and _say_proc.poll() is None: _say_proc.terminate() _say_proc = subprocess.Popen( - ["say", text], + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) diff --git a/plank.sh b/plank.sh index 2566b4e..ab62036 100644 --- a/plank.sh +++ b/plank.sh @@ -1,13 +1,25 @@ #!/bin/bash + +# Cross-platform TTS: macOS `say`, Linux `espeak` or `spd-say` +speak() { + if command -v say &>/dev/null; then + say "$1" + elif command -v espeak &>/dev/null; then + espeak "$1" + elif command -v spd-say &>/dev/null; then + spd-say "$1" + fi +} + for set in 1 2 3; do - say "Set $set. Get in position." + speak "Set $set. Get in position." sleep 3 - say "Go" + speak "Go" sleep 40 - say "Done. Set $set complete." + speak "Done. Set $set complete." if [ $set -lt 3 ]; then - say "Rest 60 seconds" + speak "Rest 60 seconds" sleep 60 fi done -say "RKC Planks done. 3 sets of 40 seconds." +speak "RKC Planks done. 3 sets of 40 seconds."