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.
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +158 -0
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +164 -0
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts.ps1 +266 -0
- package/.claude/hooks-windows/provider-manager.ps1 +158 -0
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -0
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +176 -0
- package/README.md +27 -24
- package/RELEASE_NOTES.md +3 -3
- package/WINDOWS-SETUP.md +208 -0
- package/package.json +4 -1
- package/setup-windows.ps1 +747 -0
- package/.claude/output-styles/agent-vibes-windows.md +0 -194
|
@@ -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
|
+
}
|