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.
- 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 +42 -120
- package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
- package/.claude/hooks/play-tts.sh +48 -37
- 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 -274
- 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 -481
- 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 -515
- package/RELEASE_NOTES.md +1132 -1884
- 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/console/tabs/setup-tab.js +9 -6
- package/src/console/tabs/voices-tab.js +9 -3
- package/src/installer.js +42 -5
- package/src/services/llm-provider-service.js +13 -0
- 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,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
|
-
#
|
|
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
|
|
159
|
-
|
|
21
|
+
# Use project .claude if running from there, otherwise use user profile
|
|
22
|
+
if (Test-Path $ProjectClaudeDir) {
|
|
23
|
+
$ClaudeDir = $ProjectClaudeDir
|
|
160
24
|
} else {
|
|
161
|
-
$
|
|
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
|
-
|
|
282
|
-
$
|
|
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
|
-
|
|
294
|
-
$ProviderScript = "$HooksDir\play-tts-sapi.ps1"
|
|
50
|
+
"windows-sapi" {
|
|
51
|
+
$ProviderScript = "$HooksDir\play-tts-windows-sapi.ps1"
|
|
295
52
|
}
|
|
296
|
-
|
|
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
|
-
$
|
|
324
|
-
if (Test-Path $
|
|
325
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
$
|
|
357
|
-
|
|
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
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
$
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
124
|
+
$env:AGENTVIBES_NO_PLAY = $null
|
|
468
125
|
|
|
469
|
-
#
|
|
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 =
|
|
475
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
#
|
|
155
|
+
# Get background track - default to bachata, or read from config
|
|
510
156
|
$TracksDir = "$ClaudeDir\audio\tracks"
|
|
511
|
-
$DefaultTrack = ""
|
|
512
|
-
$
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
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=
|
|
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 "
|
|
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
|
|
206
|
+
# Play the mixed audio
|
|
207
|
+
$player = $null
|
|
612
208
|
try {
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|