agentvibes 5.2.0 → 5.3.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 (52) hide show
  1. package/.claude/config/audio-effects.cfg +1 -1
  2. package/.claude/hooks/audio-cache-utils.sh +246 -246
  3. package/.claude/hooks/background-music-manager.sh +404 -404
  4. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  5. package/.claude/hooks/bmad-speak.sh +290 -290
  6. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  7. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  8. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  9. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  10. package/.claude/hooks/clean-audio-cache.sh +22 -22
  11. package/.claude/hooks/cleanup-cache.sh +106 -106
  12. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  13. package/.claude/hooks/download-extra-voices.sh +244 -244
  14. package/.claude/hooks/effects-manager.sh +268 -268
  15. package/.claude/hooks/github-star-reminder.sh +154 -154
  16. package/.claude/hooks/language-manager.sh +362 -362
  17. package/.claude/hooks/learn-manager.sh +492 -492
  18. package/.claude/hooks/macos-voice-manager.sh +205 -205
  19. package/.claude/hooks/migrate-background-music.sh +125 -125
  20. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  21. package/.claude/hooks/optimize-background-music.sh +87 -87
  22. package/.claude/hooks/path-resolver.sh +60 -60
  23. package/.claude/hooks/personality-manager.sh +448 -448
  24. package/.claude/hooks/piper-installer.sh +292 -292
  25. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  26. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  27. package/.claude/hooks/play-tts-ssh-remote.sh +104 -10
  28. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  29. package/.claude/hooks/play-tts.sh +31 -11
  30. package/.claude/hooks/prepare-release.sh +54 -54
  31. package/.claude/hooks/provider-commands.sh +617 -617
  32. package/.claude/hooks/provider-manager.sh +399 -399
  33. package/.claude/hooks/replay-target-audio.sh +95 -95
  34. package/.claude/hooks/sentiment-manager.sh +201 -201
  35. package/.claude/hooks/speed-manager.sh +291 -291
  36. package/.claude/hooks/stop-tts.sh +84 -84
  37. package/.claude/hooks/termux-installer.sh +261 -261
  38. package/.claude/hooks/translate-manager.sh +341 -341
  39. package/.claude/hooks/tts-queue-worker.sh +145 -145
  40. package/.claude/hooks/tts-queue.sh +165 -165
  41. package/.claude/hooks/voice-manager.sh +552 -548
  42. package/.claude/hooks-windows/bmad-party-speak.ps1 +5 -1
  43. package/.claude/hooks-windows/play-tts.ps1 +91 -59
  44. package/README.md +21 -2
  45. package/RELEASE_NOTES.md +130 -0
  46. package/bin/mcp-server.sh +206 -206
  47. package/mcp-server/server.py +35 -6
  48. package/package.json +1 -1
  49. package/src/console/tabs/setup-tab.js +68 -29
  50. package/src/console/tabs/voices-tab.js +9 -3
  51. package/src/installer.js +79 -213
  52. package/src/services/llm-provider-service.js +139 -75
@@ -1,549 +1,553 @@
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" || "$ACTIVE_PROVIDER" == "ssh-remote" || "$ACTIVE_PROVIDER" == "agentvibes-receiver" ]]; 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" || "$ACTIVE_PROVIDER" == "ssh-remote" || "$ACTIVE_PROVIDER" == "agentvibes-receiver" ]]; then
255
+ # Piper voice lookup (also used by transport providers — receiver uses piper)
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" == "ssh-remote" ]]; then
397
+ echo "Provider: Piper TTS (via SSH Remote)"
398
+ elif [[ "$ACTIVE_PROVIDER" == "agentvibes-receiver" ]]; then
399
+ echo "Provider: Piper TTS (via AgentVibes Receiver)"
400
+ elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
401
+ echo "Provider: macOS Say (Built-in, Free)"
402
+ else
403
+ echo "Provider: $ACTIVE_PROVIDER"
404
+ fi
405
+ else
406
+ # Default to Piper if no provider file
407
+ echo "Provider: Piper TTS (Free, Offline)"
408
+ fi
409
+
410
+ # Get current voice
411
+ CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || get_default_voice)
412
+ echo "Voice: $CURRENT_VOICE"
413
+
414
+ # Get current sentiment (priority)
415
+ if [ -f "$HOME/.claude/tts-sentiment.txt" ]; then
416
+ SENTIMENT=$(cat "$HOME/.claude/tts-sentiment.txt")
417
+ echo "Sentiment: $SENTIMENT (active)"
418
+
419
+ # Also show personality if set
420
+ if [ -f "$HOME/.claude/tts-personality.txt" ]; then
421
+ PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt")
422
+ echo "Personality: $PERSONALITY (overridden by sentiment)"
423
+ fi
424
+ else
425
+ # No sentiment, check personality
426
+ if [ -f "$HOME/.claude/tts-personality.txt" ]; then
427
+ PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt")
428
+ echo "Personality: $PERSONALITY (active)"
429
+ else
430
+ echo "Personality: normal"
431
+ fi
432
+ fi
433
+
434
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
435
+ ;;
436
+
437
+ list-simple)
438
+ # Simple list for AI to parse and display
439
+ # Get active provider
440
+ PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt"
441
+ if [[ ! -f "$PROVIDER_FILE" ]]; then
442
+ PROVIDER_FILE="$HOME/.claude/tts-provider.txt"
443
+ fi
444
+
445
+ ACTIVE_PROVIDER="piper" # default
446
+ if [ -f "$PROVIDER_FILE" ]; then
447
+ ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE")
448
+ fi
449
+
450
+ if [[ "$ACTIVE_PROVIDER" == "piper" || "$ACTIVE_PROVIDER" == "ssh-remote" || "$ACTIVE_PROVIDER" == "agentvibes-receiver" ]]; then
451
+ # List downloaded Piper voices
452
+ if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then
453
+ source "$SCRIPT_DIR/piper-voice-manager.sh"
454
+ VOICE_DIR=$(get_voice_storage_dir)
455
+ for onnx_file in "$VOICE_DIR"/*.onnx; do
456
+ if [[ -f "$onnx_file" ]]; then
457
+ basename "$onnx_file" .onnx
458
+ fi
459
+ done | sort
460
+ fi
461
+ elif [[ "$ACTIVE_PROVIDER" == "macos" ]]; then
462
+ # List macOS voices (voice names only)
463
+ if [[ "$(uname -s)" == "Darwin" ]]; then
464
+ say -v ? 2>/dev/null | awk '{print $1}' | sort
465
+ else
466
+ echo "(macOS voices only available on macOS)"
467
+ fi
468
+ else
469
+ echo "(Unknown provider: $ACTIVE_PROVIDER)"
470
+ fi
471
+ ;;
472
+
473
+ replay)
474
+ # Replay recent TTS audio from history
475
+ # Use project-local directory with same logic as play-tts.sh
476
+ if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then
477
+ AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio"
478
+ else
479
+ # Fallback: try to find .claude directory in current path
480
+ CURRENT_DIR="$PWD"
481
+ while [[ "$CURRENT_DIR" != "/" ]]; do
482
+ if [[ -d "$CURRENT_DIR/.claude" ]]; then
483
+ AUDIO_DIR="$CURRENT_DIR/.claude/audio"
484
+ break
485
+ fi
486
+ CURRENT_DIR=$(dirname "$CURRENT_DIR")
487
+ done
488
+ # Final fallback to global if no project .claude found
489
+ if [[ -z "$AUDIO_DIR" ]]; then
490
+ AUDIO_DIR="$HOME/.claude/audio"
491
+ fi
492
+ fi
493
+
494
+ # Default to replay last audio (N=1)
495
+ N="${2:-1}"
496
+
497
+ # Validate N is a number
498
+ if ! [[ "$N" =~ ^[0-9]+$ ]]; then
499
+ echo " Invalid argument. Please use a number (1-10)"
500
+ echo "Usage: /agent-vibes:replay [N]"
501
+ echo " N=1 - Last audio (default)"
502
+ echo " N=2 - Second-to-last"
503
+ echo " N=3 - Third-to-last"
504
+ exit 1
505
+ fi
506
+
507
+ # Check bounds
508
+ if [[ $N -lt 1 || $N -gt 10 ]]; then
509
+ echo "❌ Number out of range. Please choose 1-10"
510
+ exit 1
511
+ fi
512
+
513
+ # Get list of audio files sorted by time (newest first)
514
+ if [[ ! -d "$AUDIO_DIR" ]]; then
515
+ echo "❌ No audio history found"
516
+ echo "Audio files are stored in: $AUDIO_DIR"
517
+ exit 1
518
+ fi
519
+
520
+ # Get the Nth most recent file (check all supported formats)
521
+ AUDIO_FILE=$(ls -t "$AUDIO_DIR"/tts-*.{mp3,wav,aiff} 2>/dev/null | sed -n "${N}p")
522
+
523
+ if [[ -z "$AUDIO_FILE" ]]; then
524
+ TOTAL=$(ls -t "$AUDIO_DIR"/tts-*.{mp3,wav,aiff} 2>/dev/null | wc -l)
525
+ echo "❌ Audio #$N not found in history"
526
+ echo "Total audio files available: $TOTAL"
527
+ exit 1
528
+ fi
529
+
530
+ echo "🔊 Replaying audio #$N:"
531
+ echo " File: $(basename "$AUDIO_FILE")"
532
+ echo " Path: $AUDIO_FILE"
533
+
534
+ # Play the audio file in background (afplay for macOS, paplay/aplay/mpg123 for Linux)
535
+ if [[ "$(uname -s)" == "Darwin" ]]; then
536
+ afplay "$AUDIO_FILE" &
537
+ else
538
+ (paplay "$AUDIO_FILE" 2>/dev/null || aplay "$AUDIO_FILE" 2>/dev/null || mpg123 "$AUDIO_FILE" 2>/dev/null) &
539
+ fi
540
+ ;;
541
+
542
+ *)
543
+ echo "Usage: voice-manager.sh [list|switch|get|replay|whoami] [voice_name]"
544
+ echo ""
545
+ echo "Commands:"
546
+ echo " list - List all available voices"
547
+ echo " switch <voice_name> - Switch to a different voice"
548
+ echo " get - Get current voice name"
549
+ echo " replay [N] - Replay Nth most recent audio (default: 1)"
550
+ echo " whoami - Show current voice and personality"
551
+ exit 1
552
+ ;;
549
553
  esac