agentvibes 4.6.8 → 5.1.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 (40) hide show
  1. package/.agentvibes/bmad-voice-map.json +104 -0
  2. package/.agentvibes/config.json +13 -12
  3. package/.agentvibes/copilot-sessions.log +4 -0
  4. package/.claude/audio/tracks/Drifting Down the Hall.mp3 +0 -0
  5. package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
  6. package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
  7. package/.claude/audio/tracks/README.md +51 -52
  8. package/.claude/config/audio-effects-bmad.cfg +50 -0
  9. package/.claude/config/audio-effects.cfg +4 -4
  10. package/.claude/config/background-music-enabled.txt +1 -0
  11. package/.claude/config/personality.txt +1 -0
  12. package/.claude/hooks/play-tts-piper.sh +3 -1
  13. package/.claude/hooks/play-tts.sh +380 -301
  14. package/.claude/hooks/session-start-tts.sh +81 -81
  15. package/.claude/hooks-windows/audio-processor.ps1 +181 -0
  16. package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
  17. package/.claude/hooks-windows/play-tts.ps1 +28 -6
  18. package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
  19. package/README.md +112 -6
  20. package/RELEASE_NOTES.md +83 -0
  21. package/bin/bmad-speak.js +16 -8
  22. package/mcp-server/server.py +15 -8
  23. package/package.json +1 -1
  24. package/src/console/app.js +899 -897
  25. package/src/console/footer-config.js +50 -50
  26. package/src/console/navigation.js +65 -65
  27. package/src/console/tabs/agents-tab.js +1899 -1886
  28. package/src/console/tabs/music-tab.js +1076 -1039
  29. package/src/console/tabs/placeholder-tab.js +81 -80
  30. package/src/console/tabs/settings-tab.js +941 -3988
  31. package/src/console/tabs/setup-tab.js +2071 -0
  32. package/src/console/tabs/voices-tab.js +1843 -1714
  33. package/src/console/widgets/format-utils.js +92 -89
  34. package/src/console/widgets/track-picker.js +325 -322
  35. package/src/installer.js +6147 -6092
  36. package/src/services/llm-provider-service.js +486 -0
  37. package/src/services/navigation-service.js +123 -123
  38. package/src/services/tts-engine-service.js +69 -0
  39. package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
  40. package/src/console/tabs/install-tab.js +0 -1081
@@ -1,301 +1,380 @@
1
- #!/usr/bin/env bash
2
- #
3
- # File: .claude/hooks/play-tts.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, including but not limited to the warranties of
26
- # merchantability, fitness for a particular purpose and noninfringement.
27
- # In no event shall the authors or copyright holders be liable for any claim,
28
- # damages or other liability, whether in an action of contract, tort or
29
- # otherwise, arising from, out of or in connection with the software or the
30
- # use or other dealings in the software.
31
- #
32
- # ---
33
- #
34
- # @fileoverview TTS Provider Router with Translation and Language Learning Support
35
- # @context Routes TTS requests to active provider (Piper or macOS) with optional translation
36
- # @architecture Provider abstraction layer - single entry point for all TTS, handles translation and learning mode
37
- # @dependencies provider-manager.sh, play-tts-piper.sh, translator.py, translate-manager.sh, learn-manager.sh
38
- # @entrypoints Called by hooks, slash commands, personality-manager.sh, and all TTS features
39
- # @patterns Provider pattern - delegates to provider-specific implementations, auto-detects provider from voice name
40
- # @related provider-manager.sh, play-tts-piper.sh, learn-manager.sh, translate-manager.sh
41
- #
42
-
43
- set -euo pipefail
44
-
45
- # Fix locale warnings
46
- export LC_ALL=C
47
-
48
- # Get script directory (needed for mute file check)
49
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
50
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
51
- export PROJECT_ROOT # Export for child scripts
52
-
53
- # Check if muted (persists across sessions)
54
- # Project settings always override global settings:
55
- # - .claude/agentvibes-unmuted = project explicitly unmuted (overrides global mute)
56
- # - .claude/agentvibes-muted = project muted (overrides global unmute)
57
- # - ~/.agentvibes-muted = global mute (only if no project-level setting)
58
- GLOBAL_MUTE_FILE="$HOME/.agentvibes-muted"
59
- PROJECT_MUTE_FILE="$PROJECT_ROOT/.claude/agentvibes-muted"
60
- PROJECT_UNMUTE_FILE="$PROJECT_ROOT/.claude/agentvibes-unmuted"
61
-
62
- # Check project-level settings first (project overrides global)
63
- if [[ -f "$PROJECT_UNMUTE_FILE" ]]; then
64
- # Project explicitly unmuted - ignore global mute
65
- : # Continue (do nothing, will not exit)
66
- elif [[ -f "$PROJECT_MUTE_FILE" ]]; then
67
- # Project explicitly muted
68
- if [[ -f "$GLOBAL_MUTE_FILE" ]]; then
69
- echo "🔇 TTS muted (project + global)" >&2
70
- else
71
- echo "🔇 TTS muted (project)" >&2
72
- fi
73
- exit 0
74
- elif [[ -f "$GLOBAL_MUTE_FILE" ]]; then
75
- # Global mute and no project-level override
76
- echo "🔇 TTS muted (global)" >&2
77
- exit 0
78
- fi
79
-
80
- TEXT="${1:-}"
81
- VOICE_OVERRIDE="${2:-}" # Optional: voice name or ID
82
- AGENT_PROFILE_FILE="${3:-}" # Optional: path to per-agent profile JSON (from bmad-speak.sh)
83
-
84
- # Security: Validate inputs
85
- if [[ -z "$TEXT" ]]; then
86
- echo "Error: No text provided" >&2
87
- exit 1
88
- fi
89
-
90
- # Security: Validate voice override doesn't contain dangerous characters
91
- if [[ -n "$VOICE_OVERRIDE" ]] && [[ "$VOICE_OVERRIDE" =~ [';|&$`<>(){}'] ]]; then
92
- echo "Error: Invalid characters in voice parameter" >&2
93
- exit 1
94
- fi
95
-
96
- # Remove backslash escaping that Claude might add for SAFE special chars only
97
- # SECURITY: Only unescape punctuation chars that cannot form shell commands (#127)
98
- # Never unescape $, `, \, or other shell metacharacters
99
- TEXT="${TEXT//\\!/!}" # Remove \!
100
- TEXT="${TEXT//\\?/?}" # Remove \?
101
- TEXT="${TEXT//\\,/,}" # Remove \,
102
- TEXT="${TEXT//\\./.}" # Remove \. (keep the period)
103
-
104
- # Prepend intro text (pretext) if configured
105
- # Check project-local first, then global
106
- _PRETEXT_FILE="$PROJECT_ROOT/.claude/config/intro-text.txt"
107
- [[ -f "$_PRETEXT_FILE" ]] || _PRETEXT_FILE="$HOME/.claude/config/intro-text.txt"
108
- if [[ -f "$_PRETEXT_FILE" ]]; then
109
- _PRETEXT="$(head -1 "$_PRETEXT_FILE" 2>/dev/null || true)"
110
- if [[ -n "$_PRETEXT" ]]; then
111
- TEXT="${_PRETEXT} ${TEXT}"
112
- fi
113
- fi
114
-
115
- # Source provider manager to get active provider
116
- source "$SCRIPT_DIR/provider-manager.sh"
117
-
118
- # Get active provider
119
- ACTIVE_PROVIDER=$(get_active_provider)
120
-
121
- # Show GitHub star reminder (once per day)
122
- "$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true
123
-
124
- # @function detect_voice_provider
125
- # @intent Auto-detect provider from voice name (for mixed-provider support)
126
- # @why Allow Piper for main language + macOS for target language
127
- # @param $1 voice name/ID
128
- # @returns Provider name (piper or macos)
129
- detect_voice_provider() {
130
- local voice="$1"
131
- # Piper voice names contain underscore and dash (e.g., es_ES-davefx-medium)
132
- if [[ "$voice" == *"_"*"-"* ]]; then
133
- echo "piper"
134
- else
135
- echo "$ACTIVE_PROVIDER"
136
- fi
137
- }
138
-
139
- # Override provider if voice indicates different provider (mixed-provider mode)
140
- if [[ -n "$VOICE_OVERRIDE" ]]; then
141
- DETECTED_PROVIDER=$(detect_voice_provider "$VOICE_OVERRIDE")
142
- if [[ "$DETECTED_PROVIDER" != "$ACTIVE_PROVIDER" ]]; then
143
- ACTIVE_PROVIDER="$DETECTED_PROVIDER"
144
- fi
145
- fi
146
-
147
- # @function speak_text
148
- # @intent Route text to appropriate TTS provider
149
- # @why Reusable function for speaking, used by both single and learning modes
150
- # @param $1 text to speak
151
- # @param $2 voice override (optional)
152
- # @param $3 provider override (optional)
153
- speak_text() {
154
- local text="$1"
155
- local voice="${2:-}"
156
- local provider="${3:-$ACTIVE_PROVIDER}"
157
- local profile_file="${4:-$AGENT_PROFILE_FILE}"
158
-
159
- case "$provider" in
160
- piper)
161
- "$SCRIPT_DIR/play-tts-piper.sh" "$text" "$voice" "$profile_file"
162
- ;;
163
- soprano)
164
- "$SCRIPT_DIR/play-tts-soprano.sh" "$text" "$voice"
165
- ;;
166
- macos)
167
- "$SCRIPT_DIR/play-tts-macos.sh" "$text" "$voice"
168
- ;;
169
- termux-ssh)
170
- "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$text" "$voice"
171
- ;;
172
- *)
173
- echo "❌ Unknown provider: $provider" >&2
174
- return 1
175
- ;;
176
- esac
177
- }
178
-
179
- # Note: learn-manager.sh and translate-manager.sh are sourced inside their
180
- # respective handler functions to avoid triggering their main handlers
181
-
182
- # @function handle_learning_mode
183
- # @intent Speak in both main language and target language for learning
184
- # @why Issue #51 - Auto-translate and speak twice for immersive language learning
185
- # @returns 0 if learning mode handled, 1 if not in learning mode
186
- handle_learning_mode() {
187
- # Source learn-manager for learning mode functions
188
- source "$SCRIPT_DIR/learn-manager.sh" 2>/dev/null || return 1
189
-
190
- # Check if learning mode is enabled
191
- if ! is_learn_mode_enabled 2>/dev/null; then
192
- return 1
193
- fi
194
-
195
- local target_lang
196
- target_lang=$(get_target_language 2>/dev/null || echo "")
197
- local target_voice
198
- target_voice=$(get_target_voice 2>/dev/null || echo "")
199
-
200
- # Need both target language and voice for learning mode
201
- if [[ -z "$target_lang" ]] || [[ -z "$target_voice" ]]; then
202
- return 1
203
- fi
204
-
205
- # 1. Speak in main language (current voice)
206
- speak_text "$TEXT" "$VOICE_OVERRIDE" "$ACTIVE_PROVIDER"
207
-
208
- # 2. Auto-translate to target language
209
- local translated
210
- # SECURITY: Add timeout to prevent hanging (#134)
211
- translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$target_lang" 2>/dev/null) || translated="$TEXT"
212
-
213
- # Small pause between languages
214
- sleep 0.5
215
-
216
- # 3. Speak translated text with target voice
217
- local target_provider
218
- target_provider=$(detect_voice_provider "$target_voice")
219
- speak_text "$translated" "$target_voice" "$target_provider"
220
-
221
- return 0
222
- }
223
-
224
- # @function handle_translation_mode
225
- # @intent Translate and speak in target language (non-learning mode)
226
- # @why Issue #50 - BMAD multi-language TTS support
227
- # @returns 0 if translation handled, 1 if not translating
228
- handle_translation_mode() {
229
- # Source translate-manager to get translation settings
230
- source "$SCRIPT_DIR/translate-manager.sh" 2>/dev/null || return 1
231
-
232
- # Check if translation is enabled
233
- if ! is_translation_enabled 2>/dev/null; then
234
- return 1
235
- fi
236
-
237
- local translate_to
238
- translate_to=$(get_translate_to 2>/dev/null || echo "")
239
-
240
- if [[ -z "$translate_to" ]] || [[ "$translate_to" == "english" ]]; then
241
- return 1
242
- fi
243
-
244
- # Translate text
245
- local translated
246
- # SECURITY: Add timeout to prevent hanging (#134)
247
- translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$translate_to" 2>/dev/null) || translated="$TEXT"
248
-
249
- # Get voice for target language if no override specified
250
- local voice_to_use="$VOICE_OVERRIDE"
251
- if [[ -z "$voice_to_use" ]]; then
252
- source "$SCRIPT_DIR/language-manager.sh" 2>/dev/null || true
253
- voice_to_use=$(get_voice_for_language "$translate_to" "$ACTIVE_PROVIDER" 2>/dev/null || echo "")
254
- fi
255
-
256
- # Update provider if voice indicates different provider
257
- local provider_to_use="$ACTIVE_PROVIDER"
258
- if [[ -n "$voice_to_use" ]]; then
259
- provider_to_use=$(detect_voice_provider "$voice_to_use")
260
- fi
261
-
262
- # Speak translated text
263
- speak_text "$translated" "$voice_to_use" "$provider_to_use"
264
- return 0
265
- }
266
-
267
- # Mode priority:
268
- # 1. Learning mode (speaks twice: main + translated)
269
- # 2. Translation mode (speaks translated only)
270
- # 3. Normal mode (speaks as-is)
271
-
272
- # Try learning mode first (Issue #51)
273
- if handle_learning_mode; then
274
- exit 0
275
- fi
276
-
277
- # Try translation mode (Issue #50)
278
- if handle_translation_mode; then
279
- exit 0
280
- fi
281
-
282
- # Normal single-language mode - route to appropriate provider implementation
283
- case "$ACTIVE_PROVIDER" in
284
- piper)
285
- exec "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "$AGENT_PROFILE_FILE"
286
- ;;
287
- soprano)
288
- exec "$SCRIPT_DIR/play-tts-soprano.sh" "$TEXT" "$VOICE_OVERRIDE"
289
- ;;
290
- macos)
291
- exec "$SCRIPT_DIR/play-tts-macos.sh" "$TEXT" "$VOICE_OVERRIDE"
292
- ;;
293
- termux-ssh)
294
- exec "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
295
- ;;
296
- *)
297
- echo "❌ Unknown provider: $ACTIVE_PROVIDER" >&2
298
- echo " Run: /agent-vibes:provider list" >&2
299
- exit 1
300
- ;;
301
- esac
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/play-tts.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, including but not limited to the warranties of
26
+ # merchantability, fitness for a particular purpose and noninfringement.
27
+ # In no event shall the authors or copyright holders be liable for any claim,
28
+ # damages or other liability, whether in an action of contract, tort or
29
+ # otherwise, arising from, out of or in connection with the software or the
30
+ # use or other dealings in the software.
31
+ #
32
+ # ---
33
+ #
34
+ # @fileoverview TTS Provider Router with Translation and Language Learning Support
35
+ # @context Routes TTS requests to active provider (Piper or macOS) with optional translation
36
+ # @architecture Provider abstraction layer - single entry point for all TTS, handles translation and learning mode
37
+ # @dependencies provider-manager.sh, play-tts-piper.sh, translator.py, translate-manager.sh, learn-manager.sh
38
+ # @entrypoints Called by hooks, slash commands, personality-manager.sh, and all TTS features
39
+ # @patterns Provider pattern - delegates to provider-specific implementations, auto-detects provider from voice name
40
+ # @related provider-manager.sh, play-tts-piper.sh, learn-manager.sh, translate-manager.sh
41
+ #
42
+
43
+ set -euo pipefail
44
+
45
+ # Fix locale warnings
46
+ export LC_ALL=C
47
+
48
+ # Get script directory (needed for mute file check)
49
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
50
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
51
+ export PROJECT_ROOT # Export for child scripts
52
+
53
+ # Check if muted (persists across sessions)
54
+ # Project settings always override global settings:
55
+ # - .claude/agentvibes-unmuted = project explicitly unmuted (overrides global mute)
56
+ # - .claude/agentvibes-muted = project muted (overrides global unmute)
57
+ # - ~/.agentvibes-muted = global mute (only if no project-level setting)
58
+ GLOBAL_MUTE_FILE="$HOME/.agentvibes-muted"
59
+ PROJECT_MUTE_FILE="$PROJECT_ROOT/.claude/agentvibes-muted"
60
+ PROJECT_UNMUTE_FILE="$PROJECT_ROOT/.claude/agentvibes-unmuted"
61
+
62
+ # Check project-level settings first (project overrides global)
63
+ if [[ -f "$PROJECT_UNMUTE_FILE" ]]; then
64
+ # Project explicitly unmuted - ignore global mute
65
+ : # Continue (do nothing, will not exit)
66
+ elif [[ -f "$PROJECT_MUTE_FILE" ]]; then
67
+ # Project explicitly muted
68
+ if [[ -f "$GLOBAL_MUTE_FILE" ]]; then
69
+ echo "🔇 TTS muted (project + global)" >&2
70
+ else
71
+ echo "🔇 TTS muted (project)" >&2
72
+ fi
73
+ exit 0
74
+ elif [[ -f "$GLOBAL_MUTE_FILE" ]]; then
75
+ # Global mute and no project-level override
76
+ echo "🔇 TTS muted (global)" >&2
77
+ exit 0
78
+ fi
79
+
80
+ # Parse arguments: positional + optional --llm <provider>
81
+ TEXT=""
82
+ VOICE_OVERRIDE=""
83
+ AGENT_PROFILE_FILE=""
84
+ LLM_PROVIDER=""
85
+
86
+ _positional=()
87
+ while [[ $# -gt 0 ]]; do
88
+ case "$1" in
89
+ --llm)
90
+ LLM_PROVIDER="${2:-}"
91
+ # Security: Validate LLM provider name (alphanumeric, hyphens, underscores only)
92
+ if [[ -n "$LLM_PROVIDER" ]] && [[ ! "$LLM_PROVIDER" =~ ^[a-zA-Z0-9_-]+$ ]]; then
93
+ echo "Error: Invalid LLM provider name" >&2
94
+ exit 1
95
+ fi
96
+ shift 2
97
+ ;;
98
+ *)
99
+ _positional+=("$1")
100
+ shift
101
+ ;;
102
+ esac
103
+ done
104
+
105
+ TEXT="${_positional[0]:-}"
106
+ VOICE_OVERRIDE="${_positional[1]:-}"
107
+ AGENT_PROFILE_FILE="${_positional[2]:-}"
108
+
109
+ # Security: Validate inputs
110
+ if [[ -z "$TEXT" ]]; then
111
+ echo "Error: No text provided" >&2
112
+ exit 1
113
+ fi
114
+
115
+ # Security: Validate voice override doesn't contain dangerous characters
116
+ if [[ -n "$VOICE_OVERRIDE" ]] && [[ "$VOICE_OVERRIDE" =~ [';|&$`<>(){}'] ]]; then
117
+ echo "Error: Invalid characters in voice parameter" >&2
118
+ exit 1
119
+ fi
120
+
121
+ # Remove backslash escaping that Claude might add for SAFE special chars only
122
+ # SECURITY: Only unescape punctuation chars that cannot form shell commands (#127)
123
+ # Never unescape $, `, \, or other shell metacharacters
124
+ TEXT="${TEXT//\\!/!}" # Remove \!
125
+ TEXT="${TEXT//\\?/?}" # Remove \?
126
+ TEXT="${TEXT//\\,/,}" # Remove \,
127
+ TEXT="${TEXT//\\./.}" # Remove \. (keep the period)
128
+
129
+ # Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
130
+ # Format: llm:<name>|REVERB_PRESET|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT
131
+ _LLM_VOICE=""
132
+ _LLM_PRETEXT=""
133
+ _LLM_REVERB=""
134
+ _LLM_ENGINE=""
135
+ if [[ -n "$LLM_PROVIDER" ]]; then
136
+ _llm_key="llm:${LLM_PROVIDER}"
137
+ for _cfg in \
138
+ "$PROJECT_ROOT/.claude/config/audio-effects.cfg" \
139
+ "$HOME/.claude/config/audio-effects.cfg"; do
140
+ if [[ -z "$_LLM_VOICE" && -z "$_LLM_PRETEXT" && -f "$_cfg" ]]; then
141
+ while IFS='|' read -r _key _reverb _bgfile _bgvol _voice _pretext _engine _rest; do
142
+ if [[ "$_key" == "$_llm_key" ]]; then
143
+ _reverb="${_reverb## }"; _reverb="${_reverb%% }"
144
+ _voice="${_voice## }"; _voice="${_voice%% }"
145
+ _pretext="${_pretext## }"; _pretext="${_pretext%% }"
146
+ _engine="${_engine## }"; _engine="${_engine%% }"
147
+ [[ -n "$_reverb" ]] && _LLM_REVERB="$_reverb"
148
+ [[ -n "$_voice" ]] && _LLM_VOICE="$_voice"
149
+ [[ -n "$_pretext" ]] && _LLM_PRETEXT="$_pretext"
150
+ [[ -n "$_engine" ]] && _LLM_ENGINE="$_engine"
151
+ break
152
+ fi
153
+ done < "$_cfg"
154
+ fi
155
+ done
156
+ # Apply LLM voice (only if no explicit voice override)
157
+ if [[ -n "$_LLM_VOICE" && -z "$VOICE_OVERRIDE" ]]; then
158
+ VOICE_OVERRIDE="$_LLM_VOICE"
159
+ fi
160
+ # Export LLM key for child scripts (process-local, not system-wide)
161
+ export AGENTVIBES_LLM_KEY="llm:${LLM_PROVIDER}"
162
+ fi
163
+
164
+ # Prepend intro text (pretext) if configured
165
+ # Priority: LLM-specific pretext → project .agentvibes/config.json → project .claude/config
166
+ # → global ~/.agentvibes/config.json → global ~/.claude/config
167
+ _PRETEXT="$_LLM_PRETEXT"
168
+ if [[ -z "$_PRETEXT" ]]; then
169
+ for _src in \
170
+ "$PROJECT_ROOT/.agentvibes/config.json" \
171
+ "$PROJECT_ROOT/.claude/config/tts-pretext.txt" \
172
+ "$PROJECT_ROOT/.claude/config/intro-text.txt" \
173
+ "$HOME/.agentvibes/config.json" \
174
+ "$HOME/.claude/config/tts-pretext.txt" \
175
+ "$HOME/.claude/config/intro-text.txt"; do
176
+ if [[ -z "$_PRETEXT" && -f "$_src" ]]; then
177
+ if [[ "$_src" == *.json ]]; then
178
+ # Extract pretext from JSON (lightweight — no jq dependency)
179
+ _PRETEXT="$(sed -n 's/.*"pretext"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_src" 2>/dev/null | head -1)"
180
+ else
181
+ _PRETEXT="$(head -1 "$_src" 2>/dev/null || true)"
182
+ fi
183
+ fi
184
+ done
185
+ fi
186
+ if [[ -n "$_PRETEXT" ]]; then
187
+ TEXT="${_PRETEXT}, ${TEXT}"
188
+ fi
189
+
190
+ # Source provider manager to get active provider
191
+ source "$SCRIPT_DIR/provider-manager.sh"
192
+
193
+ # Get active provider (LLM-specific engine overrides global)
194
+ if [[ -n "$_LLM_ENGINE" ]]; then
195
+ ACTIVE_PROVIDER="$_LLM_ENGINE"
196
+ else
197
+ ACTIVE_PROVIDER=$(get_active_provider)
198
+ fi
199
+
200
+ # Show GitHub star reminder (once per day)
201
+ "$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true
202
+
203
+ # @function detect_voice_provider
204
+ # @intent Auto-detect provider from voice name (for mixed-provider support)
205
+ # @why Allow Piper for main language + macOS for target language
206
+ # @param $1 voice name/ID
207
+ # @returns Provider name (piper or macos)
208
+ detect_voice_provider() {
209
+ local voice="$1"
210
+ # Piper voice names contain underscore and dash (e.g., es_ES-davefx-medium)
211
+ if [[ "$voice" == *"_"*"-"* ]]; then
212
+ echo "piper"
213
+ else
214
+ echo "$ACTIVE_PROVIDER"
215
+ fi
216
+ }
217
+
218
+ # Override provider if voice indicates different provider (mixed-provider mode)
219
+ if [[ -n "$VOICE_OVERRIDE" ]]; then
220
+ DETECTED_PROVIDER=$(detect_voice_provider "$VOICE_OVERRIDE")
221
+ if [[ "$DETECTED_PROVIDER" != "$ACTIVE_PROVIDER" ]]; then
222
+ ACTIVE_PROVIDER="$DETECTED_PROVIDER"
223
+ fi
224
+ fi
225
+
226
+ # @function speak_text
227
+ # @intent Route text to appropriate TTS provider
228
+ # @why Reusable function for speaking, used by both single and learning modes
229
+ # @param $1 text to speak
230
+ # @param $2 voice override (optional)
231
+ # @param $3 provider override (optional)
232
+ speak_text() {
233
+ local text="$1"
234
+ local voice="${2:-}"
235
+ local provider="${3:-$ACTIVE_PROVIDER}"
236
+ local profile_file="${4:-$AGENT_PROFILE_FILE}"
237
+
238
+ case "$provider" in
239
+ piper)
240
+ "$SCRIPT_DIR/play-tts-piper.sh" "$text" "$voice" "$profile_file"
241
+ ;;
242
+ soprano)
243
+ "$SCRIPT_DIR/play-tts-soprano.sh" "$text" "$voice"
244
+ ;;
245
+ macos)
246
+ "$SCRIPT_DIR/play-tts-macos.sh" "$text" "$voice"
247
+ ;;
248
+ termux-ssh)
249
+ "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$text" "$voice"
250
+ ;;
251
+ *)
252
+ echo "❌ Unknown provider: $provider" >&2
253
+ return 1
254
+ ;;
255
+ esac
256
+ }
257
+
258
+ # Note: learn-manager.sh and translate-manager.sh are sourced inside their
259
+ # respective handler functions to avoid triggering their main handlers
260
+
261
+ # @function handle_learning_mode
262
+ # @intent Speak in both main language and target language for learning
263
+ # @why Issue #51 - Auto-translate and speak twice for immersive language learning
264
+ # @returns 0 if learning mode handled, 1 if not in learning mode
265
+ handle_learning_mode() {
266
+ # Source learn-manager for learning mode functions
267
+ source "$SCRIPT_DIR/learn-manager.sh" 2>/dev/null || return 1
268
+
269
+ # Check if learning mode is enabled
270
+ if ! is_learn_mode_enabled 2>/dev/null; then
271
+ return 1
272
+ fi
273
+
274
+ local target_lang
275
+ target_lang=$(get_target_language 2>/dev/null || echo "")
276
+ local target_voice
277
+ target_voice=$(get_target_voice 2>/dev/null || echo "")
278
+
279
+ # Need both target language and voice for learning mode
280
+ if [[ -z "$target_lang" ]] || [[ -z "$target_voice" ]]; then
281
+ return 1
282
+ fi
283
+
284
+ # 1. Speak in main language (current voice)
285
+ speak_text "$TEXT" "$VOICE_OVERRIDE" "$ACTIVE_PROVIDER"
286
+
287
+ # 2. Auto-translate to target language
288
+ local translated
289
+ # SECURITY: Add timeout to prevent hanging (#134)
290
+ translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$target_lang" 2>/dev/null) || translated="$TEXT"
291
+
292
+ # Small pause between languages
293
+ sleep 0.5
294
+
295
+ # 3. Speak translated text with target voice
296
+ local target_provider
297
+ target_provider=$(detect_voice_provider "$target_voice")
298
+ speak_text "$translated" "$target_voice" "$target_provider"
299
+
300
+ return 0
301
+ }
302
+
303
+ # @function handle_translation_mode
304
+ # @intent Translate and speak in target language (non-learning mode)
305
+ # @why Issue #50 - BMAD multi-language TTS support
306
+ # @returns 0 if translation handled, 1 if not translating
307
+ handle_translation_mode() {
308
+ # Source translate-manager to get translation settings
309
+ source "$SCRIPT_DIR/translate-manager.sh" 2>/dev/null || return 1
310
+
311
+ # Check if translation is enabled
312
+ if ! is_translation_enabled 2>/dev/null; then
313
+ return 1
314
+ fi
315
+
316
+ local translate_to
317
+ translate_to=$(get_translate_to 2>/dev/null || echo "")
318
+
319
+ if [[ -z "$translate_to" ]] || [[ "$translate_to" == "english" ]]; then
320
+ return 1
321
+ fi
322
+
323
+ # Translate text
324
+ local translated
325
+ # SECURITY: Add timeout to prevent hanging (#134)
326
+ translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$translate_to" 2>/dev/null) || translated="$TEXT"
327
+
328
+ # Get voice for target language if no override specified
329
+ local voice_to_use="$VOICE_OVERRIDE"
330
+ if [[ -z "$voice_to_use" ]]; then
331
+ source "$SCRIPT_DIR/language-manager.sh" 2>/dev/null || true
332
+ voice_to_use=$(get_voice_for_language "$translate_to" "$ACTIVE_PROVIDER" 2>/dev/null || echo "")
333
+ fi
334
+
335
+ # Update provider if voice indicates different provider
336
+ local provider_to_use="$ACTIVE_PROVIDER"
337
+ if [[ -n "$voice_to_use" ]]; then
338
+ provider_to_use=$(detect_voice_provider "$voice_to_use")
339
+ fi
340
+
341
+ # Speak translated text
342
+ speak_text "$translated" "$voice_to_use" "$provider_to_use"
343
+ return 0
344
+ }
345
+
346
+ # Mode priority:
347
+ # 1. Learning mode (speaks twice: main + translated)
348
+ # 2. Translation mode (speaks translated only)
349
+ # 3. Normal mode (speaks as-is)
350
+
351
+ # Try learning mode first (Issue #51)
352
+ if handle_learning_mode; then
353
+ exit 0
354
+ fi
355
+
356
+ # Try translation mode (Issue #50)
357
+ if handle_translation_mode; then
358
+ exit 0
359
+ fi
360
+
361
+ # Normal single-language mode - route to appropriate provider implementation
362
+ case "$ACTIVE_PROVIDER" in
363
+ piper)
364
+ exec "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "$AGENT_PROFILE_FILE"
365
+ ;;
366
+ soprano)
367
+ exec "$SCRIPT_DIR/play-tts-soprano.sh" "$TEXT" "$VOICE_OVERRIDE"
368
+ ;;
369
+ macos)
370
+ exec "$SCRIPT_DIR/play-tts-macos.sh" "$TEXT" "$VOICE_OVERRIDE"
371
+ ;;
372
+ termux-ssh)
373
+ exec "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
374
+ ;;
375
+ *)
376
+ echo "❌ Unknown provider: $ACTIVE_PROVIDER" >&2
377
+ echo " Run: /agent-vibes:provider list" >&2
378
+ exit 1
379
+ ;;
380
+ esac