agentvibes 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.agentvibes/LITE-MODE.md +236 -0
  2. package/.agentvibes/README.md +136 -0
  3. package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +141 -0
  4. package/.agentvibes/backups/agents/analyst_20260204_144958.md +78 -0
  5. package/.agentvibes/backups/agents/architect_20260204_144958.md +72 -0
  6. package/.agentvibes/backups/agents/dev_20260204_144958.md +74 -0
  7. package/.agentvibes/backups/agents/pm_20260204_144958.md +72 -0
  8. package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +64 -0
  9. package/.agentvibes/backups/agents/sm_20260204_144958.md +87 -0
  10. package/.agentvibes/backups/agents/tea_20260204_144958.md +79 -0
  11. package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +82 -0
  12. package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +80 -0
  13. package/.agentvibes/bmad/bmad-voices.md +69 -69
  14. package/.agentvibes/config/README-personality-defaults.md +162 -0
  15. package/.agentvibes/config/mode.txt +1 -0
  16. package/.agentvibes/config/personality-voice-defaults.default.json +21 -0
  17. package/.agentvibes/config/save-audio.txt +1 -0
  18. package/.agentvibes/config/voice-metadata.json +160 -0
  19. package/.agentvibes/config.json +24 -15
  20. package/.agentvibes/hooks/help.sh +191 -0
  21. package/.agentvibes/hooks/post-tool-use-lite.sh +111 -0
  22. package/.agentvibes/hooks/save-audio-manager.sh +162 -0
  23. package/.agentvibes/hooks/session-start-full-optimized.sh +102 -0
  24. package/.agentvibes/hooks/session-start-full.sh +142 -0
  25. package/.agentvibes/hooks/session-start-lite-v2.sh +34 -0
  26. package/.agentvibes/hooks/session-start-lite.sh +29 -0
  27. package/.agentvibes/hooks/stop-lite.sh +115 -0
  28. package/.agentvibes/hooks/switch-mode.sh +215 -0
  29. package/.agentvibes/output-styles/audio-summary.md +30 -0
  30. package/.claude/activation-instructions +54 -54
  31. package/.claude/audio/voice-samples/piper/alan.wav +0 -0
  32. package/.claude/audio/voice-samples/piper/amy.wav +0 -0
  33. package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
  34. package/.claude/audio/voice-samples/piper/joe.wav +0 -0
  35. package/.claude/audio/voice-samples/piper/john.wav +0 -0
  36. package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
  37. package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
  38. package/.claude/audio/voice-samples/piper/linda.wav +0 -0
  39. package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
  40. package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
  41. package/.claude/commands/agent-vibes/add.md +21 -21
  42. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  43. package/.claude/commands/agent-vibes/agent.md +79 -79
  44. package/.claude/commands/agent-vibes/background-music.md +111 -111
  45. package/.claude/commands/agent-vibes/bmad.md +198 -198
  46. package/.claude/commands/agent-vibes/clean.md +18 -18
  47. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  48. package/.claude/commands/agent-vibes/commands.json +145 -145
  49. package/.claude/commands/agent-vibes/effects.md +97 -97
  50. package/.claude/commands/agent-vibes/get.md +9 -9
  51. package/.claude/commands/agent-vibes/hide.md +91 -91
  52. package/.claude/commands/agent-vibes/language.md +23 -23
  53. package/.claude/commands/agent-vibes/learn.md +67 -67
  54. package/.claude/commands/agent-vibes/list.md +13 -13
  55. package/.claude/commands/agent-vibes/mute.md +37 -37
  56. package/.claude/commands/agent-vibes/preview.md +17 -17
  57. package/.claude/commands/agent-vibes/provider.md +68 -68
  58. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  59. package/.claude/commands/agent-vibes/sample.md +12 -12
  60. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  61. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  62. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  63. package/.claude/commands/agent-vibes/show.md +84 -84
  64. package/.claude/commands/agent-vibes/switch.md +87 -87
  65. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  66. package/.claude/commands/agent-vibes/target.md +30 -30
  67. package/.claude/commands/agent-vibes/translate.md +68 -68
  68. package/.claude/commands/agent-vibes/unmute.md +45 -45
  69. package/.claude/commands/agent-vibes/whoami.md +7 -7
  70. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  71. package/.claude/commands/agent-vibes-rdp.md +24 -24
  72. package/.claude/config/audio-effects.cfg +16 -11
  73. package/.claude/config/audio-effects.cfg.sample +52 -52
  74. package/.claude/config/background-music-position.txt +27 -0
  75. package/.claude/config/background-music-volume.txt +1 -1
  76. package/.claude/config/background-music.cfg +1 -0
  77. package/.claude/config/background-music.txt +1 -0
  78. package/.claude/config/tts-speech-rate.txt +1 -4
  79. package/.claude/config/tts-verbosity.txt +1 -0
  80. package/.claude/docs/TERMUX_SETUP.md +408 -408
  81. package/.claude/github-star-reminder.txt +1 -1
  82. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  83. package/.claude/hooks/audio-cache-utils.sh +0 -0
  84. package/.claude/hooks/audio-processor.sh +60 -14
  85. package/.claude/hooks/background-music-manager.sh +0 -0
  86. package/.claude/hooks/bmad-party-manager.sh +225 -0
  87. package/.claude/hooks/bmad-party-speak.sh +0 -0
  88. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  89. package/.claude/hooks/bmad-speak.sh +12 -15
  90. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  91. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  92. package/.claude/hooks/clawdbot-receiver-SECURE.sh +25 -23
  93. package/.claude/hooks/clawdbot-receiver.sh +4 -28
  94. package/.claude/hooks/clean-audio-cache.sh +0 -0
  95. package/.claude/hooks/cleanup-cache.sh +0 -0
  96. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  97. package/.claude/hooks/download-extra-voices.sh +0 -0
  98. package/.claude/hooks/effects-manager.sh +0 -0
  99. package/.claude/hooks/github-star-reminder.sh +0 -0
  100. package/.claude/hooks/language-manager.sh +0 -0
  101. package/.claude/hooks/learn-manager.sh +0 -0
  102. package/.claude/hooks/macos-voice-manager.sh +0 -0
  103. package/.claude/hooks/migrate-background-music.sh +0 -0
  104. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  105. package/.claude/hooks/optimize-background-music.sh +0 -0
  106. package/.claude/hooks/personality-manager.sh +0 -0
  107. package/.claude/hooks/piper-download-voices.sh +0 -0
  108. package/.claude/hooks/piper-installer.sh +1 -1
  109. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  110. package/.claude/hooks/piper-voice-manager.sh +0 -0
  111. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  112. package/.claude/hooks/play-tts-macos.sh +6 -12
  113. package/.claude/hooks/play-tts-piper.sh +52 -81
  114. package/.claude/hooks/play-tts-soprano.sh +9 -43
  115. package/.claude/hooks/play-tts-ssh-remote.sh +43 -215
  116. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  117. package/.claude/hooks/play-tts.sh +41 -20
  118. package/.claude/hooks/post-response.sh +41 -0
  119. package/.claude/hooks/prepare-release.sh +0 -0
  120. package/.claude/hooks/provider-commands.sh +0 -0
  121. package/.claude/hooks/provider-manager.sh +0 -0
  122. package/.claude/hooks/replay-target-audio.sh +0 -0
  123. package/.claude/hooks/requirements.txt +6 -6
  124. package/.claude/hooks/sentiment-manager.sh +0 -0
  125. package/.claude/hooks/session-start-tts.sh +56 -39
  126. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  127. package/.claude/hooks/speed-manager.sh +0 -0
  128. package/.claude/hooks/stop.sh +63 -0
  129. package/.claude/hooks/termux-installer.sh +0 -0
  130. package/.claude/hooks/translate-manager.sh +0 -0
  131. package/.claude/hooks/translator.py +237 -237
  132. package/.claude/hooks/tts-queue-worker.sh +0 -0
  133. package/.claude/hooks/tts-queue.sh +0 -0
  134. package/.claude/hooks/verbosity-manager.sh +0 -0
  135. package/.claude/hooks/voice-manager.sh +26 -4
  136. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  137. package/.claude/hooks-windows/bmad-party-speak.ps1 +278 -278
  138. package/.claude/hooks-windows/bmad-speak.ps1 +264 -264
  139. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -53
  140. package/.claude/hooks-windows/effects-manager.ps1 +294 -294
  141. package/.claude/hooks-windows/language-manager.ps1 +193 -193
  142. package/.claude/hooks-windows/learn-manager.ps1 +241 -241
  143. package/.claude/hooks-windows/personality-manager.ps1 +266 -266
  144. package/.claude/hooks-windows/play-tts-soprano.ps1 +5 -5
  145. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -138
  146. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +178 -0
  147. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -0
  148. package/.claude/hooks-windows/play-tts.ps1 +265 -507
  149. package/.claude/hooks-windows/provider-manager.ps1 +158 -192
  150. package/.claude/hooks-windows/session-start-tts.ps1 +55 -46
  151. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  152. package/.claude/hooks-windows/speed-manager.ps1 +166 -166
  153. package/.claude/hooks-windows/voice-manager-windows.ps1 +176 -260
  154. package/.claude/output-styles/agent-vibes.md +202 -202
  155. package/.claude/personalities/angry.md +14 -14
  156. package/.claude/personalities/annoying.md +14 -14
  157. package/.claude/personalities/crass.md +14 -14
  158. package/.claude/personalities/dramatic.md +14 -14
  159. package/.claude/personalities/dry-humor.md +50 -50
  160. package/.claude/personalities/flirty.md +20 -20
  161. package/.claude/personalities/funny.md +14 -14
  162. package/.claude/personalities/grandpa.md +32 -32
  163. package/.claude/personalities/millennial.md +14 -14
  164. package/.claude/personalities/moody.md +14 -14
  165. package/.claude/personalities/normal.md +16 -16
  166. package/.claude/personalities/pirate.md +14 -14
  167. package/.claude/personalities/poetic.md +14 -14
  168. package/.claude/personalities/professional.md +14 -14
  169. package/.claude/personalities/rapper.md +55 -55
  170. package/.claude/personalities/robot.md +14 -14
  171. package/.claude/personalities/sarcastic.md +38 -38
  172. package/.claude/personalities/sassy.md +14 -14
  173. package/.claude/personalities/surfer-dude.md +14 -14
  174. package/.claude/personalities/zen.md +14 -14
  175. package/.claude/piper-voices-dir.txt +1 -0
  176. package/.claude/settings.json +25 -15
  177. package/.claude/verbosity.txt +1 -1
  178. package/.clawdbot/README.md +105 -105
  179. package/.clawdbot/skill/SKILL.md +149 -145
  180. package/.mcp.json +30 -11
  181. package/CLAUDE.md +170 -215
  182. package/README.md +207 -521
  183. package/RELEASE_NOTES.md +1172 -1976
  184. package/WINDOWS-SETUP.md +208 -208
  185. package/bin/agent-vibes +0 -0
  186. package/bin/agentvibes-voice-browser.js +64 -1289
  187. package/bin/agentvibes.js +28 -0
  188. package/bin/ensure-soprano-running.sh +43 -0
  189. package/bin/mcp-server.js +121 -121
  190. package/bin/mcp-server.sh +0 -0
  191. package/bin/test-bmad-pr +78 -78
  192. package/mcp-server/QUICK_START.md +203 -203
  193. package/mcp-server/README.md +345 -345
  194. package/mcp-server/WINDOWS_SETUP.md +260 -260
  195. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  196. package/mcp-server/examples/claude_desktop_config.json +11 -11
  197. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  198. package/mcp-server/examples/custom_instructions.md +169 -169
  199. package/mcp-server/install-deps.js +130 -130
  200. package/mcp-server/pyproject.toml +52 -52
  201. package/mcp-server/requirements.txt +2 -2
  202. package/mcp-server/server.py +1467 -1578
  203. package/mcp-server/test_server.py +395 -395
  204. package/package.json +1 -3
  205. package/setup-windows.ps1 +815 -815
  206. package/src/console/tabs/music-tab.js +5 -2
  207. package/src/console/tabs/voices-tab.js +71 -37
  208. package/src/installer.js +52 -5
  209. package/src/services/llm-provider-service.js +1 -1
  210. package/templates/agentvibes-receiver.sh +158 -483
  211. package/templates/audio/welcome-music.mp3 +0 -0
  212. package/.agentvibes/bmad-voice-map.json +0 -104
  213. package/.agentvibes/copilot-sessions.log +0 -4
  214. package/.claude/config/audio-effects-bmad.cfg +0 -50
  215. package/.claude/config/intro-text.txt +0 -1
  216. package/.claude/config/personality.txt +0 -1
  217. package/.claude/config/piper-speech-rate.txt +0 -4
  218. package/.claude/config/piper-target-speech-rate.txt +0 -1
  219. package/.claude/config/reverb-level.txt +0 -1
  220. package/.claude/config/tts-target-speech-rate.txt +0 -1
  221. package/voice-assignments.json +0 -8245
  222. /package/{.claude → .agentvibes}/config/agentvibes.json +0 -0
@@ -1,483 +1,158 @@
1
- #!/usr/bin/env bash
2
- #
3
- # File: agentvibes-receiver.sh
4
- # Location: User installs to ~/.agentvibes/play-remote.sh
5
- #
6
- # AgentVibes SSH-TTS Receiver (v2 — self-contained pipeline)
7
- # Receives TTS requests via SSH, generates and plays audio locally.
8
- #
9
- # Supports two payload formats:
10
- # 1. JSON payload (v2): single base64-encoded JSON with all config
11
- # 2. Legacy positional args: base64_text voice_name (backward compat)
12
- #
13
- # Pipeline: TTS (piper|soprano|macos|windows-sapi) → sox effects → ffmpeg music mix → audio player
14
- # All steps run in foreground (required for SSH ForceCommand).
15
- #
16
- # Installation:
17
- # curl -sSL https://raw.githubusercontent.com/paulpreibisch/AgentVibes/main/scripts/install-ssh-receiver.sh | bash
18
- #
19
- # Copyright (c) 2025 Paul Preibisch
20
- # Licensed under Apache-2.0
21
- #
22
-
23
- set -euo pipefail
24
-
25
- # ---------------------------------------------------------------------------
26
- # Environment setup for SSH ForceCommand context
27
- # ---------------------------------------------------------------------------
28
-
29
- # ForceCommand passes args via SSH_ORIGINAL_COMMAND env var
30
- # SECURITY: Use read -ra instead of eval to prevent command injection
31
- if [[ -n "${SSH_ORIGINAL_COMMAND:-}" ]]; then
32
- read -ra _ssh_args <<< "$SSH_ORIGINAL_COMMAND"
33
- set -- "${_ssh_args[@]}"
34
- fi
35
-
36
- # Handle -- argument separator (skip it if present)
37
- if [[ "${1:-}" == "--" ]]; then
38
- shift
39
- fi
40
-
41
- # ---------------------------------------------------------------------------
42
- # Configuration — customize these for your installation
43
- # ---------------------------------------------------------------------------
44
-
45
- # Ensure common tool paths are available in restricted SSH context
46
- export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
47
-
48
- # All paths use $HOME — the receiver user's own home directory.
49
- # During install, voices and tracks are symlinked here from the desktop user.
50
- # This avoids needing access to another user's home directory.
51
-
52
- # Where piper voice models are stored
53
- VOICES_DIR="${AGENTVIBES_VOICES_DIR:-$HOME/.claude/piper-voices}"
54
-
55
- # Where background music tracks are stored
56
- TRACKS_DIR="${AGENTVIBES_TRACKS_DIR:-$HOME/.claude/audio/tracks}"
57
-
58
- # Log file the TUI reads from this location
59
- LOG_FILE="${AGENTVIBES_RECEIVER_LOG:-$HOME/.agentvibes/receiver.log}"
60
-
61
- # PipeWire/PulseAudio connect to the desktop user's audio session.
62
- # Cross-user audio is tricky: Unix sockets reject different-uid callers
63
- # even with ACLs. The reliable approach is localhost TCP on a fixed port.
64
- # The setup script configures PipeWire-Pulse to listen on 127.0.0.1:34567.
65
- AGENTVIBES_PULSE_PORT="${AGENTVIBES_PULSE_PORT:-34567}"
66
-
67
- if [[ -z "${PULSE_SERVER:-}" ]]; then
68
- _own_runtime="/run/user/$(id -u)"
69
- # Detect if we're the dedicated receiver user — always use TCP to reach
70
- # the desktop user's audio session, even if we have our own pulse socket.
71
- _is_receiver_user=false
72
- [[ "$(whoami)" == "agentvibes-receiver" ]] && _is_receiver_user=true
73
-
74
- if [[ "$_is_receiver_user" == true ]]; then
75
- # Dedicated receiver user — must use TCP to desktop user's PipeWire-Pulse
76
- export PULSE_SERVER="tcp:127.0.0.1:$AGENTVIBES_PULSE_PORT"
77
- elif [[ -e "$_own_runtime/pulse/native" ]]; then
78
- # Same user — use own Unix socket (fastest)
79
- export PULSE_SERVER="unix:$_own_runtime/pulse/native"
80
- else
81
- # Different user use localhost TCP (setup by agentvibes installer)
82
- export PULSE_SERVER="tcp:127.0.0.1:$AGENTVIBES_PULSE_PORT"
83
- fi
84
- fi
85
-
86
- # XDG_RUNTIME_DIR still needed for pipewire tools (pw-play fallback)
87
- if [[ -z "${XDG_RUNTIME_DIR:-}" ]] || [[ ! -e "$XDG_RUNTIME_DIR/pipewire-0" ]]; then
88
- for _rd in /run/user/*/; do
89
- [[ -e "${_rd}pipewire-0" ]] && { export XDG_RUNTIME_DIR="${_rd%/}"; break; }
90
- done
91
- fi
92
- export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
93
-
94
- # Audio playback — detect available player
95
- # Prefer paplay over pw-play: pw-play from a different user causes
96
- # PipeWire flat-volume side effects that drop the master volume.
97
- AUDIO_PLAYER=""
98
- AUDIO_PLAYER_ARGS=()
99
-
100
- # Check for user-configured sink (set via TUI receiver tab [S] key)
101
- SINK_CONFIG="${AGENTVIBES_RECEIVER_SINK:-$HOME/.agentvibes/receiver-sink.txt}"
102
- _default_sink=""
103
- if [[ -f "$SINK_CONFIG" ]]; then
104
- _configured_sink=$(head -1 "$SINK_CONFIG" 2>/dev/null | tr -d '[:space:]')
105
- # Validate sink name format (alphanumeric, hyphens, underscores, dots)
106
- if [[ -n "$_configured_sink" ]] && [[ "$_configured_sink" =~ ^[a-zA-Z0-9._-]+$ ]]; then
107
- _default_sink="$_configured_sink"
108
- fi
109
- fi
110
- # Fall back to system default if no valid config
111
- if [[ -z "$_default_sink" ]]; then
112
- _default_sink=$(pactl get-default-sink 2>/dev/null || true)
113
- fi
114
-
115
- if command -v paplay &>/dev/null; then
116
- AUDIO_PLAYER="paplay"
117
- [[ -n "$_default_sink" ]] && AUDIO_PLAYER_ARGS=(--device="$_default_sink")
118
- elif command -v pw-play &>/dev/null; then
119
- AUDIO_PLAYER="pw-play"
120
- [[ -n "$_default_sink" ]] && AUDIO_PLAYER_ARGS=(--target="$_default_sink")
121
- elif command -v aplay &>/dev/null; then
122
- AUDIO_PLAYER="aplay"
123
- fi
124
-
125
- # ---------------------------------------------------------------------------
126
- # Input parsing
127
- # ---------------------------------------------------------------------------
128
-
129
- ENCODED_PAYLOAD="${1:-}"
130
-
131
- if [[ -z "$ENCODED_PAYLOAD" ]]; then
132
- echo "Error: No payload provided" >&2
133
- echo "Usage: $0 <base64-encoded-json-or-text> [voice]" >&2
134
- exit 1
135
- fi
136
-
137
- # SECURITY: Validate base64 format (reject shell metacharacters)
138
- if [[ ! "$ENCODED_PAYLOAD" =~ ^[A-Za-z0-9+/=]+$ ]]; then
139
- echo "Error: Payload must be base64-encoded" >&2
140
- exit 1
141
- fi
142
-
143
- # Decode base64
144
- DECODED=$(printf '%s' "$ENCODED_PAYLOAD" | base64 -d 2>/dev/null) || {
145
- echo "Error: Failed to decode base64 payload" >&2
146
- exit 1
147
- }
148
-
149
- # ---------------------------------------------------------------------------
150
- # Parse payload JSON (v2) or plain text (legacy)
151
- # ---------------------------------------------------------------------------
152
-
153
- TEXT=""
154
- VOICE="en_US-lessac-medium"
155
- SOX_EFFECTS=""
156
- BG_FILE=""
157
- BG_VOLUME="0.10"
158
- PROJECT=""
159
- PRETEXT=""
160
- SPEED=""
161
- PROVIDER="piper"
162
-
163
- # Detect JSON payload (starts with '{')
164
- if [[ "$DECODED" == "{"* ]]; then
165
- # JSON v2 payload — extract fields with lightweight parsing
166
- # SECURITY: Use parameter extraction, not eval
167
- if command -v jq &>/dev/null; then
168
- TEXT=$(printf '%s' "$DECODED" | jq -r '.text // empty' 2>/dev/null) || TEXT=""
169
- VOICE=$(printf '%s' "$DECODED" | jq -r '.voice // "en_US-lessac-medium"' 2>/dev/null) || VOICE="en_US-lessac-medium"
170
- SOX_EFFECTS=$(printf '%s' "$DECODED" | jq -r '.effects // empty' 2>/dev/null) || SOX_EFFECTS=""
171
- BG_FILE=$(printf '%s' "$DECODED" | jq -r '.music // empty' 2>/dev/null) || BG_FILE=""
172
- BG_VOLUME=$(printf '%s' "$DECODED" | jq -r '.volume // "0.10"' 2>/dev/null) || BG_VOLUME="0.10"
173
- PROJECT=$(printf '%s' "$DECODED" | jq -r '.project // empty' 2>/dev/null) || PROJECT=""
174
- PRETEXT=$(printf '%s' "$DECODED" | jq -r '.pretext // empty' 2>/dev/null) || PRETEXT=""
175
- SPEED=$(printf '%s' "$DECODED" | jq -r '.speed // empty' 2>/dev/null) || SPEED=""
176
- PROVIDER=$(printf '%s' "$DECODED" | jq -r '.provider // "piper"' 2>/dev/null) || PROVIDER="piper"
177
- else
178
- # Fallback: extract with grep/sed (no jq available)
179
- TEXT=$(printf '%s' "$DECODED" | grep -o '"text"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
180
- VOICE=$(printf '%s' "$DECODED" | grep -o '"voice"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
181
- SOX_EFFECTS=$(printf '%s' "$DECODED" | grep -o '"effects"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
182
- BG_FILE=$(printf '%s' "$DECODED" | grep -o '"music"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
183
- BG_VOLUME=$(printf '%s' "$DECODED" | grep -o '"volume"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
184
- PROJECT=$(printf '%s' "$DECODED" | grep -o '"project"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
185
- PRETEXT=$(printf '%s' "$DECODED" | grep -o '"pretext"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
186
- SPEED=$(printf '%s' "$DECODED" | grep -o '"speed"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
187
- PROVIDER=$(printf '%s' "$DECODED" | grep -o '"provider"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//;s/"$//' || true)
188
- [[ -z "$VOICE" ]] && VOICE="en_US-lessac-medium"
189
- [[ -z "$BG_VOLUME" ]] && BG_VOLUME="0.10"
190
- [[ -z "$PROVIDER" ]] && PROVIDER="piper"
191
- fi
192
- else
193
- # Legacy format: plain text, voice from positional arg
194
- TEXT="$DECODED"
195
- VOICE="${2:-en_US-lessac-medium}"
196
- fi
197
-
198
- # Validate required text
199
- if [[ -z "$TEXT" ]]; then
200
- echo "Error: No text in payload" >&2
201
- exit 1
202
- fi
203
-
204
- # SECURITY: Validate voice format (allow :: for multi-speaker, . for locale, space for names)
205
- _voice_re='^[a-zA-Z0-9_.: -]+$'
206
- if [[ ! "$VOICE" =~ $_voice_re ]]; then
207
- echo "Error: Invalid voice format" >&2
208
- exit 1
209
- fi
210
-
211
- # SECURITY: Validate volume is a number
212
- if [[ -n "$BG_VOLUME" ]] && [[ ! "$BG_VOLUME" =~ ^[0-9]+\.?[0-9]*$ ]]; then
213
- BG_VOLUME="0.10"
214
- fi
215
-
216
- # SECURITY: Validate speed is a number (prevents awk injection)
217
- if [[ -n "$SPEED" ]] && [[ ! "$SPEED" =~ ^[0-9]+\.?[0-9]*$ ]]; then
218
- SPEED=""
219
- fi
220
-
221
- # SECURITY: Validate provider format (known providers only)
222
- case "$PROVIDER" in
223
- piper|soprano|macos|windows-sapi) ;;
224
- *) PROVIDER="piper" ;;
225
- esac
226
-
227
- # Prepend pretext if provided
228
- if [[ -n "$PRETEXT" ]]; then
229
- TEXT="${PRETEXT}. ${TEXT}"
230
- fi
231
-
232
- # ---------------------------------------------------------------------------
233
- # Structured logging (for receiver tab to display)
234
- # ---------------------------------------------------------------------------
235
-
236
- LOG_ID=$(printf '%04x' $((RANDOM % 65536)))
237
-
238
- log_message() {
239
- local status="$1"
240
- local detail="${2:-}"
241
- local timestamp
242
- timestamp=$(date '+%Y-%m-%dT%H:%M:%S')
243
- local log_dir
244
- log_dir=$(dirname "$LOG_FILE")
245
- mkdir -p "$log_dir" 2>/dev/null || true
246
- # Extract sender IP from SSH_CLIENT (set by sshd: "IP PORT PORT")
247
- local sender_ip="${SSH_CLIENT%% *}"
248
- [[ -z "$sender_ip" ]] && sender_ip="local"
249
- # Format: TIMESTAMP|STATUS|PROJECT|VOICE|TEXT_PREVIEW|DETAIL|IP|LOG_ID
250
- local preview="${TEXT:0:200}"
251
- printf '%s|%s|%s|%s|%s|%s|%s|%s\n' \
252
- "$timestamp" "$status" "${PROJECT:-unknown}" "$VOICE" "$preview" "$detail" "$sender_ip" "$LOG_ID" \
253
- >> "$LOG_FILE" 2>/dev/null || true
254
- }
255
-
256
- log_message "RECEIVED" "provider=${PROVIDER} effects=${SOX_EFFECTS:-none} music=${BG_FILE:-none}"
257
-
258
- # ---------------------------------------------------------------------------
259
- # Temp files with cleanup
260
- # ---------------------------------------------------------------------------
261
-
262
- # Use own runtime dir for temp files (not the desktop user's)
263
- _TEMP_BASE="/run/user/$(id -u)"
264
- [[ -d "$_TEMP_BASE" ]] && [[ -w "$_TEMP_BASE" ]] || _TEMP_BASE="/tmp"
265
- RAW_WAV=$(mktemp "$_TEMP_BASE/agentvibes-recv-XXXXXX.wav")
266
- EFFECTS_WAV=$(mktemp "$_TEMP_BASE/agentvibes-recv-fx-XXXXXX.wav")
267
- FINAL_WAV=$(mktemp "$_TEMP_BASE/agentvibes-recv-final-XXXXXX.wav")
268
- trap 'rm -f "$RAW_WAV" "$EFFECTS_WAV" "$FINAL_WAV"' EXIT
269
-
270
- # ---------------------------------------------------------------------------
271
- # Step 1: Generate TTS audio (multi-provider dispatch)
272
- # ---------------------------------------------------------------------------
273
-
274
- _generate_tts_piper() {
275
- local model="$VOICES_DIR/${VOICE}.onnx"
276
- if [[ ! -f "$model" ]]; then
277
- # Fallback: try any available voice rather than failing
278
- local fallback
279
- fallback=$(find "$VOICES_DIR" -maxdepth 1 -name '*.onnx' -type f 2>/dev/null | head -1)
280
- if [[ -n "$fallback" ]]; then
281
- local fallback_name
282
- fallback_name=$(basename "$fallback" .onnx)
283
- log_message "WARN" "Voice $VOICE not found, falling back to $fallback_name"
284
- echo "Warning: Voice $VOICE not found, using $fallback_name" >&2
285
- VOICE="$fallback_name"
286
- model="$fallback"
287
- else
288
- log_message "ERROR" "No voice models found in $VOICES_DIR"
289
- echo "Error: No voice models found in $VOICES_DIR" >&2
290
- return 1
291
- fi
292
- fi
293
-
294
- local args=(--model "$model" --output_file "$RAW_WAV")
295
- if [[ -n "$SPEED" ]] && [[ "$SPEED" =~ ^[0-9]+\.?[0-9]*$ ]]; then
296
- args+=(--length_scale "$SPEED")
297
- fi
298
-
299
- echo "$TEXT" | piper "${args[@]}" 2>/dev/null || {
300
- log_message "ERROR" "Piper TTS failed"
301
- echo "Error: Piper TTS generation failed" >&2
302
- return 1
303
- }
304
- }
305
-
306
- _generate_tts_soprano() {
307
- local soprano_port="${SOPRANO_PORT:-7860}"
308
-
309
- # Try API mode first (OpenAI-compatible endpoint)
310
- if curl -sf -X POST "http://127.0.0.1:${soprano_port}/v1/audio/speech" \
311
- -H "Content-Type: application/json" \
312
- -d "{\"input\":$(printf '%s' "$TEXT" | jq -Rs .)}" \
313
- --output "$RAW_WAV" 2>/dev/null; then
314
- return 0
315
- fi
316
-
317
- # Try CLI mode — options before --, text as final positional arg
318
- if command -v soprano &>/dev/null; then
319
- soprano -o "$RAW_WAV" -- "$TEXT" 2>/dev/null && return 0
320
- fi
321
-
322
- log_message "ERROR" "Soprano TTS failed — is soprano running on port ${soprano_port}?"
323
- echo "Error: Soprano TTS unavailable (tried API and CLI)" >&2
324
- return 1
325
- }
326
-
327
- _generate_tts_macos() {
328
- if ! command -v say &>/dev/null; then
329
- log_message "ERROR" "macOS say command not found"
330
- echo "Error: macOS say command not available" >&2
331
- return 1
332
- fi
333
-
334
- local say_args=(-v "$VOICE")
335
- # Convert speed multiplier to WPM (say uses WPM, default ~200)
336
- if [[ -n "$SPEED" ]] && [[ "$SPEED" =~ ^[0-9]+\.?[0-9]*$ ]]; then
337
- local wpm
338
- wpm=$(awk "BEGIN {printf \"%d\", 200 * $SPEED}")
339
- say_args+=(-r "$wpm")
340
- fi
341
-
342
- # say outputs AIFF — convert to WAV for consistent pipeline
343
- local aiff_tmp="${RAW_WAV%.wav}.aiff"
344
- echo "$TEXT" | say "${say_args[@]}" -o "$aiff_tmp" 2>/dev/null || {
345
- log_message "ERROR" "macOS say failed"
346
- rm -f "$aiff_tmp"
347
- return 1
348
- }
349
-
350
- if command -v ffmpeg &>/dev/null; then
351
- ffmpeg -y -i "$aiff_tmp" "$RAW_WAV" </dev/null 2>/dev/null
352
- rm -f "$aiff_tmp"
353
- else
354
- # No ffmpeg — rename and hope player handles AIFF
355
- mv "$aiff_tmp" "$RAW_WAV"
356
- fi
357
- }
358
-
359
- _generate_tts_windows_sapi() {
360
- # Windows SAPI via PowerShell (works in WSL2 via powershell.exe)
361
- local ps_cmd=""
362
- if command -v powershell.exe &>/dev/null; then
363
- ps_cmd="powershell.exe"
364
- elif command -v pwsh &>/dev/null; then
365
- ps_cmd="pwsh"
366
- else
367
- log_message "ERROR" "PowerShell not found for Windows SAPI"
368
- echo "Error: PowerShell required for Windows SAPI" >&2
369
- return 1
370
- fi
371
-
372
- # SECURITY: Escape text for PowerShell single-quoted string
373
- local escaped_text
374
- escaped_text=$(printf '%s' "$TEXT" | sed "s/'/''/g")
375
-
376
- local rate=0
377
- if [[ -n "$SPEED" ]] && [[ "$SPEED" =~ ^[0-9]+\.?[0-9]*$ ]]; then
378
- # SAPI rate: -10 to 10, 0 is normal. Speed 1.0=0, 2.0=5, 0.5=-5
379
- rate=$(awk "BEGIN {r = ($SPEED - 1.0) * 10; if (r > 10) r = 10; if (r < -10) r = -10; printf \"%d\", r}")
380
- fi
381
-
382
- $ps_cmd -NoProfile -Command "
383
- Add-Type -AssemblyName System.Speech
384
- \$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
385
- \$synth.Rate = $rate
386
- \$synth.SetOutputToWaveFile('$(wslpath -w "$RAW_WAV" 2>/dev/null || echo "$RAW_WAV")')
387
- \$synth.Speak('$escaped_text')
388
- \$synth.Dispose()
389
- " 2>/dev/null || {
390
- log_message "ERROR" "Windows SAPI TTS failed"
391
- echo "Error: Windows SAPI generation failed" >&2
392
- return 1
393
- }
394
- }
395
-
396
- # Dispatch to the appropriate TTS provider
397
- case "$PROVIDER" in
398
- piper)
399
- _generate_tts_piper || exit 1
400
- ;;
401
- soprano)
402
- _generate_tts_soprano || exit 1
403
- ;;
404
- macos)
405
- _generate_tts_macos || exit 1
406
- ;;
407
- windows-sapi)
408
- _generate_tts_windows_sapi || exit 1
409
- ;;
410
- *)
411
- log_message "ERROR" "Unknown provider: $PROVIDER"
412
- echo "Error: Unknown TTS provider: $PROVIDER" >&2
413
- exit 1
414
- ;;
415
- esac
416
-
417
- PLAY_FILE="$RAW_WAV"
418
-
419
- # ---------------------------------------------------------------------------
420
- # Step 2: Apply sox effects (reverb, EQ, etc.)
421
- # ---------------------------------------------------------------------------
422
-
423
- if [[ -n "$SOX_EFFECTS" ]] && command -v sox &>/dev/null; then
424
- # SECURITY: Validate effects contain only safe characters (alphanumeric, spaces, dots, hyphens, underscores)
425
- if [[ "$SOX_EFFECTS" =~ ^[a-zA-Z0-9\ ._-]+$ ]]; then
426
- sox "$RAW_WAV" "$EFFECTS_WAV" $SOX_EFFECTS 2>/dev/null && PLAY_FILE="$EFFECTS_WAV"
427
- else
428
- log_message "WARN" "Rejected unsafe sox effects: ${SOX_EFFECTS:0:50}"
429
- fi
430
- fi
431
-
432
- # ---------------------------------------------------------------------------
433
- # Step 3: Mix background music (if configured)
434
- # ---------------------------------------------------------------------------
435
-
436
- if [[ -n "$BG_FILE" ]] && command -v ffmpeg &>/dev/null; then
437
- BG_PATH="$TRACKS_DIR/$BG_FILE"
438
- if [[ -f "$BG_PATH" ]]; then
439
- DURATION=$(ffprobe -v error -show_entries format=duration \
440
- -of default=noprint_wrappers=1:nokey=1 "$PLAY_FILE" 2>/dev/null || echo "")
441
- if [[ -n "$DURATION" ]]; then
442
- TOTAL_DUR=$(awk "BEGIN {printf \"%.2f\", $DURATION + 2}")
443
- FADE_OUT=$(awk "BEGIN {printf \"%.2f\", $DURATION}")
444
- timeout 20 ffmpeg -y -i "$PLAY_FILE" -stream_loop -1 -i "$BG_PATH" \
445
- -filter_complex "[1:a]volume=${BG_VOLUME},afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT}:d=2[bg];[0:a]adelay=1000|1000,volume=1.5[v];[v][bg]amix=inputs=2:duration=longest:normalize=0[out]" \
446
- -map "[out]" -t "$TOTAL_DUR" "$FINAL_WAV" </dev/null 2>/dev/null && PLAY_FILE="$FINAL_WAV"
447
- fi
448
- fi
449
- fi
450
-
451
- # ---------------------------------------------------------------------------
452
- # Step 4: Play audio in foreground (required for SSH — no backgrounding)
453
- # ---------------------------------------------------------------------------
454
-
455
- if [[ -z "$AUDIO_PLAYER" ]]; then
456
- log_message "ERROR" "No audio player found (pw-play, paplay, aplay)"
457
- echo "Error: No audio player available" >&2
458
- exit 1
459
- fi
460
-
461
- # Save master volume before playback — flat-volumes in PipeWire/PulseAudio
462
- # can change master volume when a new stream connects from another user.
463
- _saved_vol=""
464
- if command -v pactl &>/dev/null; then
465
- _saved_vol=$(pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null | grep -o '[0-9]*%' | head -1)
466
- fi
467
-
468
- log_message "PLAYING" "player=$AUDIO_PLAYER sink=${_default_sink:-unknown} vol=${_saved_vol:-?} pulse=${PULSE_SERVER:-unset}"
469
-
470
- _play_err=$($AUDIO_PLAYER "${AUDIO_PLAYER_ARGS[@]}" "$PLAY_FILE" 2>&1) || {
471
- log_message "ERROR" "Playback failed with $AUDIO_PLAYER: $_play_err"
472
- echo "Error: Audio playback failed" >&2
473
- echo "Detail: $_play_err" >&2
474
- exit 1
475
- }
476
-
477
- # Restore master volume to what it was before playback
478
- if [[ -n "$_saved_vol" ]] && command -v pactl &>/dev/null; then
479
- pactl set-sink-volume @DEFAULT_SINK@ "$_saved_vol" 2>/dev/null || true
480
- fi
481
-
482
- log_message "DONE" ""
483
- exit 0
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: agentvibes-receiver.sh
4
+ # Location: User installs to ~/.termux/agentvibes-play.sh or ~/.agentvibes/play-remote.sh
5
+ #
6
+ # AgentVibes SSH-TTS Receiver
7
+ # Receives text from remote server via SSH, plays with local AgentVibes
8
+ #
9
+ # Installation:
10
+ # curl -sSL https://raw.githubusercontent.com/paulpreibisch/AgentVibes/main/scripts/install-ssh-receiver.sh | bash
11
+ # OR
12
+ # agentvibes install --ssh-receiver
13
+ #
14
+ # Copyright (c) 2025 Paul Preibisch
15
+ # Licensed under Apache-2.0
16
+ #
17
+
18
+ set -euo pipefail
19
+
20
+ # Handle -- argument separator (skip it if present)
21
+ if [[ "${1:-}" == "--" ]]; then
22
+ shift
23
+ fi
24
+
25
+ TEXT="${1:-}"
26
+ VOICE="${2:-en_US-ryan-high}"
27
+ MUSIC="${3:-}"
28
+ REVERB="${4:-}"
29
+
30
+ if [[ -z "$TEXT" ]]; then
31
+ echo "❌ No text provided" >&2
32
+ echo "Usage: $0 [--] <text> [voice] [music] [reverb]" >&2
33
+ exit 1
34
+ fi
35
+
36
+ # SECURITY: If text is base64-encoded (from secure sender), decode it
37
+ # Base64 text won't contain spaces or special chars, so we detect it heuristically
38
+ if [[ "$TEXT" =~ ^[A-Za-z0-9+/]+=*$ ]] && [[ ${#TEXT} -gt 20 ]]; then
39
+ DECODED=$(printf '%s' "$TEXT" | base64 -d 2>/dev/null) || DECODED=""
40
+ if [[ -n "$DECODED" ]]; then
41
+ TEXT="$DECODED"
42
+ fi
43
+ fi
44
+
45
+ # SECURITY: Validate voice format (alphanumeric, hyphens, underscores only)
46
+ if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_-]+$ ]]; then
47
+ echo "❌ Invalid voice format: $VOICE" >&2
48
+ exit 1
49
+ fi
50
+
51
+ # Suppress GitHub star reminders (receiver mode)
52
+ export AGENTVIBES_NO_REMINDERS=1
53
+
54
+ # Find AgentVibes installation
55
+ # SECURITY: Uses controlled paths only, validates existence
56
+ find_agentvibes() {
57
+ # Try command lookup first
58
+ if command -v agentvibes >/dev/null 2>&1; then
59
+ local bin_path
60
+ bin_path=$(which agentvibes)
61
+ # Resolve if it's a symlink
62
+ if [[ -L "$bin_path" ]]; then
63
+ bin_path=$(readlink -f "$bin_path" 2>/dev/null || realpath "$bin_path" 2>/dev/null || echo "$bin_path")
64
+ fi
65
+ # SECURITY: Properly quote nested command substitutions
66
+ local lib_path
67
+ lib_path="$(dirname "$(dirname "$bin_path")")/lib/node_modules/agentvibes"
68
+ if [[ -d "$lib_path" ]]; then
69
+ echo "$lib_path"
70
+ return 0
71
+ fi
72
+ fi
73
+
74
+ # Check common npm global locations (controlled paths only)
75
+ local search_paths=(
76
+ "$HOME/.npm-global/lib/node_modules/agentvibes"
77
+ "/usr/local/lib/node_modules/agentvibes"
78
+ "/data/data/com.termux/files/usr/lib/node_modules/agentvibes" # Android Termux
79
+ )
80
+
81
+ # Handle nvm paths separately to avoid glob issues
82
+ if [[ -d "$HOME/.nvm/versions/node" ]]; then
83
+ local nvm_path
84
+ # SECURITY: Use find instead of unsafe glob expansion
85
+ nvm_path=$(find "$HOME/.nvm/versions/node" -maxdepth 3 -type d -name "agentvibes" -path "*/lib/node_modules/*" 2>/dev/null | head -1)
86
+ if [[ -n "$nvm_path" ]] && [[ -d "$nvm_path" ]]; then
87
+ echo "$nvm_path"
88
+ return 0
89
+ fi
90
+ fi
91
+
92
+ for path in "${search_paths[@]}"; do
93
+ if [[ -d "$path" ]]; then
94
+ echo "$path"
95
+ return 0
96
+ fi
97
+ done
98
+
99
+ return 1
100
+ }
101
+
102
+ AGENTVIBES_ROOT=$(find_agentvibes)
103
+
104
+ if [[ -z "$AGENTVIBES_ROOT" ]]; then
105
+ echo "❌ AgentVibes not found" >&2
106
+ echo "💡 Install: npm install -g agentvibes" >&2
107
+ exit 1
108
+ fi
109
+
110
+ PLAY_TTS="$AGENTVIBES_ROOT/.claude/hooks/play-tts.sh"
111
+
112
+ if [[ ! -f "$PLAY_TTS" ]]; then
113
+ echo "❌ play-tts.sh not found at $PLAY_TTS" >&2
114
+ exit 1
115
+ fi
116
+
117
+ # Configure audio effects and background music if provided
118
+ # CRITICAL: Write to AGENTVIBES package config, not HOME config
119
+ # The audio-processor reads from package config, not ~/.claude/config
120
+ if [[ -n "$MUSIC" ]] || [[ -n "$REVERB" ]]; then
121
+ CONFIG_DIR="$AGENTVIBES_ROOT/.claude/config"
122
+ mkdir -p "$CONFIG_DIR"
123
+
124
+ # Enable background music
125
+ echo "true" > "$CONFIG_DIR/background-music-enabled.txt"
126
+
127
+ # Clear position cache for fresh start
128
+ rm -f "$CONFIG_DIR/background-music-position.txt"
129
+
130
+ # Set background music track if provided
131
+ if [[ -n "$MUSIC" ]]; then
132
+ echo "$MUSIC" > "$CONFIG_DIR/background-music.txt"
133
+ fi
134
+
135
+ # Set audio effects config (default agent name is always "default")
136
+ if [[ -n "$MUSIC" ]] && [[ -n "$REVERB" ]]; then
137
+ echo "default|${REVERB}|${MUSIC}|0.10" > "$CONFIG_DIR/audio-effects.cfg"
138
+ elif [[ -n "$REVERB" ]]; then
139
+ echo "default|${REVERB}||0.0" > "$CONFIG_DIR/audio-effects.cfg"
140
+ elif [[ -n "$MUSIC" ]]; then
141
+ echo "default||${MUSIC}|0.10" > "$CONFIG_DIR/audio-effects.cfg"
142
+ fi
143
+ fi
144
+
145
+ # Log for debugging (optional, comment out in production)
146
+ if [[ "${AGENTVIBES_DEBUG:-0}" == "1" ]]; then
147
+ echo "[DEBUG] AgentVibes root: $AGENTVIBES_ROOT" >&2
148
+ echo "[DEBUG] Voice: $VOICE" >&2
149
+ echo "[DEBUG] Music: ${MUSIC:-none}" >&2
150
+ echo "[DEBUG] Reverb: ${REVERB:-none}" >&2
151
+ echo "[DEBUG] Text length: ${#TEXT}" >&2
152
+ fi
153
+
154
+ # Generate and play with full AgentVibes features
155
+ echo "🎵 Playing via AgentVibes: ${TEXT:0:50}..." >&2
156
+ bash "$PLAY_TTS" "$TEXT" "$VOICE"
157
+
158
+ exit 0
File without changes