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.
- package/.agentvibes/LITE-MODE.md +236 -0
- package/.agentvibes/README.md +136 -0
- package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +141 -0
- package/.agentvibes/backups/agents/analyst_20260204_144958.md +78 -0
- package/.agentvibes/backups/agents/architect_20260204_144958.md +72 -0
- package/.agentvibes/backups/agents/dev_20260204_144958.md +74 -0
- package/.agentvibes/backups/agents/pm_20260204_144958.md +72 -0
- package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +64 -0
- package/.agentvibes/backups/agents/sm_20260204_144958.md +87 -0
- package/.agentvibes/backups/agents/tea_20260204_144958.md +79 -0
- package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +82 -0
- package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +80 -0
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config/README-personality-defaults.md +162 -0
- package/.agentvibes/config/mode.txt +1 -0
- package/.agentvibes/config/personality-voice-defaults.default.json +21 -0
- package/.agentvibes/config/save-audio.txt +1 -0
- package/.agentvibes/config/voice-metadata.json +160 -0
- package/.agentvibes/config.json +24 -15
- package/.agentvibes/hooks/help.sh +191 -0
- package/.agentvibes/hooks/post-tool-use-lite.sh +111 -0
- package/.agentvibes/hooks/save-audio-manager.sh +162 -0
- package/.agentvibes/hooks/session-start-full-optimized.sh +102 -0
- package/.agentvibes/hooks/session-start-full.sh +142 -0
- package/.agentvibes/hooks/session-start-lite-v2.sh +34 -0
- package/.agentvibes/hooks/session-start-lite.sh +29 -0
- package/.agentvibes/hooks/stop-lite.sh +115 -0
- package/.agentvibes/hooks/switch-mode.sh +215 -0
- package/.agentvibes/output-styles/audio-summary.md +30 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/voice-samples/piper/alan.wav +0 -0
- package/.claude/audio/voice-samples/piper/amy.wav +0 -0
- package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
- package/.claude/audio/voice-samples/piper/joe.wav +0 -0
- package/.claude/audio/voice-samples/piper/john.wav +0 -0
- package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
- package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
- package/.claude/audio/voice-samples/piper/linda.wav +0 -0
- package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
- package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +4 -11
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -1
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -4
- package/.claude/config/tts-verbosity.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +0 -0
- package/.claude/hooks/audio-processor.sh +60 -14
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-manager.sh +225 -0
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +6 -13
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +25 -23
- package/.claude/hooks/clawdbot-receiver.sh +4 -28
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +0 -0
- package/.claude/hooks/piper-installer.sh +1 -1
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +0 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-macos.sh +6 -12
- package/.claude/hooks/play-tts-piper.sh +50 -79
- package/.claude/hooks/play-tts-soprano.sh +9 -43
- package/.claude/hooks/play-tts-ssh-remote.sh +43 -215
- package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
- package/.claude/hooks/play-tts.sh +31 -31
- package/.claude/hooks/post-response.sh +41 -0
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +0 -0
- package/.claude/hooks/provider-manager.sh +0 -0
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +56 -39
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop.sh +63 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +26 -4
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/bmad-party-speak.ps1 +278 -278
- package/.claude/hooks-windows/bmad-speak.ps1 +264 -264
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -53
- package/.claude/hooks-windows/effects-manager.ps1 +294 -294
- package/.claude/hooks-windows/language-manager.ps1 +193 -193
- package/.claude/hooks-windows/learn-manager.ps1 +241 -241
- package/.claude/hooks-windows/personality-manager.ps1 +266 -266
- package/.claude/hooks-windows/play-tts-soprano.ps1 +5 -5
- package/.claude/hooks-windows/play-tts-termux-ssh.ps1 +138 -138
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +164 -0
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts.ps1 +104 -513
- package/.claude/hooks-windows/provider-manager.ps1 +158 -192
- package/.claude/hooks-windows/session-start-tts.ps1 +55 -46
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -166
- package/.claude/hooks-windows/voice-manager-windows.ps1 +176 -260
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/piper-voices-dir.txt +1 -0
- package/.claude/settings.json +25 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +149 -145
- package/.mcp.json +30 -11
- package/CLAUDE.md +170 -215
- package/README.md +206 -525
- package/RELEASE_NOTES.md +1132 -1976
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +0 -0
- package/bin/agentvibes-voice-browser.js +64 -1289
- package/bin/agentvibes.js +0 -0
- package/bin/ensure-soprano-running.sh +43 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1451 -1578
- package/mcp-server/test_server.py +395 -395
- package/package.json +1 -3
- package/setup-windows.ps1 +815 -815
- package/src/installer.js +42 -5
- package/templates/agentvibes-receiver.sh +158 -483
- package/templates/audio/welcome-music.mp3 +0 -0
- package/.agentvibes/bmad-voice-map.json +0 -104
- package/.agentvibes/copilot-sessions.log +0 -4
- package/.claude/config/audio-effects-bmad.cfg +0 -50
- package/.claude/config/background-music-enabled.txt +0 -1
- package/.claude/config/intro-text.txt +0 -1
- package/.claude/config/personality.txt +0 -1
- package/.claude/config/piper-speech-rate.txt +0 -4
- package/.claude/config/piper-target-speech-rate.txt +0 -1
- package/.claude/config/reverb-level.txt +0 -1
- package/.claude/config/tts-target-speech-rate.txt +0 -1
- package/voice-assignments.json +0 -8245
- /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
|
-
#
|
|
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
|
|
166
|
-
|
|
21
|
+
# Use project .claude if running from there, otherwise use user profile
|
|
22
|
+
if (Test-Path $ProjectClaudeDir) {
|
|
23
|
+
$ClaudeDir = $ProjectClaudeDir
|
|
167
24
|
} else {
|
|
168
|
-
$
|
|
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
|
-
|
|
304
|
-
$
|
|
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
|
-
|
|
316
|
-
$ProviderScript = "$HooksDir\play-tts-sapi.ps1"
|
|
50
|
+
"windows-sapi" {
|
|
51
|
+
$ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1"
|
|
317
52
|
}
|
|
318
|
-
|
|
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
|
-
$
|
|
346
|
-
if (Test-Path $
|
|
347
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
$
|
|
379
|
-
|
|
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
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
$
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
124
|
+
$env:AGENTVIBES_NO_PLAY = $null
|
|
490
125
|
|
|
491
|
-
#
|
|
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 =
|
|
497
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
#
|
|
155
|
+
# Get background track - default to bachata, or read from config
|
|
532
156
|
$TracksDir = "$ClaudeDir\audio\tracks"
|
|
533
|
-
$DefaultTrack = ""
|
|
534
|
-
$
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
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=
|
|
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 "
|
|
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
|
|
206
|
+
# Play the mixed audio
|
|
207
|
+
$player = $null
|
|
644
208
|
try {
|
|
645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|