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