agentvibes 5.2.1 → 5.4.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 +4 -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-speak-enhanced.sh +0 -0
  88. package/.claude/hooks/bmad-speak.sh +6 -13
  89. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  90. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  91. package/.claude/hooks/clawdbot-receiver-SECURE.sh +25 -23
  92. package/.claude/hooks/clawdbot-receiver.sh +4 -28
  93. package/.claude/hooks/clean-audio-cache.sh +0 -0
  94. package/.claude/hooks/cleanup-cache.sh +0 -0
  95. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  96. package/.claude/hooks/download-extra-voices.sh +0 -0
  97. package/.claude/hooks/effects-manager.sh +0 -0
  98. package/.claude/hooks/github-star-reminder.sh +0 -0
  99. package/.claude/hooks/language-manager.sh +0 -0
  100. package/.claude/hooks/learn-manager.sh +0 -0
  101. package/.claude/hooks/macos-voice-manager.sh +0 -0
  102. package/.claude/hooks/migrate-background-music.sh +0 -0
  103. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  104. package/.claude/hooks/optimize-background-music.sh +0 -0
  105. package/.claude/hooks/personality-manager.sh +0 -0
  106. package/.claude/hooks/piper-download-voices.sh +0 -0
  107. package/.claude/hooks/piper-installer.sh +1 -1
  108. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  109. package/.claude/hooks/piper-voice-manager.sh +0 -0
  110. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  111. package/.claude/hooks/play-tts-macos.sh +6 -12
  112. package/.claude/hooks/play-tts-piper.sh +50 -79
  113. package/.claude/hooks/play-tts-soprano.sh +9 -43
  114. package/.claude/hooks/play-tts-ssh-remote.sh +42 -120
  115. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  116. package/.claude/hooks/play-tts.sh +48 -37
  117. package/.claude/hooks/post-response.sh +41 -0
  118. package/.claude/hooks/prepare-release.sh +0 -0
  119. package/.claude/hooks/provider-commands.sh +0 -0
  120. package/.claude/hooks/provider-manager.sh +0 -0
  121. package/.claude/hooks/replay-target-audio.sh +0 -0
  122. package/.claude/hooks/requirements.txt +6 -6
  123. package/.claude/hooks/sentiment-manager.sh +0 -0
  124. package/.claude/hooks/session-start-tts.sh +56 -39
  125. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  126. package/.claude/hooks/speed-manager.sh +0 -0
  127. package/.claude/hooks/stop.sh +63 -0
  128. package/.claude/hooks/termux-installer.sh +0 -0
  129. package/.claude/hooks/translate-manager.sh +0 -0
  130. package/.claude/hooks/translator.py +237 -237
  131. package/.claude/hooks/tts-queue-worker.sh +0 -0
  132. package/.claude/hooks/tts-queue.sh +0 -0
  133. package/.claude/hooks/verbosity-manager.sh +0 -0
  134. package/.claude/hooks/voice-manager.sh +26 -4
  135. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  136. package/.claude/hooks-windows/bmad-party-speak.ps1 +278 -274
  137. package/.claude/hooks-windows/bmad-speak.ps1 +264 -264
  138. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -53
  139. package/.claude/hooks-windows/effects-manager.ps1 +294 -294
  140. package/.claude/hooks-windows/language-manager.ps1 +193 -193
  141. package/.claude/hooks-windows/learn-manager.ps1 +241 -241
  142. package/.claude/hooks-windows/personality-manager.ps1 +266 -266
  143. package/.claude/hooks-windows/play-tts-soprano.ps1 +5 -5
  144. package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -138
  145. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +164 -0
  146. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -0
  147. package/.claude/hooks-windows/play-tts.ps1 +104 -481
  148. package/.claude/hooks-windows/provider-manager.ps1 +158 -192
  149. package/.claude/hooks-windows/session-start-tts.ps1 +55 -46
  150. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  151. package/.claude/hooks-windows/speed-manager.ps1 +166 -166
  152. package/.claude/hooks-windows/voice-manager-windows.ps1 +176 -260
  153. package/.claude/output-styles/agent-vibes.md +202 -202
  154. package/.claude/personalities/angry.md +14 -14
  155. package/.claude/personalities/annoying.md +14 -14
  156. package/.claude/personalities/crass.md +14 -14
  157. package/.claude/personalities/dramatic.md +14 -14
  158. package/.claude/personalities/dry-humor.md +50 -50
  159. package/.claude/personalities/flirty.md +20 -20
  160. package/.claude/personalities/funny.md +14 -14
  161. package/.claude/personalities/grandpa.md +32 -32
  162. package/.claude/personalities/millennial.md +14 -14
  163. package/.claude/personalities/moody.md +14 -14
  164. package/.claude/personalities/normal.md +16 -16
  165. package/.claude/personalities/pirate.md +14 -14
  166. package/.claude/personalities/poetic.md +14 -14
  167. package/.claude/personalities/professional.md +14 -14
  168. package/.claude/personalities/rapper.md +55 -55
  169. package/.claude/personalities/robot.md +14 -14
  170. package/.claude/personalities/sarcastic.md +38 -38
  171. package/.claude/personalities/sassy.md +14 -14
  172. package/.claude/personalities/surfer-dude.md +14 -14
  173. package/.claude/personalities/zen.md +14 -14
  174. package/.claude/piper-voices-dir.txt +1 -0
  175. package/.claude/settings.json +25 -15
  176. package/.claude/verbosity.txt +1 -1
  177. package/.clawdbot/README.md +105 -105
  178. package/.clawdbot/skill/SKILL.md +149 -145
  179. package/.mcp.json +30 -11
  180. package/CLAUDE.md +170 -215
  181. package/README.md +206 -515
  182. package/RELEASE_NOTES.md +1132 -1884
  183. package/WINDOWS-SETUP.md +208 -208
  184. package/bin/agent-vibes +0 -0
  185. package/bin/agentvibes-voice-browser.js +64 -1289
  186. package/bin/agentvibes.js +0 -0
  187. package/bin/ensure-soprano-running.sh +43 -0
  188. package/bin/mcp-server.js +121 -121
  189. package/bin/mcp-server.sh +0 -0
  190. package/bin/test-bmad-pr +78 -78
  191. package/mcp-server/QUICK_START.md +203 -203
  192. package/mcp-server/README.md +345 -345
  193. package/mcp-server/WINDOWS_SETUP.md +260 -260
  194. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  195. package/mcp-server/examples/claude_desktop_config.json +11 -11
  196. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  197. package/mcp-server/examples/custom_instructions.md +169 -169
  198. package/mcp-server/install-deps.js +130 -130
  199. package/mcp-server/pyproject.toml +52 -52
  200. package/mcp-server/requirements.txt +2 -2
  201. package/mcp-server/server.py +1451 -1578
  202. package/mcp-server/test_server.py +395 -395
  203. package/package.json +1 -3
  204. package/setup-windows.ps1 +815 -815
  205. package/src/console/tabs/setup-tab.js +9 -6
  206. package/src/console/tabs/voices-tab.js +9 -3
  207. package/src/installer.js +42 -5
  208. package/src/services/llm-provider-service.js +13 -0
  209. package/templates/agentvibes-receiver.sh +158 -483
  210. package/templates/audio/welcome-music.mp3 +0 -0
  211. package/.agentvibes/bmad-voice-map.json +0 -104
  212. package/.agentvibes/copilot-sessions.log +0 -4
  213. package/.claude/config/audio-effects-bmad.cfg +0 -50
  214. package/.claude/config/background-music-enabled.txt +0 -1
  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
@@ -10,162 +10,19 @@ param(
10
10
  [string]$Text,
11
11
 
12
12
  [Parameter(Mandatory = $false, Position = 1)]
13
- [string]$VoiceOverride,
14
-
15
- [Parameter(Mandatory = $false)]
16
- [string]$llm = ""
13
+ [string]$VoiceOverride
17
14
  )
18
15
 
19
- # Security: Validate LLM provider name (alphanumeric, hyphens, underscores
20
- # only) -- mirrors play-tts.sh line 92. This prevents weird values from
21
- # poisoning the audio-effects.cfg lookup or the AGENTVIBES_LLM_KEY env var
22
- # we export to child scripts. An invalid value is treated as unset rather
23
- # than aborting, so the script falls back to the default config and the
24
- # rest of TTS still works.
25
- if ($llm -and $llm -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
26
- 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_-]*$')
27
- $llm = ""
28
- }
29
-
30
- # When no -llm is supplied, route through the "default" pseudo-LLM so the
31
- # user-managed `llm:default` row in audio-effects.cfg becomes the global
32
- # fallback for voice / pretext / music / effects. This is configured via
33
- # Setup -> Default -> Configure in the TUI. If `llm:default` doesn't exist,
34
- # the lookup will return empty and the script falls through to the
35
- # legacy global config chain (project / user .agentvibes/config.json).
36
- if (-not $llm) {
37
- $llm = "default"
38
- }
39
-
40
- # --- Cross-process playback serialization ---
41
- # Without this, any two callers of play-tts.ps1 (Claude Code PostToolUse hook,
42
- # Codex MCP text_to_speech, Copilot MCP text_to_speech, direct CLI) race each
43
- # other and produce overlapping / interleaved audio. Party mode already has
44
- # its own mutex (AgentVibesPartyModeTTSQueue) at the bmad-party-speak.ps1
45
- # level, but MCP-initiated calls bypass it entirely.
46
- #
47
- # We use a DIFFERENT mutex name ("AgentVibesPlaybackLock") so there's no
48
- # deadlock risk with the party-mode mutex -- they can be held independently
49
- # by nested processes.
50
- #
51
- # The mutex is acquired immediately before PlaySync() and released right
52
- # after, so CPU-bound synthesis/ffmpeg work can overlap with another
53
- # process's playback.
54
- $_PlaybackMutex = New-Object System.Threading.Mutex($false, "AgentVibesPlaybackLock")
55
-
56
- # --- Script-level watchdog ---
57
- # If anything in this script hangs (SoundPlayer deadlock, audio device
58
- # locked, ffmpeg stuck, etc.), a sibling PowerShell job waits 25 seconds
59
- # and force-kills this process. Without this, a stuck play-tts.ps1 holds
60
- # the playback mutex forever and silently blocks every subsequent TTS
61
- # call across all LLMs. The watchdog guarantees forward progress.
62
- #
63
- # 25s is chosen to be LONGER than the mutex timeout (15s) but SHORT
64
- # enough that a stuck process clears before the user's next turn. If
65
- # you fire two calls per turn and the first is stuck, the watchdog kills
66
- # it before the second turn arrives so the audio subsystem recovers
67
- # without manual intervention. Long legitimate messages (>25s of speech)
68
- # are rare at default verbosity levels; when they do occur the watchdog
69
- # kills playback mid-sentence, which is acceptable degradation vs. a
70
- # deadlocked queue.
71
- $_WatchdogJob = $null
72
- try {
73
- $_WatchdogJob = Start-Job -ArgumentList $PID -ScriptBlock {
74
- param($parentPid)
75
- Start-Sleep -Seconds 25
76
- try {
77
- # Only kill if still alive -- harmless if already exited
78
- $p = Get-Process -Id $parentPid -ErrorAction SilentlyContinue
79
- if ($p) {
80
- [Console]::Error.WriteLine("[AgentVibes] play-tts.ps1 watchdog fired -- force-killing pid $parentPid after 25s")
81
- Stop-Process -Id $parentPid -Force -ErrorAction SilentlyContinue
82
- }
83
- } catch { }
84
- }
85
- } catch {
86
- # If Start-Job fails (rare), just continue without the watchdog -- no
87
- # regression from pre-watchdog behavior.
88
- $_WatchdogJob = $null
89
- }
90
-
91
- function Invoke-SerializedPlay {
92
- param([Parameter(Mandatory)][string]$WavPath)
93
- $acquired = $false
94
- try {
95
- try {
96
- # 15s timeout to acquire the playback mutex. If we can't get
97
- # it in 15s, the holder is almost certainly a stuck/crashed
98
- # prior run. AbandonedMutexException means the holder's
99
- # process actually died -- we inherit ownership.
100
- $acquired = $_PlaybackMutex.WaitOne(15000)
101
- } catch [System.Threading.AbandonedMutexException] {
102
- $acquired = $true
103
- }
104
- if (-not $acquired) {
105
- # Self-heal: kill any stuck play-tts.ps1 processes (other than
106
- # ourselves) that have been alive longer than 20 seconds. This
107
- # frees the mutex so the NEXT call can succeed without the user
108
- # running taskkill manually. We still exit with code 2 because
109
- # this call's audio is lost, but the queue recovers immediately.
110
- try {
111
- $myPid = $PID
112
- $cutoff = (Get-Date).AddSeconds(-20)
113
- $stuck = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
114
- Where-Object {
115
- $_.Name -eq 'powershell.exe' -and
116
- $_.ProcessId -ne $myPid -and
117
- $_.CommandLine -like '*play-tts.ps1*' -and
118
- $_.CreationDate -lt $cutoff
119
- }
120
- foreach ($p in $stuck) {
121
- [Console]::Error.WriteLine("[AgentVibes] Self-heal: killing stuck play-tts.ps1 pid $($p.ProcessId) (alive since $($p.CreationDate))")
122
- Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
123
- }
124
- } catch { }
125
- [Console]::Error.WriteLine("[AgentVibes] ERROR: play-tts.ps1 could not acquire playback mutex within 15s. A prior play-tts.ps1 process was stuck holding it and has been killed; the next TTS call should succeed.")
126
- exit 2
127
- }
128
- $player = $null
129
- try {
130
- $player = New-Object System.Media.SoundPlayer $WavPath
131
- $player.PlaySync()
132
- } finally {
133
- if ($player) { $player.Dispose() }
134
- }
135
- } finally {
136
- if ($acquired) {
137
- try { $_PlaybackMutex.ReleaseMutex() } catch { }
138
- }
139
- }
140
- }
141
-
142
- # Register an exit handler that stops the watchdog job on normal exit so
143
- # it doesn't fire on successful short runs.
144
- Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
145
- try {
146
- if ($_WatchdogJob) {
147
- Stop-Job -Job $_WatchdogJob -ErrorAction SilentlyContinue
148
- Remove-Job -Job $_WatchdogJob -Force -ErrorAction SilentlyContinue
149
- }
150
- } catch { }
151
- } | Out-Null
152
-
153
16
  # Configuration paths
154
- # Priority: CLAUDE_PROJECT_DIR env var -> script's parent project -> user profile
155
- # Local project settings ALWAYS override global (~/.claude)
17
+ # First check if we're running from a project directory with .claude
156
18
  $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
19
+ $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
157
20
 
158
- if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
159
- $ClaudeDir = "$env:CLAUDE_PROJECT_DIR\.claude"
21
+ # Use project .claude if running from there, otherwise use user profile
22
+ if (Test-Path $ProjectClaudeDir) {
23
+ $ClaudeDir = $ProjectClaudeDir
160
24
  } else {
161
- $PackageClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
162
- if (Test-Path $PackageClaudeDir) {
163
- $ClaudeDir = $PackageClaudeDir
164
- } elseif (Test-Path "$env:USERPROFILE\.claude\tts-provider.txt") {
165
- $ClaudeDir = "$env:USERPROFILE\.claude"
166
- } else {
167
- $ClaudeDir = "$env:USERPROFILE\.claude"
168
- }
25
+ $ClaudeDir = "$env:USERPROFILE\.claude"
169
26
  }
170
27
 
171
28
  $HooksDir = "$ClaudeDir\hooks-windows"
@@ -180,109 +37,9 @@ if (Test-Path $MuteFile) {
180
37
  }
181
38
  }
182
39
 
183
- # Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
184
- # Format: llm:<name>|REVERB|BG_FILE|BG_VOLUME|VOICE|PRETEXT|ENGINE
185
- $LlmVoice = ""
186
- $LlmPretext = ""
187
- $LlmReverb = ""
188
- $LlmEngine = ""
189
- $LlmBgTrack = ""
190
- $LlmBgVolume = ""
191
- $ProjectRoot = Split-Path -Parent $ClaudeDir
192
- $ConfigDir = "$ClaudeDir\config"
193
-
194
- if ($llm) {
195
- $llmKey = "llm:$llm"
196
- $llmKeyPattern = '^' + [regex]::Escape($llmKey) + '\|'
197
- # Check project-local audio-effects.cfg first, then global
198
- $cfgPaths = @(
199
- "$ConfigDir\audio-effects.cfg",
200
- "$env:USERPROFILE\.claude\config\audio-effects.cfg"
201
- )
202
- foreach ($cfgPath in $cfgPaths) {
203
- if (-not $LlmVoice -and -not $LlmPretext -and (Test-Path $cfgPath)) {
204
- foreach ($line in (Get-Content $cfgPath)) {
205
- if ($line -match $llmKeyPattern) {
206
- $parts = $line -split '\|'
207
- # parts: [0]=key [1]=reverb [2]=bg_file [3]=bg_vol [4]=voice [5]=pretext [6]=engine
208
- if ($parts.Length -ge 2 -and $parts[1].Trim()) {
209
- $LlmReverb = $parts[1].Trim()
210
- }
211
- if ($parts.Length -ge 3 -and $parts[2].Trim()) {
212
- $LlmBgTrack = $parts[2].Trim()
213
- }
214
- if ($parts.Length -ge 4 -and $parts[3].Trim()) {
215
- $LlmBgVolume = $parts[3].Trim()
216
- }
217
- if ($parts.Length -ge 5 -and $parts[4].Trim()) {
218
- $LlmVoice = $parts[4].Trim()
219
- }
220
- if ($parts.Length -ge 6 -and $parts[5].Trim()) {
221
- $LlmPretext = $parts[5].Trim()
222
- }
223
- if ($parts.Length -ge 7 -and $parts[6].Trim()) {
224
- $LlmEngine = $parts[6].Trim()
225
- }
226
- break
227
- }
228
- }
229
- }
230
- }
231
- # LLM per-LLM voice routing.
232
- #
233
- # PRIORITY CHANGE: when -llm is passed AND the llm row has a voice,
234
- # the per-LLM voice always wins — even over an explicit VoiceOverride
235
- # parameter passed by the MCP caller. Rationale: Codex / Copilot /
236
- # Claude Code all call `get_config` at session start and then echo
237
- # the global voice back on every `text_to_speech` call. With the
238
- # old "explicit wins" priority, that global voice overrode our
239
- # per-LLM routing and broke the entire point of having llm:<key>
240
- # rows in audio-effects.cfg.
241
- #
242
- # To request a specific voice for a specific call that bypasses the
243
- # LLM routing, the caller should NOT pass -llm, or should use the
244
- # `llm:default` row (which has no voice column to override).
245
- if ($LlmVoice) {
246
- $VoiceOverride = $LlmVoice
247
- }
248
- # Export LLM key for child scripts (process-local, not system-wide)
249
- $env:AGENTVIBES_LLM_KEY = "llm:$llm"
250
- }
251
-
252
- # Prepend pretext if configured
253
- # Priority: LLM-specific pretext -> project .agentvibes/config.json -> project .claude/config/tts-pretext.txt
254
- # -> global ~/.agentvibes/config.json -> global ~/.claude/config/tts-pretext.txt
255
- $Pretext = $LlmPretext
256
- if (-not $Pretext) {
257
- $PretextSources = @(
258
- (Join-Path $ProjectRoot ".agentvibes\config.json"),
259
- "$ClaudeDir\config\tts-pretext.txt",
260
- "$env:USERPROFILE\.agentvibes\config.json",
261
- "$env:USERPROFILE\.claude\config\tts-pretext.txt"
262
- )
263
- foreach ($src in $PretextSources) {
264
- if (-not $Pretext -and (Test-Path $src)) {
265
- if ($src -match '\.json$') {
266
- try {
267
- $avCfg = Get-Content $src -Raw | ConvertFrom-Json
268
- if ($avCfg.pretext) { $Pretext = $avCfg.pretext.Trim() }
269
- } catch { }
270
- } else {
271
- $val = (Get-Content $src -Raw).Trim()
272
- if ($val) { $Pretext = $val }
273
- }
274
- }
275
- }
276
- }
277
- if ($Pretext) {
278
- $Text = "$Pretext, $Text"
279
- }
280
40
  # Determine active provider
281
- # LLM-specific engine overrides global provider
282
- $ActiveProvider = "sapi"
283
- if ($LlmEngine) {
284
- $ActiveProvider = $LlmEngine
285
- } elseif (Test-Path $ProviderFile) {
41
+ $ActiveProvider = "windows-sapi"
42
+ if (Test-Path $ProviderFile) {
286
43
  $ActiveProvider = (Get-Content $ProviderFile -Raw).Trim()
287
44
  }
288
45
 
@@ -290,18 +47,15 @@ if ($LlmEngine) {
290
47
  $ProviderScript = ""
291
48
 
292
49
  switch ($ActiveProvider) {
293
- { $_ -in "sapi", "windows-sapi" } {
294
- $ProviderScript = "$HooksDir\play-tts-sapi.ps1"
50
+ "windows-sapi" {
51
+ $ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1"
295
52
  }
296
- { $_ -in "piper", "windows-piper" } {
297
- $ProviderScript = "$HooksDir\play-tts-piper.ps1"
53
+ "windows-piper" {
54
+ $ProviderScript = "$HooksDir\play-tts-windows-piper.ps1"
298
55
  }
299
56
  "soprano" {
300
57
  $ProviderScript = "$HooksDir\play-tts-soprano.ps1"
301
58
  }
302
- "termux-ssh" {
303
- $ProviderScript = "$HooksDir\play-tts-termux-ssh.ps1"
304
- }
305
59
  default {
306
60
  Write-Host "[ERROR] Unknown provider: $ActiveProvider" -ForegroundColor Red
307
61
  Write-Host "Use: .\provider-manager.ps1 list" -ForegroundColor Yellow
@@ -316,168 +70,63 @@ if (-not (Test-Path $ProviderScript)) {
316
70
  }
317
71
 
318
72
  # Check if background music is enabled
319
- # Primary source of truth: .agentvibes/config.json (used by TUI console)
320
- # Fallback: .claude/config/background-music-enabled.txt (legacy PowerShell config)
321
73
  $ConfigDir = "$ClaudeDir\config"
322
74
  $BgEnabled = $false
323
- $AgentVibesConfig = Join-Path (Split-Path -Parent $ClaudeDir) ".agentvibes\config.json"
324
- if (Test-Path $AgentVibesConfig) {
325
- try {
326
- $json = Get-Content $AgentVibesConfig -Raw | ConvertFrom-Json
327
- if ($json.backgroundMusic -and $null -ne $json.backgroundMusic.enabled) {
328
- $BgEnabled = [bool]$json.backgroundMusic.enabled
329
- }
330
- } catch {
331
- $BgEnabled = $false
332
- }
333
- } else {
334
- # Fallback to legacy txt config
335
- $BgEnabledFile = "$ConfigDir\background-music-enabled.txt"
336
- if (Test-Path $BgEnabledFile) {
337
- $BgEnabled = (Get-Content $BgEnabledFile -Raw).Trim() -eq "true"
338
- }
339
- }
340
-
341
- # When a per-LLM row in audio-effects.cfg has a background track configured,
342
- # that's an implicit "bg music enabled for this LLM" — force it on regardless
343
- # of the global backgroundMusic.enabled flag. Without this, setting a per-LLM
344
- # track in the TUI's Configure modal would have no effect unless the user
345
- # ALSO toggled global bg music on.
346
- if ($LlmBgTrack) {
347
- $BgEnabled = $true
75
+ $BgEnabledFile = "$ConfigDir\background-music-enabled.txt"
76
+ if (Test-Path $BgEnabledFile) {
77
+ $BgEnabled = (Get-Content $BgEnabledFile -Raw).Trim() -eq "true"
348
78
  }
349
79
 
350
80
  # Check if reverb is enabled (allowlist validation)
351
- # LLM-specific reverb overrides global setting
352
81
  $ReverbLevel = "off"
353
- if ($LlmReverb -and $LlmReverb -in @("off", "light", "medium", "heavy", "cathedral")) {
354
- $ReverbLevel = $LlmReverb
355
- } else {
356
- $ReverbFile = "$ConfigDir\reverb-level.txt"
357
- if (Test-Path $ReverbFile) {
358
- $reverbVal = (Get-Content $ReverbFile -Raw).Trim()
359
- if ($reverbVal -in @("off", "light", "medium", "heavy", "cathedral")) {
360
- $ReverbLevel = $reverbVal
361
- }
82
+ $ReverbFile = "$ConfigDir\reverb-level.txt"
83
+ if (Test-Path $ReverbFile) {
84
+ $reverbVal = (Get-Content $ReverbFile -Raw).Trim()
85
+ if ($reverbVal -in @("off", "light", "medium", "heavy", "cathedral")) {
86
+ $ReverbLevel = $reverbVal
362
87
  }
363
88
  }
364
89
  $HasReverb = $ReverbLevel -ne "off"
365
90
 
366
91
  # Check ffmpeg availability for background music mixing or reverb
367
- # Refresh PATH from registry so newly-installed tools are found without shell restart
368
92
  $HasFfmpeg = $false
369
93
  if ($BgEnabled -or $HasReverb) {
370
94
  try {
371
95
  $null = Get-Command ffmpeg -ErrorAction Stop
372
96
  $HasFfmpeg = $true
373
- } catch {
374
- # PATH may be stale (common after winget install); refresh from registry
375
- $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
376
- try {
377
- $null = Get-Command ffmpeg -ErrorAction Stop
378
- $HasFfmpeg = $true
379
- } catch {}
380
- }
97
+ } catch {}
381
98
  }
382
99
 
383
- # Check for pre-synthesized WAV (party mode optimization -- synthesis done before mutex acquisition)
384
- $PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
385
- $UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
386
- (Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
387
-
388
100
  # If background music or reverb enabled and ffmpeg available, tell provider to skip playback
389
101
  if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
390
102
  $env:AGENTVIBES_NO_PLAY = "1"
391
103
  }
392
104
 
393
- # Call the provider script (skip if using pre-synthesized audio)
394
- # When post-processing (reverb/music), capture output preserving InformationRecord colors.
395
- # Otherwise call directly so Write-Host colors pass through to the terminal.
396
- $NeedsPostProcess = ($BgEnabled -or $HasReverb) -and $HasFfmpeg
397
- if ($UsePreSynth) {
398
- Write-Host "[SYNTH] Using pre-synthesized audio..." -ForegroundColor Cyan
399
- # If no post-processing needed, play the pre-synth file directly and exit
400
- if (-not $NeedsPostProcess) {
401
- try {
402
- Invoke-SerializedPlay -WavPath $PreSynthWav
403
- } catch {
404
- Write-Host "[WARNING] Pre-synth playback failed: $_" -ForegroundColor Yellow
405
- }
406
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
407
- exit 0
408
- }
409
- } else {
410
- try {
411
- if ($NeedsPostProcess) {
412
- if ($VoiceOverride) {
413
- $providerOutput = & $ProviderScript $Text $VoiceOverride 6>&1 2>&1
414
- } else {
415
- $providerOutput = & $ProviderScript $Text 6>&1 2>&1
416
- }
417
- # Re-emit preserving colors from InformationRecords (Write-Host output)
418
- foreach ($item in $providerOutput) {
419
- if ($item -is [System.Management.Automation.InformationRecord]) {
420
- $msg = $item.MessageData
421
- if ($msg -is [System.Management.Automation.HostInformationMessage]) {
422
- Write-Host $msg.Message -ForegroundColor $msg.ForegroundColor -NoNewline:$msg.NoNewLine
423
- if (-not $msg.NoNewLine) { Write-Host }
424
- } else {
425
- Write-Host "$item"
426
- }
427
- } else {
428
- Write-Host "$item"
429
- }
430
- }
431
- # Parse the provider output for "[OK] Saved to: <path>" so we can
432
- # use the EXACT file the provider just wrote. This replaces the
433
- # old "pick most recent tts-XXXXXXXX.wav" heuristic which would
434
- # silently replay stale audio whenever synthesis failed.
435
- $FreshSynthFile = $null
436
- foreach ($item in $providerOutput) {
437
- $line = if ($item -is [System.Management.Automation.InformationRecord]) {
438
- $m = $item.MessageData
439
- if ($m -is [System.Management.Automation.HostInformationMessage]) { $m.Message } else { "$item" }
440
- } else { "$item" }
441
- if ($line -match '^\[OK\] Saved to:\s*(.+\.wav)\s*$') {
442
- $FreshSynthFile = $Matches[1].Trim()
443
- }
444
- }
445
- if (-not $FreshSynthFile -or -not (Test-Path $FreshSynthFile)) {
446
- [Console]::Error.WriteLine("[AgentVibes] ERROR: Provider synthesis did not produce an output file. NOT falling back to stale audio. Check provider logs above.")
447
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
448
- exit 3
449
- }
450
- } else {
451
- if ($VoiceOverride) {
452
- & $ProviderScript $Text $VoiceOverride
453
- } else {
454
- & $ProviderScript $Text
455
- }
456
- }
105
+ # Call the provider script
106
+ try {
107
+ if ($VoiceOverride) {
108
+ $providerOutput = & $ProviderScript $Text $VoiceOverride 2>&1
457
109
  }
458
- catch {
459
- Write-Host "[ERROR] TTS Error: $_" -ForegroundColor Red
460
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
461
- exit 1
110
+ else {
111
+ $providerOutput = & $ProviderScript $Text 2>&1
462
112
  }
113
+ # Show provider output
114
+ $providerOutput | ForEach-Object { Write-Host $_ }
115
+ }
116
+ catch {
117
+ Write-Host "[ERROR] TTS Error: $_" -ForegroundColor Red
118
+ $env:AGENTVIBES_NO_PLAY = $null
119
+ exit 1
463
120
  }
464
121
 
465
122
  # Apply reverb and/or mix with background music
466
123
  if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
467
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
124
+ $env:AGENTVIBES_NO_PLAY = $null
468
125
 
469
- # Use the EXACT file the provider script just wrote (captured from its
470
- # "[OK] Saved to: <path>" output line above). The old "pick most recent
471
- # tts-XXXXXXXX.wav" heuristic silently replayed stale audio whenever
472
- # synthesis failed — there is no safe way to guess which file is fresh.
126
+ # Find the most recent TTS wav file
473
127
  $AudioDir = "$ClaudeDir\audio"
474
- $RecentWav = if ($UsePreSynth) {
475
- Get-Item $PreSynthWav -ErrorAction SilentlyContinue
476
- } elseif ($FreshSynthFile -and (Test-Path $FreshSynthFile)) {
477
- Get-Item $FreshSynthFile -ErrorAction SilentlyContinue
478
- } else {
479
- $null
480
- }
128
+ $RecentWav = Get-ChildItem -Path $AudioDir -Filter "tts-*.wav" -ErrorAction SilentlyContinue |
129
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1
481
130
 
482
131
  if ($RecentWav -and $RecentWav.Length -gt 0) {
483
132
  $voicePath = $RecentWav.FullName
@@ -492,12 +141,9 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
492
141
  default { "" }
493
142
  }
494
143
  if ($reverbFilter) {
495
- # Use a fixed name OUTSIDE the `tts-XXXXXXXX` random-name
496
- # namespace so the "pick most recent tts-*.wav" logic can't
497
- # accidentally pick this post-processed file as a synth input.
498
- $reverbedFile = "$AudioDir\av-reverbed-scratch.wav"
144
+ $reverbedFile = "$AudioDir\tts-reverbed.wav"
499
145
  $reverbArgs = "-y -i `"$voicePath`" -af `"$reverbFilter`" `"$reverbedFile`""
500
- $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
146
+ $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
501
147
  if ($proc.ExitCode -eq 0 -and (Test-Path $reverbedFile)) {
502
148
  $voicePath = $reverbedFile
503
149
  }
@@ -506,91 +152,40 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
506
152
 
507
153
  # Mix with background music if enabled
508
154
  if ($BgEnabled) {
509
- # Read background track and volume from audio-effects.cfg (matches Linux behavior)
155
+ # Get background track - default to bachata, or read from config
510
156
  $TracksDir = "$ClaudeDir\audio\tracks"
511
- $DefaultTrack = ""
512
- $BgVolume = "0.25"
513
- $AudioEffectsCfg = "$ConfigDir\audio-effects.cfg"
514
-
515
- if (Test-Path $AudioEffectsCfg) {
516
- # Try agent-specific config first, then fall back to default
517
- # Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
518
- # Lookup order: agent name -> llm:<name> -> default
519
- $agentName = $env:AGENTVIBES_AGENT_NAME
520
- $configLine = $null
521
-
522
- $cfgLines = Get-Content $AudioEffectsCfg
523
- if ($agentName) {
524
- foreach ($line in $cfgLines) {
525
- if ($line -match "^$([regex]::Escape($agentName))\|") {
526
- $configLine = $line
527
- break
528
- }
529
- }
530
- }
531
- # Try LLM-specific config (--llm parameter)
532
- if (-not $configLine -and $llm) {
533
- $llmBgKey = "llm:$llm"
534
- foreach ($line in $cfgLines) {
535
- if ($line -match "^$([regex]::Escape($llmBgKey))\|") {
536
- $configLine = $line
537
- break
538
- }
539
- }
540
- }
541
- # Fall back to default
542
- if (-not $configLine) {
543
- foreach ($line in $cfgLines) {
544
- if ($line -match '^default\|') {
545
- $configLine = $line
546
- break
547
- }
548
- }
549
- }
550
-
551
- if ($configLine) {
552
- $parts = $configLine -split '\|'
553
- if ($parts.Length -ge 3 -and $parts[2]) {
554
- $trackName = $parts[2].Trim()
555
- # Validate: filename only, no path separators or traversal
556
- if ($trackName -match '^[a-zA-Z0-9_\-\. ]+$') {
557
- $DefaultTrack = $trackName
558
- }
559
- }
560
- if ($parts.Length -ge 4 -and $parts[3]) {
561
- $volVal = $parts[3].Trim()
562
- if ($volVal -match '^\d+\.?\d*$') { $BgVolume = $volVal }
563
- }
157
+ $DefaultTrack = "agent_vibes_bachata_v1_loop.mp3"
158
+ $DefaultTrackFile = "$ConfigDir\background-music-default.txt"
159
+ if (Test-Path $DefaultTrackFile) {
160
+ $configTrack = (Get-Content $DefaultTrackFile -Raw).Trim()
161
+ # Validate: filename only, no path separators or traversal
162
+ if ($configTrack -and $configTrack -match '^[a-zA-Z0-9_\-\.]+$') {
163
+ $DefaultTrack = $configTrack
564
164
  }
565
165
  }
566
-
567
- # Fallback if no track found in config
568
- if (-not $DefaultTrack) {
569
- $DefaultTrack = "agent_vibes_celtic_harp_v1_loop.mp3"
570
- }
571
-
572
166
  $BgTrackPath = Join-Path $TracksDir $DefaultTrack
573
167
  # Path containment: verify resolved path stays within tracks directory
574
168
  $ResolvedBgTrack = [System.IO.Path]::GetFullPath($BgTrackPath)
575
169
  $ResolvedTracksDir = [System.IO.Path]::GetFullPath($TracksDir)
576
170
  if (-not $ResolvedBgTrack.StartsWith($ResolvedTracksDir + [System.IO.Path]::DirectorySeparatorChar)) {
577
- $BgTrackPath = Join-Path $TracksDir "agent_vibes_celtic_harp_v1_loop.mp3"
171
+ $BgTrackPath = Join-Path $TracksDir "agent_vibes_bachata_v1_loop.mp3"
172
+ }
173
+
174
+ # Get volume (default 0.25)
175
+ $BgVolume = "0.25"
176
+ $VolumeFile = "$ConfigDir\background-music-volume.txt"
177
+ if (Test-Path $VolumeFile) {
178
+ $vol = (Get-Content $VolumeFile -Raw).Trim()
179
+ if ($vol -match '^\d+\.?\d*$') { $BgVolume = $vol }
578
180
  }
579
181
 
580
182
  if (Test-Path $BgTrackPath) {
581
- # Mixed output goes to a fixed name OUTSIDE the tts-XXXXXXXX
582
- # random-name namespace so the "pick most recent tts-*.wav"
583
- # logic can't accidentally pick this as a synth input in the
584
- # next invocation. (Previously we'd name this as
585
- # "$voicePath-mixed.wav" which generated files like
586
- # tts-xxx.wav.effected-mixed.wav that kept re-matching and
587
- # compounding on every run.)
588
- $MixedFile = "$AudioDir\av-mixed-scratch.wav"
183
+ $MixedFile = $RecentWav.FullName -replace '\.wav$', '-mixed.wav'
589
184
 
590
185
  try {
591
186
  # Get voice duration to calculate total length
592
187
  $probArgs = "-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 `"$voicePath`""
593
- $durationProc = Start-Process -FilePath "ffprobe" -ArgumentList $probArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt" -RedirectStandardOutput "$env:TEMP\agentvibes-duration.txt"
188
+ $durationProc = Start-Process -FilePath "ffprobe" -ArgumentList $probArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL" -RedirectStandardOutput "$env:TEMP\agentvibes-duration.txt"
594
189
  $voiceDuration = 5 # default fallback
595
190
  if (Test-Path "$env:TEMP\agentvibes-duration.txt") {
596
191
  $durStr = (Get-Content "$env:TEMP\agentvibes-duration.txt" -Raw).Trim()
@@ -601,43 +196,71 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
601
196
  $fadeOutStart = $totalDuration - 2
602
197
 
603
198
  # Filter: music fades in 0.5s, voice delayed 2s, music fades out last 2s
604
- $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]"
199
+ $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]"
605
200
 
606
201
  # Run ffmpeg - use Start-Process to avoid stderr issues with $ErrorActionPreference
607
202
  $ffmpegArgs = "-y -stream_loop -1 -i `"$BgTrackPath`" -i `"$voicePath`" -filter_complex `"$filter`" -map `"[out]`" -t $totalDuration `"$MixedFile`""
608
- $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "$env:TEMP\agentvibes-ffmpeg-stderr.txt"
203
+ $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
609
204
 
610
205
  if ($proc.ExitCode -eq 0 -and (Test-Path $MixedFile) -and (Get-Item $MixedFile).Length -gt 0) {
611
- # Play the mixed audio (via serialized mutex)
206
+ # Play the mixed audio
207
+ $player = $null
612
208
  try {
613
- Invoke-SerializedPlay -WavPath $MixedFile
209
+ $player = New-Object System.Media.SoundPlayer $MixedFile
210
+ $player.PlaySync()
614
211
  } catch {
615
212
  Write-Host "[WARNING] Mixed playback failed, playing voice only" -ForegroundColor Yellow
616
- Invoke-SerializedPlay -WavPath $voicePath
213
+ $player2 = $null
214
+ try {
215
+ $player2 = New-Object System.Media.SoundPlayer $voicePath
216
+ $player2.PlaySync()
217
+ } finally {
218
+ if ($player2) { $player2.Dispose() }
219
+ }
220
+ } finally {
221
+ if ($player) { $player.Dispose() }
617
222
  }
618
223
  } else {
619
224
  # Mixing failed, play voice only
620
- Invoke-SerializedPlay -WavPath $voicePath
225
+ $player = $null
226
+ try {
227
+ $player = New-Object System.Media.SoundPlayer $voicePath
228
+ $player.PlaySync()
229
+ } finally {
230
+ if ($player) { $player.Dispose() }
231
+ }
621
232
  }
622
233
  } catch {
623
234
  # ffmpeg failed, play voice only
624
- Invoke-SerializedPlay -WavPath $voicePath
235
+ $player = $null
236
+ try {
237
+ $player = New-Object System.Media.SoundPlayer $voicePath
238
+ $player.PlaySync()
239
+ } finally {
240
+ if ($player) { $player.Dispose() }
241
+ }
625
242
  }
626
243
  } else {
627
244
  # No background track found, play voice only
628
- Invoke-SerializedPlay -WavPath $voicePath
245
+ $player = $null
246
+ try {
247
+ $player = New-Object System.Media.SoundPlayer $voicePath
248
+ $player.PlaySync()
249
+ } finally {
250
+ if ($player) { $player.Dispose() }
251
+ }
629
252
  }
630
253
  } else {
631
254
  # No background music, play the (possibly reverbed) voice
632
- Invoke-SerializedPlay -WavPath $voicePath
255
+ $player = $null
256
+ try {
257
+ $player = New-Object System.Media.SoundPlayer $voicePath
258
+ $player.PlaySync()
259
+ } finally {
260
+ if ($player) { $player.Dispose() }
261
+ }
633
262
  }
634
263
  }
635
264
  } else {
636
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
265
+ $env:AGENTVIBES_NO_PLAY = $null
637
266
  }
638
-
639
- # Explicit exit 0 so that $LASTEXITCODE from native commands (piper.exe,
640
- # ffmpeg, sox, etc.) doesn't leak through as the process exit code.
641
- # Without this, bash/Claude Code sees whatever random exit code the last
642
- # native command returned (e.g. 127) and treats it as a TTS failure.
643
- exit 0