agentvibes 5.11.2 → 5.12.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 (54) hide show
  1. package/.claude/config/audio-effects.cfg +1 -1
  2. package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
  3. package/.claude/github-star-reminder.txt +1 -1
  4. package/.claude/hooks/audio-processor.sh +1 -1
  5. package/.claude/hooks/background-music-manager.sh +2 -1
  6. package/.claude/hooks/bmad-speak-enhanced.sh +37 -22
  7. package/.claude/hooks/bmad-speak.sh +26 -8
  8. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +1 -1
  9. package/.claude/hooks/play-tts-kokoro.sh +9 -0
  10. package/.claude/hooks/play-tts-piper.sh +13 -1
  11. package/.claude/hooks/play-tts-ssh-remote.sh +117 -7
  12. package/.claude/hooks/play-tts-termux-ssh.sh +1 -1
  13. package/.claude/hooks/play-tts.sh +106 -13
  14. package/.claude/hooks-windows/background-music-manager.ps1 +2 -1
  15. package/.claude/hooks-windows/play-tts-kokoro.ps1 +14 -0
  16. package/.claude/hooks-windows/play-tts.ps1 +163 -30
  17. package/.claude/hooks-windows/tts-watcher.ps1 +55 -0
  18. package/.claude/hooks-windows/voice-manager-windows.ps1 +101 -1
  19. package/README.md +11 -6
  20. package/RELEASE_NOTES.md +67 -0
  21. package/bin/mcp-server.sh +6 -19
  22. package/bin/resolve-utterance.js +137 -0
  23. package/mcp-server/server.py +280 -99
  24. package/mcp-server/test_mcp_correctness.py +486 -0
  25. package/mcp-server/test_windows_script_parity.py +341 -336
  26. package/package.json +3 -2
  27. package/setup-windows.ps1 +867 -815
  28. package/src/console/app.js +14 -11
  29. package/src/console/footer-config.js +0 -4
  30. package/src/console/navigation.js +0 -1
  31. package/src/console/tabs/agents-tab.js +96 -11
  32. package/src/console/tabs/music-tab.js +108 -3
  33. package/src/console/tabs/placeholder-tab.js +0 -2
  34. package/src/console/tabs/settings-tab.js +41 -2
  35. package/src/console/tabs/setup-tab.js +34 -6
  36. package/src/console/tabs/voices-tab.js +19 -9
  37. package/src/console/widgets/track-picker.js +3 -3
  38. package/src/i18n/de.js +0 -1
  39. package/src/i18n/en.js +0 -1
  40. package/src/i18n/es.js +0 -1
  41. package/src/i18n/fr.js +0 -1
  42. package/src/i18n/hi.js +0 -1
  43. package/src/i18n/ja.js +0 -1
  44. package/src/i18n/ko.js +0 -1
  45. package/src/i18n/pt.js +0 -1
  46. package/src/i18n/zh-CN.js +0 -1
  47. package/src/installer.js +205 -15
  48. package/src/services/config-service.js +264 -264
  49. package/src/services/llm-provider-service.js +7 -7
  50. package/src/services/navigation-service.js +1 -1
  51. package/src/services/utterance-loader.js +280 -0
  52. package/src/services/utterance-resolver.js +526 -0
  53. package/templates/agentvibes-receiver.sh +74 -12
  54. package/voice-assignments.json +8245 -0
@@ -1,6 +1,6 @@
1
1
 
2
2
  llm:default|light||0.15|en_US-lessac-high||piper
3
- llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.05||Agent Vibes local here|kokoro
3
+ llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.05|en_US-amy-medium|Agent Vibes local here|piper
4
4
  llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.15|en_US-libritts-high::Anna-11|Copilot here|piper
5
5
  llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Codex here|piper
6
6
  llm:hermes|light|agent_vibes_bachata_v1_loop.mp3|0.15|en_US-libritts-high::Leo-8|Hermes here|piper
@@ -0,0 +1,7 @@
1
+
2
+ llm:default|light||0.15|en_US-lessac-high||piper
3
+ llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.05|af_river|Agent Vibes local here|kokoro
4
+ llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.15|en_US-libritts-high::Anna-11|Copilot here|piper
5
+ llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Codex here|piper
6
+ llm:hermes|light|agent_vibes_bachata_v1_loop.mp3|0.15|en_US-libritts-high::Leo-8|Hermes here|piper
7
+ default||agent_vibes_arabic_v2_loop.mp3|0.30
@@ -1 +1 @@
1
- 20260625
1
+ 20260703
@@ -577,7 +577,7 @@ main() {
577
577
  fi
578
578
 
579
579
  local used_background=""
580
- if is_background_music_enabled && [[ -n "$background_path" ]] && [[ -f "$background_path" ]] && [[ "${bg_volume:-0}" != "0" ]] && [[ "${bg_volume:-0}" != "0.0" ]]; then
580
+ if [[ "$_bg_allowed" == "true" ]] && [[ -n "$background_path" ]] && [[ -f "$background_path" ]] && [[ "${bg_volume:-0}" != "0" ]] && [[ "${bg_volume:-0}" != "0.0" ]]; then
581
581
  local bg_display_name
582
582
  if [[ "$background_path" == "$custom_music_path" ]]; then
583
583
  bg_display_name="custom music"
@@ -33,7 +33,8 @@ ENABLED_FILE="$CONFIG_DIR/background-music-enabled.txt"
33
33
  VOLUME_FILE="$CONFIG_DIR/background-music-volume.txt"
34
34
 
35
35
  # Defaults
36
- DEFAULT_VOLUME="0.34"
36
+ # TODO(AVI-S8.6): generate this constant from the shared JSON source of truth.
37
+ DEFAULT_VOLUME="0.20"
37
38
 
38
39
  # Ensure config directory exists
39
40
  mkdir -p "$CONFIG_DIR"
@@ -134,29 +134,44 @@ TEMP_FILE="$AUDIO_DIR/tts-enhanced-$(date +%s)-$$.wav"
134
134
  # Determine voice and generate audio
135
135
  if [[ -n "$AGENT_VOICE" ]]; then
136
136
  echo "🎤 Agent: $AGENT_FOR_EFFECTS | Voice: $AGENT_VOICE"
137
- # Call piper directly to generate audio without playback
138
- "$SCRIPT_DIR/play-tts-piper.sh" "$FULL_TEXT" "$AGENT_VOICE" > /dev/null 2>&1 &
139
- PIPER_PID=$!
140
-
141
- # Wait for piper to generate (it outputs the file path)
142
- wait $PIPER_PID 2>/dev/null || true
143
-
144
- # Find the most recent TTS file
145
- GENERATED_FILE=$(ls -t "$AUDIO_DIR"/tts-padded-*.wav 2>/dev/null | head -1)
146
-
147
- if [[ -n "$GENERATED_FILE" ]] && [[ -f "$GENERATED_FILE" ]]; then
148
- # Apply audio effects and background mixing
149
- if [[ -f "$SCRIPT_DIR/audio-processor.sh" ]]; then
150
- PROCESSED_FILE="$AUDIO_DIR/tts-enhanced-processed-$(date +%s)-$$.wav"
151
- "$SCRIPT_DIR/audio-processor.sh" "$GENERATED_FILE" "$AGENT_FOR_EFFECTS" "$PROCESSED_FILE" 2>/dev/null || {
152
- # Fallback to original if processing fails
153
- PROCESSED_FILE="$GENERATED_FILE"
154
- }
155
137
 
156
- # Play the processed file
157
- if [[ -f "$PROCESSED_FILE" ]]; then
158
- (mpv "$PROCESSED_FILE" || aplay "$PROCESSED_FILE" || paplay "$PROCESSED_FILE") >/dev/null 2>&1 &
159
- fi
138
+ # Synthesize WITHOUT playback (this pipeline plays the post-processed file
139
+ # itself), CAPTURING the provider's stdout so we can read its AV_OUTPUT:
140
+ # sentinel the EXACT absolute path piper wrote for THIS invocation.
141
+ #
142
+ # We deliberately do NOT scan the audio directory for the newest file (the
143
+ # banned most-recent-file / directory-listing heuristic): it races in party
144
+ # mode (grabs another agent's audio) and masks synthesis failures. See Story
145
+ # AVI-S8.5 (R6/R7) and memory: feedback_no_most_recent_file_heuristic.
146
+ PIPER_STDOUT=""
147
+ # Skip piper's internal (default-row) audio-processor pass: we run our OWN
148
+ # per-agent audio-processor pass below, so letting piper process first would
149
+ # double the effects/background-music (double reverb, overlapping tracks).
150
+ if ! PIPER_STDOUT=$(AGENTVIBES_NO_PLAYBACK=true AGENTVIBES_SKIP_INTERNAL_PROCESSOR=1 "$SCRIPT_DIR/play-tts-piper.sh" "$FULL_TEXT" "$AGENT_VOICE" 2>/dev/null); then
151
+ echo "❌ bmad-speak: piper synthesis failed for agent '$AGENT_FOR_EFFECTS'" >&2
152
+ exit 1
153
+ fi
154
+
155
+ # Capture the exact output path from the AV_OUTPUT: sentinel. FAIL LOUD if it
156
+ # is absent — never fall back to a directory listing.
157
+ GENERATED_FILE=$(printf '%s\n' "$PIPER_STDOUT" | sed -n 's/^AV_OUTPUT://p' | tail -1)
158
+
159
+ if [[ -z "$GENERATED_FILE" ]] || [[ ! -f "$GENERATED_FILE" ]]; then
160
+ echo "❌ bmad-speak: no AV_OUTPUT sentinel from play-tts-piper.sh — refusing to guess the output file (agent '$AGENT_FOR_EFFECTS')" >&2
161
+ exit 1
162
+ fi
163
+
164
+ # Apply audio effects and background mixing
165
+ if [[ -f "$SCRIPT_DIR/audio-processor.sh" ]]; then
166
+ PROCESSED_FILE="$AUDIO_DIR/tts-enhanced-processed-$(date +%s)-$$.wav"
167
+ "$SCRIPT_DIR/audio-processor.sh" "$GENERATED_FILE" "$AGENT_FOR_EFFECTS" "$PROCESSED_FILE" 2>/dev/null || {
168
+ # Fallback to original if processing fails
169
+ PROCESSED_FILE="$GENERATED_FILE"
170
+ }
171
+
172
+ # Play the processed file
173
+ if [[ -f "$PROCESSED_FILE" ]]; then
174
+ (mpv "$PROCESSED_FILE" || aplay "$PROCESSED_FILE" || paplay "$PROCESSED_FILE") >/dev/null 2>&1 &
160
175
  fi
161
176
  fi
162
177
  else
@@ -104,7 +104,7 @@ read_agent_profile_all() {
104
104
 
105
105
  # Validate agent_id format (prevent injection)
106
106
  if [[ ! "$agent_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
107
- echo "|||||"
107
+ echo "||||||"
108
108
  return
109
109
  fi
110
110
 
@@ -173,13 +173,24 @@ map_to_agent_id() {
173
173
 
174
174
  AGENT_ID=$(map_to_agent_id "$AGENT_NAME_OR_ID")
175
175
 
176
- # Get agent's voice and intro text
177
- AGENT_VOICE=""
178
- AGENT_INTRO=""
179
- if [[ -n "$AGENT_ID" ]] && [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then
180
- AGENT_VOICE=$(cd "$PROJECT_ROOT" && "$SCRIPT_DIR/bmad-voice-manager.sh" get-voice "$AGENT_ID" 2>/dev/null)
181
- AGENT_INTRO=$(cd "$PROJECT_ROOT" && "$SCRIPT_DIR/bmad-voice-manager.sh" get-intro "$AGENT_ID" 2>/dev/null)
182
- fi
176
+ # ---------------------------------------------------------------------------
177
+ # Populate the per-agent profile (voice/pretext/reverb/personality/music) from
178
+ # bmad-voice-map.json. THIS is the population step that a botched merge
179
+ # (610af0f2) dropped, leaving $PROFILE_VOICE et al. below referenced-but-never-
180
+ # -assigned a fatal "unbound variable" under `set -u`. read_agent_profile_all()
181
+ # already existed (defined above) but was never called; wire it in here.
182
+ # Always initialize (even to "") so downstream references never crash, and a
183
+ # missing/unknown agent just falls through to normal TTS below.
184
+ PROFILE_VOICE=""
185
+ PROFILE_PRETEXT=""
186
+ PROFILE_REVERB=""
187
+ PROFILE_PERSONALITY=""
188
+ PROFILE_MUSIC_TRACK=""
189
+ PROFILE_MUSIC_VOLUME=""
190
+ PROFILE_MUSIC_ENABLED=""
191
+ _PROFILE_ALL="$(read_agent_profile_all "${AGENT_ID:-}")"
192
+ IFS='|' read -r PROFILE_VOICE PROFILE_PRETEXT PROFILE_REVERB PROFILE_PERSONALITY \
193
+ PROFILE_MUSIC_TRACK PROFILE_MUSIC_VOLUME PROFILE_MUSIC_ENABLED <<< "$_PROFILE_ALL"
183
194
 
184
195
  # Read global background music volume as fallback (stored as 0.0-1.0, convert to 0-100 integer)
185
196
  _BG_VOL_FILE="${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}/.claude/config/background-music-volume.txt"
@@ -271,6 +282,13 @@ trap 'rmdir "$SPEECH_LOCK" 2>/dev/null' EXIT
271
282
  # Speak with agent's voice, passing the temp profile path as arg 3 so
272
283
  # play-tts-piper.sh → audio-processor.sh can read per-agent music settings
273
284
  # without any env vars (safe for concurrent multi-project use).
285
+ #
286
+ # Declare voice provenance so the resolver never demotes a BMAD agent's own voice
287
+ # to the per-LLM/default row (F-1). This must be set independently of whether a
288
+ # TEMP_PROFILE file was created: a voice-only agent (no reverb/personality/music)
289
+ # has an empty TEMP_PROFILE, so the arg-3 heuristic in play-tts.sh alone would
290
+ # miss it. AGENT_VOICE is an agent-profile voice, always.
291
+ export AGENTVIBES_VOICE_SOURCE="agent-profile"
274
292
  if [[ -n "$AGENT_VOICE" ]]; then
275
293
  bash "$SCRIPT_DIR/play-tts.sh" "$FULL_TEXT" "$AGENT_VOICE" "$TEMP_PROFILE"
276
294
  else
@@ -99,7 +99,7 @@ echo "📱 Sending to $SSH_HOST for local playback..." >&2
99
99
  SSH_ARGS=()
100
100
  [[ -n "$SSH_KEY" && -f "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
101
101
  # Never force "-p 22" (ssh default) -- it would override an ~/.ssh/config
102
- # Host alias port (e.g. laptop-win -> 45217). Empty/22 => defer to ssh config.
102
+ # Host alias port (e.g. my-laptop -> 2222). Empty/22 => defer to ssh config.
103
103
  if [[ -n "$SSH_PORT" && "$SSH_PORT" != "22" ]]; then SSH_ARGS+=(-p "$SSH_PORT"); fi
104
104
 
105
105
  # Try receiver scripts in order — single SSH call, no separate probe
@@ -94,7 +94,10 @@ fi
94
94
 
95
95
  # Skip the (slow) synthesis + playback entirely in test mode — nothing to play
96
96
  # and no need to spin up the kokoro model. Mirrors the sibling Piper provider.
97
+ # Still emit the AV_OUTPUT sentinel so consumers get the exact intended path
98
+ # (Story AVI-S8.5, R6): the path is machine-parseable regardless of playback.
97
99
  if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
100
+ printf 'AV_OUTPUT:%s\n' "$TEMP_WAV"
98
101
  trap '' EXIT
99
102
  exit 0
100
103
  fi
@@ -110,6 +113,12 @@ if [[ ! -f "$TEMP_WAV" || ! -s "$TEMP_WAV" ]]; then
110
113
  exit 3
111
114
  fi
112
115
 
116
+ # AV_OUTPUT sentinel (Story AVI-S8.5, R6/R7): emit the EXACT absolute path of the
117
+ # wav this invocation produced, on its own machine-parseable stdout line. Consumers
118
+ # capture THIS path — never a directory listing (memory:
119
+ # feedback_no_most_recent_file_heuristic).
120
+ printf 'AV_OUTPUT:%s\n' "$TEMP_WAV"
121
+
113
122
  # ---------------------------------------------------------------------------
114
123
  # Play audio — try players in order (WAV-capable)
115
124
  # Skip playback in test mode or no-playback mode (matches the Piper provider).
@@ -474,7 +474,11 @@ fi
474
474
  # @returns Updates $TEMP_FILE to processed version, sets $BACKGROUND_MUSIC if used
475
475
  # @sideeffects Applies audio effects and background music
476
476
  BACKGROUND_MUSIC=""
477
- if [[ -f "$SCRIPT_DIR/audio-processor.sh" ]]; then
477
+ # AGENTVIBES_SKIP_INTERNAL_PROCESSOR=1 lets a caller that will run its OWN
478
+ # audio-processor pass (e.g. bmad-speak-enhanced.sh applying per-agent effects)
479
+ # skip this generic default-row pass — otherwise effects/background-music get
480
+ # applied twice (double reverb / overlapping music).
481
+ if [[ "${AGENTVIBES_SKIP_INTERNAL_PROCESSOR:-}" != "1" && -f "$SCRIPT_DIR/audio-processor.sh" ]]; then
478
482
  _tmp=$(mktemp "$AUDIO_DIR/tts-processed-XXXXXX"); PROCESSED_FILE="${_tmp}.wav"; mv "$_tmp" "$PROCESSED_FILE"
479
483
  _CLEANUP_FILES+=("$PROCESSED_FILE")
480
484
  # audio-processor.sh returns: FILE_PATH|BACKGROUND_FILE
@@ -630,6 +634,14 @@ fi
630
634
  # Display with file count (now showing accurate post-cleanup size)
631
635
  echo -e "${WHITE}💾 Saved to:${NC} ${CYAN}$TEMP_FILE${NC} ${YELLOW}$FILE_COUNT${NC} ${WHITE}🗄️${NC} ${CACHE_COLOR}$SIZE_HUMAN${NC} ${WHITE}🧹${NC}${GOLD}[${AUTO_CLEAN_THRESHOLD}mb]${NC}"
632
636
 
637
+ # AV_OUTPUT sentinel (Story AVI-S8.5, R6/R7): emit the EXACT absolute path of the
638
+ # wav this invocation produced, on its own machine-parseable stdout line. Consumers
639
+ # (e.g. bmad-speak-enhanced.sh) capture THIS path — never `ls -t | head -1`, which
640
+ # races in party mode and masks synthesis failures (memory:
641
+ # feedback_no_most_recent_file_heuristic). Additive: the human "Saved to:" line
642
+ # above is intact for the MCP server's loose parser.
643
+ printf 'AV_OUTPUT:%s\n' "$TEMP_FILE"
644
+
633
645
  if [[ -n "$BACKGROUND_MUSIC" ]]; then
634
646
  # Extract just the filename to save space
635
647
  MUSIC_FILENAME=$(basename "$BACKGROUND_MUSIC")
@@ -34,8 +34,32 @@ fi
34
34
  # Default to "default" so remote always has an LLM key to look up
35
35
  LLM_NAME="${LLM_NAME:-default}"
36
36
 
37
- # Validate required input
38
- if [[ -z "$TEXT" ]]; then
37
+ # Music-only preview mode: AGENTVIBES_MUSIC_ONLY=<track.mp3> tells the receiver to
38
+ # play a standalone background-music track (no speech). Used by the Music tab's
39
+ # remote preview so a track can be auditioned on the receiver. In this mode empty
40
+ # TEXT is allowed and the track name is forwarded as the "music" field with
41
+ # kind=music; the receiver resolves the file from its own ~/.claude/audio/tracks/.
42
+ PAYLOAD_KIND="speak"
43
+ MUSIC_ONLY_TRACK=""
44
+ if [[ -n "${AGENTVIBES_MUSIC_STOP:-}" ]]; then
45
+ # Stop whatever music preview is currently playing on the receiver.
46
+ PAYLOAD_KIND="music-stop"
47
+ elif [[ -n "${AGENTVIBES_MUSIC_ONLY:-}" ]]; then
48
+ # Only a bare .mp3 filename is allowed (no path separators, no leading dash) —
49
+ # the receiver resolves it against its own tracks dir, so reject path-like input.
50
+ if [[ "$AGENTVIBES_MUSIC_ONLY" =~ ^[A-Za-z0-9._][A-Za-z0-9._-]*\.mp3$ ]]; then
51
+ PAYLOAD_KIND="music"
52
+ MUSIC_ONLY_TRACK="$AGENTVIBES_MUSIC_ONLY"
53
+ else
54
+ echo "Invalid music track name: $AGENTVIBES_MUSIC_ONLY" >&2
55
+ exit 1
56
+ fi
57
+ fi
58
+
59
+ # Validate required input (music / music-stop modes carry no speech text)
60
+ if [[ "$PAYLOAD_KIND" == "music" || "$PAYLOAD_KIND" == "music-stop" ]]; then
61
+ TEXT="" # no speech in a music preview / stop
62
+ elif [[ -z "$TEXT" ]]; then
39
63
  echo "Usage: $0 <text> [voice] [agent_name]" >&2
40
64
  exit 1
41
65
  fi
@@ -46,6 +70,9 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
46
70
 
47
71
  # Derive project name from directory
48
72
  PROJECT_NAME=$(basename "$PROJECT_ROOT")
73
+ # Absolute path, forwarded so the receiver-side avatar can show/learn the real
74
+ # remote folder (mirrors the local forward-to-avatar.sh's projectPath field).
75
+ PROJECT_PATH="$PROJECT_ROOT"
49
76
 
50
77
  # ---------------------------------------------------------------------------
51
78
  # Get SSH connection details from config
@@ -149,7 +176,8 @@ fi
149
176
 
150
177
  SOX_EFFECTS=""
151
178
  BG_FILE=""
152
- BG_VOLUME="0.10"
179
+ # TODO(AVI-S8.6): generate this constant from the shared JSON source of truth.
180
+ BG_VOLUME="0.20"
153
181
 
154
182
  # Use CLAUDE_PROJECT_DIR (injected via --project-dir by play-tts.sh) so the
155
183
  # agent-name / default row is read from the user's project, not the package.
@@ -280,6 +308,76 @@ case "${PROVIDER:-}" in
280
308
  *) PROVIDER="piper" ;;
281
309
  esac
282
310
 
311
+ # ---------------------------------------------------------------------------
312
+ # Resolve the project-level .claude dir the way the local managers do:
313
+ # CLAUDE_PROJECT_DIR (the real user project, injected via --project-dir by
314
+ # play-tts.sh) wins; otherwise fall back to the package PROJECT_ROOT.
315
+ # ---------------------------------------------------------------------------
316
+ if [[ -n "${CLAUDE_PROJECT_DIR:-}" && -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
317
+ _RESOLVE_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
318
+ else
319
+ _RESOLVE_PROJECT_DIR="$PROJECT_ROOT"
320
+ fi
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # Resolve MUTE to forward (map §C 'mute never forwarded on SSH', R17).
324
+ # Mirrors play-tts.sh's 3-level file check exactly:
325
+ # project-unmute (overrides global) > project-mute > global-mute.
326
+ # The receiver is authoritative and applies a SAFETY-OR:
327
+ # effective_mute = receiver_local_mute OR forwarded_mute
328
+ # so if EITHER side is muted, playback stays silent. We forward this as a
329
+ # STRING ("true"/"false") — the receiver does a plain string compare, and it
330
+ # keeps the JSON boolean out of python's "True"/"False" capitalization.
331
+ # ---------------------------------------------------------------------------
332
+ _GLOBAL_MUTE_FILE="$HOME/.agentvibes-muted"
333
+ _PROJECT_MUTE_FILE="$_RESOLVE_PROJECT_DIR/.claude/agentvibes-muted"
334
+ _PROJECT_UNMUTE_FILE="$_RESOLVE_PROJECT_DIR/.claude/agentvibes-unmuted"
335
+ MUTE="false"
336
+ if [[ -f "$_PROJECT_UNMUTE_FILE" ]]; then
337
+ MUTE="false" # project explicitly unmuted overrides global
338
+ elif [[ -f "$_PROJECT_MUTE_FILE" ]]; then
339
+ MUTE="true"
340
+ elif [[ -f "$_GLOBAL_MUTE_FILE" ]]; then
341
+ MUTE="true"
342
+ fi
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # Resolve LANGUAGE to forward (map §D 'language | Forward on SSH', R18).
346
+ # Precedence mirrors the local chain (learn-manager.sh / language-manager.sh):
347
+ # learning-mode target (tts-target-language.txt when tts-learn-mode.txt=="ON")
348
+ # > tts-language.txt.
349
+ # Forwarded as a DEFAULT only — the receiver stays authoritative and its own
350
+ # language config (if any) wins. language-manager.sh stores tts-language.txt at
351
+ # the .claude/ root; the resolver contract tests use the .claude/config/ subdir,
352
+ # so we accept both. Project scope wins over the global home fallback.
353
+ # ---------------------------------------------------------------------------
354
+ LANGUAGE=""
355
+ for _cdir in "$_RESOLVE_PROJECT_DIR/.claude" "$HOME/.claude"; do
356
+ # Priority 1: learning-mode target language (only when learn mode is ON).
357
+ if [[ -z "$LANGUAGE" && -f "$_cdir/tts-learn-mode.txt" ]]; then
358
+ if [[ "$(cat "$_cdir/tts-learn-mode.txt" 2>/dev/null || true)" == "ON" ]]; then
359
+ [[ -f "$_cdir/tts-target-language.txt" ]] && \
360
+ LANGUAGE=$(cat "$_cdir/tts-target-language.txt" 2>/dev/null || true)
361
+ fi
362
+ fi
363
+ # Priority 2: tts-language.txt (root or config/ subdir).
364
+ if [[ -z "$LANGUAGE" && -f "$_cdir/tts-language.txt" ]]; then
365
+ LANGUAGE=$(cat "$_cdir/tts-language.txt" 2>/dev/null || true)
366
+ fi
367
+ if [[ -z "$LANGUAGE" && -f "$_cdir/config/tts-language.txt" ]]; then
368
+ LANGUAGE=$(cat "$_cdir/config/tts-language.txt" 2>/dev/null || true)
369
+ fi
370
+ [[ -n "$LANGUAGE" ]] && break
371
+ done
372
+ # Trim surrounding whitespace
373
+ LANGUAGE="${LANGUAGE#"${LANGUAGE%%[![:space:]]*}"}"
374
+ LANGUAGE="${LANGUAGE%"${LANGUAGE##*[![:space:]]}"}"
375
+ # SECURITY: only forward a plain language name (letters, spaces, hyphen); jq
376
+ # --arg escapes anyway, but reject odd values rather than propagate them.
377
+ if [[ -n "$LANGUAGE" ]] && [[ ! "$LANGUAGE" =~ ^[a-zA-Z][a-zA-Z\ -]*$ ]]; then
378
+ LANGUAGE=""
379
+ fi
380
+
283
381
  # ---------------------------------------------------------------------------
284
382
  # Build JSON payload
285
383
  # ---------------------------------------------------------------------------
@@ -294,22 +392,34 @@ build_json_payload() {
294
392
  --arg music "$BG_FILE" \
295
393
  --arg volume "$BG_VOLUME" \
296
394
  --arg project "$PROJECT_NAME" \
395
+ --arg projectPath "$PROJECT_PATH" \
297
396
  --arg pretext "$PRETEXT" \
298
397
  --arg speed "$SPEED" \
299
398
  --arg provider "$PROVIDER" \
300
399
  --arg llm "$LLM_NAME" \
301
- '{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm}'
400
+ --arg mute "$MUTE" \
401
+ --arg language "$LANGUAGE" \
402
+ --arg kind "$PAYLOAD_KIND" \
403
+ '{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, projectPath: $projectPath, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm, mute: $mute, language: $language, kind: $kind}'
302
404
  else
303
405
  # Manual JSON — escape backslashes, quotes, control chars
304
406
  local escaped_text
305
407
  escaped_text=$(printf '%s' "$TEXT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ' | sed 's/\r//g')
306
408
  local escaped_pretext
307
409
  escaped_pretext=$(printf '%s' "$PRETEXT" | sed 's/\\/\\\\/g; s/"/\\"/g')
308
- printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s"}' \
309
- "$escaped_text" "$VOICE" "$SOX_EFFECTS" "$BG_FILE" "$BG_VOLUME" "$PROJECT_NAME" "$escaped_pretext" "$SPEED" "$PROVIDER" "$LLM_NAME"
410
+ local escaped_project_path
411
+ escaped_project_path=$(printf '%s' "$PROJECT_PATH" | sed 's/\\/\\\\/g; s/"/\\"/g')
412
+ printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","projectPath":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s","mute":"%s","language":"%s","kind":"%s"}' \
413
+ "$escaped_text" "$VOICE" "$SOX_EFFECTS" "$BG_FILE" "$BG_VOLUME" "$PROJECT_NAME" "$escaped_project_path" "$escaped_pretext" "$SPEED" "$PROVIDER" "$LLM_NAME" "$MUTE" "$LANGUAGE" "$PAYLOAD_KIND"
310
414
  fi
311
415
  }
312
416
 
417
+ # Music-only preview: force the track as the "music" field (overriding any
418
+ # per-LLM/agent background-music config) so the receiver plays exactly this track.
419
+ if [[ "$PAYLOAD_KIND" == "music" ]]; then
420
+ BG_FILE="$MUSIC_ONLY_TRACK"
421
+ fi
422
+
313
423
  JSON_PAYLOAD=$(build_json_payload)
314
424
 
315
425
  # SECURITY: Base64-encode entire payload — safe for SSH transport
@@ -338,7 +448,7 @@ SSH_ARGS=()
338
448
  [[ -n "$SSH_KEY" && -f "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
339
449
  # Only pass -p for an explicit, non-default port. Port 22 is ssh's universal
340
450
  # default, so forcing "-p 22" is at best a no-op and at worst overrides the
341
- # real port of an ~/.ssh/config Host alias (e.g. laptop-win -> Port 45217),
451
+ # real port of an ~/.ssh/config Host alias (e.g. my-laptop -> Port 2222),
342
452
  # silently delivering the payload to the wrong port. Empty or "22" means
343
453
  # "use ssh / ~/.ssh/config defaults" -- never force it.
344
454
  if [[ -n "$SSH_PORT" && "$SSH_PORT" != "22" ]]; then SSH_ARGS+=(-p "$SSH_PORT"); fi
@@ -166,7 +166,7 @@ SAFE_TEXT="${TEXT//\'/\'\\\'\'}"
166
166
  SSH_ARGS=(-o ConnectTimeout=5)
167
167
  [[ -n "$SSH_KEY" && -f "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
168
168
  # Never force "-p 22" (ssh default) -- it would override an ~/.ssh/config
169
- # Host alias port (e.g. laptop-win -> 45217). Empty/22 => defer to ssh config.
169
+ # Host alias port (e.g. my-laptop -> 2222). Empty/22 => defer to ssh config.
170
170
  if [[ -n "$SSH_PORT" && "$SSH_PORT" != "22" ]]; then SSH_ARGS+=(-p "$SSH_PORT"); fi
171
171
 
172
172
  # Send TTS command to Android device via SSH
@@ -145,6 +145,7 @@ unset _POSITIONAL_ARGS
145
145
  TEXT="${1:-}"
146
146
  VOICE_OVERRIDE="${2:-}" # Optional: voice name or ID
147
147
  AGENT_PROFILE_FILE="${3:-}" # Optional: path to agent profile file
148
+ _ORIG_EXPLICIT_VOICE="$VOICE_OVERRIDE" # raw positional voice, before any per-LLM override (fed to the resolver as the explicit voice)
148
149
 
149
150
  # Security: Validate inputs
150
151
  if [[ -z "$TEXT" ]]; then
@@ -244,6 +245,75 @@ if [[ -n "$_PRETEXT" ]]; then
244
245
  TEXT="${_PRETEXT}, ${TEXT}"
245
246
  fi
246
247
 
248
+ # ── Utterance Resolver (AVI-S8.5 Stage 2) ────────────────────────────────────
249
+ # Single source of truth for the voice + engine decision. Resolve the plan once
250
+ # and adopt its voice (per-LLM voice wins over an LLM-echoed override — R2) and,
251
+ # below, its local engine (a kokoro-shaped voice forces the kokoro engine on
252
+ # Linux too — R1, curing the detect_voice_provider silence bug). FAIL-SAFE: if
253
+ # node or the resolver bundle isn't reachable (e.g. an installed ~/.claude that
254
+ # predates the bundle-shipping installer change), PLAN_OK stays empty and the
255
+ # legacy logic below runs unchanged — TTS never breaks on a missing bridge.
256
+ PLAN_OK=""
257
+ _RESOLVER_CLI=""
258
+ # AGENTVIBES_RESOLVER_CLI lets the installer (or tests) point at the resolver
259
+ # bundle when it lives outside the hooks tree — e.g. an installed ~/.claude/hooks
260
+ # whose sibling package dir holds bin/resolve-utterance.js.
261
+ # Candidate order: explicit env override; the installed dedicated bundle dir
262
+ # (has its own package.json so ESM works on every Node version — F-5); the repo
263
+ # layout (dev); a co-located copy.
264
+ for _cand in "${AGENTVIBES_RESOLVER_CLI:-}" \
265
+ "$SCRIPT_DIR/../agentvibes-resolver/bin/resolve-utterance.js" \
266
+ "$SCRIPT_DIR/../../bin/resolve-utterance.js" \
267
+ "$SCRIPT_DIR/resolve-utterance.js"; do
268
+ [[ -n "$_cand" && -f "$_cand" ]] && { _RESOLVER_CLI="$_cand"; break; }
269
+ done
270
+ if [[ -n "$_RESOLVER_CLI" ]] && command -v node &>/dev/null; then
271
+ # Voice provenance (F-1): a per-agent profile ($3) is an 'agent-profile' voice
272
+ # (BMAD party mode — each agent its OWN voice, must not be demoted to the
273
+ # per-LLM/default voice); a caller may declare provenance via
274
+ # AGENTVIBES_VOICE_SOURCE (e.g. MCP text_to_speech → 'user-explicit'); the
275
+ # voice-browser sets 'audition'; otherwise it's an 'llm-echo' (the LLM parroting
276
+ # back get_config, which the per-LLM row rightly overrides — R2).
277
+ _vsource="${AGENTVIBES_VOICE_SOURCE:-llm-echo}"
278
+ # Whitelist the provenance — an unknown value falls back to llm-echo (safe default).
279
+ case "$_vsource" in
280
+ llm-echo|user-explicit|agent-profile|translation-target|audition) ;;
281
+ *) _vsource="llm-echo" ;;
282
+ esac
283
+ # A per-agent profile ($3) also implies agent-profile provenance (belt-and-suspenders
284
+ # with bmad-speak.sh's explicit AGENTVIBES_VOICE_SOURCE export).
285
+ [[ -n "${AGENT_PROFILE_FILE:-}" && "$_vsource" == "llm-echo" ]] && _vsource="agent-profile"
286
+ [[ -n "${AGENTVIBES_EFFECTS_PREVIEW:-}" ]] && _vsource="audition"
287
+ # `timeout 5` caps a wedged bridge so the fail-safe promise ("TTS never breaks
288
+ # on the bridge") also covers a hung node, not just a missing one.
289
+ _av_timeout="timeout 5"; command -v timeout &>/dev/null || _av_timeout=""
290
+ if _plan_sh=$($_av_timeout node "$_RESOLVER_CLI" --format sh \
291
+ --text "$TEXT" \
292
+ --llm "$LLM_PROVIDER" \
293
+ --voice "$_ORIG_EXPLICIT_VOICE" \
294
+ --voice-source "$_vsource" \
295
+ --project-dir "${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" \
296
+ --package-root "$PROJECT_ROOT" 2>/dev/null); then
297
+ # Guard the eval: a malformed OR EMPTY plan must fall back to legacy, never
298
+ # abort the script mid-run under `set -euo pipefail`. `eval ""` succeeds with
299
+ # no AV_* vars set, which would falsely set PLAN_OK — so require non-empty
300
+ # output AND a field that is ALWAYS present in a valid plan. AV_TRANSPORT is
301
+ # always 'local' or a transport name (never empty), unlike AV_ENGINE which is
302
+ # legitimately empty on a remote plan (F-7).
303
+ if [[ -n "$_plan_sh" ]] && eval "$_plan_sh" 2>/dev/null && [[ -n "${AV_TRANSPORT:-}" ]]; then # AV_* shell-quoted, safe
304
+ PLAN_OK=1
305
+ # Adopt the voice into VOICE_OVERRIDE ONLY when it's a real override
306
+ # (F-2): a plain provider-file voice is left for play-tts-piper.sh's own
307
+ # file+model+speaker-id resolution (adopting it as an explicit override
308
+ # skips the multi-speaker lookup and plays speaker 0). Engine coupling (R1)
309
+ # still applies below regardless.
310
+ if [[ "${AV_VOICE_IS_OVERRIDE:-false}" == "true" && -n "${AV_VOICE:-}" ]]; then
311
+ VOICE_OVERRIDE="$AV_VOICE"
312
+ fi
313
+ fi
314
+ fi
315
+ fi
316
+
247
317
  # Source provider manager to get active provider
248
318
  source "$SCRIPT_DIR/provider-manager.sh"
249
319
 
@@ -259,7 +329,12 @@ case "$ACTIVE_PROVIDER" in
259
329
  # Transport — keep it. The receiver's audio-effects.cfg picks the engine.
260
330
  ;;
261
331
  *)
262
- if [[ -n "$_LLM_ENGINE" ]]; then
332
+ if [[ "$VOICE_OVERRIDE" =~ ^(af|am|bf|bm|jf|jm|kf|km|zf|zm|ff|fm|hf|hm|if|im|pf|pm|ef|em|nf|nm)_[a-zA-Z0-9]+$ ]]; then
333
+ # Kokoro-shaped voice override (e.g. af_heart) must win over the LLM row's
334
+ # engine column, else it is synthesized as a Piper voice, 404s downloading
335
+ # a model that doesn't exist, and stays silent. (Grafted from master WIP.)
336
+ ACTIVE_PROVIDER="kokoro"
337
+ elif [[ -n "$_LLM_ENGINE" ]]; then
263
338
  ACTIVE_PROVIDER="$_LLM_ENGINE"
264
339
  fi
265
340
  ;;
@@ -320,6 +395,17 @@ PYEOF
320
395
  fi
321
396
  fi
322
397
 
398
+ # Adopt the resolver's engine for LOCAL playback (AVI-S8.5 Stage 2): this cures
399
+ # the kokoro/piper voice→engine coupling (R1) and normalizes engine aliases,
400
+ # replacing the detect_voice_provider heuristic below. Transports are left
401
+ # untouched — the plan forwards the voice and the receiver picks its own engine.
402
+ if [[ -n "$PLAN_OK" ]]; then
403
+ case "$ACTIVE_PROVIDER" in
404
+ ssh-remote|agentvibes-receiver|termux-ssh) : ;; # transport — keep it
405
+ *) [[ -n "${AV_ENGINE:-}" ]] && ACTIVE_PROVIDER="$AV_ENGINE" ;;
406
+ esac
407
+ fi
408
+
323
409
  # Show GitHub star reminder (once per day)
324
410
  bash "$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true
325
411
 
@@ -338,10 +424,10 @@ detect_voice_provider() {
338
424
  fi
339
425
  }
340
426
 
341
- # Override provider if voice indicates different provider (mixed-provider mode)
342
- # But never override transport providers (ssh-remote, agentvibes-receiver, termux-ssh)
343
- # those are transport layers, not synth engines. The receiver picks its own engine.
344
- if [[ -n "$VOICE_OVERRIDE" ]]; then
427
+ # Legacy mixed-provider heuristic ONLY on the fallback path (no resolver plan).
428
+ # When PLAN_OK is set, the resolver already coupled voice→engine correctly above
429
+ # (including kokoro, which this heuristic misses the R1 silence bug), so skip it.
430
+ if [[ -z "$PLAN_OK" && -n "$VOICE_OVERRIDE" ]]; then
345
431
  case "$ACTIVE_PROVIDER" in
346
432
  ssh-remote|agentvibes-receiver|termux-ssh)
347
433
  # Transport provider — don't override, voice info is forwarded to receiver
@@ -432,12 +518,17 @@ speak_text() {
432
518
  termux-ssh)
433
519
  bash "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$text" "$voice"
434
520
  ;;
435
- ssh-remote)
521
+ ssh-remote|agentvibes-receiver)
522
+ # Both route through the base64-JSON sender that the modern receiver
523
+ # (templates/agentvibes-receiver.ps1 / .sh, installed as play-remote.*)
524
+ # understands. The old voiceless-connections sender spoke a 3-positional-arg
525
+ # protocol for a legacy ~/.agentvibes/play-remote.sh receiver that no current
526
+ # receiver install produces, so it delivered a non-base64 command the modern
527
+ # receiver rejects ("Payload must be base64-encoded"). play-tts-ssh-remote.sh
528
+ # resolves the host from any transport-config mode=remote entry (Priority 2b),
529
+ # so the agentvibes-receiver section is found without an ssh-remote section.
436
530
  bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$text" "$voice" "" "${profile_file:-}"
437
531
  ;;
438
- agentvibes-receiver)
439
- bash "$SCRIPT_DIR/play-tts-agentvibes-receiver-for-voiceless-connections.sh" "$text" "$voice"
440
- ;;
441
532
  *)
442
533
  echo "❌ Unknown provider: $provider" >&2
443
534
  return 1
@@ -572,12 +663,14 @@ case "$ACTIVE_PROVIDER" in
572
663
  termux-ssh)
573
664
  exec bash "$SCRIPT_DIR/play-tts-termux-ssh.sh" "$TEXT" "$VOICE_OVERRIDE"
574
665
  ;;
575
- ssh-remote)
666
+ ssh-remote|agentvibes-receiver)
667
+ # Both route through the base64-JSON sender the modern receiver understands.
668
+ # The legacy voiceless-connections sender spoke a 3-arg protocol for an old
669
+ # ~/.agentvibes/play-remote.sh receiver that no current install produces, so it
670
+ # delivered a non-base64 command the modern receiver rejects with
671
+ # "Payload must be base64-encoded".
576
672
  exec bash "$SCRIPT_DIR/play-tts-ssh-remote.sh" "$TEXT" "$VOICE_OVERRIDE" "" "${AGENT_PROFILE_FILE:-}"
577
673
  ;;
578
- agentvibes-receiver)
579
- exec bash "$SCRIPT_DIR/play-tts-agentvibes-receiver-for-voiceless-connections.sh" "$TEXT" "$VOICE_OVERRIDE"
580
- ;;
581
674
  *)
582
675
  echo "❌ Unknown provider: $ACTIVE_PROVIDER" >&2
583
676
  echo " Run: /agent-vibes:provider list" >&2
@@ -25,7 +25,8 @@ $EnabledFile = Join-Path $ConfigDir "background-music-enabled.txt"
25
25
  $VolumeFile = Join-Path $ConfigDir "background-music-volume.txt"
26
26
  $AudioEffectsCfg = Join-Path $ConfigDir "audio-effects.cfg"
27
27
 
28
- $DefaultVolume = "0.34"
28
+ # TODO(AVI-S8.6): generate this constant from the shared JSON source of truth.
29
+ $DefaultVolume = "0.20"
29
30
 
30
31
  # Ensure config directory exists
31
32
  if (-not (Test-Path $ConfigDir)) {
@@ -160,6 +160,20 @@ try {
160
160
  Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
161
161
  Write-Host "[VOICE] Voice used: $VoiceName (Kokoro TTS)" -ForegroundColor Green
162
162
 
163
+ # Kokoro outputs at a lower level than Piper/SAPI, and ffmpeg's amix halves
164
+ # the voice when background music is mixed in. Apply a 2.5x (≈8 dB) volume
165
+ # boost here so the voice is audible in all playback paths.
166
+ $ffplayCheck = Get-Command ffmpeg -ErrorAction SilentlyContinue
167
+ if ($ffplayCheck) {
168
+ $boostedFile = "$AudioDir\tts-boosted-$([System.IO.Path]::GetRandomFileName() -replace '\..*').wav"
169
+ $boostArgs = "-y -i `"$AudioFile`" -af `"volume=2.5`" `"$boostedFile`""
170
+ $boostProc = Start-Process -FilePath "ffmpeg" -ArgumentList $boostArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
171
+ if ($boostProc.ExitCode -eq 0 -and (Test-Path $boostedFile) -and (Get-Item $boostedFile).Length -gt 0) {
172
+ Remove-Item $AudioFile -Force -ErrorAction SilentlyContinue
173
+ $AudioFile = $boostedFile
174
+ }
175
+ }
176
+
163
177
  # Apply per-agent audio effects via audio-processor.ps1, but SKIP when
164
178
  # AGENTVIBES_NO_PLAY is set - that means the parent play-tts.ps1 will do its
165
179
  # own reverb / background-music post-processing and running effects here too