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.
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +290 -290
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-ssh-remote.sh +104 -10
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +31 -11
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/voice-manager.sh +552 -548
- package/.claude/hooks-windows/bmad-party-speak.ps1 +5 -1
- package/.claude/hooks-windows/play-tts.ps1 +91 -59
- package/README.md +21 -2
- package/RELEASE_NOTES.md +130 -0
- package/bin/mcp-server.sh +206 -206
- package/mcp-server/server.py +35 -6
- package/package.json +1 -1
- package/src/console/tabs/setup-tab.js +68 -29
- package/src/console/tabs/voices-tab.js +9 -3
- package/src/installer.js +79 -213
- 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
|
|
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" == "
|
|
397
|
-
echo "Provider:
|
|
398
|
-
|
|
399
|
-
echo "Provider:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
# Get current
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if [
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
echo "
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
echo "
|
|
544
|
-
echo "
|
|
545
|
-
echo "
|
|
546
|
-
echo "
|
|
547
|
-
|
|
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
|