agentvibes 4.6.8 → 5.1.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/bmad-voice-map.json +104 -0
- package/.agentvibes/config.json +13 -12
- package/.agentvibes/copilot-sessions.log +4 -0
- package/.claude/audio/tracks/Drifting Down the Hall.mp3 +0 -0
- package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
- package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
- package/.claude/audio/tracks/README.md +51 -52
- package/.claude/config/audio-effects-bmad.cfg +50 -0
- package/.claude/config/audio-effects.cfg +4 -4
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/hooks/play-tts-piper.sh +3 -1
- package/.claude/hooks/play-tts.sh +380 -301
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks-windows/audio-processor.ps1 +181 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
- package/.claude/hooks-windows/play-tts.ps1 +28 -6
- package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
- package/README.md +112 -6
- package/RELEASE_NOTES.md +83 -0
- package/bin/bmad-speak.js +16 -8
- package/mcp-server/server.py +15 -8
- package/package.json +1 -1
- package/src/console/app.js +899 -897
- package/src/console/footer-config.js +50 -50
- package/src/console/navigation.js +65 -65
- package/src/console/tabs/agents-tab.js +1899 -1886
- package/src/console/tabs/music-tab.js +1076 -1039
- package/src/console/tabs/placeholder-tab.js +81 -80
- package/src/console/tabs/settings-tab.js +941 -3988
- package/src/console/tabs/setup-tab.js +2071 -0
- package/src/console/tabs/voices-tab.js +1843 -1714
- package/src/console/widgets/format-utils.js +92 -89
- package/src/console/widgets/track-picker.js +325 -322
- package/src/installer.js +6147 -6092
- package/src/services/llm-provider-service.js +486 -0
- package/src/services/navigation-service.js +123 -123
- package/src/services/tts-engine-service.js +69 -0
- package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
- package/src/console/tabs/install-tab.js +0 -1081
|
@@ -1,245 +1,259 @@
|
|
|
1
|
-
#
|
|
2
|
-
# File: .claude/hooks-windows/play-tts-piper.ps1
|
|
3
|
-
#
|
|
4
|
-
# AgentVibes - Windows Piper TTS Provider
|
|
5
|
-
# High-quality neural TTS using Piper.exe
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
param(
|
|
9
|
-
[Parameter(Mandatory = $true)]
|
|
10
|
-
[string]$Text,
|
|
11
|
-
|
|
12
|
-
[Parameter(Mandatory = $false)]
|
|
13
|
-
[string]$VoiceOverride
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
# Configuration paths
|
|
17
|
-
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
18
|
-
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
19
|
-
|
|
20
|
-
if (Test-Path $ProjectClaudeDir) {
|
|
21
|
-
$ClaudeDir = $ProjectClaudeDir
|
|
22
|
-
} else {
|
|
23
|
-
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Audio cache and voice config use project-local .claude
|
|
27
|
-
$AudioDir = "$ClaudeDir\audio"
|
|
28
|
-
# Try provider-specific file first, then generic tts-voice.txt (set by TUI)
|
|
29
|
-
$VoiceFile = "$ClaudeDir\tts-voice-piper.txt"
|
|
30
|
-
if (-not (Test-Path $VoiceFile)) {
|
|
31
|
-
$VoiceFile = "$ClaudeDir\tts-voice.txt"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
# Voices and Piper binary are global (shared across projects, ~100MB+)
|
|
35
|
-
$UserClaudeDir = "$env:USERPROFILE\.claude"
|
|
36
|
-
$VoicesDir = "$UserClaudeDir\piper-voices"
|
|
37
|
-
# Try standard install location first, then fall back to PATH
|
|
38
|
-
$PiperExe = "$env:LOCALAPPDATA\Programs\Piper\piper.exe"
|
|
39
|
-
if (-not (Test-Path $PiperExe)) {
|
|
40
|
-
$found = Get-Command piper.exe -ErrorAction SilentlyContinue
|
|
41
|
-
if (-not $found) {
|
|
42
|
-
# PATH may be stale (SSH sessions inherit minimal PATH); refresh from registry
|
|
43
|
-
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
44
|
-
$found = Get-Command piper.exe -ErrorAction SilentlyContinue
|
|
45
|
-
}
|
|
46
|
-
if ($found) { $PiperExe = $found.Source }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# Ensure directories exist
|
|
50
|
-
foreach ($dir in @($AudioDir, $VoicesDir)) {
|
|
51
|
-
if (-not (Test-Path $dir)) {
|
|
52
|
-
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# Check if Piper is installed
|
|
57
|
-
if (-not (Test-Path $PiperExe)) {
|
|
58
|
-
Write-Host "[ERROR] Piper not found at: $PiperExe" -ForegroundColor Red
|
|
59
|
-
Write-Host "Run: .\setup-windows.ps1 to install Piper" -ForegroundColor Yellow
|
|
60
|
-
exit 1
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
# Determine voice to use
|
|
64
|
-
$VoiceName = ""
|
|
65
|
-
|
|
66
|
-
if ($VoiceOverride) {
|
|
67
|
-
$VoiceName = $VoiceOverride
|
|
68
|
-
}
|
|
69
|
-
elseif (Test-Path $VoiceFile) {
|
|
70
|
-
$VoiceName = (Get-Content $VoiceFile -Raw).Trim()
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
# Strip display name suffix (e.g. "en_US-libritts-high::Holly-7" -> "en_US-libritts-high")
|
|
74
|
-
# and resolve the real Piper speaker index.
|
|
75
|
-
# IMPORTANT: The trailing number in a speaker name (e.g. "Holly-7") is a disambiguation
|
|
76
|
-
# suffix, NOT the speaker index. Real index must be looked up from voice-assignments.json.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
$
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
$
|
|
88
|
-
$
|
|
89
|
-
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (Test-Path $VoiceAssignmentsPath) {
|
|
95
|
-
try {
|
|
96
|
-
$vaData = Get-Content $VoiceAssignmentsPath -Raw | ConvertFrom-Json
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
$
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
#
|
|
152
|
-
$
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (-not (
|
|
162
|
-
Write-Host "[
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
$
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Write-Host "
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
#
|
|
203
|
-
$
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
1
|
+
#
|
|
2
|
+
# File: .claude/hooks-windows/play-tts-piper.ps1
|
|
3
|
+
#
|
|
4
|
+
# AgentVibes - Windows Piper TTS Provider
|
|
5
|
+
# High-quality neural TTS using Piper.exe
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
param(
|
|
9
|
+
[Parameter(Mandatory = $true)]
|
|
10
|
+
[string]$Text,
|
|
11
|
+
|
|
12
|
+
[Parameter(Mandatory = $false)]
|
|
13
|
+
[string]$VoiceOverride
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Configuration paths
|
|
17
|
+
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
18
|
+
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
19
|
+
|
|
20
|
+
if (Test-Path $ProjectClaudeDir) {
|
|
21
|
+
$ClaudeDir = $ProjectClaudeDir
|
|
22
|
+
} else {
|
|
23
|
+
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Audio cache and voice config use project-local .claude
|
|
27
|
+
$AudioDir = "$ClaudeDir\audio"
|
|
28
|
+
# Try provider-specific file first, then generic tts-voice.txt (set by TUI)
|
|
29
|
+
$VoiceFile = "$ClaudeDir\tts-voice-piper.txt"
|
|
30
|
+
if (-not (Test-Path $VoiceFile)) {
|
|
31
|
+
$VoiceFile = "$ClaudeDir\tts-voice.txt"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Voices and Piper binary are global (shared across projects, ~100MB+)
|
|
35
|
+
$UserClaudeDir = "$env:USERPROFILE\.claude"
|
|
36
|
+
$VoicesDir = "$UserClaudeDir\piper-voices"
|
|
37
|
+
# Try standard install location first, then fall back to PATH
|
|
38
|
+
$PiperExe = "$env:LOCALAPPDATA\Programs\Piper\piper.exe"
|
|
39
|
+
if (-not (Test-Path $PiperExe)) {
|
|
40
|
+
$found = Get-Command piper.exe -ErrorAction SilentlyContinue
|
|
41
|
+
if (-not $found) {
|
|
42
|
+
# PATH may be stale (SSH sessions inherit minimal PATH); refresh from registry
|
|
43
|
+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
|
44
|
+
$found = Get-Command piper.exe -ErrorAction SilentlyContinue
|
|
45
|
+
}
|
|
46
|
+
if ($found) { $PiperExe = $found.Source }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Ensure directories exist
|
|
50
|
+
foreach ($dir in @($AudioDir, $VoicesDir)) {
|
|
51
|
+
if (-not (Test-Path $dir)) {
|
|
52
|
+
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Check if Piper is installed
|
|
57
|
+
if (-not (Test-Path $PiperExe)) {
|
|
58
|
+
Write-Host "[ERROR] Piper not found at: $PiperExe" -ForegroundColor Red
|
|
59
|
+
Write-Host "Run: .\setup-windows.ps1 to install Piper" -ForegroundColor Yellow
|
|
60
|
+
exit 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Determine voice to use
|
|
64
|
+
$VoiceName = ""
|
|
65
|
+
|
|
66
|
+
if ($VoiceOverride) {
|
|
67
|
+
$VoiceName = $VoiceOverride
|
|
68
|
+
}
|
|
69
|
+
elseif (Test-Path $VoiceFile) {
|
|
70
|
+
$VoiceName = (Get-Content $VoiceFile -Raw).Trim()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Strip display name suffix (e.g. "en_US-libritts-high::Holly-7" -> "en_US-libritts-high")
|
|
74
|
+
# and resolve the real Piper speaker index.
|
|
75
|
+
# IMPORTANT: The trailing number in a speaker name (e.g. "Holly-7") is a disambiguation
|
|
76
|
+
# suffix, NOT the speaker index. Real index must be looked up from voice-assignments.json.
|
|
77
|
+
$SpeakerId = $null
|
|
78
|
+
|
|
79
|
+
if ($VoiceName -match '::') {
|
|
80
|
+
$parts = $VoiceName -split '::'
|
|
81
|
+
$VoiceName = $parts[0]
|
|
82
|
+
$SpeakerName = if ($parts.Length -ge 2) { $parts[1] } else { "" }
|
|
83
|
+
|
|
84
|
+
if ($SpeakerName) {
|
|
85
|
+
# Primary: look up in voice-assignments.json catalog (libritts_speakers keyed by speaker index)
|
|
86
|
+
# Derive project root from this script's location: .claude/hooks-windows/ -> project root
|
|
87
|
+
$PiperScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
88
|
+
$PiperProjectRoot = Split-Path -Parent (Split-Path -Parent $PiperScriptRoot)
|
|
89
|
+
$VoiceAssignmentsPath = Join-Path $PiperProjectRoot "voice-assignments.json"
|
|
90
|
+
# Fallback: global AgentVibes install if not found in project
|
|
91
|
+
if (-not (Test-Path $VoiceAssignmentsPath)) {
|
|
92
|
+
$VoiceAssignmentsPath = Join-Path $env:USERPROFILE "AgentVibes\voice-assignments.json"
|
|
93
|
+
}
|
|
94
|
+
if (Test-Path $VoiceAssignmentsPath) {
|
|
95
|
+
try {
|
|
96
|
+
$vaData = Get-Content $VoiceAssignmentsPath -Raw | ConvertFrom-Json
|
|
97
|
+
# First pass: try exact match
|
|
98
|
+
foreach ($prop in $vaData.libritts_speakers.PSObject.Properties) {
|
|
99
|
+
if ($prop.Value.voice_name -eq $SpeakerName) {
|
|
100
|
+
$SpeakerId = $prop.Name
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
# If no exact match, try substring match as fallback
|
|
105
|
+
if (-not $SpeakerId) {
|
|
106
|
+
foreach ($prop in $vaData.libritts_speakers.PSObject.Properties) {
|
|
107
|
+
if ($prop.Value.voice_name -like "$SpeakerName*") {
|
|
108
|
+
$SpeakerId = $prop.Name
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch { }
|
|
114
|
+
}
|
|
115
|
+
# Fallback: check patched speaker_id_map in the .onnx.json
|
|
116
|
+
if (-not $SpeakerId) {
|
|
117
|
+
$OnnxJsonPath = "$VoicesDir\$VoiceName.onnx.json"
|
|
118
|
+
if (Test-Path $OnnxJsonPath) {
|
|
119
|
+
try {
|
|
120
|
+
$onnxData = Get-Content $OnnxJsonPath -Raw | ConvertFrom-Json
|
|
121
|
+
$speakerIdMap = $onnxData.speaker_id_map
|
|
122
|
+
if ($speakerIdMap -and $speakerIdMap.PSObject.Properties[$SpeakerName]) {
|
|
123
|
+
$SpeakerId = [string]$speakerIdMap.PSObject.Properties[$SpeakerName].Value
|
|
124
|
+
}
|
|
125
|
+
} catch { }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Default voice if not specified
|
|
132
|
+
# Prefer en_US-lessac-medium (bundled/commonly installed) over en_US-ryan-high
|
|
133
|
+
if (-not $VoiceName) {
|
|
134
|
+
$UserClaudePiperDir = "$env:USERPROFILE\.claude\piper-voices"
|
|
135
|
+
if (Test-Path "$UserClaudePiperDir\en_US-lessac-medium.onnx") {
|
|
136
|
+
$VoiceName = "en_US-lessac-medium"
|
|
137
|
+
} elseif (Test-Path "$UserClaudePiperDir\en_US-ryan-high.onnx") {
|
|
138
|
+
$VoiceName = "en_US-ryan-high"
|
|
139
|
+
} else {
|
|
140
|
+
# Fallback: use first available .onnx file, or default name for auto-download
|
|
141
|
+
$firstVoice = Get-ChildItem -Path $UserClaudePiperDir -Filter "*.onnx" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
142
|
+
if ($firstVoice) {
|
|
143
|
+
$VoiceName = $firstVoice.BaseName
|
|
144
|
+
} else {
|
|
145
|
+
$VoiceName = "en_US-lessac-medium"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Security: Validate voice name to prevent path traversal
|
|
151
|
+
# Only allow alphanumeric, underscore, hyphen, and period
|
|
152
|
+
if ($VoiceName -notmatch '^[a-zA-Z0-9_\-\.]+$') {
|
|
153
|
+
Write-Host "[ERROR] Invalid voice name: $VoiceName" -ForegroundColor Red
|
|
154
|
+
exit 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Resolve voice model path and validate it stays within VoicesDir
|
|
158
|
+
$VoiceModelFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx")
|
|
159
|
+
$VoiceJsonFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx.json")
|
|
160
|
+
$ResolvedVoicesDir = [System.IO.Path]::GetFullPath($VoicesDir)
|
|
161
|
+
if (-not $VoiceModelFile.StartsWith($ResolvedVoicesDir)) {
|
|
162
|
+
Write-Host "[ERROR] Voice path outside voices directory" -ForegroundColor Red
|
|
163
|
+
exit 1
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Check if voice model exists, download if missing
|
|
167
|
+
if (-not (Test-Path $VoiceModelFile)) {
|
|
168
|
+
Write-Host "[DOWNLOAD] Voice model: $VoiceName" -ForegroundColor Yellow
|
|
169
|
+
|
|
170
|
+
# Try to download from Hugging Face
|
|
171
|
+
# Voice name format: {lang}_{region}-{speaker}-{quality}
|
|
172
|
+
# HF path format: {lang}/{lang}_{region}/{speaker}/{quality}/{voicename}.onnx
|
|
173
|
+
try {
|
|
174
|
+
# Parse voice name to build correct HF path
|
|
175
|
+
# e.g. en_US-ryan-high -> en/en_US/ryan/high/en_US-ryan-high.onnx
|
|
176
|
+
if ($VoiceName -match '^([a-z]{2})_([A-Z]{2})-([a-zA-Z0-9_]+)-([a-z]+)$') {
|
|
177
|
+
$Lang = $Matches[1]
|
|
178
|
+
$LangRegion = "$($Matches[1])_$($Matches[2])"
|
|
179
|
+
$Speaker = $Matches[3]
|
|
180
|
+
$Quality = $Matches[4]
|
|
181
|
+
$HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$Lang/$LangRegion/$Speaker/$Quality"
|
|
182
|
+
} else {
|
|
183
|
+
# Fallback for non-standard voice names
|
|
184
|
+
$HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high"
|
|
185
|
+
}
|
|
186
|
+
$ModelUrl = "$HFBase/$VoiceName.onnx"
|
|
187
|
+
$JsonUrl = "$HFBase/$VoiceName.onnx.json"
|
|
188
|
+
|
|
189
|
+
Write-Host " Downloading model..." -ForegroundColor Cyan
|
|
190
|
+
Invoke-WebRequest -Uri $ModelUrl -OutFile $VoiceModelFile -ErrorAction Stop
|
|
191
|
+
Write-Host " Downloading config..." -ForegroundColor Cyan
|
|
192
|
+
Invoke-WebRequest -Uri $JsonUrl -OutFile $VoiceJsonFile -ErrorAction Stop
|
|
193
|
+
Write-Host "[OK] Voice model downloaded" -ForegroundColor Green
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
Write-Host "[ERROR] Failed to download voice model: $_" -ForegroundColor Red
|
|
197
|
+
Write-Host "Make sure you have internet connection" -ForegroundColor Yellow
|
|
198
|
+
exit 1
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# Sanitize text for speech - strip only dangerous shell metacharacters
|
|
203
|
+
$Text = $Text -replace '\\', ' '
|
|
204
|
+
$Text = $Text -replace '[{}<>|`~^;]', ''
|
|
205
|
+
$Text = $Text -replace '\s+', ' '
|
|
206
|
+
$Text = $Text.Trim()
|
|
207
|
+
|
|
208
|
+
# Create audio file path — SECURITY: use random name instead of predictable timestamp (#130)
|
|
209
|
+
$AudioFile = "$AudioDir\tts-$([System.IO.Path]::GetRandomFileName() -replace '\..*').wav"
|
|
210
|
+
|
|
211
|
+
# Synthesize with Piper
|
|
212
|
+
try {
|
|
213
|
+
Write-Host "[SYNTH] Synthesizing with Piper..." -ForegroundColor Cyan
|
|
214
|
+
|
|
215
|
+
# Run Piper with text input
|
|
216
|
+
# Add --speaker for multi-speaker models (e.g. libritts-high with speaker 66 for Derek)
|
|
217
|
+
$piperArgs = @("--model", $VoiceModelFile, "--output-file", $AudioFile)
|
|
218
|
+
if ($SpeakerId) {
|
|
219
|
+
$piperArgs += @("--speaker", $SpeakerId)
|
|
220
|
+
}
|
|
221
|
+
$Text | & $PiperExe @piperArgs 2>$null
|
|
222
|
+
|
|
223
|
+
if (-not (Test-Path $AudioFile)) {
|
|
224
|
+
Write-Host "[ERROR] Piper synthesis failed" -ForegroundColor Red
|
|
225
|
+
exit 1
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Display results
|
|
229
|
+
Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
|
|
230
|
+
Write-Host "[VOICE] Voice used: $VoiceName (Piper)" -ForegroundColor Green
|
|
231
|
+
|
|
232
|
+
# Apply audio effects (reverb, background music) if processor script exists
|
|
233
|
+
$ProcessorScript = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "audio-processor.ps1"
|
|
234
|
+
$ProcessedFile = $AudioFile
|
|
235
|
+
if (Test-Path $ProcessorScript) {
|
|
236
|
+
# Lookup order: agent name → LLM key (from --llm) → default
|
|
237
|
+
$AgentName = if ($env:AGENTVIBES_AGENT_NAME) { $env:AGENTVIBES_AGENT_NAME } elseif ($env:AGENTVIBES_LLM_KEY) { $env:AGENTVIBES_LLM_KEY } else { "default" }
|
|
238
|
+
$EffectedFile = "$AudioFile.effected.wav"
|
|
239
|
+
try {
|
|
240
|
+
& $ProcessorScript $AudioFile $AgentName $EffectedFile
|
|
241
|
+
if ((Test-Path $EffectedFile) -and (Get-Item $EffectedFile).Length -gt 0) {
|
|
242
|
+
$ProcessedFile = $EffectedFile
|
|
243
|
+
Write-Host "[EFFECTS] Audio effects applied" -ForegroundColor Cyan
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
Write-Host "[WARNING] Audio effects processing skipped: $_" -ForegroundColor Yellow
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Return path to processed audio file for parent (play-tts.ps1) to handle playback
|
|
252
|
+
# This allows play-tts.ps1 to apply additional post-processing (reverb, background music)
|
|
253
|
+
# DO NOT play here - let play-tts.ps1 coordinate all audio playback
|
|
254
|
+
Write-Host "[OUTPUT] Processed audio: $ProcessedFile" -ForegroundColor Gray
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
Write-Host "[ERROR] Error running Piper: $_" -ForegroundColor Red
|
|
258
|
+
exit 1
|
|
259
|
+
}
|
|
@@ -133,6 +133,10 @@ $PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
|
|
|
133
133
|
$UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
|
|
134
134
|
(Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
|
|
135
135
|
|
|
136
|
+
# Tracks the path of the WAV the provider just synthesized.
|
|
137
|
+
# Used to defend against playing stale cached audio when synthesis silently fails.
|
|
138
|
+
$SynthesizedWavPath = $null
|
|
139
|
+
|
|
136
140
|
# If background music or reverb enabled and ffmpeg available, tell provider to skip playback
|
|
137
141
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
138
142
|
$env:AGENTVIBES_NO_PLAY = "1"
|
|
@@ -167,17 +171,25 @@ if ($UsePreSynth) {
|
|
|
167
171
|
$providerOutput = & $ProviderScript $Text 6>&1 2>&1
|
|
168
172
|
}
|
|
169
173
|
# Re-emit preserving colors from InformationRecords (Write-Host output)
|
|
174
|
+
# Also extract the synthesized WAV path from the provider's OK line.
|
|
170
175
|
foreach ($item in $providerOutput) {
|
|
176
|
+
$lineText = $null
|
|
171
177
|
if ($item -is [System.Management.Automation.InformationRecord]) {
|
|
172
178
|
$msg = $item.MessageData
|
|
173
179
|
if ($msg -is [System.Management.Automation.HostInformationMessage]) {
|
|
174
180
|
Write-Host $msg.Message -ForegroundColor $msg.ForegroundColor -NoNewline:$msg.NoNewLine
|
|
175
181
|
if (-not $msg.NoNewLine) { Write-Host }
|
|
182
|
+
$lineText = $msg.Message
|
|
176
183
|
} else {
|
|
177
184
|
Write-Host "$item"
|
|
185
|
+
$lineText = "$item"
|
|
178
186
|
}
|
|
179
187
|
} else {
|
|
180
188
|
Write-Host "$item"
|
|
189
|
+
$lineText = "$item"
|
|
190
|
+
}
|
|
191
|
+
if ($lineText -and $lineText -match '\[OK\]\s+Saved to:\s+(.+\.wav)') {
|
|
192
|
+
$SynthesizedWavPath = $Matches[1].Trim()
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
195
|
} else {
|
|
@@ -199,13 +211,23 @@ if ($UsePreSynth) {
|
|
|
199
211
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
200
212
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
201
213
|
|
|
202
|
-
# Find the WAV to post-process
|
|
214
|
+
# Find the WAV to post-process. Never fall back to "most recent on disk"
|
|
215
|
+
# because that plays a random stale cache file when synthesis silently fails.
|
|
203
216
|
$AudioDir = "$ClaudeDir\audio"
|
|
204
|
-
$RecentWav =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
$RecentWav = $null
|
|
218
|
+
if ($UsePreSynth) {
|
|
219
|
+
$RecentWav = Get-Item $PreSynthWav -ErrorAction SilentlyContinue
|
|
220
|
+
}
|
|
221
|
+
elseif ($SynthesizedWavPath -and (Test-Path $SynthesizedWavPath)) {
|
|
222
|
+
$cand = Get-Item $SynthesizedWavPath -ErrorAction SilentlyContinue
|
|
223
|
+
if ($cand -and $cand.Length -gt 0) {
|
|
224
|
+
$RecentWav = $cand
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (-not $RecentWav) {
|
|
228
|
+
Write-Host "[ERROR] No fresh synthesized WAV - refusing stale cache" -ForegroundColor Red
|
|
229
|
+
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
230
|
+
exit 1
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
if ($RecentWav -and $RecentWav.Length -gt 0) {
|