agentvibes 5.4.0 → 5.6.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 +9 -1
- package/.claude/config/audio-effects.cfg +12 -0
- package/.claude/config/background-music-enabled.txt +1 -0
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +6 -2
- package/.claude/hooks/play-tts-piper.sh +2 -2
- package/.claude/hooks/play-tts.sh +22 -1
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +178 -164
- package/.claude/hooks-windows/play-tts.ps1 +208 -41
- package/README.md +72 -85
- package/RELEASE_NOTES.md +63 -0
- package/bin/agentvibes.js +28 -0
- package/mcp-server/server.py +17 -1
- package/package.json +1 -1
- package/src/console/tabs/music-tab.js +5 -2
- package/src/console/tabs/voices-tab.js +71 -37
- package/src/installer.js +10 -0
- package/src/services/llm-provider-service.js +1 -1
package/.agentvibes/config.json
CHANGED
|
@@ -28,5 +28,13 @@
|
|
|
28
28
|
"voice": "en_US-ljspeech-high",
|
|
29
29
|
"pretext": "Agent Vibes Here",
|
|
30
30
|
"setupCompleted": true,
|
|
31
|
-
"ttsEngine": "piper"
|
|
31
|
+
"ttsEngine": "piper",
|
|
32
|
+
"thumbsUp": [
|
|
33
|
+
"en_US-libritts-high::Adam",
|
|
34
|
+
"en_US-libritts_r-medium::Adam-2"
|
|
35
|
+
],
|
|
36
|
+
"favorites": [
|
|
37
|
+
"en_US-libritts-high::Adam",
|
|
38
|
+
"en_US-libritts_r-medium::Adam-2"
|
|
39
|
+
]
|
|
32
40
|
}
|
|
@@ -50,3 +50,15 @@ _party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.4
|
|
|
50
50
|
|||
|
|
51
51
|
# Default (no agent specified) - clean with Bachata background|||
|
|
52
52
|
default|reverb 20 50 50|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
53
|
+
|||
|
|
54
|
+
# Per-LLM routing rows — looked up by play-tts.sh / play-tts.ps1 when -llm is passed|||
|
|
55
|
+
# Format: llm:<name>|REVERB_PRESET|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT|ENGINE|||
|
|
56
|
+
|||
|
|
57
|
+
# Claude Code (claude-code CLI via CLAUDECODE=1 env or AGENTVIBES_LLM=claude-code)|||
|
|
58
|
+
llm:claude-code|||0.15||Agent Vibes Here|piper
|
|
59
|
+
|||
|
|
60
|
+
# GitHub Copilot Chat (VS Code MCP or AGENTVIBES_LLM=copilot)|||
|
|
61
|
+
llm:copilot|off||||Agent Vibes Here|piper
|
|
62
|
+
|||
|
|
63
|
+
# OpenAI Codex CLI (AGENTVIBES_LLM=codex)|||
|
|
64
|
+
llm:codex|off||||Agent Vibes Here|piper
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
true
|
|
@@ -24,4 +24,4 @@ agent_vibes_arabic_v2_loop.mp3:.00000000000000000006132724
|
|
|
24
24
|
agent_vibes_chillwave_v2_loop.mp3:14.628390
|
|
25
25
|
agent_vibes_bachata_v1_loop.mp3:.00000000000000000005344000
|
|
26
26
|
agent_vibes_goa_trance_v2_loop.mp3:.00000000000000000002499918
|
|
27
|
-
agentvibes_soft_flamenco_loop.mp3:
|
|
27
|
+
agentvibes_soft_flamenco_loop.mp3:9.432018
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20260428
|
|
File without changes
|
|
@@ -50,11 +50,15 @@ if [[ ! -f "$PROJECT_ROOT/_bmad/_config/agent-manifest.csv" ]]; then
|
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# ---------------------------------------------------------------------------
|
|
53
|
-
# Per-agent profile reader — reads from
|
|
53
|
+
# Per-agent profile reader — reads from project .agentvibes/bmad-voice-map.json (falls back to global)
|
|
54
54
|
# Uses node for reliable JSON parsing (jq may not be installed)
|
|
55
55
|
# Returns empty string if field not found or file missing
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
if [[ -f "$PROJECT_ROOT/.agentvibes/bmad-voice-map.json" ]]; then
|
|
58
|
+
VOICE_MAP_FILE="$PROJECT_ROOT/.agentvibes/bmad-voice-map.json"
|
|
59
|
+
else
|
|
60
|
+
VOICE_MAP_FILE="$HOME/.agentvibes/bmad-voice-map.json"
|
|
61
|
+
fi
|
|
58
62
|
|
|
59
63
|
# Read a field from the per-agent profile in bmad-voice-map.json
|
|
60
64
|
# Usage: read_agent_profile <agent_id> <field>
|
|
@@ -551,8 +551,8 @@ if [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]] && [[ "${AGENTVIBES_NO_PLAYB
|
|
|
551
551
|
termux-media-player play "$TEMP_FILE" >/dev/null 2>&1 &
|
|
552
552
|
PLAYER_PID=$!
|
|
553
553
|
else
|
|
554
|
-
# Linux/WSL:
|
|
555
|
-
(paplay "$TEMP_FILE" || mpv "$TEMP_FILE" || aplay "$TEMP_FILE") >/dev/null 2>&1 &
|
|
554
|
+
# Linux/WSL: paplay with 500ms latency buffer prevents choppiness over RDP/network audio
|
|
555
|
+
(paplay --latency-msec=500 "$TEMP_FILE" || mpv "$TEMP_FILE" || aplay -B 2000000 "$TEMP_FILE") >/dev/null 2>&1 &
|
|
556
556
|
PLAYER_PID=$!
|
|
557
557
|
fi
|
|
558
558
|
fi
|
|
@@ -103,8 +103,29 @@ elif [[ -f "$GLOBAL_MUTE_FILE" ]]; then
|
|
|
103
103
|
exit 0
|
|
104
104
|
fi
|
|
105
105
|
|
|
106
|
+
# Parse named flags (e.g. --llm) before positional arguments.
|
|
107
|
+
# This allows callers to pass: play-tts.sh --llm claude-code "text to speak"
|
|
108
|
+
# Named args are extracted; remaining positional args are shifted into $1/$2/$3.
|
|
109
|
+
LLM_PROVIDER="${LLM_PROVIDER:-}"
|
|
110
|
+
_POSITIONAL_ARGS=()
|
|
111
|
+
while [[ $# -gt 0 ]]; do
|
|
112
|
+
case "$1" in
|
|
113
|
+
--llm)
|
|
114
|
+
LLM_PROVIDER="${2:-}"
|
|
115
|
+
shift 2
|
|
116
|
+
;;
|
|
117
|
+
*)
|
|
118
|
+
_POSITIONAL_ARGS+=("$1")
|
|
119
|
+
shift
|
|
120
|
+
;;
|
|
121
|
+
esac
|
|
122
|
+
done
|
|
123
|
+
set -- "${_POSITIONAL_ARGS[@]+"${_POSITIONAL_ARGS[@]}"}"
|
|
124
|
+
unset _POSITIONAL_ARGS
|
|
125
|
+
|
|
106
126
|
TEXT="${1:-}"
|
|
107
127
|
VOICE_OVERRIDE="${2:-}" # Optional: voice name or ID
|
|
128
|
+
AGENT_PROFILE_FILE="${3:-}" # Optional: path to agent profile file
|
|
108
129
|
|
|
109
130
|
# Security: Validate inputs
|
|
110
131
|
if [[ -z "$TEXT" ]]; then
|
|
@@ -397,7 +418,7 @@ fi
|
|
|
397
418
|
# Normal single-language mode - route to appropriate provider implementation
|
|
398
419
|
case "$ACTIVE_PROVIDER" in
|
|
399
420
|
piper)
|
|
400
|
-
exec bash "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "$AGENT_PROFILE_FILE"
|
|
421
|
+
exec bash "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" "${AGENT_PROFILE_FILE:-}"
|
|
401
422
|
;;
|
|
402
423
|
soprano)
|
|
403
424
|
exec bash "$SCRIPT_DIR/play-tts-soprano.sh" "$TEXT" "$VOICE_OVERRIDE"
|
|
@@ -1,164 +1,178 @@
|
|
|
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
|
|
146
|
-
if (-not $env:AGENTVIBES_NO_PLAY) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 (skip if AGENTVIBES_NO_PLAY is set)
|
|
146
|
+
if (-not $env:AGENTVIBES_NO_PLAY) {
|
|
147
|
+
# Prefer ffplay: handles 22050 Hz → 48000 Hz resampling cleanly (SoundPlayer uses
|
|
148
|
+
# WinMM's low-quality resampler which produces choppy audio at non-native rates).
|
|
149
|
+
$ffplayPath = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
|
|
150
|
+
if (-not $ffplayPath) {
|
|
151
|
+
# SSH/watcher sessions may have a minimal PATH — refresh from registry
|
|
152
|
+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
|
|
153
|
+
[System.Environment]::GetEnvironmentVariable("Path","User")
|
|
154
|
+
$ffplayPath = (Get-Command ffplay -ErrorAction SilentlyContinue)?.Source
|
|
155
|
+
}
|
|
156
|
+
if ($ffplayPath) {
|
|
157
|
+
& $ffplayPath -autoexit -nodisp -loglevel quiet $AudioFile 2>$null
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
$player = $null
|
|
161
|
+
try {
|
|
162
|
+
$player = New-Object System.Media.SoundPlayer $AudioFile
|
|
163
|
+
$player.PlaySync()
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
Write-Host "[WARNING] Could not play audio (SoundPlayer unavailable)" -ForegroundColor Yellow
|
|
167
|
+
Write-Host "Audio saved to: $AudioFile" -ForegroundColor Gray
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if ($player) { $player.Dispose() }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
Write-Host "[ERROR] Error running Piper: $_" -ForegroundColor Red
|
|
177
|
+
exit 1
|
|
178
|
+
}
|