agentvibes 4.2.0 → 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 +2 -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 -433
  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 -269
  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 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  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 -81
  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 -84
  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 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  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 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  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 -1453
  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 -110
  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 -824
  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 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  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 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  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 -285
  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 -482
  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,549 +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
- # 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
- ;;
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
+ ;;
549
549
  esac