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.
Files changed (129) hide show
  1. package/.agentvibes/config.json +15 -1
  2. package/.agentvibes/install-manifest.json +342 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/commands/agent-vibes/commands.json +0 -20
  5. package/.claude/commands/agent-vibes/unmute.md +6 -2
  6. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  7. package/.claude/commands/agent-vibes-rdp.md +24 -24
  8. package/.claude/config/audio-effects.cfg +6 -6
  9. package/.claude/config/audio-effects.cfg.bak-kokoro +7 -0
  10. package/.claude/config/background-music-enabled.txt +1 -1
  11. package/.claude/config/background-music-position.txt +27 -0
  12. package/.claude/config/background-music-volume.txt +1 -0
  13. package/.claude/config/background-music.cfg +1 -0
  14. package/.claude/config/background-music.txt +1 -0
  15. package/.claude/config/language.txt +1 -0
  16. package/.claude/config/personality.txt +1 -0
  17. package/.claude/config/tts-speech-rate.txt +1 -0
  18. package/.claude/config/tts-verbosity.txt +1 -0
  19. package/.claude/docs/TERMUX_SETUP.md +408 -408
  20. package/.claude/github-star-reminder.txt +1 -1
  21. package/.claude/hooks/agentvibes-session-id.sh +69 -0
  22. package/.claude/hooks/bmad-party-speak.sh +20 -4
  23. package/.claude/hooks/bmad-speak.sh +60 -2
  24. package/.claude/hooks/bmad-tts-injector.sh +20 -1
  25. package/.claude/hooks/bmad-voice-manager.sh +25 -3
  26. package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
  27. package/.claude/hooks/clawdbot-receiver.sh +19 -1
  28. package/.claude/hooks/elevenlabs-voices.sh +62 -0
  29. package/.claude/hooks/kokoro-installer.sh +20 -10
  30. package/.claude/hooks/language-manager.sh +10 -3
  31. package/.claude/hooks/party-set-room.sh +71 -0
  32. package/.claude/hooks/party-stage-roster.py +328 -0
  33. package/.claude/hooks/personality-manager.sh +19 -2
  34. package/.claude/hooks/piper-voice-manager.sh +3 -2
  35. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
  36. package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
  37. package/.claude/hooks/play-tts-kokoro.sh +33 -6
  38. package/.claude/hooks/play-tts-soprano.sh +3 -2
  39. package/.claude/hooks/play-tts-ssh-remote.sh +72 -27
  40. package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
  41. package/.claude/hooks/play-tts.sh +86 -69
  42. package/.claude/hooks/provider-catalog.json +352 -0
  43. package/.claude/hooks/provider-catalog.sh +161 -0
  44. package/.claude/hooks/provider-commands.sh +2 -1
  45. package/.claude/hooks/provider-manager.sh +47 -9
  46. package/.claude/hooks/python-resolver.sh +117 -0
  47. package/.claude/hooks/session-id.sh +56 -0
  48. package/.claude/hooks/session-start-tts.sh +39 -0
  49. package/.claude/hooks/speed-manager.sh +1 -1
  50. package/.claude/hooks/translate-manager.sh +3 -2
  51. package/.claude/hooks/translator.py +1 -1
  52. package/.claude/hooks/voice-manager.sh +242 -10
  53. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  54. package/.claude/hooks-windows/language-manager.ps1 +7 -1
  55. package/.claude/hooks-windows/personality-manager.ps1 +16 -1
  56. package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
  57. package/.claude/hooks-windows/play-tts.ps1 +32 -3
  58. package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
  59. package/.claude/hooks-windows/provider-manager.ps1 +63 -8
  60. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  61. package/.claude/hooks-windows/tts-watcher.ps1 +84 -8
  62. package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
  63. package/.claude/piper-voices-dir.txt +1 -0
  64. package/.clawdbot/README.md +105 -105
  65. package/.mcp.json +15 -0
  66. package/LICENSE +190 -190
  67. package/README.md +20 -6
  68. package/RELEASE_NOTES.md +81 -0
  69. package/WINDOWS-SETUP.md +208 -208
  70. package/bin/agent-vibes +39 -39
  71. package/bin/mcp-server.js +121 -121
  72. package/bin/test-bmad-pr +78 -78
  73. package/mcp-server/QUICK_START.md +203 -203
  74. package/mcp-server/README.md +345 -345
  75. package/mcp-server/examples/claude_desktop_config.json +11 -11
  76. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  77. package/mcp-server/examples/custom_instructions.md +169 -169
  78. package/mcp-server/pyproject.toml +52 -52
  79. package/mcp-server/requirements.txt +2 -2
  80. package/mcp-server/server.py +146 -49
  81. package/mcp-server/test_mcp_correctness.py +20 -2
  82. package/mcp-server/test_server.py +395 -395
  83. package/mcp-server/test_windows_script_parity.py +0 -2
  84. package/package.json +1 -1
  85. package/src/cli/list-personalities.js +110 -110
  86. package/src/cli/list-voices.js +115 -11
  87. package/src/commands/bmad-voices.js +394 -394
  88. package/src/console/app.js +1 -9
  89. package/src/console/bling.js +71 -0
  90. package/src/console/brand-colors.js +13 -13
  91. package/src/console/constants/personalities.js +44 -44
  92. package/src/console/footer-config.js +0 -4
  93. package/src/console/music-preview.js +79 -0
  94. package/src/console/navigation.js +0 -1
  95. package/src/console/tabs/agents-tab.js +14 -2
  96. package/src/console/tabs/help-tab.js +314 -314
  97. package/src/console/tabs/music-tab.js +81 -5
  98. package/src/console/tabs/placeholder-tab.js +0 -2
  99. package/src/console/tabs/readme-tab.js +272 -272
  100. package/src/console/tabs/settings-tab.js +207 -14
  101. package/src/console/tabs/setup-tab.js +39 -36
  102. package/src/console/tabs/voices-tab.js +102 -23
  103. package/src/console/widgets/destroy-list.js +25 -25
  104. package/src/console/widgets/notice.js +55 -55
  105. package/src/console/widgets/track-picker.js +82 -0
  106. package/src/installer/language-screen.js +31 -31
  107. package/src/installer/music-file-input.js +304 -304
  108. package/src/installer.js +124 -10
  109. package/src/services/language-service.js +47 -47
  110. package/src/services/navigation-service.js +1 -1
  111. package/src/services/provider-catalog.js +412 -0
  112. package/src/services/provider-voice-catalog.js +52 -73
  113. package/src/services/tts-engine-service.js +29 -0
  114. package/src/utils/audio-format-validator.js +277 -277
  115. package/src/utils/dependency-checker.js +469 -469
  116. package/src/utils/file-ownership-verifier.js +358 -358
  117. package/src/utils/music-file-validator.js +285 -285
  118. package/src/utils/preview-list-prompt.js +144 -144
  119. package/src/utils/provider-validator.js +62 -12
  120. package/src/utils/secure-music-storage.js +412 -412
  121. package/voice-assignments.json +8244 -8244
  122. package/.claude/commands/agent-vibes/language.md +0 -23
  123. package/.claude/commands/agent-vibes/learn.md +0 -67
  124. package/.claude/commands/agent-vibes/replay-target.md +0 -14
  125. package/.claude/commands/agent-vibes/target-voice.md +0 -26
  126. package/.claude/commands/agent-vibes/target.md +0 -30
  127. package/.claude/hooks/learn-manager.sh +0 -492
  128. package/.claude/hooks/replay-target-audio.sh +0 -95
  129. 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
- $multilingual = @("Antoni", "Rachel", "Domi", "Bella", "Charlotte", "Matilda")
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) { $assignedVoice = "en_US-lessac-medium" }
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 = "af_heart" }
87
+ if (-not $VoiceName) { $VoiceName = $KokoroDefault }
74
88
 
75
89
  # Security: validate voice id (lowercase prefix + underscore + name).
76
- # Rejects path-traversal / injection before it reaches the command line.
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 af_heart" -ForegroundColor Yellow
79
- $VoiceName = "af_heart"
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: lower than per-LLM $_LlmEngine (audio-effects.cfg row, set later), higher
120
- # than the global tts-provider.txt default set above.
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 @('piper','soprano','macos','windows-sapi','sapi','kokoro','elevenlabs','windows-piper') } {
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 = @('windows-piper', 'windows-sapi', 'soprano')
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
- try {
64
- $null = Invoke-WebRequest -Uri "http://127.0.0.1:${sopranoPort}/gradio_api/info" -TimeoutSec 1 -UseBasicParsing -ErrorAction Stop
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)