-
Notifications
You must be signed in to change notification settings - Fork 1
Add Linux TTS support (espeak/spd-say) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
Comment on lines
+168
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This helper now invokes Useful? React with 👍 / 👎. |
||
| ) | ||
| == 0 | ||
| ) | ||
| return _which_cache[cmd] | ||
|
|
||
|
|
||
| _which_cache: dict[str, bool] = {} | ||
|
Comment on lines
+164
to
+178
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two issues in this block:
I suggest moving the definition of Note: You'll need to add _which_cache: dict[str, bool] = {}
def _which(cmd: str) -> bool:
"""Check whether *cmd* is on PATH (cached)."""
if cmd not in _which_cache:
_which_cache[cmd] = shutil.which(cmd) is not None
return _which_cache[cmd] |
||
|
|
||
|
|
||
| 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 | ||
|
Comment on lines
+202
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic causes 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, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation has two areas for improvement:
Here is a suggested refactoring that addresses both points by finding the appropriate TTS command once and defining a
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| 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." | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-wtospd-sayin the synchronous pathWhen
spd-sayis the only Linux backend,say_sync()no longer blocks until the prompt finishes. Thespd-say(1)docs say-w, --waitwill "Wait till the message is spoken or discarded" (https://manpages.debian.org/testing/speech-dispatcher/spd-say.1.en.html), but_tts_cmd()currently returns only['spd-say', text]. Intimed_hold(), that makes the 3-2-1 countdown start while "Get in position" is still being spoken, so timed sets begin early on those machines.Useful? React with 👍 / 👎.