agentvibes 4.6.8 → 5.0.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 (35) 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/README.md +51 -52
  5. package/.claude/config/audio-effects-bmad.cfg +50 -0
  6. package/.claude/config/audio-effects.cfg +4 -4
  7. package/.claude/config/background-music-enabled.txt +1 -0
  8. package/.claude/config/personality.txt +1 -0
  9. package/.claude/hooks/play-tts-piper.sh +3 -1
  10. package/.claude/hooks/play-tts.sh +373 -301
  11. package/.claude/hooks/session-start-tts.sh +81 -81
  12. package/.claude/hooks-windows/audio-processor.ps1 +181 -0
  13. package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
  14. package/.claude/hooks-windows/play-tts.ps1 +101 -9
  15. package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
  16. package/README.md +98 -6
  17. package/RELEASE_NOTES.md +35 -0
  18. package/bin/bmad-speak.js +16 -8
  19. package/mcp-server/server.py +15 -8
  20. package/package.json +1 -1
  21. package/src/console/app.js +899 -897
  22. package/src/console/footer-config.js +50 -50
  23. package/src/console/navigation.js +65 -65
  24. package/src/console/tabs/agents-tab.js +1896 -1886
  25. package/src/console/tabs/music-tab.js +1046 -1039
  26. package/src/console/tabs/placeholder-tab.js +81 -80
  27. package/src/console/tabs/settings-tab.js +939 -3988
  28. package/src/console/tabs/setup-tab.js +1811 -0
  29. package/src/console/tabs/voices-tab.js +1720 -1714
  30. package/src/installer.js +6147 -6092
  31. package/src/services/llm-provider-service.js +407 -0
  32. package/src/services/navigation-service.js +123 -123
  33. package/src/services/tts-engine-service.js +69 -0
  34. package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
  35. package/src/console/tabs/install-tab.js +0 -1081
@@ -1,301 +1,373 @@
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
+ if [[ -n "$LLM_PROVIDER" ]]; then
135
+ _llm_key="llm:${LLM_PROVIDER}"
136
+ for _cfg in \
137
+ "$PROJECT_ROOT/.claude/config/audio-effects.cfg" \
138
+ "$HOME/.claude/config/audio-effects.cfg"; do
139
+ if [[ -z "$_LLM_VOICE" && -z "$_LLM_PRETEXT" && -f "$_cfg" ]]; then
140
+ while IFS='|' read -r _key _reverb _bgfile _bgvol _voice _pretext _rest; do
141
+ if [[ "$_key" == "$_llm_key" ]]; then
142
+ _reverb="${_reverb## }"; _reverb="${_reverb%% }"
143
+ _voice="${_voice## }"; _voice="${_voice%% }"
144
+ _pretext="${_pretext## }"; _pretext="${_pretext%% }"
145
+ [[ -n "$_reverb" ]] && _LLM_REVERB="$_reverb"
146
+ [[ -n "$_voice" ]] && _LLM_VOICE="$_voice"
147
+ [[ -n "$_pretext" ]] && _LLM_PRETEXT="$_pretext"
148
+ break
149
+ fi
150
+ done < "$_cfg"
151
+ fi
152
+ done
153
+ # Apply LLM voice (only if no explicit voice override)
154
+ if [[ -n "$_LLM_VOICE" && -z "$VOICE_OVERRIDE" ]]; then
155
+ VOICE_OVERRIDE="$_LLM_VOICE"
156
+ fi
157
+ # Export LLM key for child scripts (process-local, not system-wide)
158
+ export AGENTVIBES_LLM_KEY="llm:${LLM_PROVIDER}"
159
+ fi
160
+
161
+ # Prepend intro text (pretext) if configured
162
+ # Priority: LLM-specific pretext → project .agentvibes/config.json → project .claude/config
163
+ # → global ~/.agentvibes/config.json → global ~/.claude/config
164
+ _PRETEXT="$_LLM_PRETEXT"
165
+ if [[ -z "$_PRETEXT" ]]; then
166
+ for _src in \
167
+ "$PROJECT_ROOT/.agentvibes/config.json" \
168
+ "$PROJECT_ROOT/.claude/config/tts-pretext.txt" \
169
+ "$PROJECT_ROOT/.claude/config/intro-text.txt" \
170
+ "$HOME/.agentvibes/config.json" \
171
+ "$HOME/.claude/config/tts-pretext.txt" \
172
+ "$HOME/.claude/config/intro-text.txt"; do
173
+ if [[ -z "$_PRETEXT" && -f "$_src" ]]; then
174
+ if [[ "$_src" == *.json ]]; then
175
+ # Extract pretext from JSON (lightweight — no jq dependency)
176
+ _PRETEXT="$(sed -n 's/.*"pretext"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_src" 2>/dev/null | head -1)"
177
+ else
178
+ _PRETEXT="$(head -1 "$_src" 2>/dev/null || true)"
179
+ fi
180
+ fi
181
+ done
182
+ fi
183
+ if [[ -n "$_PRETEXT" ]]; then
184
+ TEXT="${_PRETEXT}, ${TEXT}"
185
+ fi
186
+
187
+ # Source provider manager to get active provider
188
+ source "$SCRIPT_DIR/provider-manager.sh"
189
+
190
+ # Get active provider
191
+ ACTIVE_PROVIDER=$(get_active_provider)
192
+
193
+ # Show GitHub star reminder (once per day)
194
+ "$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true
195
+
196
+ # @function detect_voice_provider
197
+ # @intent Auto-detect provider from voice name (for mixed-provider support)
198
+ # @why Allow Piper for main language + macOS for target language
199
+ # @param $1 voice name/ID
200
+ # @returns Provider name (piper or macos)
201
+ detect_voice_provider() {
202
+ local voice="$1"
203
+ # Piper voice names contain underscore and dash (e.g., es_ES-davefx-medium)
204
+ if [[ "$voice" == *"_"*"-"* ]]; then
205
+ echo "piper"
206
+ else
207
+ echo "$ACTIVE_PROVIDER"
208
+ fi
209
+ }
210
+
211
+ # Override provider if voice indicates different provider (mixed-provider mode)
212
+ if [[ -n "$VOICE_OVERRIDE" ]]; then
213
+ DETECTED_PROVIDER=$(detect_voice_provider "$VOICE_OVERRIDE")
214
+ if [[ "$DETECTED_PROVIDER" != "$ACTIVE_PROVIDER" ]]; then
215
+ ACTIVE_PROVIDER="$DETECTED_PROVIDER"
216
+ fi
217
+ fi
218
+
219
+ # @function speak_text
220
+ # @intent Route text to appropriate TTS provider
221
+ # @why Reusable function for speaking, used by both single and learning modes
222
+ # @param $1 text to speak
223
+ # @param $2 voice override (optional)
224
+ # @param $3 provider override (optional)
225
+ speak_text() {
226
+ local text="$1"
227
+ local voice="${2:-}"
228
+ local provider="${3:-$ACTIVE_PROVIDER}"
229
+ local profile_file="${4:-$AGENT_PROFILE_FILE}"
230
+
231
+ case "$provider" in
232
+ piper)
233
+ "$SCRIPT_DIR/play-tts-piper.sh" "$text" "$voice" "$profile_file"
234
+ ;;
235
+ soprano)
236
+ "$SCRIPT_DIR/play-tts-soprano.sh" "$text" "$voice"
237
+ ;;
238
+ macos)
239
+ "$SCRIPT_DIR/play-tts-macos.sh" "$text" "$voice"
240
+ ;;
241
+ termux-ssh)
242
+ "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$text" "$voice"
243
+ ;;
244
+ *)
245
+ echo "❌ Unknown provider: $provider" >&2
246
+ return 1
247
+ ;;
248
+ esac
249
+ }
250
+
251
+ # Note: learn-manager.sh and translate-manager.sh are sourced inside their
252
+ # respective handler functions to avoid triggering their main handlers
253
+
254
+ # @function handle_learning_mode
255
+ # @intent Speak in both main language and target language for learning
256
+ # @why Issue #51 - Auto-translate and speak twice for immersive language learning
257
+ # @returns 0 if learning mode handled, 1 if not in learning mode
258
+ handle_learning_mode() {
259
+ # Source learn-manager for learning mode functions
260
+ source "$SCRIPT_DIR/learn-manager.sh" 2>/dev/null || return 1
261
+
262
+ # Check if learning mode is enabled
263
+ if ! is_learn_mode_enabled 2>/dev/null; then
264
+ return 1
265
+ fi
266
+
267
+ local target_lang
268
+ target_lang=$(get_target_language 2>/dev/null || echo "")
269
+ local target_voice
270
+ target_voice=$(get_target_voice 2>/dev/null || echo "")
271
+
272
+ # Need both target language and voice for learning mode
273
+ if [[ -z "$target_lang" ]] || [[ -z "$target_voice" ]]; then
274
+ return 1
275
+ fi
276
+
277
+ # 1. Speak in main language (current voice)
278
+ speak_text "$TEXT" "$VOICE_OVERRIDE" "$ACTIVE_PROVIDER"
279
+
280
+ # 2. Auto-translate to target language
281
+ local translated
282
+ # SECURITY: Add timeout to prevent hanging (#134)
283
+ translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$target_lang" 2>/dev/null) || translated="$TEXT"
284
+
285
+ # Small pause between languages
286
+ sleep 0.5
287
+
288
+ # 3. Speak translated text with target voice
289
+ local target_provider
290
+ target_provider=$(detect_voice_provider "$target_voice")
291
+ speak_text "$translated" "$target_voice" "$target_provider"
292
+
293
+ return 0
294
+ }
295
+
296
+ # @function handle_translation_mode
297
+ # @intent Translate and speak in target language (non-learning mode)
298
+ # @why Issue #50 - BMAD multi-language TTS support
299
+ # @returns 0 if translation handled, 1 if not translating
300
+ handle_translation_mode() {
301
+ # Source translate-manager to get translation settings
302
+ source "$SCRIPT_DIR/translate-manager.sh" 2>/dev/null || return 1
303
+
304
+ # Check if translation is enabled
305
+ if ! is_translation_enabled 2>/dev/null; then
306
+ return 1
307
+ fi
308
+
309
+ local translate_to
310
+ translate_to=$(get_translate_to 2>/dev/null || echo "")
311
+
312
+ if [[ -z "$translate_to" ]] || [[ "$translate_to" == "english" ]]; then
313
+ return 1
314
+ fi
315
+
316
+ # Translate text
317
+ local translated
318
+ # SECURITY: Add timeout to prevent hanging (#134)
319
+ translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$translate_to" 2>/dev/null) || translated="$TEXT"
320
+
321
+ # Get voice for target language if no override specified
322
+ local voice_to_use="$VOICE_OVERRIDE"
323
+ if [[ -z "$voice_to_use" ]]; then
324
+ source "$SCRIPT_DIR/language-manager.sh" 2>/dev/null || true
325
+ voice_to_use=$(get_voice_for_language "$translate_to" "$ACTIVE_PROVIDER" 2>/dev/null || echo "")
326
+ fi
327
+
328
+ # Update provider if voice indicates different provider
329
+ local provider_to_use="$ACTIVE_PROVIDER"
330
+ if [[ -n "$voice_to_use" ]]; then
331
+ provider_to_use=$(detect_voice_provider "$voice_to_use")
332
+ fi
333
+
334
+ # Speak translated text
335
+ speak_text "$translated" "$voice_to_use" "$provider_to_use"
336
+ return 0
337
+ }
338
+
339
+ # Mode priority:
340
+ # 1. Learning mode (speaks twice: main + translated)
341
+ # 2. Translation mode (speaks translated only)
342
+ # 3. Normal mode (speaks as-is)
343
+
344
+ # Try learning mode first (Issue #51)
345
+ if handle_learning_mode; then
346
+ exit 0
347
+ fi
348
+
349
+ # Try translation mode (Issue #50)
350
+ if handle_translation_mode; then
351
+ exit 0
352
+ fi
353
+
354
+ # Normal single-language mode - route to appropriate provider implementation
355
+ case "$ACTIVE_PROVIDER" in
356
+ piper)
357
+ exec "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "$AGENT_PROFILE_FILE"
358
+ ;;
359
+ soprano)
360
+ exec "$SCRIPT_DIR/play-tts-soprano.sh" "$TEXT" "$VOICE_OVERRIDE"
361
+ ;;
362
+ macos)
363
+ exec "$SCRIPT_DIR/play-tts-macos.sh" "$TEXT" "$VOICE_OVERRIDE"
364
+ ;;
365
+ termux-ssh)
366
+ exec "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
367
+ ;;
368
+ *)
369
+ echo "❌ Unknown provider: $ACTIVE_PROVIDER" >&2
370
+ echo " Run: /agent-vibes:provider list" >&2
371
+ exit 1
372
+ ;;
373
+ esac