agentvibes 5.12.0-alpha.0 → 5.13.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/config.json +15 -1
- package/.agentvibes/install-manifest.json +342 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/commands/agent-vibes/commands.json +0 -20
- package/.claude/commands/agent-vibes/unmute.md +6 -2
- 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 +6 -6
- package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/language.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -0
- 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/agentvibes-session-id.sh +69 -0
- package/.claude/hooks/bmad-party-speak.sh +20 -4
- package/.claude/hooks/bmad-speak.sh +60 -2
- package/.claude/hooks/bmad-tts-injector.sh +20 -1
- package/.claude/hooks/bmad-voice-manager.sh +25 -3
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
- package/.claude/hooks/clawdbot-receiver.sh +19 -1
- package/.claude/hooks/elevenlabs-voices.sh +62 -0
- package/.claude/hooks/kokoro-installer.sh +20 -10
- package/.claude/hooks/language-manager.sh +10 -3
- package/.claude/hooks/party-set-room.sh +71 -0
- package/.claude/hooks/party-stage-roster.py +328 -0
- package/.claude/hooks/personality-manager.sh +19 -2
- package/.claude/hooks/piper-voice-manager.sh +3 -2
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
- package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
- package/.claude/hooks/play-tts-kokoro.sh +33 -6
- package/.claude/hooks/play-tts-soprano.sh +3 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +72 -27
- package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
- package/.claude/hooks/play-tts.sh +86 -69
- package/.claude/hooks/provider-catalog.json +352 -0
- package/.claude/hooks/provider-catalog.sh +161 -0
- package/.claude/hooks/provider-commands.sh +2 -1
- package/.claude/hooks/provider-manager.sh +47 -9
- package/.claude/hooks/python-resolver.sh +117 -0
- package/.claude/hooks/session-id.sh +56 -0
- package/.claude/hooks/session-start-tts.sh +39 -0
- package/.claude/hooks/speed-manager.sh +1 -1
- package/.claude/hooks/translate-manager.sh +3 -2
- package/.claude/hooks/translator.py +1 -1
- package/.claude/hooks/voice-manager.sh +242 -10
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
- package/.claude/hooks-windows/language-manager.ps1 +7 -1
- package/.claude/hooks-windows/personality-manager.ps1 +16 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
- package/.claude/hooks-windows/play-tts.ps1 +32 -3
- package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
- package/.claude/hooks-windows/provider-manager.ps1 +63 -8
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
- package/.claude/hooks-windows/tts-watcher.ps1 +84 -8
- package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
- package/.claude/piper-voices-dir.txt +1 -0
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +15 -0
- package/LICENSE +190 -190
- package/README.md +20 -6
- package/RELEASE_NOTES.md +81 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/mcp-server.js +121 -121
- 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/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/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +146 -49
- package/mcp-server/test_mcp_correctness.py +20 -2
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +0 -2
- package/package.json +1 -1
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +115 -11
- package/src/commands/bmad-voices.js +394 -394
- package/src/console/app.js +1 -9
- package/src/console/bling.js +71 -0
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +0 -4
- package/src/console/music-preview.js +79 -0
- package/src/console/navigation.js +0 -1
- package/src/console/tabs/agents-tab.js +14 -2
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +81 -5
- package/src/console/tabs/placeholder-tab.js +0 -2
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/settings-tab.js +207 -14
- package/src/console/tabs/setup-tab.js +39 -36
- package/src/console/tabs/voices-tab.js +102 -23
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/track-picker.js +82 -0
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +124 -10
- package/src/services/language-service.js +47 -47
- package/src/services/navigation-service.js +1 -1
- package/src/services/provider-catalog.js +412 -0
- package/src/services/provider-voice-catalog.js +52 -73
- package/src/services/tts-engine-service.js +29 -0
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -144
- package/src/utils/provider-validator.js +62 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/voice-assignments.json +8244 -8244
- package/.claude/commands/agent-vibes/language.md +0 -23
- package/.claude/commands/agent-vibes/learn.md +0 -67
- package/.claude/commands/agent-vibes/replay-target.md +0 -14
- package/.claude/commands/agent-vibes/target-voice.md +0 -26
- package/.claude/commands/agent-vibes/target.md +0 -30
- package/.claude/hooks/learn-manager.sh +0 -492
- package/.claude/hooks/replay-target-audio.sh +0 -95
- package/.claude/hooks-windows/learn-manager.ps1 +0 -241
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#
|
|
2
|
+
# File: .claude/hooks-windows/audio-cache-utils.ps1
|
|
3
|
+
#
|
|
4
|
+
# AgentVibes Audio Cache Utilities for Windows
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
param(
|
|
8
|
+
[Parameter(Position = 0)]
|
|
9
|
+
[ValidateSet('cleanup', 'stats', 'clear')]
|
|
10
|
+
[string]$Command
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Detect project-local audio dir (same logic as TTS scripts)
|
|
14
|
+
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
15
|
+
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
16
|
+
if (Test-Path $ProjectClaudeDir) {
|
|
17
|
+
$AudioDir = "$ProjectClaudeDir\audio"
|
|
18
|
+
} else {
|
|
19
|
+
$AudioDir = "$env:USERPROFILE\.claude\audio"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Ensure-AudioDir {
|
|
23
|
+
if (-not (Test-Path $AudioDir)) {
|
|
24
|
+
New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Get-AudioCacheSize {
|
|
29
|
+
Ensure-AudioDir
|
|
30
|
+
|
|
31
|
+
if (-not (Test-Path $AudioDir)) {
|
|
32
|
+
return 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$files = Get-ChildItem -Path $AudioDir -Filter "*.wav" -ErrorAction SilentlyContinue
|
|
36
|
+
$totalSize = 0
|
|
37
|
+
|
|
38
|
+
foreach ($file in $files) {
|
|
39
|
+
$totalSize += $file.Length
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return $totalSize
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Format-FileSize {
|
|
46
|
+
param([long]$Size)
|
|
47
|
+
|
|
48
|
+
if ($Size -lt 1KB) { return "$Size B" }
|
|
49
|
+
if ($Size -lt 1MB) { return "{0:N2} KB" -f ($Size / 1KB) }
|
|
50
|
+
if ($Size -lt 1GB) { return "{0:N2} MB" -f ($Size / 1MB) }
|
|
51
|
+
return "{0:N2} GB" -f ($Size / 1GB)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Get-CacheStats {
|
|
55
|
+
Ensure-AudioDir
|
|
56
|
+
|
|
57
|
+
$files = Get-ChildItem -Path $AudioDir -Filter "*.wav" -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum
|
|
58
|
+
|
|
59
|
+
$count = if ($files.Count -eq $null) { 0 } else { $files.Count }
|
|
60
|
+
$totalSize = if ($files.Sum -eq $null) { 0 } else { $files.Sum }
|
|
61
|
+
|
|
62
|
+
return @{
|
|
63
|
+
FileCount = $count
|
|
64
|
+
TotalSize = $totalSize
|
|
65
|
+
FormattedSize = Format-FileSize $totalSize
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Clear-Cache {
|
|
70
|
+
Ensure-AudioDir
|
|
71
|
+
|
|
72
|
+
$files = Get-ChildItem -Path $AudioDir -Filter "*.wav" -ErrorAction SilentlyContinue
|
|
73
|
+
|
|
74
|
+
if ($files.Count -eq 0) {
|
|
75
|
+
Write-Host "[OK] Cache already empty" -ForegroundColor Green
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$stats = Get-CacheStats
|
|
80
|
+
Write-Host "[CLEANUP] Clearing $($stats.FileCount) audio files ($($stats.FormattedSize))" -ForegroundColor Yellow
|
|
81
|
+
|
|
82
|
+
foreach ($file in $files) {
|
|
83
|
+
Remove-Item $file.FullName -Force -ErrorAction SilentlyContinue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Write-Host "[OK] Cache cleared" -ForegroundColor Green
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Show-CacheStats {
|
|
90
|
+
Ensure-AudioDir
|
|
91
|
+
|
|
92
|
+
$stats = Get-CacheStats
|
|
93
|
+
|
|
94
|
+
Write-Host ""
|
|
95
|
+
Write-Host "[STATS] Audio Cache Statistics" -ForegroundColor Cyan
|
|
96
|
+
Write-Host " Location: $AudioDir"
|
|
97
|
+
Write-Host " Files: $($stats.FileCount)"
|
|
98
|
+
Write-Host " Total Size: $($stats.FormattedSize)"
|
|
99
|
+
Write-Host ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Main command routing
|
|
103
|
+
switch ($Command) {
|
|
104
|
+
'stats' {
|
|
105
|
+
Show-CacheStats
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
'cleanup' {
|
|
109
|
+
Clear-Cache
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
'clear' {
|
|
113
|
+
Clear-Cache
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
default {
|
|
117
|
+
Show-CacheStats
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -146,7 +146,13 @@ switch ($Command) {
|
|
|
146
146
|
Write-Output "Usage: language-manager.ps1 check-voice <voice-name>"
|
|
147
147
|
exit 1
|
|
148
148
|
}
|
|
149
|
-
|
|
149
|
+
# AVI-S9.6 AC1: trimmed to names that exist in the 21-voice ElevenLabs
|
|
150
|
+
# catalog (src/services/provider-catalog.js ELEVENLABS_VOICES) — the
|
|
151
|
+
# list previously included "Antoni"/"Rachel"/"Domi"/"Charlotte", none of
|
|
152
|
+
# which exist there (the same class of bug design row 21 found in the
|
|
153
|
+
# now-deleted learn-manager.sh). Parity-asserted against the catalog by
|
|
154
|
+
# test/unit/provider-catalog-conformance.test.js.
|
|
155
|
+
$multilingual = @("Bella", "Matilda")
|
|
150
156
|
if ($Arg1 -in $multilingual) { Write-Output "yes" } else { Write-Output "no" }
|
|
151
157
|
}
|
|
152
158
|
|
|
@@ -29,6 +29,13 @@ if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude"))
|
|
|
29
29
|
|
|
30
30
|
$PersonalityFile = Join-Path $ConfigClaudeDir "tts-personality.txt"
|
|
31
31
|
|
|
32
|
+
# Load the generated Provider Catalog (SSOT) for the Piper default voice.
|
|
33
|
+
# FAIL-SAFE: consumers probe `Get-Command Get-CatalogDefaultVoice` and fall back
|
|
34
|
+
# to the legacy literal when the artifact is missing (installed-tree skew) —
|
|
35
|
+
# mirrors voice-manager-windows.ps1 / play-tts-kokoro.ps1 (AVI-S9.3/9.4).
|
|
36
|
+
$CatalogPs1 = Join-Path $ScriptDir 'provider-catalog.ps1'
|
|
37
|
+
if (Test-Path $CatalogPs1) { . $CatalogPs1 }
|
|
38
|
+
|
|
32
39
|
function Get-PersonalityData {
|
|
33
40
|
param([string]$Personality, [string]$Field)
|
|
34
41
|
$file = Join-Path $PersonalitiesDir "$Personality.md"
|
|
@@ -116,7 +123,15 @@ switch ($Command) {
|
|
|
116
123
|
|
|
117
124
|
# Get assigned voice
|
|
118
125
|
$assignedVoice = Get-PersonalityData $personality "piper_voice"
|
|
119
|
-
if (-not $assignedVoice) {
|
|
126
|
+
if (-not $assignedVoice) {
|
|
127
|
+
# Fallback to default Piper voice — sourced from the generated Provider
|
|
128
|
+
# Catalog SSOT (AVI-S9.4). FAIL-SAFE: legacy literal if artifact missing.
|
|
129
|
+
$assignedVoice = $null
|
|
130
|
+
if (Get-Command Get-CatalogDefaultVoice -ErrorAction SilentlyContinue) {
|
|
131
|
+
$assignedVoice = Get-CatalogDefaultVoice -Provider 'piper'
|
|
132
|
+
}
|
|
133
|
+
if (-not $assignedVoice) { $assignedVoice = "en_US-lessac-medium" }
|
|
134
|
+
}
|
|
120
135
|
|
|
121
136
|
if ($assignedVoice) {
|
|
122
137
|
Write-Output "Switching to assigned voice: $assignedVoice"
|
|
@@ -27,6 +27,20 @@ $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
|
27
27
|
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
28
28
|
if (Test-Path $ProjectClaudeDir) { $ClaudeDir = $ProjectClaudeDir } else { $ClaudeDir = "$env:USERPROFILE\.claude" }
|
|
29
29
|
|
|
30
|
+
# --- Default voice from the generated Provider Catalog (SSOT) -----------------
|
|
31
|
+
# FAIL-SAFE: keep the literal fallback for installed-tree skew (mirrors play-tts.sh
|
|
32
|
+
# PLAN_OK). The synth-time shape check below stays LENIENT (design §3.3) so a voice
|
|
33
|
+
# from a newer kokoro model still degrades audibly instead of bricking.
|
|
34
|
+
$KokoroDefault = "af_heart"
|
|
35
|
+
$CatalogPs1 = Join-Path $ScriptPath 'provider-catalog.ps1'
|
|
36
|
+
if (Test-Path $CatalogPs1) {
|
|
37
|
+
. $CatalogPs1
|
|
38
|
+
if (Get-Command Get-CatalogDefaultVoice -ErrorAction SilentlyContinue) {
|
|
39
|
+
$d = Get-CatalogDefaultVoice -Provider 'kokoro'
|
|
40
|
+
if ($d) { $KokoroDefault = $d }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
$AudioDir = "$ClaudeDir\audio"
|
|
31
45
|
$ConfigDir = "$ClaudeDir\config"
|
|
32
46
|
if (-not (Test-Path $AudioDir)) { New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null }
|
|
@@ -70,13 +84,15 @@ if ($VoiceOverride) {
|
|
|
70
84
|
}
|
|
71
85
|
# Kokoro voice ids look like af_heart, am_michael, bf_emma. Strip any ::display suffix.
|
|
72
86
|
if ($VoiceName -match '::') { $VoiceName = ($VoiceName -split '::')[0] }
|
|
73
|
-
if (-not $VoiceName) { $VoiceName =
|
|
87
|
+
if (-not $VoiceName) { $VoiceName = $KokoroDefault }
|
|
74
88
|
|
|
75
89
|
# Security: validate voice id (lowercase prefix + underscore + name).
|
|
76
|
-
#
|
|
90
|
+
# LENIENT shape check (design §3.3) — rejects path-traversal / injection but lets
|
|
91
|
+
# an unknown-but-shape-valid voice from a newer kokoro model through; falls back
|
|
92
|
+
# to the catalog default only on a shape violation.
|
|
77
93
|
if ($VoiceName -notmatch '^[a-z]{2}_[a-z0-9_]+$') {
|
|
78
|
-
Write-Host "[WARNING] Invalid Kokoro voice '$VoiceName' - falling back to
|
|
79
|
-
$VoiceName =
|
|
94
|
+
Write-Host "[WARNING] Invalid Kokoro voice '$VoiceName' - falling back to $KokoroDefault" -ForegroundColor Yellow
|
|
95
|
+
$VoiceName = $KokoroDefault
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
# --- Optional speed ----------------------------------------------------------
|
|
@@ -116,8 +116,9 @@ switch ($ActiveProvider) {
|
|
|
116
116
|
# by the SSH-receiver watcher via -ProviderOverride). This lets the Linux-side
|
|
117
117
|
# audio-effects.cfg row for llm:claude-code specify "piper" and have it honoured
|
|
118
118
|
# on Windows without requiring the Windows tts-provider.txt to be reconfigured.
|
|
119
|
-
# Priority:
|
|
120
|
-
#
|
|
119
|
+
# Priority: an explicit -ProviderOverride is AUTHORITATIVE — it wins over both the
|
|
120
|
+
# per-LLM $_LlmEngine column and the global tts-provider.txt default (see the
|
|
121
|
+
# $ProviderOverride guard in the engine-resolution block below).
|
|
121
122
|
if ($ProviderOverride) {
|
|
122
123
|
switch ($ProviderOverride) {
|
|
123
124
|
{ $_ -in "windows-piper", "piper" } {
|
|
@@ -308,8 +309,30 @@ $PlanVoiceIsOverride = $false
|
|
|
308
309
|
if ($ProviderOverride) {
|
|
309
310
|
# A fresh -ProviderOverride for THIS request must win over any stale/inherited
|
|
310
311
|
# AGENTVIBES_FORCE_PROVIDER in the environment (a validated allowlist only).
|
|
312
|
+
#
|
|
313
|
+
# The allowlist DERIVES from the Provider Catalog (SSOT): the Windows platform
|
|
314
|
+
# set PLUS the cross-platform forwarding aliases this script normalizes
|
|
315
|
+
# (piper→windows-piper, sapi→windows-sapi, and macos forwarded verbatim for
|
|
316
|
+
# SSH-relayed senders). FAIL-SAFE: the literal below is the legacy fallback
|
|
317
|
+
# used when the generated provider-catalog.ps1 is missing (installed-tree skew).
|
|
318
|
+
$ProviderOverrideAllowlist = @('piper','soprano','macos','windows-sapi','sapi','kokoro','windows-piper')
|
|
319
|
+
$__CatalogPs1 = Join-Path $ScriptPath 'provider-catalog.ps1'
|
|
320
|
+
if (Test-Path $__CatalogPs1) {
|
|
321
|
+
try {
|
|
322
|
+
. $__CatalogPs1
|
|
323
|
+
if (Get-Command Get-CatalogProvidersForPlatform -ErrorAction SilentlyContinue) {
|
|
324
|
+
$__WinSet = @(Get-CatalogProvidersForPlatform 'windows')
|
|
325
|
+
if ($__WinSet.Count -gt 0) {
|
|
326
|
+
# Windows synth providers + the forwarding aliases play-tts.ps1 normalizes.
|
|
327
|
+
$ProviderOverrideAllowlist = @($__WinSet + @('piper','sapi','macos') | Select-Object -Unique)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
# Keep the legacy fallback allowlist on any catalog load error.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
311
334
|
switch ($ProviderOverride) {
|
|
312
|
-
{ $_ -in
|
|
335
|
+
{ $_ -in $ProviderOverrideAllowlist } {
|
|
313
336
|
$env:AGENTVIBES_FORCE_PROVIDER = $ProviderOverride
|
|
314
337
|
}
|
|
315
338
|
}
|
|
@@ -522,6 +545,12 @@ else {
|
|
|
522
545
|
# ENGINE column or the global tts-provider.txt default.
|
|
523
546
|
$ProviderScript = "$HooksDir\play-tts-kokoro.ps1"
|
|
524
547
|
}
|
|
548
|
+
elseif ($ProviderOverride) {
|
|
549
|
+
# An explicit -ProviderOverride (forwarded from the SSH payload's "provider"
|
|
550
|
+
# field, or a preview) is AUTHORITATIVE and was already applied above. Do NOT
|
|
551
|
+
# let the per-LLM ENGINE column override it, or a receiver whose llm row
|
|
552
|
+
# defaults to piper silently swallows a forwarded windows-sapi request.
|
|
553
|
+
}
|
|
525
554
|
elseif ($_LlmEngine) {
|
|
526
555
|
# Accept both canonical Windows names and the cross-platform aliases the TUI
|
|
527
556
|
# writes (e.g. "piper" saved on a Linux/WSL install that is later read on
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# provider-catalog.ps1 — GENERATED FILE. DO NOT EDIT.
|
|
2
|
+
# DO NOT EDIT — generated from src/services/provider-catalog.js
|
|
3
|
+
# Regenerate with: node scripts/generate-provider-catalog.mjs
|
|
4
|
+
# content-hash: sha256:5e717024f1250a8f54d07291256e532606dba7b4a15ab9589305d1af6bba7f35
|
|
5
|
+
# Dot-source this file to load the catalog accessors; do not execute it.
|
|
6
|
+
|
|
7
|
+
$script:AgentVibesCatalogVersion = '5e717024f1250a8f'
|
|
8
|
+
|
|
9
|
+
$script:CatalogDefaults = @{
|
|
10
|
+
'soprano' = 'soprano-default'
|
|
11
|
+
'piper' = 'en_US-lessac-medium'
|
|
12
|
+
'kokoro' = 'af_heart'
|
|
13
|
+
'elevenlabs' = 'Sarah'
|
|
14
|
+
'macos' = 'Samantha'
|
|
15
|
+
'windows-sapi' = ''
|
|
16
|
+
'windows-piper' = 'en_US-lessac-medium'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
$script:CatalogDisplayNames = @{
|
|
20
|
+
'soprano' = 'Soprano TTS'
|
|
21
|
+
'piper' = 'Piper TTS'
|
|
22
|
+
'kokoro' = 'Kokoro TTS'
|
|
23
|
+
'elevenlabs' = 'ElevenLabs'
|
|
24
|
+
'macos' = 'macOS Say'
|
|
25
|
+
'windows-sapi' = 'Windows SAPI'
|
|
26
|
+
'windows-piper' = 'Piper TTS'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
$script:CatalogAlias = @{
|
|
30
|
+
'macos-say' = 'macos'
|
|
31
|
+
'say' = 'macos'
|
|
32
|
+
'sapi' = 'windows-sapi'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$script:CatalogPlatform = @{
|
|
36
|
+
'unix' = @('soprano','piper','kokoro','elevenlabs')
|
|
37
|
+
'darwin' = @('soprano','piper','kokoro','elevenlabs','macos')
|
|
38
|
+
'windows' = @('soprano','kokoro','windows-sapi','windows-piper')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
$script:CatalogVoices = @{
|
|
42
|
+
'soprano' = @('soprano-default')
|
|
43
|
+
'piper' = @()
|
|
44
|
+
'kokoro' = @('af_heart','af_alloy','af_aoede','af_bella','af_jessica','af_kore','af_nicole','af_nova','af_river','af_sarah','af_sky','am_adam','am_echo','am_eric','am_fenrir','am_liam','am_michael','am_onyx','am_puck','bf_alice','bf_emma','bf_isabella','bf_lily','bm_daniel','bm_fable','bm_george','bm_lewis','jf_alpha','jf_gongitsune','jf_nezumi','jf_tebukuro','jm_kumo','zf_xiaobei','zf_xiaoni','zf_xiaoxiao','zf_xiaoyi','zm_yunxi','zm_yunxia','zm_yunyang','ef_dora','em_alex','em_santa','ff_siwis','hf_alpha','hm_omega','if_sara','im_nicola','pf_dora','pm_alex','pm_santa','kf_alpha','km_hyunsu')
|
|
45
|
+
'elevenlabs' = @('Sarah','Roger','Laura','Charlie','George','Callum','River','Harry','Liam','Alice','Matilda','Will','Jessica','Eric','Bella','Chris','Brian','Daniel','Lily','Adam','Bill')
|
|
46
|
+
'macos' = @()
|
|
47
|
+
'windows-sapi' = @()
|
|
48
|
+
'windows-piper' = @()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$script:CatalogElevenLabs = @{
|
|
52
|
+
'sarah' = 'EXAVITQu4vr4xnSDxMaL'
|
|
53
|
+
'roger' = 'CwhRBWXzGAHq8TQ4Fs17'
|
|
54
|
+
'laura' = 'FGY2WhTYpPnrIDTdsKH5'
|
|
55
|
+
'charlie' = 'IKne3meq5aSn9XLyUdCD'
|
|
56
|
+
'george' = 'JBFqnCBsd6RMkjVDRZzb'
|
|
57
|
+
'callum' = 'N2lVS1w4EtoT3dr4eOWO'
|
|
58
|
+
'river' = 'SAz9YHcvj6GT2YYXdXww'
|
|
59
|
+
'harry' = 'SOYHLrjzK2X1ezoPC6cr'
|
|
60
|
+
'liam' = 'TX3LPaxmHKxFdv7VOQHJ'
|
|
61
|
+
'alice' = 'Xb7hH8MSUJpSbSDYk0k2'
|
|
62
|
+
'matilda' = 'XrExE9yKIg1WjnnlVkGX'
|
|
63
|
+
'will' = 'bIHbv24MWmeRgasZH58o'
|
|
64
|
+
'jessica' = 'cgSgspJ2msm6clMCkdW9'
|
|
65
|
+
'eric' = 'cjVigY5qzO86Huf0OWal'
|
|
66
|
+
'bella' = 'hpp4J3VqNfWAUOO0d1Us'
|
|
67
|
+
'chris' = 'iP95p4xoKVk53GoZ742B'
|
|
68
|
+
'brian' = 'nPczCjzI2devNBz1zQrb'
|
|
69
|
+
'daniel' = 'onwK4e9ZLuTAKqWW03F9'
|
|
70
|
+
'lily' = 'pFZP5JQG7iQjIQuC4Bku'
|
|
71
|
+
'adam' = 'pNInz6obpgDQGcFmaJgB'
|
|
72
|
+
'bill' = 'pqHfZKP75CvOlQylNhV4'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Get-CatalogCanonicalProvider {
|
|
76
|
+
param([Parameter(Mandatory)][string]$Provider)
|
|
77
|
+
$key = $Provider.Trim().ToLowerInvariant()
|
|
78
|
+
if ($script:CatalogDefaults.ContainsKey($key)) { return $key }
|
|
79
|
+
if ($script:CatalogAlias.ContainsKey($key)) { return $script:CatalogAlias[$key] }
|
|
80
|
+
return $null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Get-CatalogDefaultVoice {
|
|
84
|
+
param([Parameter(Mandatory)][string]$Provider)
|
|
85
|
+
$p = Get-CatalogCanonicalProvider $Provider
|
|
86
|
+
if ($null -eq $p) { return $null }
|
|
87
|
+
return $script:CatalogDefaults[$p]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Get-CatalogDisplayName {
|
|
91
|
+
param([Parameter(Mandatory)][string]$Provider)
|
|
92
|
+
$p = Get-CatalogCanonicalProvider $Provider
|
|
93
|
+
if ($null -eq $p) { return $Provider }
|
|
94
|
+
return $script:CatalogDisplayNames[$p]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Get-CatalogProvidersForPlatform {
|
|
98
|
+
param([Parameter(Mandatory)][string]$Platform)
|
|
99
|
+
$key = $Platform.Trim().ToLowerInvariant()
|
|
100
|
+
if ($script:CatalogPlatform.ContainsKey($key)) { return $script:CatalogPlatform[$key] }
|
|
101
|
+
return @()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function Get-CatalogVoices {
|
|
105
|
+
param([Parameter(Mandatory)][string]$Provider)
|
|
106
|
+
$p = Get-CatalogCanonicalProvider $Provider
|
|
107
|
+
if ($null -eq $p) { return @() }
|
|
108
|
+
return $script:CatalogVoices[$p]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Test-CatalogVoice {
|
|
112
|
+
param([Parameter(Mandatory)][string]$Provider, [Parameter(Mandatory)][AllowEmptyString()][string]$Voice)
|
|
113
|
+
$p = Get-CatalogCanonicalProvider $Provider
|
|
114
|
+
if ($null -eq $p) { return $null }
|
|
115
|
+
$lc = $Voice.Trim().ToLowerInvariant()
|
|
116
|
+
switch ($p) {
|
|
117
|
+
'kokoro' {
|
|
118
|
+
if ($script:CatalogVoices['kokoro'] -contains $lc) { return $lc }
|
|
119
|
+
return $null
|
|
120
|
+
}
|
|
121
|
+
'elevenlabs' {
|
|
122
|
+
if ($script:CatalogElevenLabs.ContainsKey($lc)) { return $script:CatalogElevenLabs[$lc] }
|
|
123
|
+
if ($Voice -match '^[A-Za-z0-9]{20}$') { return $Voice }
|
|
124
|
+
return $null
|
|
125
|
+
}
|
|
126
|
+
'soprano' {
|
|
127
|
+
if ($lc -eq '' -or $lc -eq 'soprano' -or $lc -eq 'soprano-default') { return 'soprano-default' }
|
|
128
|
+
return $null
|
|
129
|
+
}
|
|
130
|
+
{ $_ -eq 'piper' -or $_ -eq 'windows-piper' } {
|
|
131
|
+
if ($Voice -match '^[a-z]{2,3}_[A-Z]{2}-' -or $Voice -like '*::*') { return $Voice }
|
|
132
|
+
return $null
|
|
133
|
+
}
|
|
134
|
+
default {
|
|
135
|
+
if ($Voice -ne '') { return $Voice }
|
|
136
|
+
if ($p -eq 'windows-sapi') { return '' }
|
|
137
|
+
return $null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -15,7 +15,25 @@ param(
|
|
|
15
15
|
|
|
16
16
|
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
17
17
|
$ProviderFile = "$ClaudeDir\tts-provider.txt"
|
|
18
|
-
$ValidProviders
|
|
18
|
+
# $ValidProviders DERIVES from the generated Provider Catalog (SSOT): the Windows
|
|
19
|
+
# platform set (kokoro is cross-platform — play-tts-kokoro.ps1 exists; elevenlabs
|
|
20
|
+
# is NOT playable on Windows and is therefore absent). Dot-source the generated
|
|
21
|
+
# provider-catalog.ps1 and pull `windows`. FAIL-SAFE: the literal below is the
|
|
22
|
+
# legacy fallback used when the catalog artifact is missing (installed-tree skew)
|
|
23
|
+
# — mirrors AVI-S9.3's probe/fallback pattern. See AVI-S8.1 / AVI-S9.5.
|
|
24
|
+
$ValidProviders = @('windows-piper', 'windows-sapi', 'soprano', 'kokoro')
|
|
25
|
+
$__CatalogPs1 = Join-Path $PSScriptRoot 'provider-catalog.ps1'
|
|
26
|
+
if (Test-Path $__CatalogPs1) {
|
|
27
|
+
try {
|
|
28
|
+
. $__CatalogPs1
|
|
29
|
+
if (Get-Command Get-CatalogProvidersForPlatform -ErrorAction SilentlyContinue) {
|
|
30
|
+
$__WinSet = @(Get-CatalogProvidersForPlatform 'windows')
|
|
31
|
+
if ($__WinSet.Count -gt 0) { $ValidProviders = $__WinSet }
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
# Keep the legacy fallback literal on any catalog load error.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
|
|
20
38
|
# Ensure claude directory exists
|
|
21
39
|
if (-not (Test-Path $ClaudeDir)) {
|
|
@@ -35,6 +53,14 @@ function Get-ActiveProvider {
|
|
|
35
53
|
function Get-AvailableProviders {
|
|
36
54
|
$available = @()
|
|
37
55
|
|
|
56
|
+
# Fast path: skip the live install probes (soprano HTTP + kokoro python
|
|
57
|
+
# spawns). Those probes are what make `provider list` occasionally stall for
|
|
58
|
+
# tens of seconds under load; when the caller only needs the provider NAMES
|
|
59
|
+
# (e.g. the parallel test harness, or an offline listing) set
|
|
60
|
+
# AGENTVIBES_PROVIDER_LIST_NO_PROBE=1 to report installed=$false without
|
|
61
|
+
# touching the network or spawning interpreters. Default behavior unchanged.
|
|
62
|
+
$noProbe = ($env:AGENTVIBES_PROVIDER_LIST_NO_PROBE -eq '1')
|
|
63
|
+
|
|
38
64
|
# Always available
|
|
39
65
|
$available += @{
|
|
40
66
|
name = "windows-sapi"
|
|
@@ -59,15 +85,18 @@ function Get-AvailableProviders {
|
|
|
59
85
|
$null = Get-Command soprano -ErrorAction Stop
|
|
60
86
|
$sopranoInstalled = $true
|
|
61
87
|
} catch {
|
|
62
|
-
# Also check if a Soprano server is running (try both API paths)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
$sopranoInstalled = $true
|
|
66
|
-
} catch {
|
|
88
|
+
# Also check if a Soprano server is running (try both API paths) — unless
|
|
89
|
+
# the fast path is requested, in which case skip the network probes.
|
|
90
|
+
if (-not $noProbe) {
|
|
67
91
|
try {
|
|
68
|
-
$null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
|
|
92
|
+
$null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/gradio_api/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
|
|
69
93
|
$sopranoInstalled = $true
|
|
70
|
-
} catch {
|
|
94
|
+
} catch {
|
|
95
|
+
try {
|
|
96
|
+
$null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
|
|
97
|
+
$sopranoInstalled = $true
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
71
100
|
}
|
|
72
101
|
}
|
|
73
102
|
|
|
@@ -77,6 +106,32 @@ function Get-AvailableProviders {
|
|
|
77
106
|
installed = $sopranoInstalled
|
|
78
107
|
}
|
|
79
108
|
|
|
109
|
+
# Check if Kokoro is installed. Mirror validateKokoroInstallation() in
|
|
110
|
+
# src/utils/provider-validator.js, but use importlib.util.find_spec so we do
|
|
111
|
+
# NOT actually import kokoro — importing the 82M-param neural lib takes
|
|
112
|
+
# several seconds and would make `provider list` hang (AVI-S8.1). find_spec
|
|
113
|
+
# only confirms the packages are importable, which is all we need here.
|
|
114
|
+
$kokoroInstalled = $false
|
|
115
|
+
$kokoroProbe = "import importlib.util as u,sys; sys.exit(0 if all(u.find_spec(m) for m in ('kokoro','soundfile','numpy')) else 1)"
|
|
116
|
+
foreach ($py in ($(if ($noProbe) { @() } else { @('py', 'python', 'python3') }))) {
|
|
117
|
+
if (-not (Get-Command $py -ErrorAction SilentlyContinue)) { continue }
|
|
118
|
+
try {
|
|
119
|
+
$null = & $py -c $kokoroProbe 2>$null
|
|
120
|
+
# Only stop once an interpreter actually HAS the packages — mirrors
|
|
121
|
+
# validateKokoroInstallation(), which tries every interpreter (the
|
|
122
|
+
# packages may live under python/python3, not the `py` launcher).
|
|
123
|
+
if ($LASTEXITCODE -eq 0) { $kokoroInstalled = $true; break }
|
|
124
|
+
} catch {
|
|
125
|
+
# interpreter present but failed to run the probe — try next
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$available += @{
|
|
130
|
+
name = "kokoro"
|
|
131
|
+
description = "Kokoro TTS (Neural, pip install kokoro-onnx soundfile numpy)"
|
|
132
|
+
installed = $kokoroInstalled
|
|
133
|
+
}
|
|
134
|
+
|
|
80
135
|
return $available
|
|
81
136
|
}
|
|
82
137
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# File: .claude/hooks-windows/soprano-gradio-synth.py
|
|
4
|
+
#
|
|
5
|
+
# AgentVibes - Finally, your AI Agents can Talk Back!
|
|
6
|
+
# Website: https://agentvibes.org
|
|
7
|
+
# Repository: https://github.com/paulpreibisch/AgentVibes
|
|
8
|
+
#
|
|
9
|
+
# Co-created by Paul Preibisch with Claude AI
|
|
10
|
+
# Copyright (c) 2025 Paul Preibisch
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
#
|
|
24
|
+
# ---
|
|
25
|
+
#
|
|
26
|
+
# Soprano Gradio WebUI synthesizer helper.
|
|
27
|
+
# Calls the Soprano WebUI's Gradio API and saves the result as a WAV file.
|
|
28
|
+
# Uses only Python stdlib (json, sys, urllib) — no extra dependencies.
|
|
29
|
+
#
|
|
30
|
+
# Usage: python3 soprano-gradio-synth.py "text to speak" output.wav [port]
|
|
31
|
+
#
|
|
32
|
+
"""
|
|
33
|
+
Soprano Gradio WebUI synthesizer helper for AgentVibes.
|
|
34
|
+
|
|
35
|
+
Calls the Soprano WebUI's Gradio API via the Server-Sent Events (SSE) protocol
|
|
36
|
+
and downloads the generated audio as a WAV file.
|
|
37
|
+
|
|
38
|
+
Flow:
|
|
39
|
+
1. Submit generation request -> get event_id
|
|
40
|
+
2. Poll SSE stream for audio file URL
|
|
41
|
+
3. Download WAV file to output path
|
|
42
|
+
|
|
43
|
+
See: https://github.com/ekwek1/soprano
|
|
44
|
+
"""
|
|
45
|
+
import json
|
|
46
|
+
import sys
|
|
47
|
+
import urllib.request
|
|
48
|
+
import urllib.error
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def synth(text: str, output_path: str, port: int = 7860) -> None:
|
|
52
|
+
base = f"http://127.0.0.1:{port}"
|
|
53
|
+
|
|
54
|
+
# Step 1: Submit generation request
|
|
55
|
+
# Args: text, temperature, top_p, repetition_penalty, chunk_size, streaming
|
|
56
|
+
payload = json.dumps({
|
|
57
|
+
"data": [text, 0.0, 0.95, 1.2, 1, False]
|
|
58
|
+
}).encode()
|
|
59
|
+
|
|
60
|
+
event_id = submit_request(base, payload)
|
|
61
|
+
|
|
62
|
+
# Step 2: Poll SSE stream for audio file URL
|
|
63
|
+
audio_url = poll_for_result(base, event_id)
|
|
64
|
+
|
|
65
|
+
# Step 3: Download the audio file (validate localhost to prevent SSRF)
|
|
66
|
+
download_file(audio_url, output_path, port)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def submit_request(base: str, payload: bytes) -> str:
|
|
70
|
+
"""Submit generation request to Gradio API, return event_id."""
|
|
71
|
+
for api_base in ["/gradio_api/call", "/call"]:
|
|
72
|
+
url = f"{base}{api_base}/generate_speech"
|
|
73
|
+
req = urllib.request.Request(
|
|
74
|
+
url,
|
|
75
|
+
data=payload,
|
|
76
|
+
headers={"Content-Type": "application/json"},
|
|
77
|
+
)
|
|
78
|
+
try:
|
|
79
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
80
|
+
return json.loads(resp.read())["event_id"]
|
|
81
|
+
except urllib.error.URLError:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
raise RuntimeError("Could not reach Soprano WebUI API")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def poll_for_result(base: str, event_id: str) -> str:
|
|
88
|
+
"""Poll SSE endpoint until audio file URL is returned."""
|
|
89
|
+
for api_base in ["/gradio_api/call", "/call"]:
|
|
90
|
+
url = f"{base}{api_base}/generate_speech/{event_id}"
|
|
91
|
+
req = urllib.request.Request(url)
|
|
92
|
+
try:
|
|
93
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
94
|
+
for raw_line in resp:
|
|
95
|
+
line = raw_line.decode("utf-8", errors="replace").strip()
|
|
96
|
+
if not line.startswith("data: "):
|
|
97
|
+
continue
|
|
98
|
+
try:
|
|
99
|
+
parsed = json.loads(line[6:])
|
|
100
|
+
except json.JSONDecodeError:
|
|
101
|
+
continue
|
|
102
|
+
# Response format: [{"path": "...", "url": "...", ...}, "status string"]
|
|
103
|
+
if isinstance(parsed, list) and len(parsed) >= 1:
|
|
104
|
+
audio = parsed[0]
|
|
105
|
+
if isinstance(audio, dict) and "url" in audio:
|
|
106
|
+
return audio["url"]
|
|
107
|
+
break
|
|
108
|
+
except urllib.error.URLError:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
raise RuntimeError("No audio URL in Soprano response")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def download_file(url: str, output_path: str, port: int = 7860) -> None:
|
|
115
|
+
"""Download audio file from Gradio file server.
|
|
116
|
+
|
|
117
|
+
Security: Validates URL points to localhost to prevent SSRF.
|
|
118
|
+
"""
|
|
119
|
+
# Validate URL is localhost to prevent SSRF via crafted Gradio responses
|
|
120
|
+
allowed_prefixes = [
|
|
121
|
+
f"http://127.0.0.1:{port}/",
|
|
122
|
+
f"http://localhost:{port}/",
|
|
123
|
+
]
|
|
124
|
+
if not any(url.startswith(prefix) for prefix in allowed_prefixes):
|
|
125
|
+
raise RuntimeError(f"Refused download from non-localhost URL: {url[:80]}")
|
|
126
|
+
|
|
127
|
+
req = urllib.request.Request(url)
|
|
128
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
129
|
+
with open(output_path, "wb") as f:
|
|
130
|
+
while True:
|
|
131
|
+
chunk = resp.read(8192)
|
|
132
|
+
if not chunk:
|
|
133
|
+
break
|
|
134
|
+
f.write(chunk)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
if __name__ == "__main__":
|
|
138
|
+
if len(sys.argv) < 3:
|
|
139
|
+
print(f"Usage: {sys.argv[0]} \"text\" output.wav [port]", file=sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
text = sys.argv[1]
|
|
143
|
+
output = sys.argv[2]
|
|
144
|
+
port = int(sys.argv[3]) if len(sys.argv) > 3 else 7860
|
|
145
|
+
if not (1 <= port <= 65535):
|
|
146
|
+
print(f"Error: Invalid port number: {port}", file=sys.stderr)
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
synth(text, output, port)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
153
|
+
sys.exit(1)
|