agentvibes 3.5.0 → 3.5.2

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.
@@ -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
+ }
@@ -0,0 +1,158 @@
1
+ #
2
+ # File: .claude/hooks-windows/play-tts-soprano.ps1
3
+ #
4
+ # AgentVibes - Soprano TTS Provider for Windows
5
+ # Ultra-fast neural TTS via Soprano (80M params)
6
+ #
7
+ # Supports three modes (auto-detected in priority order):
8
+ # 1. WebUI mode: Gradio WebUI running (soprano-webui), uses Python helper
9
+ # 2. API mode: OpenAI-compatible server, uses Invoke-RestMethod
10
+ # 3. CLI mode: Direct soprano command (reloads model each call, slowest)
11
+ #
12
+
13
+ param(
14
+ [Parameter(Mandatory = $true, Position = 0)]
15
+ [string]$Text,
16
+
17
+ [Parameter(Mandatory = $false, Position = 1)]
18
+ [string]$VoiceOverride # Ignored - Soprano has a single voice
19
+ )
20
+
21
+ $ErrorActionPreference = "Stop"
22
+
23
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
24
+ # Validate port is numeric to prevent injection
25
+ $SopranoPort = "7860"
26
+ if ($env:SOPRANO_PORT -and $env:SOPRANO_PORT -match '^\d+$') {
27
+ $portNum = [int]$env:SOPRANO_PORT
28
+ if ($portNum -gt 0 -and $portNum -le 65535) {
29
+ $SopranoPort = $env:SOPRANO_PORT
30
+ }
31
+ }
32
+ $SopranoDevice = if ($env:SOPRANO_DEVICE) { $env:SOPRANO_DEVICE } else { "auto" }
33
+
34
+ # Sanitize text for TTS - strip shell metacharacters and PS special chars
35
+ $Text = $Text -replace '[\\`"{}$<>|~^;''()]', '' -replace '\s+', ' '
36
+ $Text = $Text.Trim()
37
+
38
+ if (-not $Text) {
39
+ Write-Host "Usage: play-tts-soprano.ps1 'text to speak' [voice_override]"
40
+ exit 1
41
+ }
42
+
43
+ # Determine audio directory
44
+ $ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptDir)) ".claude"
45
+ if (Test-Path $ProjectClaudeDir) {
46
+ $AudioDir = Join-Path $ProjectClaudeDir "audio"
47
+ } else {
48
+ $AudioDir = "$env:USERPROFILE\.claude\audio"
49
+ }
50
+
51
+ if (-not (Test-Path $AudioDir)) {
52
+ New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null
53
+ }
54
+
55
+ $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
56
+ $random = Get-Random -Maximum 9999
57
+ $TempFile = Join-Path $AudioDir "tts-$timestamp-$random.wav"
58
+
59
+ # Check WebUI server
60
+ function Test-WebUI {
61
+ try {
62
+ $null = Invoke-WebRequest -Uri "http://127.0.0.1:${SopranoPort}/gradio_api/info" -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop
63
+ return $true
64
+ } catch {
65
+ try {
66
+ $null = Invoke-WebRequest -Uri "http://127.0.0.1:${SopranoPort}/info" -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop
67
+ return $true
68
+ } catch {
69
+ return $false
70
+ }
71
+ }
72
+ }
73
+
74
+ # Check API server
75
+ function Test-APIServer {
76
+ try {
77
+ $body = '{"input":"test"}'
78
+ $null = Invoke-RestMethod -Uri "http://127.0.0.1:${SopranoPort}/v1/audio/speech" `
79
+ -Method POST -ContentType "application/json" -Body $body -TimeoutSec 2 -ErrorAction Stop
80
+ return $true
81
+ } catch {
82
+ return $false
83
+ }
84
+ }
85
+
86
+ # Check CLI availability
87
+ function Test-SopranoCLI {
88
+ try {
89
+ $null = Get-Command soprano -ErrorAction Stop
90
+ return $true
91
+ } catch {
92
+ return $false
93
+ }
94
+ }
95
+
96
+ # Synthesize speech
97
+ $SynthMode = ""
98
+
99
+ if (Test-WebUI) {
100
+ # Gradio WebUI mode - use Python helper for SSE protocol
101
+ $SynthMode = "webui"
102
+ $pythonHelper = Join-Path $ScriptDir "soprano-gradio-synth.py"
103
+ if (Test-Path $pythonHelper) {
104
+ & python $pythonHelper $Text $TempFile $SopranoPort 2>$null
105
+ } else {
106
+ Write-Host "[ERROR] soprano-gradio-synth.py not found" -ForegroundColor Red
107
+ exit 1
108
+ }
109
+ } elseif (Test-APIServer) {
110
+ # OpenAI-compatible API mode
111
+ $SynthMode = "api"
112
+ # Build JSON safely using ConvertTo-Json to avoid injection
113
+ $bodyObj = @{ input = $Text }
114
+ $body = $bodyObj | ConvertTo-Json -Compress
115
+ try {
116
+ Invoke-RestMethod -Uri "http://127.0.0.1:${SopranoPort}/v1/audio/speech" `
117
+ -Method POST -ContentType "application/json" -Body $body `
118
+ -OutFile $TempFile -ErrorAction Stop
119
+ } catch {
120
+ Write-Host "[ERROR] API synthesis failed: $_" -ForegroundColor Red
121
+ exit 4
122
+ }
123
+ } elseif (Test-SopranoCLI) {
124
+ # CLI fallback - reloads model each call (slowest)
125
+ $SynthMode = "cli"
126
+ & soprano $Text -o $TempFile -d $SopranoDevice 2>$null
127
+ } else {
128
+ Write-Host "[ERROR] Soprano TTS not installed and no server running on port $SopranoPort" -ForegroundColor Red
129
+ Write-Host ""
130
+ Write-Host "Install: pip install soprano-tts" -ForegroundColor Yellow
131
+ Write-Host " (GPU): pip install soprano-tts[lmdeploy]" -ForegroundColor Yellow
132
+ Write-Host ""
133
+ Write-Host "Start WebUI: soprano-webui" -ForegroundColor Yellow
134
+ Write-Host "Start API: uvicorn soprano.server:app --host 127.0.0.1 --port $SopranoPort" -ForegroundColor Yellow
135
+ exit 2
136
+ }
137
+
138
+ # Verify output
139
+ if (-not (Test-Path $TempFile) -or (Get-Item $TempFile).Length -eq 0) {
140
+ Write-Host "[ERROR] Failed to synthesize speech with Soprano ($SynthMode mode)" -ForegroundColor Red
141
+ exit 4
142
+ }
143
+
144
+ # Play audio with proper resource cleanup (skip if AGENTVIBES_NO_PLAY is set)
145
+ if (-not $env:AGENTVIBES_NO_PLAY) {
146
+ $player = $null
147
+ try {
148
+ $player = New-Object System.Media.SoundPlayer $TempFile
149
+ $player.PlaySync()
150
+ } catch {
151
+ Write-Host "[ERROR] Audio playback failed: $_" -ForegroundColor Red
152
+ } finally {
153
+ if ($player) { $player.Dispose() }
154
+ }
155
+ }
156
+
157
+ Write-Host "Saved to: $TempFile"
158
+ Write-Host "Voice: Soprano-1.1-80M (Soprano TTS, $SynthMode mode)"
@@ -0,0 +1,164 @@
1
+ #
2
+ # File: .claude/hooks-windows/play-tts-windows-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
+ $VoiceFile = "$ClaudeDir\tts-voice-piper.txt"
29
+
30
+ # Voices and Piper binary are global (shared across projects, ~100MB+)
31
+ $UserClaudeDir = "$env:USERPROFILE\.claude"
32
+ $VoicesDir = "$UserClaudeDir\piper-voices"
33
+ $PiperExe = "$env:LOCALAPPDATA\Programs\Piper\piper.exe"
34
+
35
+ # Ensure directories exist
36
+ foreach ($dir in @($AudioDir, $VoicesDir)) {
37
+ if (-not (Test-Path $dir)) {
38
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
39
+ }
40
+ }
41
+
42
+ # Check if Piper is installed
43
+ if (-not (Test-Path $PiperExe)) {
44
+ Write-Host "[ERROR] Piper not found at: $PiperExe" -ForegroundColor Red
45
+ Write-Host "Run: .\setup-windows.ps1 to install Piper" -ForegroundColor Yellow
46
+ exit 1
47
+ }
48
+
49
+ # Determine voice to use
50
+ $VoiceName = ""
51
+
52
+ if ($VoiceOverride) {
53
+ $VoiceName = $VoiceOverride
54
+ }
55
+ elseif (Test-Path $VoiceFile) {
56
+ $VoiceName = (Get-Content $VoiceFile -Raw).Trim()
57
+ }
58
+
59
+ # Default voice if not specified
60
+ if (-not $VoiceName) {
61
+ $VoiceName = "en_US-ryan-high"
62
+ }
63
+
64
+ # Security: Validate voice name to prevent path traversal
65
+ # Only allow alphanumeric, underscore, hyphen, and period
66
+ if ($VoiceName -notmatch '^[a-zA-Z0-9_\-\.]+$') {
67
+ Write-Host "[ERROR] Invalid voice name: $VoiceName" -ForegroundColor Red
68
+ exit 1
69
+ }
70
+
71
+ # Resolve voice model path and validate it stays within VoicesDir
72
+ $VoiceModelFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx")
73
+ $VoiceJsonFile = [System.IO.Path]::GetFullPath("$VoicesDir\$VoiceName.onnx.json")
74
+ $ResolvedVoicesDir = [System.IO.Path]::GetFullPath($VoicesDir)
75
+ if (-not $VoiceModelFile.StartsWith($ResolvedVoicesDir)) {
76
+ Write-Host "[ERROR] Voice path outside voices directory" -ForegroundColor Red
77
+ exit 1
78
+ }
79
+
80
+ # Check if voice model exists, download if missing
81
+ if (-not (Test-Path $VoiceModelFile)) {
82
+ Write-Host "[DOWNLOAD] Voice model: $VoiceName" -ForegroundColor Yellow
83
+
84
+ # Try to download from Hugging Face
85
+ # Voice name format: {lang}_{region}-{speaker}-{quality}
86
+ # HF path format: {lang}/{lang}_{region}/{speaker}/{quality}/{voicename}.onnx
87
+ try {
88
+ # Parse voice name to build correct HF path
89
+ # e.g. en_US-ryan-high -> en/en_US/ryan/high/en_US-ryan-high.onnx
90
+ if ($VoiceName -match '^([a-z]{2})_([A-Z]{2})-([a-zA-Z0-9_]+)-([a-z]+)$') {
91
+ $Lang = $Matches[1]
92
+ $LangRegion = "$($Matches[1])_$($Matches[2])"
93
+ $Speaker = $Matches[3]
94
+ $Quality = $Matches[4]
95
+ $HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$Lang/$LangRegion/$Speaker/$Quality"
96
+ } else {
97
+ # Fallback for non-standard voice names
98
+ $HFBase = "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high"
99
+ }
100
+ $ModelUrl = "$HFBase/$VoiceName.onnx"
101
+ $JsonUrl = "$HFBase/$VoiceName.onnx.json"
102
+
103
+ Write-Host " Downloading model..." -ForegroundColor Cyan
104
+ Invoke-WebRequest -Uri $ModelUrl -OutFile $VoiceModelFile -ErrorAction Stop
105
+ Write-Host " Downloading config..." -ForegroundColor Cyan
106
+ Invoke-WebRequest -Uri $JsonUrl -OutFile $VoiceJsonFile -ErrorAction Stop
107
+ Write-Host "[OK] Voice model downloaded" -ForegroundColor Green
108
+ }
109
+ catch {
110
+ Write-Host "[ERROR] Failed to download voice model: $_" -ForegroundColor Red
111
+ Write-Host "Make sure you have internet connection" -ForegroundColor Yellow
112
+ exit 1
113
+ }
114
+ }
115
+
116
+ # Sanitize text for speech - strip shell metacharacters and PS special chars
117
+ $Text = $Text -replace '\\', ' '
118
+ $Text = $Text -replace '[{}<>|`~^$;"''()]', ''
119
+ $Text = $Text -replace '\s+', ' '
120
+ $Text = $Text.Trim()
121
+
122
+ # Create audio file path
123
+ $Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss-ffff'
124
+ $AudioFile = "$AudioDir\tts-$Timestamp.wav"
125
+
126
+ # Synthesize with Piper
127
+ try {
128
+ Write-Host "[SYNTH] Synthesizing with Piper..." -ForegroundColor Cyan
129
+
130
+ # Run Piper with text input
131
+ $Text | & $PiperExe `
132
+ --model $VoiceModelFile `
133
+ --output-file $AudioFile `
134
+ 2>$null
135
+
136
+ if (-not (Test-Path $AudioFile)) {
137
+ Write-Host "[ERROR] Piper synthesis failed" -ForegroundColor Red
138
+ exit 1
139
+ }
140
+
141
+ # Display results
142
+ Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
143
+ Write-Host "[VOICE] Voice used: $VoiceName (Piper)" -ForegroundColor Green
144
+
145
+ # Play the audio using built-in Windows audio player (skip if AGENTVIBES_NO_PLAY is set)
146
+ if (-not $env:AGENTVIBES_NO_PLAY) {
147
+ $player = $null
148
+ try {
149
+ $player = New-Object System.Media.SoundPlayer $AudioFile
150
+ $player.PlaySync()
151
+ }
152
+ catch {
153
+ Write-Host "[WARNING] Could not play audio (SoundPlayer unavailable)" -ForegroundColor Yellow
154
+ Write-Host "Audio saved to: $AudioFile" -ForegroundColor Gray
155
+ }
156
+ finally {
157
+ if ($player) { $player.Dispose() }
158
+ }
159
+ }
160
+ }
161
+ catch {
162
+ Write-Host "[ERROR] Error running Piper: $_" -ForegroundColor Red
163
+ exit 1
164
+ }
@@ -0,0 +1,108 @@
1
+ #
2
+ # File: .claude/hooks-windows/play-tts-windows-sapi.ps1
3
+ #
4
+ # AgentVibes - Windows SAPI TTS Provider (Zero Dependencies)
5
+ # Uses built-in Windows System.Speech API
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
+ $AudioDir = "$ClaudeDir\audio"
27
+ $VoiceFile = "$ClaudeDir\tts-voice-sapi.txt"
28
+
29
+ # Ensure directories exist
30
+ if (-not (Test-Path $AudioDir)) {
31
+ New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null
32
+ }
33
+
34
+ # Load System.Speech assembly
35
+ try {
36
+ Add-Type -AssemblyName System.Speech
37
+ }
38
+ catch {
39
+ Write-Host "[ERROR] System.Speech assembly not available" -ForegroundColor Red
40
+ exit 1
41
+ }
42
+
43
+ # Determine voice to use
44
+ $VoiceName = ""
45
+
46
+ if ($VoiceOverride) {
47
+ $VoiceName = $VoiceOverride
48
+ }
49
+ elseif (Test-Path $VoiceFile) {
50
+ $VoiceName = (Get-Content $VoiceFile -Raw).Trim()
51
+ }
52
+
53
+ # Initialize speech synthesizer
54
+ $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
55
+
56
+ # Set voice if specified
57
+ if ($VoiceName) {
58
+ try {
59
+ $synth.SelectVoice($VoiceName)
60
+ }
61
+ catch {
62
+ Write-Host "[WARNING] Voice '$VoiceName' not found, using default" -ForegroundColor Yellow
63
+ }
64
+ }
65
+
66
+ # Sanitize text for speech - strip shell metacharacters, PS special chars, and SSML tags
67
+ $Text = $Text -replace '\\', ' '
68
+ $Text = $Text -replace '[{}<>|`~^$;"''()]', ''
69
+ $Text = $Text -replace '&[a-zA-Z]+;', ''
70
+ $Text = $Text -replace '\s+', ' '
71
+ $Text = $Text.Trim()
72
+
73
+ # Get actual voice name (after selection or default)
74
+ $ActualVoice = $synth.Voice.Name
75
+
76
+ # Create audio file path
77
+ $Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss-ffff'
78
+ $AudioFile = "$AudioDir\tts-$Timestamp.wav"
79
+
80
+ # Save to WAV file with proper resource cleanup
81
+ $player = $null
82
+ try {
83
+ $synth.SetOutputToWaveFile($AudioFile)
84
+ $synth.Speak($Text)
85
+
86
+ # Display results
87
+ Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
88
+ Write-Host "[VOICE] Voice used: $ActualVoice (Windows SAPI)" -ForegroundColor Green
89
+
90
+ # Play the audio using built-in Windows audio player (skip if AGENTVIBES_NO_PLAY is set)
91
+ if (-not $env:AGENTVIBES_NO_PLAY) {
92
+ try {
93
+ $player = New-Object System.Media.SoundPlayer $AudioFile
94
+ $player.PlaySync()
95
+ }
96
+ catch {
97
+ Write-Host "[WARNING] Could not play audio (SoundPlayer unavailable)" -ForegroundColor Yellow
98
+ }
99
+ }
100
+ }
101
+ catch {
102
+ Write-Host "[ERROR] Error synthesizing speech: $_" -ForegroundColor Red
103
+ exit 1
104
+ }
105
+ finally {
106
+ if ($synth) { $synth.Dispose() }
107
+ if ($player) { $player.Dispose() }
108
+ }