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.
Files changed (35) hide show
  1. package/.agentvibes/bmad-voice-map.json +104 -0
  2. package/.agentvibes/config.json +13 -12
  3. package/.agentvibes/copilot-sessions.log +4 -0
  4. package/.claude/audio/tracks/README.md +51 -52
  5. package/.claude/config/audio-effects-bmad.cfg +50 -0
  6. package/.claude/config/audio-effects.cfg +4 -4
  7. package/.claude/config/background-music-enabled.txt +1 -0
  8. package/.claude/config/personality.txt +1 -0
  9. package/.claude/hooks/play-tts-piper.sh +3 -1
  10. package/.claude/hooks/play-tts.sh +373 -301
  11. package/.claude/hooks/session-start-tts.sh +81 -81
  12. package/.claude/hooks-windows/audio-processor.ps1 +181 -0
  13. package/.claude/hooks-windows/play-tts-piper.ps1 +259 -245
  14. package/.claude/hooks-windows/play-tts.ps1 +101 -9
  15. package/.claude/hooks-windows/session-start-tts.ps1 +114 -114
  16. package/README.md +98 -6
  17. package/RELEASE_NOTES.md +35 -0
  18. package/bin/bmad-speak.js +16 -8
  19. package/mcp-server/server.py +15 -8
  20. package/package.json +1 -1
  21. package/src/console/app.js +899 -897
  22. package/src/console/footer-config.js +50 -50
  23. package/src/console/navigation.js +65 -65
  24. package/src/console/tabs/agents-tab.js +1896 -1886
  25. package/src/console/tabs/music-tab.js +1046 -1039
  26. package/src/console/tabs/placeholder-tab.js +81 -80
  27. package/src/console/tabs/settings-tab.js +939 -3988
  28. package/src/console/tabs/setup-tab.js +1811 -0
  29. package/src/console/tabs/voices-tab.js +1720 -1714
  30. package/src/installer.js +6147 -6092
  31. package/src/services/llm-provider-service.js +407 -0
  32. package/src/services/navigation-service.js +123 -123
  33. package/src/services/tts-engine-service.js +69 -0
  34. package/.claude/audio/tracks/dreamy_house_loop.mp3 +0 -0
  35. 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
- if ($VoiceName -match '::') {
78
- $parts = $VoiceName -split '::'
79
- $VoiceName = $parts[0]
80
- $SpeakerName = if ($parts.Length -ge 2) { $parts[1] } else { "" }
81
- Remove-Item env:PIPER_SPEAKER -ErrorAction SilentlyContinue
82
-
83
- if ($SpeakerName) {
84
- # Primary: look up in voice-assignments.json catalog (libritts_speakers keyed by speaker index)
85
- # Derive project root from this script's location: .claude/hooks-windows/ -> project root
86
- $PiperScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
87
- $PiperProjectRoot = Split-Path -Parent (Split-Path -Parent $PiperScriptRoot)
88
- $VoiceAssignmentsPath = Join-Path $PiperProjectRoot "voice-assignments.json"
89
- # Fallback: global AgentVibes install if not found in project
90
- if (-not (Test-Path $VoiceAssignmentsPath)) {
91
- $VoiceAssignmentsPath = Join-Path $env:USERPROFILE "AgentVibes\voice-assignments.json"
92
- }
93
- $SpeakerResolved = $false
94
- if (Test-Path $VoiceAssignmentsPath) {
95
- try {
96
- $vaData = Get-Content $VoiceAssignmentsPath -Raw | ConvertFrom-Json
97
- foreach ($prop in $vaData.libritts_speakers.PSObject.Properties) {
98
- if ($prop.Value.voice_name -eq $SpeakerName) {
99
- $env:PIPER_SPEAKER = $prop.Name
100
- $SpeakerResolved = $true
101
- break
102
- }
103
- }
104
- } catch { }
105
- }
106
- # Fallback: check patched speaker_id_map in the .onnx.json
107
- if (-not $SpeakerResolved) {
108
- $OnnxJsonPath = "$VoicesDir\$VoiceName.onnx.json"
109
- if (Test-Path $OnnxJsonPath) {
110
- try {
111
- $onnxData = Get-Content $OnnxJsonPath -Raw | ConvertFrom-Json
112
- $speakerIdMap = $onnxData.speaker_id_map
113
- if ($speakerIdMap -and $speakerIdMap.PSObject.Properties[$SpeakerName]) {
114
- $env:PIPER_SPEAKER = [string]$speakerIdMap.PSObject.Properties[$SpeakerName].Value
115
- }
116
- } catch { }
117
- }
118
- }
119
- }
120
- } else {
121
- # No multi-speaker syntax — clear any stale speaker env var
122
- Remove-Item env:PIPER_SPEAKER -ErrorAction SilentlyContinue
123
- }
124
-
125
- # Default voice if not specified
126
- # Prefer en_US-lessac-medium (bundled/commonly installed) over en_US-ryan-high
127
- if (-not $VoiceName) {
128
- $UserClaudePiperDir = "$env:USERPROFILE\.claude\piper-voices"
129
- if (Test-Path "$UserClaudePiperDir\en_US-lessac-medium.onnx") {
130
- $VoiceName = "en_US-lessac-medium"
131
- } elseif (Test-Path "$UserClaudePiperDir\en_US-ryan-high.onnx") {
132
- $VoiceName = "en_US-ryan-high"
133
- } else {
134
- # Fallback: use first available .onnx file, or default name for auto-download
135
- $firstVoice = Get-ChildItem -Path $UserClaudePiperDir -Filter "*.onnx" -ErrorAction SilentlyContinue | Select-Object -First 1
136
- if ($firstVoice) {
137
- $VoiceName = $firstVoice.BaseName
138
- } else {
139
- $VoiceName = "en_US-lessac-medium"
140
- }
141
- }
142
- }
143
-
144
- # Security: Validate voice name to prevent path traversal
145
- # Only allow alphanumeric, underscore, hyphen, and period
146
- if ($VoiceName -notmatch '^[a-zA-Z0-9_\-\.]+$') {
147
- Write-Host "[ERROR] Invalid voice name: $VoiceName" -ForegroundColor Red
148
- exit 1
149
- }
150
-
151
- # Resolve voice model path and validate it stays within VoicesDir
152
- $VoiceModelFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx")
153
- $VoiceJsonFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx.json")
154
- $ResolvedVoicesDir = [System.IO.Path]::GetFullPath($VoicesDir)
155
- if (-not $VoiceModelFile.StartsWith($ResolvedVoicesDir)) {
156
- Write-Host "[ERROR] Voice path outside voices directory" -ForegroundColor Red
157
- exit 1
158
- }
159
-
160
- # Check if voice model exists, download if missing
161
- if (-not (Test-Path $VoiceModelFile)) {
162
- Write-Host "[DOWNLOAD] Voice model: $VoiceName" -ForegroundColor Yellow
163
-
164
- # Try to download from Hugging Face
165
- # Voice name format: {lang}_{region}-{speaker}-{quality}
166
- # HF path format: {lang}/{lang}_{region}/{speaker}/{quality}/{voicename}.onnx
167
- try {
168
- # Parse voice name to build correct HF path
169
- # e.g. en_US-ryan-high -> en/en_US/ryan/high/en_US-ryan-high.onnx
170
- if ($VoiceName -match '^([a-z]{2})_([A-Z]{2})-([a-zA-Z0-9_]+)-([a-z]+)$') {
171
- $Lang = $Matches[1]
172
- $LangRegion = "$($Matches[1])_$($Matches[2])"
173
- $Speaker = $Matches[3]
174
- $Quality = $Matches[4]
175
- $HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$Lang/$LangRegion/$Speaker/$Quality"
176
- } else {
177
- # Fallback for non-standard voice names
178
- $HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high"
179
- }
180
- $ModelUrl = "$HFBase/$VoiceName.onnx"
181
- $JsonUrl = "$HFBase/$VoiceName.onnx.json"
182
-
183
- Write-Host " Downloading model..." -ForegroundColor Cyan
184
- Invoke-WebRequest -Uri $ModelUrl -OutFile $VoiceModelFile -ErrorAction Stop
185
- Write-Host " Downloading config..." -ForegroundColor Cyan
186
- Invoke-WebRequest -Uri $JsonUrl -OutFile $VoiceJsonFile -ErrorAction Stop
187
- Write-Host "[OK] Voice model downloaded" -ForegroundColor Green
188
- }
189
- catch {
190
- Write-Host "[ERROR] Failed to download voice model: $_" -ForegroundColor Red
191
- Write-Host "Make sure you have internet connection" -ForegroundColor Yellow
192
- exit 1
193
- }
194
- }
195
-
196
- # Sanitize text for speech - strip only dangerous shell metacharacters
197
- $Text = $Text -replace '\\', ' '
198
- $Text = $Text -replace '[{}<>|`~^;]', ''
199
- $Text = $Text -replace '\s+', ' '
200
- $Text = $Text.Trim()
201
-
202
- # Create audio file path SECURITY: use random name instead of predictable timestamp (#130)
203
- $AudioFile = "$AudioDir\tts-$([System.IO.Path]::GetRandomFileName() -replace '\..*').wav"
204
-
205
- # Synthesize with Piper
206
- try {
207
- Write-Host "[SYNTH] Synthesizing with Piper..." -ForegroundColor Cyan
208
-
209
- # Run Piper with text input
210
- # Add --speaker for multi-speaker models (e.g. libritts-high with speaker 9)
211
- $piperArgs = @("--model", $VoiceModelFile, "--output-file", $AudioFile)
212
- if ($env:PIPER_SPEAKER) {
213
- $piperArgs += @("--speaker", $env:PIPER_SPEAKER)
214
- }
215
- $Text | & $PiperExe @piperArgs 2>$null
216
-
217
- if (-not (Test-Path $AudioFile)) {
218
- Write-Host "[ERROR] Piper synthesis failed" -ForegroundColor Red
219
- exit 1
220
- }
221
-
222
- # Display results
223
- Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
224
- Write-Host "[VOICE] Voice used: $VoiceName (Piper)" -ForegroundColor Green
225
-
226
- # Play the audio using built-in Windows audio player (skip if AGENTVIBES_NO_PLAY is set)
227
- if (-not $env:AGENTVIBES_NO_PLAY) {
228
- $player = $null
229
- try {
230
- $player = New-Object System.Media.SoundPlayer $AudioFile
231
- $player.PlaySync()
232
- }
233
- catch {
234
- Write-Host "[WARNING] Could not play audio (SoundPlayer unavailable)" -ForegroundColor Yellow
235
- Write-Host "Audio saved to: $AudioFile" -ForegroundColor Gray
236
- }
237
- finally {
238
- if ($player) { $player.Dispose() }
239
- }
240
- }
241
- }
242
- catch {
243
- Write-Host "[ERROR] Error running Piper: $_" -ForegroundColor Red
244
- exit 1
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
+ }