agentvibes 5.3.0 → 5.5.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 (222) hide show
  1. package/.agentvibes/LITE-MODE.md +236 -0
  2. package/.agentvibes/README.md +136 -0
  3. package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +141 -0
  4. package/.agentvibes/backups/agents/analyst_20260204_144958.md +78 -0
  5. package/.agentvibes/backups/agents/architect_20260204_144958.md +72 -0
  6. package/.agentvibes/backups/agents/dev_20260204_144958.md +74 -0
  7. package/.agentvibes/backups/agents/pm_20260204_144958.md +72 -0
  8. package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +64 -0
  9. package/.agentvibes/backups/agents/sm_20260204_144958.md +87 -0
  10. package/.agentvibes/backups/agents/tea_20260204_144958.md +79 -0
  11. package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +82 -0
  12. package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +80 -0
  13. package/.agentvibes/bmad/bmad-voices.md +69 -69
  14. package/.agentvibes/config/README-personality-defaults.md +162 -0
  15. package/.agentvibes/config/mode.txt +1 -0
  16. package/.agentvibes/config/personality-voice-defaults.default.json +21 -0
  17. package/.agentvibes/config/save-audio.txt +1 -0
  18. package/.agentvibes/config/voice-metadata.json +160 -0
  19. package/.agentvibes/config.json +24 -15
  20. package/.agentvibes/hooks/help.sh +191 -0
  21. package/.agentvibes/hooks/post-tool-use-lite.sh +111 -0
  22. package/.agentvibes/hooks/save-audio-manager.sh +162 -0
  23. package/.agentvibes/hooks/session-start-full-optimized.sh +102 -0
  24. package/.agentvibes/hooks/session-start-full.sh +142 -0
  25. package/.agentvibes/hooks/session-start-lite-v2.sh +34 -0
  26. package/.agentvibes/hooks/session-start-lite.sh +29 -0
  27. package/.agentvibes/hooks/stop-lite.sh +115 -0
  28. package/.agentvibes/hooks/switch-mode.sh +215 -0
  29. package/.agentvibes/output-styles/audio-summary.md +30 -0
  30. package/.claude/activation-instructions +54 -54
  31. package/.claude/audio/voice-samples/piper/alan.wav +0 -0
  32. package/.claude/audio/voice-samples/piper/amy.wav +0 -0
  33. package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
  34. package/.claude/audio/voice-samples/piper/joe.wav +0 -0
  35. package/.claude/audio/voice-samples/piper/john.wav +0 -0
  36. package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
  37. package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
  38. package/.claude/audio/voice-samples/piper/linda.wav +0 -0
  39. package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
  40. package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
  41. package/.claude/commands/agent-vibes/add.md +21 -21
  42. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  43. package/.claude/commands/agent-vibes/agent.md +79 -79
  44. package/.claude/commands/agent-vibes/background-music.md +111 -111
  45. package/.claude/commands/agent-vibes/bmad.md +198 -198
  46. package/.claude/commands/agent-vibes/clean.md +18 -18
  47. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  48. package/.claude/commands/agent-vibes/commands.json +145 -145
  49. package/.claude/commands/agent-vibes/effects.md +97 -97
  50. package/.claude/commands/agent-vibes/get.md +9 -9
  51. package/.claude/commands/agent-vibes/hide.md +91 -91
  52. package/.claude/commands/agent-vibes/language.md +23 -23
  53. package/.claude/commands/agent-vibes/learn.md +67 -67
  54. package/.claude/commands/agent-vibes/list.md +13 -13
  55. package/.claude/commands/agent-vibes/mute.md +37 -37
  56. package/.claude/commands/agent-vibes/preview.md +17 -17
  57. package/.claude/commands/agent-vibes/provider.md +68 -68
  58. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  59. package/.claude/commands/agent-vibes/sample.md +12 -12
  60. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  61. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  62. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  63. package/.claude/commands/agent-vibes/show.md +84 -84
  64. package/.claude/commands/agent-vibes/switch.md +87 -87
  65. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  66. package/.claude/commands/agent-vibes/target.md +30 -30
  67. package/.claude/commands/agent-vibes/translate.md +68 -68
  68. package/.claude/commands/agent-vibes/unmute.md +45 -45
  69. package/.claude/commands/agent-vibes/whoami.md +7 -7
  70. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  71. package/.claude/commands/agent-vibes-rdp.md +24 -24
  72. package/.claude/config/audio-effects.cfg +16 -11
  73. package/.claude/config/audio-effects.cfg.sample +52 -52
  74. package/.claude/config/background-music-position.txt +27 -0
  75. package/.claude/config/background-music-volume.txt +1 -1
  76. package/.claude/config/background-music.cfg +1 -0
  77. package/.claude/config/background-music.txt +1 -0
  78. package/.claude/config/tts-speech-rate.txt +1 -4
  79. package/.claude/config/tts-verbosity.txt +1 -0
  80. package/.claude/docs/TERMUX_SETUP.md +408 -408
  81. package/.claude/github-star-reminder.txt +1 -1
  82. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  83. package/.claude/hooks/audio-cache-utils.sh +0 -0
  84. package/.claude/hooks/audio-processor.sh +60 -14
  85. package/.claude/hooks/background-music-manager.sh +0 -0
  86. package/.claude/hooks/bmad-party-manager.sh +225 -0
  87. package/.claude/hooks/bmad-party-speak.sh +0 -0
  88. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  89. package/.claude/hooks/bmad-speak.sh +12 -15
  90. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  91. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  92. package/.claude/hooks/clawdbot-receiver-SECURE.sh +25 -23
  93. package/.claude/hooks/clawdbot-receiver.sh +4 -28
  94. package/.claude/hooks/clean-audio-cache.sh +0 -0
  95. package/.claude/hooks/cleanup-cache.sh +0 -0
  96. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  97. package/.claude/hooks/download-extra-voices.sh +0 -0
  98. package/.claude/hooks/effects-manager.sh +0 -0
  99. package/.claude/hooks/github-star-reminder.sh +0 -0
  100. package/.claude/hooks/language-manager.sh +0 -0
  101. package/.claude/hooks/learn-manager.sh +0 -0
  102. package/.claude/hooks/macos-voice-manager.sh +0 -0
  103. package/.claude/hooks/migrate-background-music.sh +0 -0
  104. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  105. package/.claude/hooks/optimize-background-music.sh +0 -0
  106. package/.claude/hooks/personality-manager.sh +0 -0
  107. package/.claude/hooks/piper-download-voices.sh +0 -0
  108. package/.claude/hooks/piper-installer.sh +1 -1
  109. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  110. package/.claude/hooks/piper-voice-manager.sh +0 -0
  111. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  112. package/.claude/hooks/play-tts-macos.sh +6 -12
  113. package/.claude/hooks/play-tts-piper.sh +52 -81
  114. package/.claude/hooks/play-tts-soprano.sh +9 -43
  115. package/.claude/hooks/play-tts-ssh-remote.sh +43 -215
  116. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  117. package/.claude/hooks/play-tts.sh +41 -20
  118. package/.claude/hooks/post-response.sh +41 -0
  119. package/.claude/hooks/prepare-release.sh +0 -0
  120. package/.claude/hooks/provider-commands.sh +0 -0
  121. package/.claude/hooks/provider-manager.sh +0 -0
  122. package/.claude/hooks/replay-target-audio.sh +0 -0
  123. package/.claude/hooks/requirements.txt +6 -6
  124. package/.claude/hooks/sentiment-manager.sh +0 -0
  125. package/.claude/hooks/session-start-tts.sh +56 -39
  126. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  127. package/.claude/hooks/speed-manager.sh +0 -0
  128. package/.claude/hooks/stop.sh +63 -0
  129. package/.claude/hooks/termux-installer.sh +0 -0
  130. package/.claude/hooks/translate-manager.sh +0 -0
  131. package/.claude/hooks/translator.py +237 -237
  132. package/.claude/hooks/tts-queue-worker.sh +0 -0
  133. package/.claude/hooks/tts-queue.sh +0 -0
  134. package/.claude/hooks/verbosity-manager.sh +0 -0
  135. package/.claude/hooks/voice-manager.sh +26 -4
  136. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  137. package/.claude/hooks-windows/bmad-party-speak.ps1 +278 -278
  138. package/.claude/hooks-windows/bmad-speak.ps1 +264 -264
  139. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -53
  140. package/.claude/hooks-windows/effects-manager.ps1 +294 -294
  141. package/.claude/hooks-windows/language-manager.ps1 +193 -193
  142. package/.claude/hooks-windows/learn-manager.ps1 +241 -241
  143. package/.claude/hooks-windows/personality-manager.ps1 +266 -266
  144. package/.claude/hooks-windows/play-tts-soprano.ps1 +5 -5
  145. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -138
  146. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +178 -0
  147. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -0
  148. package/.claude/hooks-windows/play-tts.ps1 +265 -507
  149. package/.claude/hooks-windows/provider-manager.ps1 +158 -192
  150. package/.claude/hooks-windows/session-start-tts.ps1 +55 -46
  151. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  152. package/.claude/hooks-windows/speed-manager.ps1 +166 -166
  153. package/.claude/hooks-windows/voice-manager-windows.ps1 +176 -260
  154. package/.claude/output-styles/agent-vibes.md +202 -202
  155. package/.claude/personalities/angry.md +14 -14
  156. package/.claude/personalities/annoying.md +14 -14
  157. package/.claude/personalities/crass.md +14 -14
  158. package/.claude/personalities/dramatic.md +14 -14
  159. package/.claude/personalities/dry-humor.md +50 -50
  160. package/.claude/personalities/flirty.md +20 -20
  161. package/.claude/personalities/funny.md +14 -14
  162. package/.claude/personalities/grandpa.md +32 -32
  163. package/.claude/personalities/millennial.md +14 -14
  164. package/.claude/personalities/moody.md +14 -14
  165. package/.claude/personalities/normal.md +16 -16
  166. package/.claude/personalities/pirate.md +14 -14
  167. package/.claude/personalities/poetic.md +14 -14
  168. package/.claude/personalities/professional.md +14 -14
  169. package/.claude/personalities/rapper.md +55 -55
  170. package/.claude/personalities/robot.md +14 -14
  171. package/.claude/personalities/sarcastic.md +38 -38
  172. package/.claude/personalities/sassy.md +14 -14
  173. package/.claude/personalities/surfer-dude.md +14 -14
  174. package/.claude/personalities/zen.md +14 -14
  175. package/.claude/piper-voices-dir.txt +1 -0
  176. package/.claude/settings.json +25 -15
  177. package/.claude/verbosity.txt +1 -1
  178. package/.clawdbot/README.md +105 -105
  179. package/.clawdbot/skill/SKILL.md +149 -145
  180. package/.mcp.json +30 -11
  181. package/CLAUDE.md +170 -215
  182. package/README.md +207 -521
  183. package/RELEASE_NOTES.md +1172 -1976
  184. package/WINDOWS-SETUP.md +208 -208
  185. package/bin/agent-vibes +0 -0
  186. package/bin/agentvibes-voice-browser.js +64 -1289
  187. package/bin/agentvibes.js +28 -0
  188. package/bin/ensure-soprano-running.sh +43 -0
  189. package/bin/mcp-server.js +121 -121
  190. package/bin/mcp-server.sh +0 -0
  191. package/bin/test-bmad-pr +78 -78
  192. package/mcp-server/QUICK_START.md +203 -203
  193. package/mcp-server/README.md +345 -345
  194. package/mcp-server/WINDOWS_SETUP.md +260 -260
  195. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  196. package/mcp-server/examples/claude_desktop_config.json +11 -11
  197. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  198. package/mcp-server/examples/custom_instructions.md +169 -169
  199. package/mcp-server/install-deps.js +130 -130
  200. package/mcp-server/pyproject.toml +52 -52
  201. package/mcp-server/requirements.txt +2 -2
  202. package/mcp-server/server.py +1467 -1578
  203. package/mcp-server/test_server.py +395 -395
  204. package/package.json +1 -3
  205. package/setup-windows.ps1 +815 -815
  206. package/src/console/tabs/music-tab.js +5 -2
  207. package/src/console/tabs/voices-tab.js +71 -37
  208. package/src/installer.js +52 -5
  209. package/src/services/llm-provider-service.js +1 -1
  210. package/templates/agentvibes-receiver.sh +158 -483
  211. package/templates/audio/welcome-music.mp3 +0 -0
  212. package/.agentvibes/bmad-voice-map.json +0 -104
  213. package/.agentvibes/copilot-sessions.log +0 -4
  214. package/.claude/config/audio-effects-bmad.cfg +0 -50
  215. package/.claude/config/intro-text.txt +0 -1
  216. package/.claude/config/personality.txt +0 -1
  217. package/.claude/config/piper-speech-rate.txt +0 -4
  218. package/.claude/config/piper-target-speech-rate.txt +0 -1
  219. package/.claude/config/reverb-level.txt +0 -1
  220. package/.claude/config/tts-target-speech-rate.txt +0 -1
  221. package/voice-assignments.json +0 -8245
  222. /package/{.claude → .agentvibes}/config/agentvibes.json +0 -0
@@ -12,167 +12,23 @@ param(
12
12
  [Parameter(Mandatory = $false, Position = 1)]
13
13
  [string]$VoiceOverride,
14
14
 
15
+ # LLM identity for per-LLM audio routing (e.g. "claude-code", "copilot", "codex").
16
+ # When provided, the router looks up an `llm:<name>` row in audio-effects.cfg
17
+ # to apply LLM-specific voice, pretext, reverb, and engine settings.
15
18
  [Parameter(Mandatory = $false)]
16
19
  [string]$llm = ""
17
20
  )
18
21
 
19
- # Text-file handoff: Windows command-line arg passing mangles text with
20
- # quotes, newlines, or non-ASCII characters. The SSH receiver watcher
21
- # (setup-ssh-receiver.ps1) writes long/special-char text to a UTF-8 temp
22
- # file and passes the sentinel "__from_file__" + AGENTVIBES_TEXT_FILE env
23
- # var. Load the real text here before any validation or synthesis.
24
- if ($Text -eq "__from_file__" -and $env:AGENTVIBES_TEXT_FILE) {
25
- if (Test-Path $env:AGENTVIBES_TEXT_FILE) {
26
- $Text = [System.IO.File]::ReadAllText($env:AGENTVIBES_TEXT_FILE, [System.Text.UTF8Encoding]::new($false))
27
- } else {
28
- Write-Error "AGENTVIBES_TEXT_FILE set to missing path: $($env:AGENTVIBES_TEXT_FILE)"
29
- exit 1
30
- }
31
- }
32
-
33
- # Security: Validate LLM provider name (alphanumeric, hyphens, underscores
34
- # only) -- mirrors play-tts.sh line 92. This prevents weird values from
35
- # poisoning the audio-effects.cfg lookup or the AGENTVIBES_LLM_KEY env var
36
- # we export to child scripts. An invalid value is treated as unset rather
37
- # than aborting, so the script falls back to the default config and the
38
- # rest of TTS still works.
39
- if ($llm -and $llm -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
40
- Write-Error ("Invalid LLM provider name: '{0}' - must match {1}. Falling back to default config." -f $llm, '^[a-zA-Z0-9][a-zA-Z0-9_-]*$')
41
- $llm = ""
42
- }
43
-
44
- # When no -llm is supplied, route through the "default" pseudo-LLM so the
45
- # user-managed `llm:default` row in audio-effects.cfg becomes the global
46
- # fallback for voice / pretext / music / effects. This is configured via
47
- # Setup -> Default -> Configure in the TUI. If `llm:default` doesn't exist,
48
- # the lookup will return empty and the script falls through to the
49
- # legacy global config chain (project / user .agentvibes/config.json).
50
- if (-not $llm) {
51
- $llm = "default"
52
- }
53
-
54
- # --- Cross-process playback serialization ---
55
- # Without this, any two callers of play-tts.ps1 (Claude Code PostToolUse hook,
56
- # Codex MCP text_to_speech, Copilot MCP text_to_speech, direct CLI) race each
57
- # other and produce overlapping / interleaved audio. Party mode already has
58
- # its own mutex (AgentVibesPartyModeTTSQueue) at the bmad-party-speak.ps1
59
- # level, but MCP-initiated calls bypass it entirely.
60
- #
61
- # We use a DIFFERENT mutex name ("AgentVibesPlaybackLock") so there's no
62
- # deadlock risk with the party-mode mutex -- they can be held independently
63
- # by nested processes.
64
- #
65
- # The mutex is acquired immediately before PlaySync() and released right
66
- # after, so CPU-bound synthesis/ffmpeg work can overlap with another
67
- # process's playback.
68
- $_PlaybackMutex = New-Object System.Threading.Mutex($false, "AgentVibesPlaybackLock")
69
-
70
- # --- Playback watchdog ---
71
- # If playback itself hangs (SoundPlayer deadlock, audio device locked,
72
- # etc.), a sibling PowerShell job waits 120 seconds from the moment
73
- # playback STARTS and force-kills this process. Without this, a stuck
74
- # play-tts.ps1 holds the playback mutex forever and silently blocks every
75
- # subsequent TTS call across all LLMs.
76
- #
77
- # IMPORTANT: the watchdog is started AFTER mutex acquisition (inside
78
- # Invoke-SerializedPlay), not at script entry. Starting it at script
79
- # entry caused round-robin / party-mode cut-offs: when 9 agents fire
80
- # text_to_speech in quick succession, later calls spend most of their
81
- # 120s budget waiting for the mutex, then get killed mid-playback.
82
- # The mutex WaitOne() bounds queue waiting separately.
83
-
84
- function Invoke-SerializedPlay {
85
- param([Parameter(Mandatory)][string]$WavPath)
86
- $acquired = $false
87
- $watchdogJob = $null
88
- try {
89
- try {
90
- # 600s timeout to acquire the playback mutex. Covers worst-case
91
- # queue depth (round-robin with 9 agents x ~60s of playback each).
92
- # AbandonedMutexException means the holder's process actually
93
- # died -- we inherit ownership.
94
- $acquired = $_PlaybackMutex.WaitOne(600000)
95
- } catch [System.Threading.AbandonedMutexException] {
96
- $acquired = $true
97
- }
98
- if (-not $acquired) {
99
- # Self-heal: kill any stuck play-tts.ps1 processes (other than
100
- # ourselves) that have been alive longer than 10 minutes. Past
101
- # any legitimate playback window, so only truly stuck processes
102
- # get killed.
103
- try {
104
- $myPid = $PID
105
- $cutoff = (Get-Date).AddSeconds(-600)
106
- $stuck = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
107
- Where-Object {
108
- $_.Name -eq 'powershell.exe' -and
109
- $_.ProcessId -ne $myPid -and
110
- $_.CommandLine -like '*play-tts.ps1*' -and
111
- $_.CreationDate -lt $cutoff
112
- }
113
- foreach ($p in $stuck) {
114
- [Console]::Error.WriteLine("[AgentVibes] Self-heal: killing stuck play-tts.ps1 pid $($p.ProcessId) (alive since $($p.CreationDate))")
115
- Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
116
- }
117
- } catch { }
118
- [Console]::Error.WriteLine("[AgentVibes] ERROR: play-tts.ps1 could not acquire playback mutex within 600s. A prior play-tts.ps1 process was stuck holding it and has been killed; the next TTS call should succeed.")
119
- exit 2
120
- }
121
-
122
- # Start the watchdog NOW (after mutex acquisition) so its 120s
123
- # budget covers only the playback itself, not time spent queued.
124
- try {
125
- $watchdogJob = Start-Job -ArgumentList $PID -ScriptBlock {
126
- param($parentPid)
127
- Start-Sleep -Seconds 120
128
- try {
129
- $p = Get-Process -Id $parentPid -ErrorAction SilentlyContinue
130
- if ($p) {
131
- [Console]::Error.WriteLine("[AgentVibes] play-tts.ps1 playback watchdog fired -- force-killing pid $parentPid after 120s of playback")
132
- Stop-Process -Id $parentPid -Force -ErrorAction SilentlyContinue
133
- }
134
- } catch { }
135
- }
136
- } catch {
137
- $watchdogJob = $null
138
- }
139
-
140
- $player = $null
141
- try {
142
- $player = New-Object System.Media.SoundPlayer $WavPath
143
- $player.PlaySync()
144
- } finally {
145
- if ($player) { $player.Dispose() }
146
- }
147
- } finally {
148
- if ($watchdogJob) {
149
- try {
150
- Stop-Job -Job $watchdogJob -ErrorAction SilentlyContinue
151
- Remove-Job -Job $watchdogJob -Force -ErrorAction SilentlyContinue
152
- } catch { }
153
- }
154
- if ($acquired) {
155
- try { $_PlaybackMutex.ReleaseMutex() } catch { }
156
- }
157
- }
158
- }
159
-
160
22
  # Configuration paths
161
- # Priority: CLAUDE_PROJECT_DIR env var -> script's parent project -> user profile
162
- # Local project settings ALWAYS override global (~/.claude)
23
+ # First check if we're running from a project directory with .claude
163
24
  $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
25
+ $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
164
26
 
165
- if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
166
- $ClaudeDir = "$env:CLAUDE_PROJECT_DIR\.claude"
27
+ # Use project .claude if running from there, otherwise use user profile
28
+ if (Test-Path $ProjectClaudeDir) {
29
+ $ClaudeDir = $ProjectClaudeDir
167
30
  } else {
168
- $PackageClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
169
- if (Test-Path $PackageClaudeDir) {
170
- $ClaudeDir = $PackageClaudeDir
171
- } elseif (Test-Path "$env:USERPROFILE\.claude\tts-provider.txt") {
172
- $ClaudeDir = "$env:USERPROFILE\.claude"
173
- } else {
174
- $ClaudeDir = "$env:USERPROFILE\.claude"
175
- }
31
+ $ClaudeDir = "$env:USERPROFILE\.claude"
176
32
  }
177
33
 
178
34
  $HooksDir = "$ClaudeDir\hooks-windows"
@@ -187,124 +43,9 @@ if (Test-Path $MuteFile) {
187
43
  }
188
44
  }
189
45
 
190
- # Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
191
- # Format: llm:<name>|REVERB|BG_FILE|BG_VOLUME|VOICE|PRETEXT|ENGINE
192
- $LlmVoice = ""
193
- $LlmPretext = ""
194
- $LlmReverb = ""
195
- $LlmEngine = ""
196
- $LlmBgTrack = ""
197
- $LlmBgVolume = ""
198
- $ProjectRoot = Split-Path -Parent $ClaudeDir
199
- $ConfigDir = "$ClaudeDir\config"
200
-
201
- if ($llm) {
202
- $llmKey = "llm:$llm"
203
- $llmKeyPattern = '^' + [regex]::Escape($llmKey) + '\|'
204
- # Check project-local audio-effects.cfg first, then global
205
- $cfgPaths = @(
206
- "$ConfigDir\audio-effects.cfg",
207
- "$env:USERPROFILE\.claude\config\audio-effects.cfg"
208
- )
209
- foreach ($cfgPath in $cfgPaths) {
210
- if (-not $LlmVoice -and -not $LlmPretext -and (Test-Path $cfgPath)) {
211
- foreach ($line in (Get-Content $cfgPath)) {
212
- if ($line -match $llmKeyPattern) {
213
- $parts = $line -split '\|'
214
- # parts: [0]=key [1]=reverb [2]=bg_file [3]=bg_vol [4]=voice [5]=pretext [6]=engine
215
- if ($parts.Length -ge 2 -and $parts[1].Trim()) {
216
- $LlmReverb = $parts[1].Trim()
217
- }
218
- if ($parts.Length -ge 3 -and $parts[2].Trim()) {
219
- $LlmBgTrack = $parts[2].Trim()
220
- }
221
- if ($parts.Length -ge 4 -and $parts[3].Trim()) {
222
- $LlmBgVolume = $parts[3].Trim()
223
- }
224
- if ($parts.Length -ge 5 -and $parts[4].Trim()) {
225
- $LlmVoice = $parts[4].Trim()
226
- }
227
- if ($parts.Length -ge 6 -and $parts[5].Trim()) {
228
- $LlmPretext = $parts[5].Trim()
229
- }
230
- if ($parts.Length -ge 7 -and $parts[6].Trim()) {
231
- $LlmEngine = $parts[6].Trim()
232
- }
233
- break
234
- }
235
- }
236
- }
237
- }
238
- # LLM per-LLM voice routing.
239
- #
240
- # PRIORITY CHANGE: when -llm is passed AND the llm row has a voice,
241
- # the per-LLM voice always wins — even over an explicit VoiceOverride
242
- # parameter passed by the MCP caller. Rationale: Codex / Copilot /
243
- # Claude Code all call `get_config` at session start and then echo
244
- # the global voice back on every `text_to_speech` call. With the
245
- # old "explicit wins" priority, that global voice overrode our
246
- # per-LLM routing and broke the entire point of having llm:<key>
247
- # rows in audio-effects.cfg.
248
- #
249
- # To request a specific voice for a specific call that bypasses the
250
- # LLM routing, the caller should NOT pass -llm, or should use the
251
- # `llm:default` row (which has no voice column to override).
252
- if ($LlmVoice) {
253
- $VoiceOverride = $LlmVoice
254
- }
255
- # Export LLM key for child scripts (process-local, not system-wide)
256
- $env:AGENTVIBES_LLM_KEY = "llm:$llm"
257
- }
258
-
259
- # ---------------------------------------------------------------------------
260
- # Per-call env-var overrides (set by the SSH watcher from queue JSON).
261
- # These win over audio-effects.cfg lookup results for this call only.
262
- # ---------------------------------------------------------------------------
263
- if ($env:AGENTVIBES_OVERRIDE_MUSIC) { $LlmBgTrack = $env:AGENTVIBES_OVERRIDE_MUSIC }
264
- if ($env:AGENTVIBES_OVERRIDE_VOLUME) { $LlmBgVolume = $env:AGENTVIBES_OVERRIDE_VOLUME }
265
- if ($env:AGENTVIBES_OVERRIDE_EFFECTS) { $LlmReverb = $env:AGENTVIBES_OVERRIDE_EFFECTS }
266
-
267
- # Prepend pretext if configured
268
- # Priority: LLM-specific pretext -> project .agentvibes/config.json -> project .claude/config/tts-pretext.txt
269
- # -> global ~/.agentvibes/config.json -> global ~/.claude/config/tts-pretext.txt
270
- #
271
- # Honor AGENTVIBES_NO_PRETEXT=1 for callers that already prepended a pretext
272
- # (e.g., the SSH receiver watcher — server already added its own pretext
273
- # before sending; double-prepending here would say "AgentVibes here, server-pretext, message").
274
- $Pretext = ""
275
- if ($env:AGENTVIBES_NO_PRETEXT -ne "1") {
276
- $Pretext = $LlmPretext
277
- }
278
- if (-not $Pretext -and $env:AGENTVIBES_NO_PRETEXT -ne "1") {
279
- $PretextSources = @(
280
- (Join-Path $ProjectRoot ".agentvibes\config.json"),
281
- "$ClaudeDir\config\tts-pretext.txt",
282
- "$env:USERPROFILE\.agentvibes\config.json",
283
- "$env:USERPROFILE\.claude\config\tts-pretext.txt"
284
- )
285
- foreach ($src in $PretextSources) {
286
- if (-not $Pretext -and (Test-Path $src)) {
287
- if ($src -match '\.json$') {
288
- try {
289
- $avCfg = Get-Content $src -Raw | ConvertFrom-Json
290
- if ($avCfg.pretext) { $Pretext = $avCfg.pretext.Trim() }
291
- } catch { }
292
- } else {
293
- $val = (Get-Content $src -Raw).Trim()
294
- if ($val) { $Pretext = $val }
295
- }
296
- }
297
- }
298
- }
299
- if ($Pretext) {
300
- $Text = "$Pretext, $Text"
301
- }
302
46
  # Determine active provider
303
- # LLM-specific engine overrides global provider
304
- $ActiveProvider = "sapi"
305
- if ($LlmEngine) {
306
- $ActiveProvider = $LlmEngine
307
- } elseif (Test-Path $ProviderFile) {
47
+ $ActiveProvider = "windows-sapi"
48
+ if (Test-Path $ProviderFile) {
308
49
  $ActiveProvider = (Get-Content $ProviderFile -Raw).Trim()
309
50
  }
310
51
 
@@ -312,18 +53,15 @@ if ($LlmEngine) {
312
53
  $ProviderScript = ""
313
54
 
314
55
  switch ($ActiveProvider) {
315
- { $_ -in "sapi", "windows-sapi" } {
316
- $ProviderScript = "$HooksDir\play-tts-sapi.ps1"
56
+ "windows-sapi" {
57
+ $ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1"
317
58
  }
318
- { $_ -in "piper", "windows-piper" } {
319
- $ProviderScript = "$HooksDir\play-tts-piper.ps1"
59
+ "windows-piper" {
60
+ $ProviderScript = "$HooksDir\play-tts-windows-piper.ps1"
320
61
  }
321
62
  "soprano" {
322
63
  $ProviderScript = "$HooksDir\play-tts-soprano.ps1"
323
64
  }
324
- "termux-ssh" {
325
- $ProviderScript = "$HooksDir\play-tts-termux-ssh.ps1"
326
- }
327
65
  default {
328
66
  Write-Host "[ERROR] Unknown provider: $ActiveProvider" -ForegroundColor Red
329
67
  Write-Host "Use: .\provider-manager.ps1 list" -ForegroundColor Yellow
@@ -338,168 +76,258 @@ if (-not (Test-Path $ProviderScript)) {
338
76
  }
339
77
 
340
78
  # Check if background music is enabled
341
- # Primary source of truth: .agentvibes/config.json (used by TUI console)
342
- # Fallback: .claude/config/background-music-enabled.txt (legacy PowerShell config)
343
79
  $ConfigDir = "$ClaudeDir\config"
344
80
  $BgEnabled = $false
345
- $AgentVibesConfig = Join-Path (Split-Path -Parent $ClaudeDir) ".agentvibes\config.json"
346
- if (Test-Path $AgentVibesConfig) {
347
- try {
348
- $json = Get-Content $AgentVibesConfig -Raw | ConvertFrom-Json
349
- if ($json.backgroundMusic -and $null -ne $json.backgroundMusic.enabled) {
350
- $BgEnabled = [bool]$json.backgroundMusic.enabled
351
- }
352
- } catch {
353
- $BgEnabled = $false
354
- }
355
- } else {
356
- # Fallback to legacy txt config
357
- $BgEnabledFile = "$ConfigDir\background-music-enabled.txt"
358
- if (Test-Path $BgEnabledFile) {
359
- $BgEnabled = (Get-Content $BgEnabledFile -Raw).Trim() -eq "true"
360
- }
361
- }
362
-
363
- # When a per-LLM row in audio-effects.cfg has a background track configured,
364
- # that's an implicit "bg music enabled for this LLM" — force it on regardless
365
- # of the global backgroundMusic.enabled flag. Without this, setting a per-LLM
366
- # track in the TUI's Configure modal would have no effect unless the user
367
- # ALSO toggled global bg music on.
368
- if ($LlmBgTrack) {
369
- $BgEnabled = $true
81
+ $BgEnabledFile = "$ConfigDir\background-music-enabled.txt"
82
+ if (Test-Path $BgEnabledFile) {
83
+ $BgEnabled = (Get-Content $BgEnabledFile -Raw).Trim() -eq "true"
370
84
  }
371
85
 
372
86
  # Check if reverb is enabled (allowlist validation)
373
- # LLM-specific reverb overrides global setting
374
87
  $ReverbLevel = "off"
375
- if ($LlmReverb -and $LlmReverb -in @("off", "light", "medium", "heavy", "cathedral")) {
376
- $ReverbLevel = $LlmReverb
377
- } else {
378
- $ReverbFile = "$ConfigDir\reverb-level.txt"
379
- if (Test-Path $ReverbFile) {
380
- $reverbVal = (Get-Content $ReverbFile -Raw).Trim()
381
- if ($reverbVal -in @("off", "light", "medium", "heavy", "cathedral")) {
382
- $ReverbLevel = $reverbVal
383
- }
88
+ $ReverbFile = "$ConfigDir\reverb-level.txt"
89
+ if (Test-Path $ReverbFile) {
90
+ $reverbVal = (Get-Content $ReverbFile -Raw).Trim()
91
+ if ($reverbVal -in @("off", "light", "medium", "heavy", "cathedral")) {
92
+ $ReverbLevel = $reverbVal
384
93
  }
385
94
  }
386
95
  $HasReverb = $ReverbLevel -ne "off"
387
96
 
388
97
  # Check ffmpeg availability for background music mixing or reverb
389
- # Refresh PATH from registry so newly-installed tools are found without shell restart
390
98
  $HasFfmpeg = $false
391
99
  if ($BgEnabled -or $HasReverb) {
392
100
  try {
393
101
  $null = Get-Command ffmpeg -ErrorAction Stop
394
102
  $HasFfmpeg = $true
395
- } catch {
396
- # PATH may be stale (common after winget install); refresh from registry
397
- $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
398
- try {
399
- $null = Get-Command ffmpeg -ErrorAction Stop
400
- $HasFfmpeg = $true
401
- } catch {}
103
+ } catch {}
104
+ }
105
+
106
+ # ===========================================================================
107
+ # Per-LLM Audio Routing
108
+ # ===========================================================================
109
+ # When mcp-server/server.py invokes play-tts.ps1 on Windows it passes the
110
+ # -llm flag with the active LLM identity (e.g. "claude-code", "copilot",
111
+ # "codex"). The router reads audio-effects.cfg and looks up the row whose
112
+ # key is `llm:<name>`, allowing each LLM to have its own voice, pretext,
113
+ # reverb, and engine without requiring global settings to be reconfigured.
114
+ #
115
+ # Expected audio-effects.cfg row format (pipe-delimited):
116
+ # llm:<name>|REVERB_PRESET|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT|ENGINE
117
+ #
118
+ # Column descriptions:
119
+ # 1. Key - Must start with "llm:" followed by the LLM name
120
+ # 2. REVERB_PRESET - One of: off, light, medium, heavy, cathedral (or blank)
121
+ # 3. BACKGROUND_FILE - Filename relative to .claude/audio/tracks/ (or blank)
122
+ # 4. BACKGROUND_VOLUME - Float 0.0-1.0 (or blank for default 0.25)
123
+ # 5. VOICE - Provider voice name to use (or blank for global default)
124
+ # 6. PRETEXT - Text prepended to all TTS utterances (or blank)
125
+ # 7. ENGINE - Windows engine: windows-sapi, windows-piper, soprano (or blank)
126
+ #
127
+ # Example rows:
128
+ # llm:claude-code|off|||en_US-amy-medium|Agent Vibes Here|windows-piper
129
+ # llm:copilot|light|||en_US-ryan-low||windows-sapi
130
+ # llm:codex|off||||Code complete|windows-piper
131
+ # llm:default|off|||||
132
+ #
133
+ # The "default" key is always looked up when no explicit -llm flag is
134
+ # provided. Configure it via Setup → Default → Configure in the TUI to
135
+ # apply consistent audio settings across all LLM sessions.
136
+ #
137
+ # Security: The -llm value is validated against an allowlist regex so that
138
+ # injected values like "-rf" or path-traversal strings are rejected before
139
+ # they can appear in lookup keys, environment variables, or file paths.
140
+
141
+ # --- Validate -llm parameter format ------------------------------------------
142
+ if ($llm -and $llm -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
143
+ Write-Host "[WARNING] play-tts.ps1: Invalid -llm value '$llm' ignored" `
144
+ "(must match ^[a-zA-Z0-9][a-zA-Z0-9_-]*`$)" -ForegroundColor Yellow
145
+ $llm = ""
146
+ }
147
+
148
+ # --- Default fallback --------------------------------------------------------
149
+ # An empty $llm routes through the "default" pseudo-LLM. Users who configure
150
+ # an `llm:default` row in audio-effects.cfg get consistent audio settings for
151
+ # every LLM that doesn't pass its own -llm flag — a convenient global override
152
+ # that doesn't require per-LLM configuration.
153
+ if (-not $llm) {
154
+ $llm = "default"
155
+ }
156
+
157
+ # --- Export LLM key for child scripts ----------------------------------------
158
+ # Provider scripts (play-tts-windows-*.ps1) and any other downstream tooling
159
+ # can inspect AGENTVIBES_LLM_KEY to identify which LLM is currently speaking.
160
+ # This mirrors the `export AGENTVIBES_LLM_KEY="llm:${LLM_PROVIDER}"` line in
161
+ # the POSIX play-tts.sh so the cross-platform contract is symmetric.
162
+ $env:AGENTVIBES_LLM_KEY = "llm:$llm"
163
+
164
+ # --- Lookup per-LLM config in audio-effects.cfg ------------------------------
165
+ # Scan project config first, then user-profile config. Stop at first match.
166
+ # Variables are intentionally prefixed with _ to distinguish LLM-local state
167
+ # from the global session state set earlier in this script.
168
+ $_LlmVoice = ""
169
+ $_LlmPretext = ""
170
+ $_LlmReverb = ""
171
+ $_LlmEngine = ""
172
+ $_LlmBgFile = ""
173
+ $_LlmBgVol = ""
174
+ $_LlmKey = "llm:$llm"
175
+
176
+ $_AudioEffectsCfgPaths = @(
177
+ (Join-Path $ClaudeDir "config\audio-effects.cfg"),
178
+ (Join-Path $env:USERPROFILE ".claude\config\audio-effects.cfg")
179
+ )
180
+
181
+ :llmCfgSearch foreach ($_cfgFile in $_AudioEffectsCfgPaths) {
182
+ if ((-not $_LlmVoice) -and (-not $_LlmPretext) -and (Test-Path $_cfgFile)) {
183
+ $cfgContent = Get-Content $_cfgFile -ErrorAction SilentlyContinue
184
+ if ($null -ne $cfgContent) {
185
+ foreach ($_cfgLine in $cfgContent) {
186
+ # Skip blank lines and comment / separator lines
187
+ $stripped = $_cfgLine.Trim()
188
+ if ($stripped.Length -eq 0 -or $stripped.StartsWith('#')) { continue }
189
+
190
+ # Split on pipe; expect at least the key column
191
+ $_cols = $_cfgLine -split '\|'
192
+ if ($_cols.Count -ge 1 -and $_cols[0].Trim() -eq $_LlmKey) {
193
+ # Unpack columns defensively — missing columns stay empty
194
+ if ($_cols.Count -ge 2) { $_LlmReverb = $_cols[1].Trim() }
195
+ if ($_cols.Count -ge 3) { $_LlmBgFile = $_cols[2].Trim() }
196
+ if ($_cols.Count -ge 4) { $_LlmBgVol = $_cols[3].Trim() }
197
+ if ($_cols.Count -ge 5) { $_LlmVoice = $_cols[4].Trim() }
198
+ if ($_cols.Count -ge 6) { $_LlmPretext = $_cols[5].Trim() }
199
+ if ($_cols.Count -ge 7) { $_LlmEngine = $_cols[6].Trim() }
200
+ break llmCfgSearch
201
+ }
202
+ }
203
+ }
402
204
  }
403
205
  }
404
206
 
405
- # Check for pre-synthesized WAV (party mode optimization -- synthesis done before mutex acquisition)
406
- $PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
407
- $UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
408
- (Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
207
+ # --- Voice priority order (highest wins) -------------------------------------
208
+ # 1. Explicit -VoiceOverride parameter (caller always wins)
209
+ # 2. LLM-specific voice from audio-effects.cfg llm:<key> row
210
+ # 3. BMAD agent voice from bmad-voice-map.json (resolved in provider scripts)
211
+ # 4. Global active voice from tts-provider.txt / active-voice.txt
409
212
 
410
- # If background music or reverb enabled and ffmpeg available, tell provider to skip playback
411
- if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
412
- $env:AGENTVIBES_NO_PLAY = "1"
213
+ # Apply LLM-specific voice only when no explicit -VoiceOverride was passed
214
+ if ($_LlmVoice -and -not $VoiceOverride) {
215
+ $VoiceOverride = $_LlmVoice
216
+ }
217
+
218
+ # --- Apply LLM-specific pretext ----------------------------------------------
219
+ # Prepend the configured pretext (e.g. "Agent Vibes Here") to the speech
220
+ # text. Guard against double-prefixing on re-entrant or looped calls by
221
+ # checking whether the text already starts with the pretext string.
222
+ if ($_LlmPretext -and -not $Text.StartsWith($_LlmPretext)) {
223
+ $Text = "$_LlmPretext, $Text"
413
224
  }
414
225
 
415
- # Call the provider script (skip if using pre-synthesized audio)
416
- # When post-processing (reverb/music), capture output preserving InformationRecord colors.
417
- # Otherwise call directly so Write-Host colors pass through to the terminal.
418
- $NeedsPostProcess = ($BgEnabled -or $HasReverb) -and $HasFfmpeg
419
- if ($UsePreSynth) {
420
- Write-Host "[SYNTH] Using pre-synthesized audio..." -ForegroundColor Cyan
421
- # If no post-processing needed, play the pre-synth file directly and exit
422
- if (-not $NeedsPostProcess) {
423
- try {
424
- Invoke-SerializedPlay -WavPath $PreSynthWav
425
- } catch {
426
- Write-Host "[WARNING] Pre-synth playback failed: $_" -ForegroundColor Yellow
226
+ # --- Reverb override from per-LLM config -------------------------------------
227
+ # If the llm:<key> row specifies a reverb preset, override the file-based
228
+ # $ReverbLevel that was set from reverb-level.txt earlier. The allowlist
229
+ # check is repeated here so a malformed config row can't inject arbitrary
230
+ # strings into the ffmpeg filter chain.
231
+ if ($_LlmReverb) {
232
+ $validReverbLevels = @("off", "light", "medium", "heavy", "cathedral")
233
+ if ($validReverbLevels -contains $_LlmReverb) {
234
+ $ReverbLevel = $_LlmReverb
235
+ $HasReverb = $ReverbLevel -ne "off"
236
+ # If the LLM config enables reverb and ffmpeg wasn't found yet, retry
237
+ if ($HasReverb -and -not $HasFfmpeg) {
238
+ try { $null = Get-Command ffmpeg -ErrorAction Stop; $HasFfmpeg = $true } catch {}
427
239
  }
428
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
429
- exit 0
430
240
  }
431
- } else {
432
- try {
433
- if ($NeedsPostProcess) {
434
- if ($VoiceOverride) {
435
- $providerOutput = & $ProviderScript $Text $VoiceOverride 6>&1 2>&1
436
- } else {
437
- $providerOutput = & $ProviderScript $Text 6>&1 2>&1
438
- }
439
- # Re-emit preserving colors from InformationRecords (Write-Host output)
440
- foreach ($item in $providerOutput) {
441
- if ($item -is [System.Management.Automation.InformationRecord]) {
442
- $msg = $item.MessageData
443
- if ($msg -is [System.Management.Automation.HostInformationMessage]) {
444
- Write-Host $msg.Message -ForegroundColor $msg.ForegroundColor -NoNewline:$msg.NoNewLine
445
- if (-not $msg.NoNewLine) { Write-Host }
446
- } else {
447
- Write-Host "$item"
448
- }
449
- } else {
450
- Write-Host "$item"
451
- }
452
- }
453
- # Parse the provider output for "[OK] Saved to: <path>" so we can
454
- # use the EXACT file the provider just wrote. This replaces the
455
- # old "pick most recent tts-XXXXXXXX.wav" heuristic which would
456
- # silently replay stale audio whenever synthesis failed.
457
- $FreshSynthFile = $null
458
- foreach ($item in $providerOutput) {
459
- $line = if ($item -is [System.Management.Automation.InformationRecord]) {
460
- $m = $item.MessageData
461
- if ($m -is [System.Management.Automation.HostInformationMessage]) { $m.Message } else { "$item" }
462
- } else { "$item" }
463
- if ($line -match '^\[OK\] Saved to:\s*(.+\.wav)\s*$') {
464
- $FreshSynthFile = $Matches[1].Trim()
465
- }
466
- }
467
- if (-not $FreshSynthFile -or -not (Test-Path $FreshSynthFile)) {
468
- [Console]::Error.WriteLine("[AgentVibes] ERROR: Provider synthesis did not produce an output file. NOT falling back to stale audio. Check provider logs above.")
469
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
470
- exit 3
471
- }
472
- } else {
473
- if ($VoiceOverride) {
474
- & $ProviderScript $Text $VoiceOverride
475
- } else {
476
- & $ProviderScript $Text
477
- }
241
+ }
242
+
243
+ # --- Apply LLM-specific engine override --------------------------------------
244
+ # Allowed local Windows engines: windows-sapi, windows-piper, soprano.
245
+ # Transport providers (ssh-remote etc.) are not listed because they forward
246
+ # TTS to a remote host — overriding with a local engine would synthesize on
247
+ # the wrong machine.
248
+ if ($_LlmEngine) {
249
+ $allowedLocalEngines = @("windows-sapi", "windows-piper", "soprano")
250
+ if ($allowedLocalEngines -contains $_LlmEngine) {
251
+ switch ($_LlmEngine) {
252
+ "windows-sapi" { $ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1" }
253
+ "windows-piper" { $ProviderScript = "$HooksDir\play-tts-windows-piper.ps1" }
254
+ "soprano" { $ProviderScript = "$HooksDir\play-tts-soprano.ps1" }
478
255
  }
256
+ } else {
257
+ Write-Host "[INFO] play-tts.ps1: Unrecognised engine '$_LlmEngine' in audio-effects.cfg — keeping default provider" -ForegroundColor DarkGray
479
258
  }
480
- catch {
481
- Write-Host "[ERROR] TTS Error: $_" -ForegroundColor Red
482
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
483
- exit 1
259
+ }
260
+
261
+ # --- BMAD Party Mode note ----------------------------------------------------
262
+ # When BMAD party mode is active, multiple agents speak in rapid succession.
263
+ # Each agent's voice is resolved from bmad-voice-map.json inside the provider
264
+ # scripts — that BMAD-level routing is independent of this per-LLM system.
265
+ # The -llm flag is still used to set AGENTVIBES_LLM_KEY and can supply a
266
+ # background music track and reverb preset that stays consistent throughout
267
+ # the entire party mode session regardless of which agent is speaking.
268
+
269
+ # --- Diagnostic output -------------------------------------------------------
270
+ # Set AGENTVIBES_VERBOSE=1 in the shell environment to print routing state.
271
+ if ($env:AGENTVIBES_VERBOSE -eq "1") {
272
+ Write-Host "[DEBUG] play-tts.ps1 LLM routing: llm=$llm | voice=$VoiceOverride | engine=$_LlmEngine | pretext=$_LlmPretext" -ForegroundColor DarkCyan
273
+ Write-Host "[DEBUG] play-tts.ps1 LLM routing: reverb=$ReverbLevel | HasFfmpeg=$HasFfmpeg | BgEnabled=$BgEnabled | script=$ProviderScript" -ForegroundColor DarkCyan
274
+ }
275
+
276
+ # ===========================================================================
277
+ # End of Per-LLM Audio Routing
278
+ # ===========================================================================
279
+
280
+ # Helper: play a WAV file preferring ffplay over SoundPlayer.
281
+ # SoundPlayer uses WinMM's low-quality resampler (22050 Hz → 48000 Hz is choppy);
282
+ # ffplay uses libswresample with sinc resampling — no artefacts.
283
+ function Invoke-AudioPlay {
284
+ param([string]$FilePath)
285
+ $fp = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
286
+ if (-not $fp) {
287
+ # Watcher sessions may inherit a minimal PATH — refresh from registry
288
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
289
+ [System.Environment]::GetEnvironmentVariable("Path","User")
290
+ $fp = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
484
291
  }
292
+ if ($fp) {
293
+ & $fp -autoexit -nodisp -loglevel quiet $FilePath 2>$null
294
+ } else {
295
+ $p = $null
296
+ try { $p = New-Object System.Media.SoundPlayer $FilePath; $p.PlaySync() }
297
+ finally { if ($p) { $p.Dispose() } }
298
+ }
299
+ }
300
+
301
+ # If background music or reverb enabled and ffmpeg available, tell provider to skip playback
302
+ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
303
+ $env:AGENTVIBES_NO_PLAY = "1"
304
+ }
305
+
306
+ # Call the provider script
307
+ try {
308
+ if ($VoiceOverride) {
309
+ $providerOutput = & $ProviderScript $Text $VoiceOverride 2>&1
310
+ }
311
+ else {
312
+ $providerOutput = & $ProviderScript $Text 2>&1
313
+ }
314
+ # Show provider output
315
+ $providerOutput | ForEach-Object { Write-Host $_ }
316
+ }
317
+ catch {
318
+ Write-Host "[ERROR] TTS Error: $_" -ForegroundColor Red
319
+ $env:AGENTVIBES_NO_PLAY = $null
320
+ exit 1
485
321
  }
486
322
 
487
323
  # Apply reverb and/or mix with background music
488
324
  if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
489
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
325
+ $env:AGENTVIBES_NO_PLAY = $null
490
326
 
491
- # Use the EXACT file the provider script just wrote (captured from its
492
- # "[OK] Saved to: <path>" output line above). The old "pick most recent
493
- # tts-XXXXXXXX.wav" heuristic silently replayed stale audio whenever
494
- # synthesis failed — there is no safe way to guess which file is fresh.
327
+ # Find the most recent TTS wav file
495
328
  $AudioDir = "$ClaudeDir\audio"
496
- $RecentWav = if ($UsePreSynth) {
497
- Get-Item $PreSynthWav -ErrorAction SilentlyContinue
498
- } elseif ($FreshSynthFile -and (Test-Path $FreshSynthFile)) {
499
- Get-Item $FreshSynthFile -ErrorAction SilentlyContinue
500
- } else {
501
- $null
502
- }
329
+ $RecentWav = Get-ChildItem -Path $AudioDir -Filter "tts-*.wav" -ErrorAction SilentlyContinue |
330
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1
503
331
 
504
332
  if ($RecentWav -and $RecentWav.Length -gt 0) {
505
333
  $voicePath = $RecentWav.FullName
@@ -514,12 +342,9 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
514
342
  default { "" }
515
343
  }
516
344
  if ($reverbFilter) {
517
- # Use a fixed name OUTSIDE the `tts-XXXXXXXX` random-name
518
- # namespace so the "pick most recent tts-*.wav" logic can't
519
- # accidentally pick this post-processed file as a synth input.
520
- $reverbedFile = "$AudioDir\av-reverbed-scratch.wav"
345
+ $reverbedFile = "$AudioDir\tts-reverbed.wav"
521
346
  $reverbArgs = "-y -i `"$voicePath`" -af `"$reverbFilter`" `"$reverbedFile`""
522
- $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
347
+ $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
523
348
  if ($proc.ExitCode -eq 0 -and (Test-Path $reverbedFile)) {
524
349
  $voicePath = $reverbedFile
525
350
  }
@@ -528,101 +353,40 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
528
353
 
529
354
  # Mix with background music if enabled
530
355
  if ($BgEnabled) {
531
- # Read background track and volume from audio-effects.cfg (matches Linux behavior)
356
+ # Get background track - default to bachata, or read from config
532
357
  $TracksDir = "$ClaudeDir\audio\tracks"
533
- $DefaultTrack = ""
534
- $BgVolume = "0.25"
535
- $AudioEffectsCfg = "$ConfigDir\audio-effects.cfg"
536
-
537
- if (Test-Path $AudioEffectsCfg) {
538
- # Try agent-specific config first, then fall back to default
539
- # Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
540
- # Lookup order: agent name -> llm:<name> -> default
541
- $agentName = $env:AGENTVIBES_AGENT_NAME
542
- $configLine = $null
543
-
544
- $cfgLines = Get-Content $AudioEffectsCfg
545
- if ($agentName) {
546
- foreach ($line in $cfgLines) {
547
- if ($line -match "^$([regex]::Escape($agentName))\|") {
548
- $configLine = $line
549
- break
550
- }
551
- }
552
- }
553
- # Try LLM-specific config (--llm parameter)
554
- if (-not $configLine -and $llm) {
555
- $llmBgKey = "llm:$llm"
556
- foreach ($line in $cfgLines) {
557
- if ($line -match "^$([regex]::Escape($llmBgKey))\|") {
558
- $configLine = $line
559
- break
560
- }
561
- }
562
- }
563
- # Fall back to default
564
- if (-not $configLine) {
565
- foreach ($line in $cfgLines) {
566
- if ($line -match '^default\|') {
567
- $configLine = $line
568
- break
569
- }
570
- }
571
- }
572
-
573
- if ($configLine) {
574
- $parts = $configLine -split '\|'
575
- if ($parts.Length -ge 3 -and $parts[2]) {
576
- $trackName = $parts[2].Trim()
577
- # Validate: filename only, no path separators or traversal
578
- if ($trackName -match '^[a-zA-Z0-9_\-\. ]+$') {
579
- $DefaultTrack = $trackName
580
- }
581
- }
582
- if ($parts.Length -ge 4 -and $parts[3]) {
583
- $volVal = $parts[3].Trim()
584
- if ($volVal -match '^\d+\.?\d*$') { $BgVolume = $volVal }
585
- }
358
+ $DefaultTrack = "agent_vibes_bachata_v1_loop.mp3"
359
+ $DefaultTrackFile = "$ConfigDir\background-music-default.txt"
360
+ if (Test-Path $DefaultTrackFile) {
361
+ $configTrack = (Get-Content $DefaultTrackFile -Raw).Trim()
362
+ # Validate: filename only, no path separators or traversal
363
+ if ($configTrack -and $configTrack -match '^[a-zA-Z0-9_\-\.]+$') {
364
+ $DefaultTrack = $configTrack
586
365
  }
587
366
  }
588
-
589
- # Fallback if no track found in config
590
- if (-not $DefaultTrack) {
591
- $DefaultTrack = "agent_vibes_celtic_harp_v1_loop.mp3"
592
- }
593
-
594
- # Per-call env-var overrides (set by SSH watcher from queue JSON).
595
- # Win over audio-effects.cfg lookup above. Validate filename to
596
- # prevent path traversal before accepting.
597
- if ($env:AGENTVIBES_OVERRIDE_MUSIC -and $env:AGENTVIBES_OVERRIDE_MUSIC -match '^[a-zA-Z0-9_\-\. ]+$') {
598
- $DefaultTrack = $env:AGENTVIBES_OVERRIDE_MUSIC
599
- }
600
- if ($env:AGENTVIBES_OVERRIDE_VOLUME -and $env:AGENTVIBES_OVERRIDE_VOLUME -match '^\d+\.?\d*$') {
601
- $BgVolume = $env:AGENTVIBES_OVERRIDE_VOLUME
602
- }
603
-
604
367
  $BgTrackPath = Join-Path $TracksDir $DefaultTrack
605
368
  # Path containment: verify resolved path stays within tracks directory
606
369
  $ResolvedBgTrack = [System.IO.Path]::GetFullPath($BgTrackPath)
607
370
  $ResolvedTracksDir = [System.IO.Path]::GetFullPath($TracksDir)
608
371
  if (-not $ResolvedBgTrack.StartsWith($ResolvedTracksDir + [System.IO.Path]::DirectorySeparatorChar)) {
609
- $BgTrackPath = Join-Path $TracksDir "agent_vibes_celtic_harp_v1_loop.mp3"
372
+ $BgTrackPath = Join-Path $TracksDir "agent_vibes_bachata_v1_loop.mp3"
373
+ }
374
+
375
+ # Get volume (default 0.25)
376
+ $BgVolume = "0.25"
377
+ $VolumeFile = "$ConfigDir\background-music-volume.txt"
378
+ if (Test-Path $VolumeFile) {
379
+ $vol = (Get-Content $VolumeFile -Raw).Trim()
380
+ if ($vol -match '^\d+\.?\d*$') { $BgVolume = $vol }
610
381
  }
611
382
 
612
383
  if (Test-Path $BgTrackPath) {
613
- # Mixed output goes to a fixed name OUTSIDE the tts-XXXXXXXX
614
- # random-name namespace so the "pick most recent tts-*.wav"
615
- # logic can't accidentally pick this as a synth input in the
616
- # next invocation. (Previously we'd name this as
617
- # "$voicePath-mixed.wav" which generated files like
618
- # tts-xxx.wav.effected-mixed.wav that kept re-matching and
619
- # compounding on every run.)
620
- $MixedFile = "$AudioDir\av-mixed-scratch.wav"
384
+ $MixedFile = $RecentWav.FullName -replace '\.wav$', '-mixed.wav'
621
385
 
622
386
  try {
623
387
  # Get voice duration to calculate total length
624
388
  $probArgs = "-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 `"$voicePath`""
625
- $durationProc = Start-Process -FilePath "ffprobe" -ArgumentList $probArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt" -RedirectStandardOutput "$env:TEMP\agentvibes-duration.txt"
389
+ $durationProc = Start-Process -FilePath "ffprobe" -ArgumentList $probArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL" -RedirectStandardOutput "$env:TEMP\agentvibes-duration.txt"
626
390
  $voiceDuration = 5 # default fallback
627
391
  if (Test-Path "$env:TEMP\agentvibes-duration.txt") {
628
392
  $durStr = (Get-Content "$env:TEMP\agentvibes-duration.txt" -Raw).Trim()
@@ -633,43 +397,37 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
633
397
  $fadeOutStart = $totalDuration - 2
634
398
 
635
399
  # Filter: music fades in 0.5s, voice delayed 2s, music fades out last 2s
636
- $filter = "[0:a]volume=${BgVolume},afade=t=in:d=0.5,afade=t=out:st=${fadeOutStart}:d=2[bg];[1:a]adelay=1000|1000,volume=1.5,apad=pad_dur=2[voice];[bg][voice]amix=inputs=2:duration=longest:dropout_transition=2:normalize=0[out]"
400
+ $filter = "[0:a]volume=${BgVolume},afade=t=in:d=0.5,afade=t=out:st=${fadeOutStart}:d=2[bg];[1:a]adelay=2000|2000,apad=pad_dur=2[voice];[bg][voice]amix=inputs=2:duration=longest:dropout_transition=2[out]"
637
401
 
638
402
  # Run ffmpeg - use Start-Process to avoid stderr issues with $ErrorActionPreference
639
403
  $ffmpegArgs = "-y -stream_loop -1 -i `"$BgTrackPath`" -i `"$voicePath`" -filter_complex `"$filter`" -map `"[out]`" -t $totalDuration `"$MixedFile`""
640
- $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
404
+ $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
641
405
 
642
406
  if ($proc.ExitCode -eq 0 -and (Test-Path $MixedFile) -and (Get-Item $MixedFile).Length -gt 0) {
643
- # Play the mixed audio (via serialized mutex)
407
+ # Play the mixed audio
644
408
  try {
645
- Invoke-SerializedPlay -WavPath $MixedFile
409
+ Invoke-AudioPlay $MixedFile
646
410
  } catch {
647
411
  Write-Host "[WARNING] Mixed playback failed, playing voice only" -ForegroundColor Yellow
648
- Invoke-SerializedPlay -WavPath $voicePath
412
+ Invoke-AudioPlay $voicePath
649
413
  }
650
414
  } else {
651
415
  # Mixing failed, play voice only
652
- Invoke-SerializedPlay -WavPath $voicePath
416
+ Invoke-AudioPlay $voicePath
653
417
  }
654
418
  } catch {
655
419
  # ffmpeg failed, play voice only
656
- Invoke-SerializedPlay -WavPath $voicePath
420
+ Invoke-AudioPlay $voicePath
657
421
  }
658
422
  } else {
659
423
  # No background track found, play voice only
660
- Invoke-SerializedPlay -WavPath $voicePath
424
+ Invoke-AudioPlay $voicePath
661
425
  }
662
426
  } else {
663
427
  # No background music, play the (possibly reverbed) voice
664
- Invoke-SerializedPlay -WavPath $voicePath
428
+ Invoke-AudioPlay $voicePath
665
429
  }
666
430
  }
667
431
  } else {
668
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
432
+ $env:AGENTVIBES_NO_PLAY = $null
669
433
  }
670
-
671
- # Explicit exit 0 so that $LASTEXITCODE from native commands (piper.exe,
672
- # ffmpeg, sox, etc.) doesn't leak through as the process exit code.
673
- # Without this, bash/Claude Code sees whatever random exit code the last
674
- # native command returned (e.g. 127) and treats it as a TTS failure.
675
- exit 0