agentvibes 4.0.1 → 4.4.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 (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +3 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -389
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -112
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -70
  79. package/.claude/hooks/play-tts-macos.sh +368 -345
  80. package/.claude/hooks/play-tts-piper.sh +679 -578
  81. package/.claude/hooks/play-tts-soprano.sh +356 -320
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -88
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -298
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -71
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -0
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -114
  99. package/.claude/hooks/tts-queue.sh +165 -136
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -544
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -181
  149. package/README.md +2029 -1909
  150. package/RELEASE_NOTES.md +1310 -66
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1826
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1417
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -112
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -806
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -0
  182. package/src/console/footer-config.js +50 -46
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -61
  185. package/src/console/tabs/agents-tab.js +1684 -369
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -46
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -0
  192. package/src/console/tabs/settings-tab.js +185 -402
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -0
  195. package/src/console/widgets/format-utils.js +89 -0
  196. package/src/console/widgets/notice.js +55 -0
  197. package/src/console/widgets/personality-picker.js +185 -0
  198. package/src/console/widgets/reverb-picker.js +94 -0
  199. package/src/console/widgets/track-picker.js +285 -0
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5777
  202. package/src/services/agent-voice-store.js +423 -163
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -275
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -162
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -1,545 +1,549 @@
1
- #!/usr/bin/env bash
2
- #
3
- # File: .claude/hooks/voice-manager.sh
4
- #
5
- # AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants!
6
- # Website: https://agentvibes.org
7
- # Repository: https://github.com/paulpreibisch/AgentVibes
8
- #
9
- # Co-created by Paul Preibisch with Claude AI
10
- # Copyright (c) 2025 Paul Preibisch
11
- #
12
- # Licensed under the Apache License, Version 2.0 (the "License");
13
- # you may not use this file except in compliance with the License.
14
- # You may obtain a copy of the License at
15
- #
16
- # http://www.apache.org/licenses/LICENSE-2.0
17
- #
18
- # Unless required by applicable law or agreed to in writing, software
19
- # distributed under the License is distributed on an "AS IS" BASIS,
20
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
- # See the License for the specific language governing permissions and
22
- # limitations under the License.
23
- #
24
- # DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
- # express or implied. Use at your own risk. See the Apache License for details.
26
- #
27
- # ---
28
- #
29
- # @fileoverview Voice Manager - Unified voice management for Piper and macOS providers
30
- # @context Central interface for listing, switching, previewing, and replaying TTS voices across providers
31
- # @architecture Provider-aware operations with dynamic voice listing based on active provider
32
- # @dependencies piper-voice-manager.sh (Piper voices), provider-manager.sh
33
- # @entrypoints Called by /agent-vibes:switch, /agent-vibes:list, /agent-vibes:whoami, /agent-vibes:replay commands
34
- # @patterns Provider abstraction, numbered selection UI, silent mode for programmatic switching
35
- # @related piper-voice-manager.sh, .claude/tts-voice.txt, .claude/audio/ (replay)
36
-
37
- # Get script directory (physical path for sourcing files)
38
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
39
-
40
- # Bash 3.2 compatible lowercase function (macOS ships with bash 3.2)
41
- # ${var,,} syntax requires bash 4.0+
42
- to_lower() {
43
- echo "$1" | tr '[:upper:]' '[:lower:]'
44
- }
45
-
46
- # Determine target .claude directory based on context
47
- # Priority:
48
- # 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings)
49
- # 2. Script location (for direct slash command usage)
50
- # 3. Global ~/.claude (fallback)
51
-
52
- if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
53
- # MCP context: Use the project directory where MCP was invoked
54
- CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude"
55
- else
56
- # Direct usage context: Use script location
57
- SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
58
- CLAUDE_DIR="$(dirname "$SCRIPT_PATH")"
59
-
60
- # If script is in global ~/.claude, use that
61
- if [[ "$CLAUDE_DIR" == "$HOME/.claude" ]]; then
62
- CLAUDE_DIR="$HOME/.claude"
63
- elif [[ ! -d "$CLAUDE_DIR" ]]; then
64
- # Fallback to global if directory doesn't exist
65
- CLAUDE_DIR="$HOME/.claude"
66
- fi
67
- fi
68
-
69
- VOICE_FILE="$CLAUDE_DIR/tts-voice.txt"
70
-
71
- # Helper function to get default voice based on active provider
72
- get_default_voice() {
73
- local provider_file="$CLAUDE_DIR/tts-provider.txt"
74
- [[ ! -f "$provider_file" ]] && provider_file="$HOME/.claude/tts-provider.txt"
75
-
76
- local active_provider="piper"
77
- [[ -f "$provider_file" ]] && active_provider=$(cat "$provider_file")
78
-
79
- case "$active_provider" in
80
- piper)
81
- echo "en_US-lessac-medium" # Piper default
82
- ;;
83
- macos)
84
- echo "Samantha" # macOS default
85
- ;;
86
- *)
87
- echo "en_US-lessac-medium" # Default to Piper
88
- ;;
89
- esac
90
- }
91
-
92
- case "$1" in
93
- list)
94
- # Get active provider
95
- PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
96
- if [[ ! -f "$PROVIDER_FILE" ]]; then
97
- PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
98
- fi
99
-
100
- ACTIVE_PROVIDER="piper" # default
101
- if [ -f "$PROVIDER_FILE" ]; then
102
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
103
- fi
104
-
105
- CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
106
-
107
- # Use Node.js formatter for beautiful boxen display
108
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
109
- FORMATTER="$PROJECT_ROOT/src/cli/list-voices.js"
110
-
111
- if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
112
- # Get voice directory for Piper
113
- if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then
114
- source "$SCRIPT_DIR/piper-voice-manager.sh"
115
- VOICE_DIR=$(get_voice_storage_dir)
116
-
117
- # Use Node.js formatter if available
118
- if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
119
- node "$FORMATTER" "piper" "$CURRENT_VOICE" "$VOICE_DIR"
120
- else
121
- # Fallback to plain text display
122
- echo "🎤 Available Piper TTS Voices:"
123
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
124
-
125
- VOICE_LIST=()
126
- for onnx_file in "$VOICE_DIR"/*.onnx; do
127
- if [[ -f "$onnx_file" ]]; then
128
- voice=$(basename "$onnx_file" .onnx)
129
- if [ "$voice" = "$CURRENT_VOICE" ]; then
130
- VOICE_LIST+=("$voice (current)")
131
- else
132
- VOICE_LIST+=(" $voice")
133
- fi
134
- fi
135
- done
136
-
137
- if [[ ${#VOICE_LIST[@]} -eq 0 ]]; then
138
- echo " (No Piper voices downloaded yet)"
139
- else
140
- printf "%s\n" "${VOICE_LIST[@]}" | sort
141
- fi
142
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
143
- fi
144
- fi
145
- elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
146
- # Use Node.js formatter if available
147
- if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
148
- node "$FORMATTER" "macos" "$CURRENT_VOICE"
149
- else
150
- # Fallback to plain text display
151
- echo "🎤 Available macOS TTS Voices:"
152
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
153
-
154
- if [[ "$(uname -s)" == "Darwin" ]]; then
155
- say -v ? 2>/dev/null | while read -r line; do
156
- voice=$(echo "$line" | awk '{print $1}')
157
- lang=$(echo "$line" | awk '{print $2}')
158
- if [ "$voice" = "$CURRENT_VOICE" ]; then
159
- printf " ▶ %-15s %s (current)\n" "$voice" "$lang"
160
- else
161
- printf " %-15s %s\n" "$voice" "$lang"
162
- fi
163
- done
164
- else
165
- echo " (macOS voices only available on macOS)"
166
- fi
167
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
168
- fi
169
- else
170
- echo "❌ Unknown provider: $ACTIVE_PROVIDER"
171
- echo ""
172
- echo "Available providers:"
173
- echo " - piper (Free, Offline)"
174
- echo " - macos (Built-in, macOS only)"
175
- echo ""
176
- echo "Switch provider with: /agent-vibes:provider switch piper"
177
- fi
178
- ;;
179
-
180
- preview)
181
- echo "❌ Preview feature is not supported for this provider"
182
- echo ""
183
- echo "Try switching to a voice to hear it:"
184
- echo " /agent-vibes:switch <voice-name>"
185
- echo ""
186
- echo "Or list available voices:"
187
- echo " /agent-vibes:list"
188
- ;;
189
-
190
- switch)
191
- VOICE_NAME="$2"
192
- SILENT_MODE=false
193
-
194
- # Check for --silent flag
195
- if [[ "$2" == "--silent" ]] || [[ "$3" == "--silent" ]]; then
196
- SILENT_MODE=true
197
- # If --silent is first arg, voice name is in $3
198
- [[ "$2" == "--silent" ]] && VOICE_NAME="$3"
199
- fi
200
-
201
- if [[ -z "$VOICE_NAME" ]]; then
202
- echo " No voice name provided"
203
- echo ""
204
- echo "Usage: /agent-vibes:switch <voice-name>"
205
- echo ""
206
- echo "List available voices with: /agent-vibes:list"
207
- exit 1
208
- fi
209
-
210
- # Detect active TTS provider
211
- PROVIDER_FILE=""
212
- if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then
213
- PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
214
- elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then
215
- PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
216
- fi
217
-
218
- ACTIVE_PROVIDER="piper" # default
219
- if [[ -n "$PROVIDER_FILE" ]]; then
220
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
221
- fi
222
-
223
- # Voice lookup strategy depends on active provider
224
- if [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
225
- # macOS voice lookup using say -v ?
226
- if [[ "$(uname -s)" != "Darwin" ]]; then
227
- echo "❌ macOS voices only available on macOS"
228
- echo "Switch to another provider: /agent-vibes:provider switch piper"
229
- exit 1
230
- fi
231
-
232
- # Check if voice exists (case-insensitive match against first column)
233
- FOUND=""
234
- while IFS= read -r line; do
235
- voice=$(echo "$line" | awk '{print $1}')
236
- if [[ "$(to_lower "$voice")" == "$(to_lower "$VOICE_NAME")" ]]; then
237
- FOUND="$voice"
238
- break
239
- fi
240
- done < <(say -v ? 2>/dev/null)
241
-
242
- if [[ -z "$FOUND" ]]; then
243
- echo "❌ macOS voice not found: $VOICE_NAME"
244
- echo ""
245
- echo "Available macOS voices:"
246
- say -v ? 2>/dev/null | awk '{printf " - %-15s %s\n", $1, $2}' | head -20
247
- echo " ... (use /agent-vibes:list to see all)"
248
- exit 1
249
- fi
250
- elif [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
251
- # Piper voice lookup: Scan voice directory for .onnx files
252
- source "$SCRIPT_DIR/piper-voice-manager.sh"
253
- VOICE_DIR=$(get_voice_storage_dir)
254
-
255
- # Check if voice file exists (case-insensitive)
256
- FOUND=""
257
- shopt -s nullglob
258
- for onnx_file in "$VOICE_DIR"/*.onnx; do
259
- if [[ -f "$onnx_file" ]]; then
260
- voice=$(basename "$onnx_file" .onnx)
261
- if [[ "$(to_lower "$voice")" == "$(to_lower "$VOICE_NAME")" ]]; then
262
- FOUND="$voice"
263
- break
264
- fi
265
- fi
266
- done
267
- shopt -u nullglob
268
-
269
- # If not found, check multi-speaker registry
270
- if [[ -z "$FOUND" ]] && [[ -f "$SCRIPT_DIR/piper-multispeaker-registry.sh" ]]; then
271
- source "$SCRIPT_DIR/piper-multispeaker-registry.sh"
272
-
273
- MULTISPEAKER_INFO=$(get_multispeaker_info "$VOICE_NAME")
274
- if [[ -n "$MULTISPEAKER_INFO" ]]; then
275
- MODEL="${MULTISPEAKER_INFO%%:*}"
276
- SPEAKER_ID="${MULTISPEAKER_INFO#*:}"
277
-
278
- # Verify the model file exists
279
- if [[ -f "$VOICE_DIR/${MODEL}.onnx" ]]; then
280
- # Store speaker name in tts-voice.txt
281
- echo "$VOICE_NAME" > "$VOICE_FILE"
282
-
283
- # Store model and speaker ID separately for play-tts-piper.sh
284
- echo "$MODEL" > "$CLAUDE_DIR/tts-piper-model.txt"
285
- echo "$SPEAKER_ID" > "$CLAUDE_DIR/tts-piper-speaker-id.txt"
286
-
287
- DESCRIPTION=$(get_multispeaker_description "$VOICE_NAME")
288
- echo " Multi-speaker voice switched to: $VOICE_NAME"
289
- echo "🎤 Model: $MODEL.onnx (Speaker ID: $SPEAKER_ID)"
290
- if [[ -n "$DESCRIPTION" ]]; then
291
- echo "📝 Description: $DESCRIPTION"
292
- fi
293
-
294
- # Have the new voice introduce itself (unless silent mode)
295
- if [[ "$SILENT_MODE" != "true" ]]; then
296
- PLAY_TTS="$SCRIPT_DIR/play-tts.sh"
297
- if [ -x "$PLAY_TTS" ]; then
298
- "$PLAY_TTS" "Hi, I'm $VOICE_NAME. I'll be your voice assistant moving forward." > /dev/null 2>&1 &
299
- fi
300
-
301
- echo ""
302
- echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:"
303
- echo " /output-style Agent Vibes"
304
- fi
305
- exit 0
306
- else
307
- echo "❌ Multi-speaker model not found: $MODEL.onnx"
308
- echo ""
309
- echo "Download it with: /agent-vibes:provider download"
310
- exit 1
311
- fi
312
- fi
313
- fi
314
-
315
- # In test mode, allow switching to any voice name without file validation
316
- if [[ -z "$FOUND" ]] && [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]]; then
317
- echo "❌ Piper voice not found: $VOICE_NAME"
318
- echo ""
319
- echo "Available Piper voices:"
320
- shopt -s nullglob
321
- for onnx_file in "$VOICE_DIR"/*.onnx; do
322
- if [[ -f "$onnx_file" ]]; then
323
- echo " - $(basename "$onnx_file" .onnx)"
324
- fi
325
- done | sort
326
- shopt -u nullglob
327
- echo ""
328
- if [[ -f "$SCRIPT_DIR/piper-multispeaker-registry.sh" ]]; then
329
- echo "Multi-speaker voices (requires 16Speakers.onnx):"
330
- source "$SCRIPT_DIR/piper-multispeaker-registry.sh"
331
- for entry in "${MULTISPEAKER_VOICES[@]}"; do
332
- name="${entry%%:*}"
333
- echo " - $name"
334
- done | sort
335
- echo ""
336
- fi
337
- echo "Download extra voices with: /agent-vibes:provider download"
338
- exit 1
339
- fi
340
- else
341
- echo " Unknown provider: $ACTIVE_PROVIDER"
342
- echo ""
343
- echo "Available providers:"
344
- echo " - piper (Free, Offline)"
345
- echo " - macos (Built-in, macOS only)"
346
- echo ""
347
- echo "Switch provider with: /agent-vibes:provider switch piper"
348
- exit 1
349
- fi
350
-
351
- # In test mode, use the requested voice name even if not found
352
- VOICE_TO_SAVE="${FOUND:-$VOICE_NAME}"
353
- echo "$VOICE_TO_SAVE" > "$VOICE_FILE"
354
- echo "✅ Voice switched to: $VOICE_TO_SAVE"
355
-
356
- # Have the new voice introduce itself (unless silent mode)
357
- if [[ "$SILENT_MODE" != "true" ]] && [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]]; then
358
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
359
- PLAY_TTS="$SCRIPT_DIR/play-tts.sh"
360
- if [ -x "$PLAY_TTS" ]; then
361
- "$PLAY_TTS" "Hi, I'm $VOICE_TO_SAVE. I'll be your voice assistant moving forward." "$VOICE_TO_SAVE" > /dev/null 2>&1 &
362
- fi
363
-
364
- echo ""
365
- echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:"
366
- echo " /output-style Agent Vibes"
367
- fi
368
- ;;
369
-
370
- get)
371
- if [ -f "$VOICE_FILE" ]; then
372
- cat "$VOICE_FILE"
373
- else
374
- get_default_voice
375
- fi
376
- ;;
377
-
378
- whoami)
379
- echo "🎤 Current Voice Configuration"
380
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
381
-
382
- # Get active TTS provider
383
- PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
384
- if [[ ! -f "$PROVIDER_FILE" ]]; then
385
- PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
386
- fi
387
-
388
- if [ -f "$PROVIDER_FILE" ]; then
389
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
390
- if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
391
- echo "Provider: Piper TTS (Free, Offline)"
392
- elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
393
- echo "Provider: macOS Say (Built-in, Free)"
394
- else
395
- echo "Provider: $ACTIVE_PROVIDER"
396
- fi
397
- else
398
- # Default to Piper if no provider file
399
- echo "Provider: Piper TTS (Free, Offline)"
400
- fi
401
-
402
- # Get current voice
403
- CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
404
- echo "Voice: $CURRENT_VOICE"
405
-
406
- # Get current sentiment (priority)
407
- if [ -f "$HOME/.claude/tts-sentiment.txt" ]; then
408
- SENTIMENT=$(cat "$HOME/.claude/tts-sentiment.txt")
409
- echo "Sentiment: $SENTIMENT (active)"
410
-
411
- # Also show personality if set
412
- if [ -f "$HOME/.claude/tts-personality.txt" ]; then
413
- PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt")
414
- echo "Personality: $PERSONALITY (overridden by sentiment)"
415
- fi
416
- else
417
- # No sentiment, check personality
418
- if [ -f "$HOME/.claude/tts-personality.txt" ]; then
419
- PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt")
420
- echo "Personality: $PERSONALITY (active)"
421
- else
422
- echo "Personality: normal"
423
- fi
424
- fi
425
-
426
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
427
- ;;
428
-
429
- list-simple)
430
- # Simple list for AI to parse and display
431
- # Get active provider
432
- PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
433
- if [[ ! -f "$PROVIDER_FILE" ]]; then
434
- PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
435
- fi
436
-
437
- ACTIVE_PROVIDER="piper" # default
438
- if [ -f "$PROVIDER_FILE" ]; then
439
- ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
440
- fi
441
-
442
- if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
443
- # List downloaded Piper voices
444
- if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then
445
- source "$SCRIPT_DIR/piper-voice-manager.sh"
446
- VOICE_DIR=$(get_voice_storage_dir)
447
- for onnx_file in "$VOICE_DIR"/*.onnx; do
448
- if [[ -f "$onnx_file" ]]; then
449
- basename "$onnx_file" .onnx
450
- fi
451
- done | sort
452
- fi
453
- elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
454
- # List macOS voices (voice names only)
455
- if [[ "$(uname -s)" == "Darwin" ]]; then
456
- say -v ? 2>/dev/null | awk '{print $1}' | sort
457
- else
458
- echo "(macOS voices only available on macOS)"
459
- fi
460
- else
461
- echo "(Unknown provider: $ACTIVE_PROVIDER)"
462
- fi
463
- ;;
464
-
465
- replay)
466
- # Replay recent TTS audio from history
467
- # Use project-local directory with same logic as play-tts.sh
468
- if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then
469
- AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio"
470
- else
471
- # Fallback: try to find .claude directory in current path
472
- CURRENT_DIR="$PWD"
473
- while [[ "$CURRENT_DIR" != "/" ]]; do
474
- if [[ -d "$CURRENT_DIR/.claude" ]]; then
475
- AUDIO_DIR="$CURRENT_DIR/.claude/audio"
476
- break
477
- fi
478
- CURRENT_DIR=$(dirname "$CURRENT_DIR")
479
- done
480
- # Final fallback to global if no project .claude found
481
- if [[ -z "$AUDIO_DIR" ]]; then
482
- AUDIO_DIR="$HOME/.claude/audio"
483
- fi
484
- fi
485
-
486
- # Default to replay last audio (N=1)
487
- N="${2:-1}"
488
-
489
- # Validate N is a number
490
- if ! [[ "$N" =~ ^[0-9]+$ ]]; then
491
- echo "❌ Invalid argument. Please use a number (1-10)"
492
- echo "Usage: /agent-vibes:replay [N]"
493
- echo " N=1 - Last audio (default)"
494
- echo " N=2 - Second-to-last"
495
- echo " N=3 - Third-to-last"
496
- exit 1
497
- fi
498
-
499
- # Check bounds
500
- if [[ $N -lt 1 || $N -gt 10 ]]; then
501
- echo "❌ Number out of range. Please choose 1-10"
502
- exit 1
503
- fi
504
-
505
- # Get list of audio files sorted by time (newest first)
506
- if [[ ! -d "$AUDIO_DIR" ]]; then
507
- echo "❌ No audio history found"
508
- echo "Audio files are stored in: $AUDIO_DIR"
509
- exit 1
510
- fi
511
-
512
- # Get the Nth most recent file (check all supported formats)
513
- AUDIO_FILE=$(ls -t "$AUDIO_DIR"/tts-*.{mp3,wav,aiff} 2>/dev/null | sed -n "${N}p")
514
-
515
- if [[ -z "$AUDIO_FILE" ]]; then
516
- TOTAL=$(ls -t "$AUDIO_DIR"/tts-*.{mp3,wav,aiff} 2>/dev/null | wc -l)
517
- echo " Audio #$N not found in history"
518
- echo "Total audio files available: $TOTAL"
519
- exit 1
520
- fi
521
-
522
- echo "🔊 Replaying audio #$N:"
523
- echo " File: $(basename "$AUDIO_FILE")"
524
- echo " Path: $AUDIO_FILE"
525
-
526
- # Play the audio file in background (afplay for macOS, paplay/aplay/mpg123 for Linux)
527
- if [[ "$(uname -s)" == "Darwin" ]]; then
528
- afplay "$AUDIO_FILE" &
529
- else
530
- (paplay "$AUDIO_FILE" 2>/dev/null || aplay "$AUDIO_FILE" 2>/dev/null || mpg123 "$AUDIO_FILE" 2>/dev/null) &
531
- fi
532
- ;;
533
-
534
- *)
535
- echo "Usage: voice-manager.sh [list|switch|get|replay|whoami] [voice_name]"
536
- echo ""
537
- echo "Commands:"
538
- echo " list - List all available voices"
539
- echo " switch <voice_name> - Switch to a different voice"
540
- echo " get - Get current voice name"
541
- echo " replay [N] - Replay Nth most recent audio (default: 1)"
542
- echo " whoami - Show current voice and personality"
543
- exit 1
544
- ;;
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/voice-manager.sh
4
+ #
5
+ # AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants!
6
+ # Website: https://agentvibes.org
7
+ # Repository: https://github.com/paulpreibisch/AgentVibes
8
+ #
9
+ # Co-created by Paul Preibisch with Claude AI
10
+ # Copyright (c) 2025 Paul Preibisch
11
+ #
12
+ # Licensed under the Apache License, Version 2.0 (the "License");
13
+ # you may not use this file except in compliance with the License.
14
+ # You may obtain a copy of the License at
15
+ #
16
+ # http://www.apache.org/licenses/LICENSE-2.0
17
+ #
18
+ # Unless required by applicable law or agreed to in writing, software
19
+ # distributed under the License is distributed on an "AS IS" BASIS,
20
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ # See the License for the specific language governing permissions and
22
+ # limitations under the License.
23
+ #
24
+ # DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
25
+ # express or implied. Use at your own risk. See the Apache License for details.
26
+ #
27
+ # ---
28
+ #
29
+ # @fileoverview Voice Manager - Unified voice management for Piper and macOS providers
30
+ # @context Central interface for listing, switching, previewing, and replaying TTS voices across providers
31
+ # @architecture Provider-aware operations with dynamic voice listing based on active provider
32
+ # @dependencies piper-voice-manager.sh (Piper voices), provider-manager.sh
33
+ # @entrypoints Called by /agent-vibes:switch, /agent-vibes:list, /agent-vibes:whoami, /agent-vibes:replay commands
34
+ # @patterns Provider abstraction, numbered selection UI, silent mode for programmatic switching
35
+ # @related piper-voice-manager.sh, .claude/tts-voice.txt, .claude/audio/ (replay)
36
+
37
+ # Get script directory (physical path for sourcing files)
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
39
+
40
+ # Bash 3.2 compatible lowercase function (macOS ships with bash 3.2)
41
+ # ${var,,} syntax requires bash 4.0+
42
+ to_lower() {
43
+ echo "$1" | tr '[:upper:]' '[:lower:]'
44
+ }
45
+
46
+ # Determine target .claude directory based on context
47
+ # Priority:
48
+ # 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings)
49
+ # 2. Script location (for direct slash command usage)
50
+ # 3. Global ~/.claude (fallback)
51
+
52
+ # SECURITY: Canonicalize path to prevent traversal (#128)
53
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
54
+ CLAUDE_PROJECT_DIR=$(cd "${CLAUDE_PROJECT_DIR}" 2>/dev/null && pwd -P) || CLAUDE_PROJECT_DIR=""
55
+ fi
56
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
57
+ # MCP context: Use the project directory where MCP was invoked
58
+ CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude"
59
+ else
60
+ # Direct usage context: Use script location
61
+ SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
62
+ CLAUDE_DIR="$(dirname "$SCRIPT_PATH")"
63
+
64
+ # If script is in global ~/.claude, use that
65
+ if [[ "$CLAUDE_DIR" == "$HOME/.claude" ]]; then
66
+ CLAUDE_DIR="$HOME/.claude"
67
+ elif [[ ! -d "$CLAUDE_DIR" ]]; then
68
+ # Fallback to global if directory doesn't exist
69
+ CLAUDE_DIR="$HOME/.claude"
70
+ fi
71
+ fi
72
+
73
+ VOICE_FILE="$CLAUDE_DIR/tts-voice.txt"
74
+
75
+ # Helper function to get default voice based on active provider
76
+ get_default_voice() {
77
+ local provider_file="$CLAUDE_DIR/tts-provider.txt"
78
+ [[ ! -f "$provider_file" ]] && provider_file="$HOME/.claude/tts-provider.txt"
79
+
80
+ local active_provider="piper"
81
+ [[ -f "$provider_file" ]] && active_provider=$(cat "$provider_file")
82
+
83
+ case "$active_provider" in
84
+ piper)
85
+ echo "en_US-lessac-medium" # Piper default
86
+ ;;
87
+ macos)
88
+ echo "Samantha" # macOS default
89
+ ;;
90
+ *)
91
+ echo "en_US-lessac-medium" # Default to Piper
92
+ ;;
93
+ esac
94
+ }
95
+
96
+ case "$1" in
97
+ list)
98
+ # Get active provider
99
+ PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
100
+ if [[ ! -f "$PROVIDER_FILE" ]]; then
101
+ PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
102
+ fi
103
+
104
+ ACTIVE_PROVIDER="piper" # default
105
+ if [ -f "$PROVIDER_FILE" ]; then
106
+ ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
107
+ fi
108
+
109
+ CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
110
+
111
+ # Use Node.js formatter for beautiful boxen display
112
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
113
+ FORMATTER="$PROJECT_ROOT/src/cli/list-voices.js"
114
+
115
+ if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
116
+ # Get voice directory for Piper
117
+ if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then
118
+ source "$SCRIPT_DIR/piper-voice-manager.sh"
119
+ VOICE_DIR=$(get_voice_storage_dir)
120
+
121
+ # Use Node.js formatter if available
122
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
123
+ node "$FORMATTER" "piper" "$CURRENT_VOICE" "$VOICE_DIR"
124
+ else
125
+ # Fallback to plain text display
126
+ echo "🎤 Available Piper TTS Voices:"
127
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
128
+
129
+ VOICE_LIST=()
130
+ for onnx_file in "$VOICE_DIR"/*.onnx; do
131
+ if [[ -f "$onnx_file" ]]; then
132
+ voice=$(basename "$onnx_file" .onnx)
133
+ if [ "$voice" = "$CURRENT_VOICE" ]; then
134
+ VOICE_LIST+=(" ▶ $voice (current)")
135
+ else
136
+ VOICE_LIST+=(" $voice")
137
+ fi
138
+ fi
139
+ done
140
+
141
+ if [[ ${#VOICE_LIST[@]} -eq 0 ]]; then
142
+ echo " (No Piper voices downloaded yet)"
143
+ else
144
+ printf "%s\n" "${VOICE_LIST[@]}" | sort
145
+ fi
146
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
147
+ fi
148
+ fi
149
+ elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
150
+ # Use Node.js formatter if available
151
+ if [[ -f "$FORMATTER" ]] && command -v node &> /dev/null; then
152
+ node "$FORMATTER" "macos" "$CURRENT_VOICE"
153
+ else
154
+ # Fallback to plain text display
155
+ echo "🎤 Available macOS TTS Voices:"
156
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
157
+
158
+ if [[ "$(uname -s)" == "Darwin" ]]; then
159
+ say -v ? 2>/dev/null | while read -r line; do
160
+ voice=$(echo "$line" | awk '{print $1}')
161
+ lang=$(echo "$line" | awk '{print $2}')
162
+ if [ "$voice" = "$CURRENT_VOICE" ]; then
163
+ printf " ▶ %-15s %s (current)\n" "$voice" "$lang"
164
+ else
165
+ printf " %-15s %s\n" "$voice" "$lang"
166
+ fi
167
+ done
168
+ else
169
+ echo " (macOS voices only available on macOS)"
170
+ fi
171
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
172
+ fi
173
+ else
174
+ echo " Unknown provider: $ACTIVE_PROVIDER"
175
+ echo ""
176
+ echo "Available providers:"
177
+ echo " - piper (Free, Offline)"
178
+ echo " - macos (Built-in, macOS only)"
179
+ echo ""
180
+ echo "Switch provider with: /agent-vibes:provider switch piper"
181
+ fi
182
+ ;;
183
+
184
+ preview)
185
+ echo "❌ Preview feature is not supported for this provider"
186
+ echo ""
187
+ echo "Try switching to a voice to hear it:"
188
+ echo " /agent-vibes:switch <voice-name>"
189
+ echo ""
190
+ echo "Or list available voices:"
191
+ echo " /agent-vibes:list"
192
+ ;;
193
+
194
+ switch)
195
+ VOICE_NAME="$2"
196
+ SILENT_MODE=false
197
+
198
+ # Check for --silent flag
199
+ if [[ "$2" == "--silent" ]] || [[ "$3" == "--silent" ]]; then
200
+ SILENT_MODE=true
201
+ # If --silent is first arg, voice name is in $3
202
+ [[ "$2" == "--silent" ]] && VOICE_NAME="$3"
203
+ fi
204
+
205
+ if [[ -z "$VOICE_NAME" ]]; then
206
+ echo " No voice name provided"
207
+ echo ""
208
+ echo "Usage: /agent-vibes:switch <voice-name>"
209
+ echo ""
210
+ echo "List available voices with: /agent-vibes:list"
211
+ exit 1
212
+ fi
213
+
214
+ # Detect active TTS provider
215
+ PROVIDER_FILE=""
216
+ if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then
217
+ PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
218
+ elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then
219
+ PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
220
+ fi
221
+
222
+ ACTIVE_PROVIDER="piper" # default
223
+ if [[ -n "$PROVIDER_FILE" ]]; then
224
+ ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
225
+ fi
226
+
227
+ # Voice lookup strategy depends on active provider
228
+ if [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
229
+ # macOS voice lookup using say -v ?
230
+ if [[ "$(uname -s)" != "Darwin" ]]; then
231
+ echo "❌ macOS voices only available on macOS"
232
+ echo "Switch to another provider: /agent-vibes:provider switch piper"
233
+ exit 1
234
+ fi
235
+
236
+ # Check if voice exists (case-insensitive match against first column)
237
+ FOUND=""
238
+ while IFS= read -r line; do
239
+ voice=$(echo "$line" | awk '{print $1}')
240
+ if [[ "$(to_lower "$voice")" == "$(to_lower "$VOICE_NAME")" ]]; then
241
+ FOUND="$voice"
242
+ break
243
+ fi
244
+ done < <(say -v ? 2>/dev/null)
245
+
246
+ if [[ -z "$FOUND" ]]; then
247
+ echo " macOS voice not found: $VOICE_NAME"
248
+ echo ""
249
+ echo "Available macOS voices:"
250
+ say -v ? 2>/dev/null | awk '{printf " - %-15s %s\n", $1, $2}' | head -20
251
+ echo " ... (use /agent-vibes:list to see all)"
252
+ exit 1
253
+ fi
254
+ elif [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
255
+ # Piper voice lookup: Scan voice directory for .onnx files
256
+ source "$SCRIPT_DIR/piper-voice-manager.sh"
257
+ VOICE_DIR=$(get_voice_storage_dir)
258
+
259
+ # Check if voice file exists (case-insensitive)
260
+ FOUND=""
261
+ shopt -s nullglob
262
+ for onnx_file in "$VOICE_DIR"/*.onnx; do
263
+ if [[ -f "$onnx_file" ]]; then
264
+ voice=$(basename "$onnx_file" .onnx)
265
+ if [[ "$(to_lower "$voice")" == "$(to_lower "$VOICE_NAME")" ]]; then
266
+ FOUND="$voice"
267
+ break
268
+ fi
269
+ fi
270
+ done
271
+ shopt -u nullglob
272
+
273
+ # If not found, check multi-speaker registry
274
+ if [[ -z "$FOUND" ]] && [[ -f "$SCRIPT_DIR/piper-multispeaker-registry.sh" ]]; then
275
+ source "$SCRIPT_DIR/piper-multispeaker-registry.sh"
276
+
277
+ MULTISPEAKER_INFO=$(get_multispeaker_info "$VOICE_NAME")
278
+ if [[ -n "$MULTISPEAKER_INFO" ]]; then
279
+ MODEL="${MULTISPEAKER_INFO%%:*}"
280
+ SPEAKER_ID="${MULTISPEAKER_INFO#*:}"
281
+
282
+ # Verify the model file exists
283
+ if [[ -f "$VOICE_DIR/${MODEL}.onnx" ]]; then
284
+ # Store speaker name in tts-voice.txt
285
+ echo "$VOICE_NAME" > "$VOICE_FILE"
286
+
287
+ # Store model and speaker ID separately for play-tts-piper.sh
288
+ echo "$MODEL" > "$CLAUDE_DIR/tts-piper-model.txt"
289
+ echo "$SPEAKER_ID" > "$CLAUDE_DIR/tts-piper-speaker-id.txt"
290
+
291
+ DESCRIPTION=$(get_multispeaker_description "$VOICE_NAME")
292
+ echo "✅ Multi-speaker voice switched to: $VOICE_NAME"
293
+ echo "🎤 Model: $MODEL.onnx (Speaker ID: $SPEAKER_ID)"
294
+ if [[ -n "$DESCRIPTION" ]]; then
295
+ echo "📝 Description: $DESCRIPTION"
296
+ fi
297
+
298
+ # Have the new voice introduce itself (unless silent mode)
299
+ if [[ "$SILENT_MODE" != "true" ]]; then
300
+ PLAY_TTS="$SCRIPT_DIR/play-tts.sh"
301
+ if [ -x "$PLAY_TTS" ]; then
302
+ "$PLAY_TTS" "Hi, I'm $VOICE_NAME. I'll be your voice assistant moving forward." > /dev/null 2>&1 &
303
+ fi
304
+
305
+ echo ""
306
+ echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:"
307
+ echo " /output-style Agent Vibes"
308
+ fi
309
+ exit 0
310
+ else
311
+ echo "❌ Multi-speaker model not found: $MODEL.onnx"
312
+ echo ""
313
+ echo "Download it with: /agent-vibes:provider download"
314
+ exit 1
315
+ fi
316
+ fi
317
+ fi
318
+
319
+ # In test mode, allow switching to any voice name without file validation
320
+ if [[ -z "$FOUND" ]] && [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]]; then
321
+ echo "❌ Piper voice not found: $VOICE_NAME"
322
+ echo ""
323
+ echo "Available Piper voices:"
324
+ shopt -s nullglob
325
+ for onnx_file in "$VOICE_DIR"/*.onnx; do
326
+ if [[ -f "$onnx_file" ]]; then
327
+ echo " - $(basename "$onnx_file" .onnx)"
328
+ fi
329
+ done | sort
330
+ shopt -u nullglob
331
+ echo ""
332
+ if [[ -f "$SCRIPT_DIR/piper-multispeaker-registry.sh" ]]; then
333
+ echo "Multi-speaker voices (requires 16Speakers.onnx):"
334
+ source "$SCRIPT_DIR/piper-multispeaker-registry.sh"
335
+ for entry in "${MULTISPEAKER_VOICES[@]}"; do
336
+ name="${entry%%:*}"
337
+ echo " - $name"
338
+ done | sort
339
+ echo ""
340
+ fi
341
+ echo "Download extra voices with: /agent-vibes:provider download"
342
+ exit 1
343
+ fi
344
+ else
345
+ echo " Unknown provider: $ACTIVE_PROVIDER"
346
+ echo ""
347
+ echo "Available providers:"
348
+ echo " - piper (Free, Offline)"
349
+ echo " - macos (Built-in, macOS only)"
350
+ echo ""
351
+ echo "Switch provider with: /agent-vibes:provider switch piper"
352
+ exit 1
353
+ fi
354
+
355
+ # In test mode, use the requested voice name even if not found
356
+ VOICE_TO_SAVE="${FOUND:-$VOICE_NAME}"
357
+ echo "$VOICE_TO_SAVE" > "$VOICE_FILE"
358
+ echo " Voice switched to: $VOICE_TO_SAVE"
359
+
360
+ # Have the new voice introduce itself (unless silent mode)
361
+ if [[ "$SILENT_MODE" != "true" ]] && [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]]; then
362
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
363
+ PLAY_TTS="$SCRIPT_DIR/play-tts.sh"
364
+ if [ -x "$PLAY_TTS" ]; then
365
+ "$PLAY_TTS" "Hi, I'm $VOICE_TO_SAVE. I'll be your voice assistant moving forward." "$VOICE_TO_SAVE" > /dev/null 2>&1 &
366
+ fi
367
+
368
+ echo ""
369
+ echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:"
370
+ echo " /output-style Agent Vibes"
371
+ fi
372
+ ;;
373
+
374
+ get)
375
+ if [ -f "$VOICE_FILE" ]; then
376
+ cat "$VOICE_FILE"
377
+ else
378
+ get_default_voice
379
+ fi
380
+ ;;
381
+
382
+ whoami)
383
+ echo "🎤 Current Voice Configuration"
384
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
385
+
386
+ # Get active TTS provider
387
+ PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
388
+ if [[ ! -f "$PROVIDER_FILE" ]]; then
389
+ PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
390
+ fi
391
+
392
+ if [ -f "$PROVIDER_FILE" ]; then
393
+ ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
394
+ if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
395
+ echo "Provider: Piper TTS (Free, Offline)"
396
+ elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
397
+ echo "Provider: macOS Say (Built-in, Free)"
398
+ else
399
+ echo "Provider: $ACTIVE_PROVIDER"
400
+ fi
401
+ else
402
+ # Default to Piper if no provider file
403
+ echo "Provider: Piper TTS (Free, Offline)"
404
+ fi
405
+
406
+ # Get current voice
407
+ CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
408
+ echo "Voice: $CURRENT_VOICE"
409
+
410
+ # Get current sentiment (priority)
411
+ if [ -f "$HOME/.claude/tts-sentiment.txt" ]; then
412
+ SENTIMENT=$(cat "$HOME/.claude/tts-sentiment.txt")
413
+ echo "Sentiment: $SENTIMENT (active)"
414
+
415
+ # Also show personality if set
416
+ if [ -f "$HOME/.claude/tts-personality.txt" ]; then
417
+ PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt")
418
+ echo "Personality: $PERSONALITY (overridden by sentiment)"
419
+ fi
420
+ else
421
+ # No sentiment, check personality
422
+ if [ -f "$HOME/.claude/tts-personality.txt" ]; then
423
+ PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt")
424
+ echo "Personality: $PERSONALITY (active)"
425
+ else
426
+ echo "Personality: normal"
427
+ fi
428
+ fi
429
+
430
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
431
+ ;;
432
+
433
+ list-simple)
434
+ # Simple list for AI to parse and display
435
+ # Get active provider
436
+ PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
437
+ if [[ ! -f "$PROVIDER_FILE" ]]; then
438
+ PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
439
+ fi
440
+
441
+ ACTIVE_PROVIDER="piper" # default
442
+ if [ -f "$PROVIDER_FILE" ]; then
443
+ ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
444
+ fi
445
+
446
+ if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then
447
+ # List downloaded Piper voices
448
+ if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then
449
+ source "$SCRIPT_DIR/piper-voice-manager.sh"
450
+ VOICE_DIR=$(get_voice_storage_dir)
451
+ for onnx_file in "$VOICE_DIR"/*.onnx; do
452
+ if [[ -f "$onnx_file" ]]; then
453
+ basename "$onnx_file" .onnx
454
+ fi
455
+ done | sort
456
+ fi
457
+ elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
458
+ # List macOS voices (voice names only)
459
+ if [[ "$(uname -s)" == "Darwin" ]]; then
460
+ say -v ? 2>/dev/null | awk '{print $1}' | sort
461
+ else
462
+ echo "(macOS voices only available on macOS)"
463
+ fi
464
+ else
465
+ echo "(Unknown provider: $ACTIVE_PROVIDER)"
466
+ fi
467
+ ;;
468
+
469
+ replay)
470
+ # Replay recent TTS audio from history
471
+ # Use project-local directory with same logic as play-tts.sh
472
+ if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then
473
+ AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio"
474
+ else
475
+ # Fallback: try to find .claude directory in current path
476
+ CURRENT_DIR="$PWD"
477
+ while [[ "$CURRENT_DIR" != "/" ]]; do
478
+ if [[ -d "$CURRENT_DIR/.claude" ]]; then
479
+ AUDIO_DIR="$CURRENT_DIR/.claude/audio"
480
+ break
481
+ fi
482
+ CURRENT_DIR=$(dirname "$CURRENT_DIR")
483
+ done
484
+ # Final fallback to global if no project .claude found
485
+ if [[ -z "$AUDIO_DIR" ]]; then
486
+ AUDIO_DIR="$HOME/.claude/audio"
487
+ fi
488
+ fi
489
+
490
+ # Default to replay last audio (N=1)
491
+ N="${2:-1}"
492
+
493
+ # Validate N is a number
494
+ if ! [[ "$N" =~ ^[0-9]+$ ]]; then
495
+ echo " Invalid argument. Please use a number (1-10)"
496
+ echo "Usage: /agent-vibes:replay [N]"
497
+ echo " N=1 - Last audio (default)"
498
+ echo " N=2 - Second-to-last"
499
+ echo " N=3 - Third-to-last"
500
+ exit 1
501
+ fi
502
+
503
+ # Check bounds
504
+ if [[ $N -lt 1 || $N -gt 10 ]]; then
505
+ echo "❌ Number out of range. Please choose 1-10"
506
+ exit 1
507
+ fi
508
+
509
+ # Get list of audio files sorted by time (newest first)
510
+ if [[ ! -d "$AUDIO_DIR" ]]; then
511
+ echo "❌ No audio history found"
512
+ echo "Audio files are stored in: $AUDIO_DIR"
513
+ exit 1
514
+ fi
515
+
516
+ # Get the Nth most recent file (check all supported formats)
517
+ AUDIO_FILE=$(ls -t "$AUDIO_DIR"/tts-*.{mp3,wav,aiff} 2>/dev/null | sed -n "${N}p")
518
+
519
+ if [[ -z "$AUDIO_FILE" ]]; then
520
+ TOTAL=$(ls -t "$AUDIO_DIR"/tts-*.{mp3,wav,aiff} 2>/dev/null | wc -l)
521
+ echo "❌ Audio #$N not found in history"
522
+ echo "Total audio files available: $TOTAL"
523
+ exit 1
524
+ fi
525
+
526
+ echo "🔊 Replaying audio #$N:"
527
+ echo " File: $(basename "$AUDIO_FILE")"
528
+ echo " Path: $AUDIO_FILE"
529
+
530
+ # Play the audio file in background (afplay for macOS, paplay/aplay/mpg123 for Linux)
531
+ if [[ "$(uname -s)" == "Darwin" ]]; then
532
+ afplay "$AUDIO_FILE" &
533
+ else
534
+ (paplay "$AUDIO_FILE" 2>/dev/null || aplay "$AUDIO_FILE" 2>/dev/null || mpg123 "$AUDIO_FILE" 2>/dev/null) &
535
+ fi
536
+ ;;
537
+
538
+ *)
539
+ echo "Usage: voice-manager.sh [list|switch|get|replay|whoami] [voice_name]"
540
+ echo ""
541
+ echo "Commands:"
542
+ echo " list - List all available voices"
543
+ echo " switch <voice_name> - Switch to a different voice"
544
+ echo " get - Get current voice name"
545
+ echo " replay [N] - Replay Nth most recent audio (default: 1)"
546
+ echo " whoami - Show current voice and personality"
547
+ exit 1
548
+ ;;
545
549
  esac