agentvibes 5.3.0 → 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 (219) 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 +43 -215
  115. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  116. package/.claude/hooks/play-tts.sh +31 -31
  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 -278
  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 -513
  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 -525
  182. package/RELEASE_NOTES.md +1132 -1976
  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/installer.js +42 -5
  206. package/templates/agentvibes-receiver.sh +158 -483
  207. package/templates/audio/welcome-music.mp3 +0 -0
  208. package/.agentvibes/bmad-voice-map.json +0 -104
  209. package/.agentvibes/copilot-sessions.log +0 -4
  210. package/.claude/config/audio-effects-bmad.cfg +0 -50
  211. package/.claude/config/background-music-enabled.txt +0 -1
  212. package/.claude/config/intro-text.txt +0 -1
  213. package/.claude/config/personality.txt +0 -1
  214. package/.claude/config/piper-speech-rate.txt +0 -4
  215. package/.claude/config/piper-target-speech-rate.txt +0 -1
  216. package/.claude/config/reverb-level.txt +0 -1
  217. package/.claude/config/tts-target-speech-rate.txt +0 -1
  218. package/voice-assignments.json +0 -8245
  219. /package/{.claude → .agentvibes}/config/agentvibes.json +0 -0
@@ -10,169 +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
- # 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
16
  # Configuration paths
161
- # Priority: CLAUDE_PROJECT_DIR env var -> script's parent project -> user profile
162
- # Local project settings ALWAYS override global (~/.claude)
17
+ # First check if we're running from a project directory with .claude
163
18
  $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
19
+ $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
164
20
 
165
- if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
166
- $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
167
24
  } 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
- }
25
+ $ClaudeDir = "$env:USERPROFILE\.claude"
176
26
  }
177
27
 
178
28
  $HooksDir = "$ClaudeDir\hooks-windows"
@@ -187,124 +37,9 @@ if (Test-Path $MuteFile) {
187
37
  }
188
38
  }
189
39
 
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
40
  # Determine active provider
303
- # LLM-specific engine overrides global provider
304
- $ActiveProvider = "sapi"
305
- if ($LlmEngine) {
306
- $ActiveProvider = $LlmEngine
307
- } elseif (Test-Path $ProviderFile) {
41
+ $ActiveProvider = "windows-sapi"
42
+ if (Test-Path $ProviderFile) {
308
43
  $ActiveProvider = (Get-Content $ProviderFile -Raw).Trim()
309
44
  }
310
45
 
@@ -312,18 +47,15 @@ if ($LlmEngine) {
312
47
  $ProviderScript = ""
313
48
 
314
49
  switch ($ActiveProvider) {
315
- { $_ -in "sapi", "windows-sapi" } {
316
- $ProviderScript = "$HooksDir\play-tts-sapi.ps1"
50
+ "windows-sapi" {
51
+ $ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1"
317
52
  }
318
- { $_ -in "piper", "windows-piper" } {
319
- $ProviderScript = "$HooksDir\play-tts-piper.ps1"
53
+ "windows-piper" {
54
+ $ProviderScript = "$HooksDir\play-tts-windows-piper.ps1"
320
55
  }
321
56
  "soprano" {
322
57
  $ProviderScript = "$HooksDir\play-tts-soprano.ps1"
323
58
  }
324
- "termux-ssh" {
325
- $ProviderScript = "$HooksDir\play-tts-termux-ssh.ps1"
326
- }
327
59
  default {
328
60
  Write-Host "[ERROR] Unknown provider: $ActiveProvider" -ForegroundColor Red
329
61
  Write-Host "Use: .\provider-manager.ps1 list" -ForegroundColor Yellow
@@ -338,168 +70,63 @@ if (-not (Test-Path $ProviderScript)) {
338
70
  }
339
71
 
340
72
  # 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
73
  $ConfigDir = "$ClaudeDir\config"
344
74
  $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
75
+ $BgEnabledFile = "$ConfigDir\background-music-enabled.txt"
76
+ if (Test-Path $BgEnabledFile) {
77
+ $BgEnabled = (Get-Content $BgEnabledFile -Raw).Trim() -eq "true"
370
78
  }
371
79
 
372
80
  # Check if reverb is enabled (allowlist validation)
373
- # LLM-specific reverb overrides global setting
374
81
  $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
- }
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
384
87
  }
385
88
  }
386
89
  $HasReverb = $ReverbLevel -ne "off"
387
90
 
388
91
  # Check ffmpeg availability for background music mixing or reverb
389
- # Refresh PATH from registry so newly-installed tools are found without shell restart
390
92
  $HasFfmpeg = $false
391
93
  if ($BgEnabled -or $HasReverb) {
392
94
  try {
393
95
  $null = Get-Command ffmpeg -ErrorAction Stop
394
96
  $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 {}
402
- }
97
+ } catch {}
403
98
  }
404
99
 
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
409
-
410
100
  # If background music or reverb enabled and ffmpeg available, tell provider to skip playback
411
101
  if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
412
102
  $env:AGENTVIBES_NO_PLAY = "1"
413
103
  }
414
104
 
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
427
- }
428
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
429
- exit 0
430
- }
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
- }
478
- }
105
+ # Call the provider script
106
+ try {
107
+ if ($VoiceOverride) {
108
+ $providerOutput = & $ProviderScript $Text $VoiceOverride 2>&1
479
109
  }
480
- catch {
481
- Write-Host "[ERROR] TTS Error: $_" -ForegroundColor Red
482
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
483
- exit 1
110
+ else {
111
+ $providerOutput = & $ProviderScript $Text 2>&1
484
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
485
120
  }
486
121
 
487
122
  # Apply reverb and/or mix with background music
488
123
  if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
489
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
124
+ $env:AGENTVIBES_NO_PLAY = $null
490
125
 
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.
126
+ # Find the most recent TTS wav file
495
127
  $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
- }
128
+ $RecentWav = Get-ChildItem -Path $AudioDir -Filter "tts-*.wav" -ErrorAction SilentlyContinue |
129
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1
503
130
 
504
131
  if ($RecentWav -and $RecentWav.Length -gt 0) {
505
132
  $voicePath = $RecentWav.FullName
@@ -514,12 +141,9 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
514
141
  default { "" }
515
142
  }
516
143
  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"
144
+ $reverbedFile = "$AudioDir\tts-reverbed.wav"
521
145
  $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"
146
+ $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $reverbArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
523
147
  if ($proc.ExitCode -eq 0 -and (Test-Path $reverbedFile)) {
524
148
  $voicePath = $reverbedFile
525
149
  }
@@ -528,101 +152,40 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
528
152
 
529
153
  # Mix with background music if enabled
530
154
  if ($BgEnabled) {
531
- # Read background track and volume from audio-effects.cfg (matches Linux behavior)
155
+ # Get background track - default to bachata, or read from config
532
156
  $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
- }
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
586
164
  }
587
165
  }
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
166
  $BgTrackPath = Join-Path $TracksDir $DefaultTrack
605
167
  # Path containment: verify resolved path stays within tracks directory
606
168
  $ResolvedBgTrack = [System.IO.Path]::GetFullPath($BgTrackPath)
607
169
  $ResolvedTracksDir = [System.IO.Path]::GetFullPath($TracksDir)
608
170
  if (-not $ResolvedBgTrack.StartsWith($ResolvedTracksDir + [System.IO.Path]::DirectorySeparatorChar)) {
609
- $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 }
610
180
  }
611
181
 
612
182
  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"
183
+ $MixedFile = $RecentWav.FullName -replace '\.wav$', '-mixed.wav'
621
184
 
622
185
  try {
623
186
  # Get voice duration to calculate total length
624
187
  $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"
188
+ $durationProc = Start-Process -FilePath "ffprobe" -ArgumentList $probArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL" -RedirectStandardOutput "$env:TEMP\agentvibes-duration.txt"
626
189
  $voiceDuration = 5 # default fallback
627
190
  if (Test-Path "$env:TEMP\agentvibes-duration.txt") {
628
191
  $durStr = (Get-Content "$env:TEMP\agentvibes-duration.txt" -Raw).Trim()
@@ -633,43 +196,71 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
633
196
  $fadeOutStart = $totalDuration - 2
634
197
 
635
198
  # 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]"
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]"
637
200
 
638
201
  # Run ffmpeg - use Start-Process to avoid stderr issues with $ErrorActionPreference
639
202
  $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"
203
+ $proc = Start-Process -FilePath "ffmpeg" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru -RedirectStandardError "NUL"
641
204
 
642
205
  if ($proc.ExitCode -eq 0 -and (Test-Path $MixedFile) -and (Get-Item $MixedFile).Length -gt 0) {
643
- # Play the mixed audio (via serialized mutex)
206
+ # Play the mixed audio
207
+ $player = $null
644
208
  try {
645
- Invoke-SerializedPlay -WavPath $MixedFile
209
+ $player = New-Object System.Media.SoundPlayer $MixedFile
210
+ $player.PlaySync()
646
211
  } catch {
647
212
  Write-Host "[WARNING] Mixed playback failed, playing voice only" -ForegroundColor Yellow
648
- 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() }
649
222
  }
650
223
  } else {
651
224
  # Mixing failed, play voice only
652
- 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
+ }
653
232
  }
654
233
  } catch {
655
234
  # ffmpeg failed, play voice only
656
- 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
+ }
657
242
  }
658
243
  } else {
659
244
  # No background track found, play voice only
660
- 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
+ }
661
252
  }
662
253
  } else {
663
254
  # No background music, play the (possibly reverbed) voice
664
- 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
+ }
665
262
  }
666
263
  }
667
264
  } else {
668
- Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
265
+ $env:AGENTVIBES_NO_PLAY = $null
669
266
  }
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