agentvibes 4.6.8 → 5.0.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/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 +373 -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 +101 -9
- package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
- package/README.md +98 -6
- package/RELEASE_NOTES.md +35 -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 +1896 -1886
- package/src/console/tabs/music-tab.js +1046 -1039
- package/src/console/tabs/placeholder-tab.js +81 -80
- package/src/console/tabs/settings-tab.js +939 -3988
- package/src/console/tabs/setup-tab.js +1811 -0
- package/src/console/tabs/voices-tab.js +1720 -1714
- package/src/installer.js +6147 -6092
- package/src/services/llm-provider-service.js +407 -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
|
+
}
|