agentvibes 5.0.0 → 5.1.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/.claude/audio/tracks/Drifting Down the Hall.mp3 +0 -0
- package/.claude/audio/tracks/Late Night Hip Hop Groove.mp3 +0 -0
- package/.claude/audio/tracks/Midnight Charleston Stomp.mp3 +0 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/hooks/play-tts.sh +10 -3
- package/.claude/hooks-windows/play-tts.ps1 +37 -107
- package/README.md +16 -2
- package/RELEASE_NOTES.md +48 -0
- package/package.json +1 -1
- package/src/console/tabs/agents-tab.js +65 -62
- package/src/console/tabs/music-tab.js +49 -19
- package/src/console/tabs/settings-tab.js +39 -37
- package/src/console/tabs/setup-tab.js +346 -86
- package/src/console/tabs/voices-tab.js +152 -29
- package/src/console/widgets/format-utils.js +92 -89
- package/src/console/widgets/track-picker.js +325 -322
- package/src/installer.js +8 -8
- package/src/services/llm-provider-service.js +79 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -51,4 +51,4 @@ _party_mode|compand 0.3,1 6:-70,-60,-20|agent_vibes_dark_chill_step_loop.mp3|0.4
|
|
|
51
51
|
# Default (no agent specified) - clean with Bachata background|||
|
|
52
52
|
default||agent_vibes_chillwave_v2_loop.mp3|0.15
|
|
53
53
|
analyst|reverb 70 50 100|agentvibes_soft_flamenco_loop.mp3|0.30
|
|
54
|
-
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|
|
|
54
|
+
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Claude Code here|piper
|
|
@@ -131,20 +131,23 @@ TEXT="${TEXT//\\./.}" # Remove \. (keep the period)
|
|
|
131
131
|
_LLM_VOICE=""
|
|
132
132
|
_LLM_PRETEXT=""
|
|
133
133
|
_LLM_REVERB=""
|
|
134
|
+
_LLM_ENGINE=""
|
|
134
135
|
if [[ -n "$LLM_PROVIDER" ]]; then
|
|
135
136
|
_llm_key="llm:${LLM_PROVIDER}"
|
|
136
137
|
for _cfg in \
|
|
137
138
|
"$PROJECT_ROOT/.claude/config/audio-effects.cfg" \
|
|
138
139
|
"$HOME/.claude/config/audio-effects.cfg"; do
|
|
139
140
|
if [[ -z "$_LLM_VOICE" && -z "$_LLM_PRETEXT" && -f "$_cfg" ]]; then
|
|
140
|
-
while IFS='|' read -r _key _reverb _bgfile _bgvol _voice _pretext _rest; do
|
|
141
|
+
while IFS='|' read -r _key _reverb _bgfile _bgvol _voice _pretext _engine _rest; do
|
|
141
142
|
if [[ "$_key" == "$_llm_key" ]]; then
|
|
142
143
|
_reverb="${_reverb## }"; _reverb="${_reverb%% }"
|
|
143
144
|
_voice="${_voice## }"; _voice="${_voice%% }"
|
|
144
145
|
_pretext="${_pretext## }"; _pretext="${_pretext%% }"
|
|
146
|
+
_engine="${_engine## }"; _engine="${_engine%% }"
|
|
145
147
|
[[ -n "$_reverb" ]] && _LLM_REVERB="$_reverb"
|
|
146
148
|
[[ -n "$_voice" ]] && _LLM_VOICE="$_voice"
|
|
147
149
|
[[ -n "$_pretext" ]] && _LLM_PRETEXT="$_pretext"
|
|
150
|
+
[[ -n "$_engine" ]] && _LLM_ENGINE="$_engine"
|
|
148
151
|
break
|
|
149
152
|
fi
|
|
150
153
|
done < "$_cfg"
|
|
@@ -187,8 +190,12 @@ fi
|
|
|
187
190
|
# Source provider manager to get active provider
|
|
188
191
|
source "$SCRIPT_DIR/provider-manager.sh"
|
|
189
192
|
|
|
190
|
-
# Get active provider
|
|
191
|
-
|
|
193
|
+
# Get active provider (LLM-specific engine overrides global)
|
|
194
|
+
if [[ -n "$_LLM_ENGINE" ]]; then
|
|
195
|
+
ACTIVE_PROVIDER="$_LLM_ENGINE"
|
|
196
|
+
else
|
|
197
|
+
ACTIVE_PROVIDER=$(get_active_provider)
|
|
198
|
+
fi
|
|
192
199
|
|
|
193
200
|
# Show GitHub star reminder (once per day)
|
|
194
201
|
"$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true
|
|
@@ -10,25 +10,21 @@ param(
|
|
|
10
10
|
[string]$Text,
|
|
11
11
|
|
|
12
12
|
[Parameter(Mandatory = $false, Position = 1)]
|
|
13
|
-
[string]$VoiceOverride
|
|
14
|
-
|
|
15
|
-
[Parameter(Mandatory = $false)]
|
|
16
|
-
[string]$llm = ""
|
|
13
|
+
[string]$VoiceOverride
|
|
17
14
|
)
|
|
18
15
|
|
|
19
16
|
# Configuration paths
|
|
20
17
|
# Priority: CLAUDE_PROJECT_DIR env var → script's parent project → user profile
|
|
21
|
-
# Local project settings ALWAYS override global (~/.claude)
|
|
22
18
|
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
23
19
|
|
|
24
20
|
if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
|
|
25
21
|
$ClaudeDir = "$env:CLAUDE_PROJECT_DIR\.claude"
|
|
26
22
|
} else {
|
|
27
23
|
$PackageClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptPath)) ".claude"
|
|
28
|
-
if (Test-Path $
|
|
29
|
-
$ClaudeDir = $PackageClaudeDir
|
|
30
|
-
} elseif (Test-Path "$env:USERPROFILE\.claude\tts-provider.txt") {
|
|
24
|
+
if (Test-Path "$env:USERPROFILE\.claude\tts-provider.txt") {
|
|
31
25
|
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
26
|
+
} elseif (Test-Path $PackageClaudeDir) {
|
|
27
|
+
$ClaudeDir = $PackageClaudeDir
|
|
32
28
|
} else {
|
|
33
29
|
$ClaudeDir = "$env:USERPROFILE\.claude"
|
|
34
30
|
}
|
|
@@ -46,78 +42,6 @@ if (Test-Path $MuteFile) {
|
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
# Per-LLM config lookup: if --llm is passed, look up llm:<name> in audio-effects.cfg
|
|
50
|
-
# Format: llm:<name>|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME|VOICE|PRETEXT
|
|
51
|
-
$LlmVoice = ""
|
|
52
|
-
$LlmPretext = ""
|
|
53
|
-
$LlmReverb = ""
|
|
54
|
-
$ProjectRoot = Split-Path -Parent $ClaudeDir
|
|
55
|
-
$ConfigDir = "$ClaudeDir\config"
|
|
56
|
-
|
|
57
|
-
if ($llm) {
|
|
58
|
-
$llmKey = "llm:$llm"
|
|
59
|
-
# Check project-local audio-effects.cfg first, then global
|
|
60
|
-
$cfgPaths = @(
|
|
61
|
-
"$ConfigDir\audio-effects.cfg",
|
|
62
|
-
"$env:USERPROFILE\.claude\config\audio-effects.cfg"
|
|
63
|
-
)
|
|
64
|
-
foreach ($cfgPath in $cfgPaths) {
|
|
65
|
-
if (-not $LlmVoice -and -not $LlmPretext -and (Test-Path $cfgPath)) {
|
|
66
|
-
foreach ($line in (Get-Content $cfgPath)) {
|
|
67
|
-
if ($line -match "^$([regex]::Escape($llmKey))\|") {
|
|
68
|
-
$parts = $line -split '\|'
|
|
69
|
-
# parts: [0]=key [1]=reverb_preset [2]=bg_file [3]=bg_vol [4]=voice [5]=pretext
|
|
70
|
-
if ($parts.Length -ge 2 -and $parts[1].Trim()) {
|
|
71
|
-
$LlmReverb = $parts[1].Trim()
|
|
72
|
-
}
|
|
73
|
-
if ($parts.Length -ge 5 -and $parts[4].Trim()) {
|
|
74
|
-
$LlmVoice = $parts[4].Trim()
|
|
75
|
-
}
|
|
76
|
-
if ($parts.Length -ge 6 -and $parts[5].Trim()) {
|
|
77
|
-
$LlmPretext = $parts[5].Trim()
|
|
78
|
-
}
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
# Apply LLM voice override (only if no explicit VoiceOverride was passed)
|
|
85
|
-
if ($LlmVoice -and -not $VoiceOverride) {
|
|
86
|
-
$VoiceOverride = $LlmVoice
|
|
87
|
-
}
|
|
88
|
-
# Export LLM key for child scripts (process-local, not system-wide)
|
|
89
|
-
$env:AGENTVIBES_LLM_KEY = "llm:$llm"
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
# Prepend pretext if configured
|
|
93
|
-
# Priority: LLM-specific pretext → project .agentvibes/config.json → project .claude/config/tts-pretext.txt
|
|
94
|
-
# → global ~/.agentvibes/config.json → global ~/.claude/config/tts-pretext.txt
|
|
95
|
-
$Pretext = $LlmPretext
|
|
96
|
-
if (-not $Pretext) {
|
|
97
|
-
$PretextSources = @(
|
|
98
|
-
(Join-Path $ProjectRoot ".agentvibes\config.json"),
|
|
99
|
-
"$ClaudeDir\config\tts-pretext.txt",
|
|
100
|
-
"$env:USERPROFILE\.agentvibes\config.json",
|
|
101
|
-
"$env:USERPROFILE\.claude\config\tts-pretext.txt"
|
|
102
|
-
)
|
|
103
|
-
foreach ($src in $PretextSources) {
|
|
104
|
-
if (-not $Pretext -and (Test-Path $src)) {
|
|
105
|
-
if ($src -match '\.json$') {
|
|
106
|
-
try {
|
|
107
|
-
$avCfg = Get-Content $src -Raw | ConvertFrom-Json
|
|
108
|
-
if ($avCfg.pretext) { $Pretext = $avCfg.pretext.Trim() }
|
|
109
|
-
} catch { }
|
|
110
|
-
} else {
|
|
111
|
-
$val = (Get-Content $src -Raw).Trim()
|
|
112
|
-
if ($val) { $Pretext = $val }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if ($Pretext) {
|
|
118
|
-
$Text = "$Pretext, $Text"
|
|
119
|
-
}
|
|
120
|
-
|
|
121
45
|
# Determine active provider
|
|
122
46
|
$ActiveProvider = "sapi"
|
|
123
47
|
if (Test-Path $ProviderFile) {
|
|
@@ -177,17 +101,12 @@ if (Test-Path $AgentVibesConfig) {
|
|
|
177
101
|
}
|
|
178
102
|
|
|
179
103
|
# Check if reverb is enabled (allowlist validation)
|
|
180
|
-
# LLM-specific reverb overrides global setting
|
|
181
104
|
$ReverbLevel = "off"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
$
|
|
186
|
-
|
|
187
|
-
$reverbVal = (Get-Content $ReverbFile -Raw).Trim()
|
|
188
|
-
if ($reverbVal -in @("off", "light", "medium", "heavy", "cathedral")) {
|
|
189
|
-
$ReverbLevel = $reverbVal
|
|
190
|
-
}
|
|
105
|
+
$ReverbFile = "$ConfigDir\reverb-level.txt"
|
|
106
|
+
if (Test-Path $ReverbFile) {
|
|
107
|
+
$reverbVal = (Get-Content $ReverbFile -Raw).Trim()
|
|
108
|
+
if ($reverbVal -in @("off", "light", "medium", "heavy", "cathedral")) {
|
|
109
|
+
$ReverbLevel = $reverbVal
|
|
191
110
|
}
|
|
192
111
|
}
|
|
193
112
|
$HasReverb = $ReverbLevel -ne "off"
|
|
@@ -214,6 +133,10 @@ $PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
|
|
|
214
133
|
$UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
|
|
215
134
|
(Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
|
|
216
135
|
|
|
136
|
+
# Tracks the path of the WAV the provider just synthesized.
|
|
137
|
+
# Used to defend against playing stale cached audio when synthesis silently fails.
|
|
138
|
+
$SynthesizedWavPath = $null
|
|
139
|
+
|
|
217
140
|
# If background music or reverb enabled and ffmpeg available, tell provider to skip playback
|
|
218
141
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
219
142
|
$env:AGENTVIBES_NO_PLAY = "1"
|
|
@@ -248,17 +171,25 @@ if ($UsePreSynth) {
|
|
|
248
171
|
$providerOutput = & $ProviderScript $Text 6>&1 2>&1
|
|
249
172
|
}
|
|
250
173
|
# Re-emit preserving colors from InformationRecords (Write-Host output)
|
|
174
|
+
# Also extract the synthesized WAV path from the provider's OK line.
|
|
251
175
|
foreach ($item in $providerOutput) {
|
|
176
|
+
$lineText = $null
|
|
252
177
|
if ($item -is [System.Management.Automation.InformationRecord]) {
|
|
253
178
|
$msg = $item.MessageData
|
|
254
179
|
if ($msg -is [System.Management.Automation.HostInformationMessage]) {
|
|
255
180
|
Write-Host $msg.Message -ForegroundColor $msg.ForegroundColor -NoNewline:$msg.NoNewLine
|
|
256
181
|
if (-not $msg.NoNewLine) { Write-Host }
|
|
182
|
+
$lineText = $msg.Message
|
|
257
183
|
} else {
|
|
258
184
|
Write-Host "$item"
|
|
185
|
+
$lineText = "$item"
|
|
259
186
|
}
|
|
260
187
|
} else {
|
|
261
188
|
Write-Host "$item"
|
|
189
|
+
$lineText = "$item"
|
|
190
|
+
}
|
|
191
|
+
if ($lineText -and $lineText -match '\[OK\]\s+Saved to:\s+(.+\.wav)') {
|
|
192
|
+
$SynthesizedWavPath = $Matches[1].Trim()
|
|
262
193
|
}
|
|
263
194
|
}
|
|
264
195
|
} else {
|
|
@@ -280,13 +211,23 @@ if ($UsePreSynth) {
|
|
|
280
211
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
281
212
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
282
213
|
|
|
283
|
-
# Find the WAV to post-process
|
|
214
|
+
# Find the WAV to post-process. Never fall back to "most recent on disk"
|
|
215
|
+
# because that plays a random stale cache file when synthesis silently fails.
|
|
284
216
|
$AudioDir = "$ClaudeDir\audio"
|
|
285
|
-
$RecentWav =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
217
|
+
$RecentWav = $null
|
|
218
|
+
if ($UsePreSynth) {
|
|
219
|
+
$RecentWav = Get-Item $PreSynthWav -ErrorAction SilentlyContinue
|
|
220
|
+
}
|
|
221
|
+
elseif ($SynthesizedWavPath -and (Test-Path $SynthesizedWavPath)) {
|
|
222
|
+
$cand = Get-Item $SynthesizedWavPath -ErrorAction SilentlyContinue
|
|
223
|
+
if ($cand -and $cand.Length -gt 0) {
|
|
224
|
+
$RecentWav = $cand
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (-not $RecentWav) {
|
|
228
|
+
Write-Host "[ERROR] No fresh synthesized WAV - refusing stale cache" -ForegroundColor Red
|
|
229
|
+
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
230
|
+
exit 1
|
|
290
231
|
}
|
|
291
232
|
|
|
292
233
|
if ($RecentWav -and $RecentWav.Length -gt 0) {
|
|
@@ -322,7 +263,6 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
322
263
|
if (Test-Path $AudioEffectsCfg) {
|
|
323
264
|
# Try agent-specific config first, then fall back to default
|
|
324
265
|
# Format: AGENT_NAME|SOX_EFFECTS|BACKGROUND_FILE|BACKGROUND_VOLUME
|
|
325
|
-
# Lookup order: agent name → llm:<name> → default
|
|
326
266
|
$agentName = $env:AGENTVIBES_AGENT_NAME
|
|
327
267
|
$configLine = $null
|
|
328
268
|
|
|
@@ -335,16 +275,6 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
335
275
|
}
|
|
336
276
|
}
|
|
337
277
|
}
|
|
338
|
-
# Try LLM-specific config (--llm parameter)
|
|
339
|
-
if (-not $configLine -and $llm) {
|
|
340
|
-
$llmBgKey = "llm:$llm"
|
|
341
|
-
foreach ($line in $cfgLines) {
|
|
342
|
-
if ($line -match "^$([regex]::Escape($llmBgKey))\|") {
|
|
343
|
-
$configLine = $line
|
|
344
|
-
break
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
278
|
# Fall back to default
|
|
349
279
|
if (-not $configLine) {
|
|
350
280
|
foreach ($line in $cfgLines) {
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
|
|
12
12
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
13
|
|
|
14
|
-
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.1.0
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -43,7 +43,21 @@ Whether you're using Claude Code, GitHub Copilot, OpenAI Codex, Claude Desktop,
|
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## 🎙️ NEW IN v5.1.0 — Voice Picker Overhaul + Auto-Save Agent Modal
|
|
47
|
+
|
|
48
|
+
- **Auto-save in agent modal** — Voice/personality/music/reverb/pretext changes save automatically as you edit them. Brief "✓ Saved!" toast confirms each change.
|
|
49
|
+
- **Unique LibriTTS names** — 904 speakers get deterministic surnames: **Anna Bell**, **Anna Carter**, …, **Anna Quinn**. No more "Anna-2", "Anna-3" duplicates.
|
|
50
|
+
- **Pink ♀ / blue ♂ gender symbols** — Colored gender indicators in the main Voices tab and all voice picker modals.
|
|
51
|
+
- **First-letter quick jump** — Press `a`–`z` in any voice picker to jump to that letter. `q`, `j`, `k`, `g`, `h`, `l` reserved for nav/cancel.
|
|
52
|
+
- **PgUp / PgDn / Home / End** in voice pickers
|
|
53
|
+
- **3 new background music tracks** — Late Night Hip Hop Groove, Drifting Down the Hall, Midnight Charleston Stomp
|
|
54
|
+
- **Search bar removed from voice pickers** — replaced by first-letter jump (faster, no focus issues)
|
|
55
|
+
- **Voices tab corruption fix** — uninstalled rows no longer lose their Provider column when navigated onto
|
|
56
|
+
- **Music + Voices tab blink artifacts gone**
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🚀 v5.0.0 — Multi-Provider Support: Claude Code + Copilot + Codex
|
|
47
61
|
|
|
48
62
|
- **GitHub Copilot + OpenAI Codex in VS Code** — AgentVibes now supports all three major AI coding assistants. Install and configure each from the TUI.
|
|
49
63
|
- **One Setup tab** — 4-step wizard (Language → Deps → TTS Engine → Providers) replaces old installer + LLM tabs. Returning users skip to Providers.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🎙️ v5.1.0 — Voice Picker Overhaul + Auto-Save Agent Modal
|
|
4
|
+
|
|
5
|
+
**Release Date:** April 2026
|
|
6
|
+
|
|
7
|
+
### New Features
|
|
8
|
+
|
|
9
|
+
- **Auto-save in agent edit modal** — Per-agent voice/personality/music/reverb/pretext changes now save automatically as you edit them. The explicit Save button is gone; a brief "✓ Saved!" toast confirms each change. Cancel and Reset to Defaults still behave as before.
|
|
10
|
+
|
|
11
|
+
- **Unique LibriTTS speaker names** — The 904 LibriTTS speakers no longer show as "Anna", "Anna-2", "Anna-3", … "Anna-16". Each gets a deterministic surname from a 16-name pool: **Anna Bell**, **Anna Carter**, **Anna Davis**, …, **Anna Quinn**. Underlying voice IDs are unchanged so existing user configs still resolve.
|
|
12
|
+
|
|
13
|
+
- **Pink/blue gender symbols** — Female voices show **♀** in pink (magenta), male voices show **♂** in light blue (bright-cyan), unknown shows `—`. Header `Gender` column replaced with colored `♀/♂` (10 → 4 chars wide), freeing room for longer names. Applied to the main Voices tab AND all 3 voice picker modals (Setup, Agents, Settings).
|
|
14
|
+
|
|
15
|
+
- **First-letter quick jump in voice pickers** — Press any letter `a`–`z` to jump to the first voice starting with that letter. Reserved keys (`q`, `j`, `k`, `g`, `h`, `l`) are blocked so they keep their cancel / vi-nav meanings.
|
|
16
|
+
|
|
17
|
+
- **Page navigation in voice pickers** — `PgUp`, `PgDn`, `Home`, `End` now work in all voice picker modals.
|
|
18
|
+
|
|
19
|
+
- **3 new background music tracks** — `Late Night Hip Hop Groove`, `Drifting Down the Hall` (90s vibes), and `Midnight Charleston Stomp` (swing). Track count goes from 15 → 18.
|
|
20
|
+
|
|
21
|
+
### Improvements
|
|
22
|
+
|
|
23
|
+
- **Voice picker search bar removed** — Replaced with first-letter quick jump. The old search textbox had focus issues that swallowed nav keys. The jump is faster for typical "find voice X" use.
|
|
24
|
+
|
|
25
|
+
- **Track list sorting fixed** — Tracks with emoji prefixes (e.g. `🎤 Late Night Hip Hop Groove`) now sort by the alphabetic part of the name, not the emoji codepoint. Order is consistent across Node/ICU versions.
|
|
26
|
+
|
|
27
|
+
- **Favorite hotkey is now `*` only** — Removed the duplicate `f` binding for marking favorites in voice pickers and the main Voices tab. `f` is now free for first-letter jump (e.g. jumping to Frank or Felix). The `*` marker remains the canonical way to toggle favorites.
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
- **Voices tab uninstalled rows no longer corrupt** — Selecting an uninstalled voice was visually deleting its Provider column due to a regex strip that over-matched the row's `bright-black-fg` wrapper. Replaced with a precise hint anchor that only strips the exact hint text.
|
|
32
|
+
|
|
33
|
+
- **Music tab + Voices tab blink artifacts gone** — `█` cursors no longer leave stray blocks behind when scrolling rapidly through the list. Both tabs now use a precise blink-strip helper instead of the fragile position-based slicer.
|
|
34
|
+
|
|
35
|
+
- **Setup tab no longer silently fails** — `_renderScreen3` was wrapping the entire `setupCompleted` write block in a single empty `try/catch {}`. Corrupt local config files are now backed up to `config.json.bak` and rewritten fresh, with errors logged to stderr — no more "stuck repeating setup" with no explanation.
|
|
36
|
+
|
|
37
|
+
- **Voice picker `q` cancel now works** — The new first-letter jump was swallowing `q` (and other vi nav keys). Reserved key blocklist added.
|
|
38
|
+
|
|
39
|
+
- **Track picker case-insensitive sort** — New tracks with Title Case names (`Late Night Hip Hop Groove.mp3`) no longer jump to the top of the list above the lowercase `agent_vibes_*` tracks.
|
|
40
|
+
|
|
41
|
+
### User Impact
|
|
42
|
+
|
|
43
|
+
- Editing an agent's voice or settings is now faster — no need to remember to click Save
|
|
44
|
+
- The voice picker is dramatically less cluttered with 904 LibriTTS speakers all having unique, friendly names
|
|
45
|
+
- Gender at a glance via colored symbols
|
|
46
|
+
- Three new music tracks for variety
|
|
47
|
+
- Blink/scroll artifacts gone in both Voices and Music tabs
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
3
51
|
## 🚀 v5.0.0 — Multi-Provider Support: Claude Code + Copilot + Codex
|
|
4
52
|
|
|
5
53
|
**Release Date:** April 2026
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.1.0",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
|
@@ -16,7 +16,7 @@ import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
|
|
|
16
16
|
import { formatReverbState, formatTrackName, formatVoiceName } from '../widgets/format-utils.js';
|
|
17
17
|
import {
|
|
18
18
|
PIPER_VOICES_DIR, SAMPLE_PHRASES,
|
|
19
|
-
parseMultiSpeaker, scanInstalledVoices, getVoiceMeta,
|
|
19
|
+
parseMultiSpeaker, scanInstalledVoices, getVoiceMeta, genderIconTag,
|
|
20
20
|
} from './voices-tab.js';
|
|
21
21
|
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
22
22
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
@@ -729,8 +729,10 @@ ${_tl('bmadDesc')}
|
|
|
729
729
|
btnBlink.startSpinner(previewBtn);
|
|
730
730
|
});
|
|
731
731
|
|
|
732
|
-
|
|
733
|
-
|
|
732
|
+
// Auto-save the current draft (called after every field change).
|
|
733
|
+
// Only persists fields that differ from the global defaults — same logic
|
|
734
|
+
// the explicit Save button used to use, just triggered automatically.
|
|
735
|
+
function _autoSaveAgent() {
|
|
734
736
|
const toSave = {};
|
|
735
737
|
if (draft.voice && draft.voice !== globalCfg.voice) toSave.voice = draft.voice;
|
|
736
738
|
if (draft.pretext !== AgentVoiceStore.getDefaultPretext(agent.displayName, agent.title)) toSave.pretext = draft.pretext;
|
|
@@ -742,22 +744,20 @@ ${_tl('bmadDesc')}
|
|
|
742
744
|
toSave.backgroundMusic = draft.backgroundMusic;
|
|
743
745
|
}
|
|
744
746
|
voiceStore.setAgentProfile(agent.id, toSave);
|
|
745
|
-
_closeModal();
|
|
746
747
|
refreshDisplay();
|
|
747
|
-
// Show temporary "Saved!" toast
|
|
748
748
|
_showSavedToast(agent.displayName);
|
|
749
|
-
}
|
|
749
|
+
}
|
|
750
750
|
|
|
751
|
-
const
|
|
751
|
+
const resetBtn = _modalBtn('Reset to Defaults', 18, () => {
|
|
752
752
|
voiceStore.resetAgentProfile(agent.id);
|
|
753
753
|
_closeModal();
|
|
754
754
|
refreshDisplay();
|
|
755
755
|
});
|
|
756
756
|
|
|
757
|
-
const
|
|
757
|
+
const closeBtn = _modalBtn('Cancel', 42, _closeModal);
|
|
758
758
|
|
|
759
759
|
// Blinking █ cursor + preview spinner — reusable across all modal buttons
|
|
760
|
-
const btnBlink = attachBtnBlink([previewBtn,
|
|
760
|
+
const btnBlink = attachBtnBlink([previewBtn, resetBtn, closeBtn], screen);
|
|
761
761
|
|
|
762
762
|
function _closeModal() {
|
|
763
763
|
if (_closed) return;
|
|
@@ -779,6 +779,7 @@ ${_tl('bmadDesc')}
|
|
|
779
779
|
switch (field.key) {
|
|
780
780
|
case 'voice':
|
|
781
781
|
_openVoicePickerForAgent(agent, draft, () => {
|
|
782
|
+
_autoSaveAgent();
|
|
782
783
|
fieldList.setItems(_fieldItems());
|
|
783
784
|
fieldList.select(idx);
|
|
784
785
|
fieldList.focus();
|
|
@@ -788,6 +789,7 @@ ${_tl('bmadDesc')}
|
|
|
788
789
|
|
|
789
790
|
case 'pretext':
|
|
790
791
|
_openPretextEditor(modal, draft, () => {
|
|
792
|
+
_autoSaveAgent();
|
|
791
793
|
fieldList.setItems(_fieldItems());
|
|
792
794
|
fieldList.select(idx);
|
|
793
795
|
fieldList.focus();
|
|
@@ -798,6 +800,7 @@ ${_tl('bmadDesc')}
|
|
|
798
800
|
case 'reverbPreset':
|
|
799
801
|
openReverbPicker(screen, draft.reverbPreset, (val) => {
|
|
800
802
|
draft.reverbPreset = val;
|
|
803
|
+
_autoSaveAgent();
|
|
801
804
|
fieldList.setItems(_fieldItems());
|
|
802
805
|
fieldList.select(idx);
|
|
803
806
|
fieldList.focus();
|
|
@@ -811,6 +814,7 @@ ${_tl('bmadDesc')}
|
|
|
811
814
|
case 'personality':
|
|
812
815
|
openPersonalityPicker(screen, draft.personality, (val) => {
|
|
813
816
|
draft.personality = val;
|
|
817
|
+
_autoSaveAgent();
|
|
814
818
|
fieldList.setItems(_fieldItems());
|
|
815
819
|
fieldList.select(idx);
|
|
816
820
|
fieldList.focus();
|
|
@@ -825,6 +829,7 @@ ${_tl('bmadDesc')}
|
|
|
825
829
|
openTrackPicker(screen, draft.backgroundMusic.track, draft.backgroundMusic.volume, (track) => {
|
|
826
830
|
draft.backgroundMusic.track = track;
|
|
827
831
|
draft.backgroundMusic.enabled = true;
|
|
832
|
+
_autoSaveAgent();
|
|
828
833
|
fieldList.setItems(_fieldItems());
|
|
829
834
|
fieldList.select(idx);
|
|
830
835
|
fieldList.focus();
|
|
@@ -839,6 +844,7 @@ ${_tl('bmadDesc')}
|
|
|
839
844
|
openVolumeInput(screen, draft.backgroundMusic.volume, (volume) => {
|
|
840
845
|
draft.backgroundMusic.volume = volume;
|
|
841
846
|
if (draft.backgroundMusic.track) draft.backgroundMusic.enabled = true;
|
|
847
|
+
_autoSaveAgent();
|
|
842
848
|
fieldList.setItems(_fieldItems());
|
|
843
849
|
fieldList.select(idx);
|
|
844
850
|
fieldList.focus();
|
|
@@ -861,9 +867,8 @@ ${_tl('bmadDesc')}
|
|
|
861
867
|
// Escape = close
|
|
862
868
|
fieldList.key(['escape', 'q'], _closeModal);
|
|
863
869
|
previewBtn.key(['escape'], _closeModal);
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
cancelBtn.key(['escape'], _closeModal);
|
|
870
|
+
resetBtn.key(['escape'], _closeModal);
|
|
871
|
+
closeBtn.key(['escape'], _closeModal);
|
|
867
872
|
|
|
868
873
|
// Tab + arrow navigation within modal
|
|
869
874
|
fieldList.key(['tab'], () => { previewBtn.focus(); screen.render(); });
|
|
@@ -887,23 +892,19 @@ ${_tl('bmadDesc')}
|
|
|
887
892
|
_prevFieldSel = cur;
|
|
888
893
|
});
|
|
889
894
|
|
|
890
|
-
// Wrap: up on buttons → back to field list
|
|
895
|
+
// Wrap: up on buttons → back to field list
|
|
891
896
|
previewBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
cancelBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
895
|
-
|
|
896
|
-
previewBtn.key(['tab', 'right'], () => { saveBtn.focus(); screen.render(); });
|
|
897
|
-
previewBtn.key(['left'], () => { cancelBtn.focus(); screen.render(); });
|
|
897
|
+
resetBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
898
|
+
closeBtn.key(['up'], () => { fieldList.focus(); fieldList.select(FIELDS.length - 1); screen.render(); });
|
|
898
899
|
|
|
899
|
-
|
|
900
|
-
|
|
900
|
+
previewBtn.key(['tab', 'right'], () => { resetBtn.focus(); screen.render(); });
|
|
901
|
+
previewBtn.key(['left'], () => { closeBtn.focus(); screen.render(); });
|
|
901
902
|
|
|
902
|
-
|
|
903
|
-
|
|
903
|
+
resetBtn.key(['tab', 'right'], () => { closeBtn.focus(); screen.render(); });
|
|
904
|
+
resetBtn.key(['left'], () => { previewBtn.focus(); screen.render(); });
|
|
904
905
|
|
|
905
|
-
|
|
906
|
-
|
|
906
|
+
closeBtn.key(['tab', 'right'], () => { fieldList.focus(); screen.render(); });
|
|
907
|
+
closeBtn.key(['left'], () => { resetBtn.focus(); screen.render(); });
|
|
907
908
|
|
|
908
909
|
fieldList.focus();
|
|
909
910
|
screen.render();
|
|
@@ -914,7 +915,6 @@ ${_tl('bmadDesc')}
|
|
|
914
915
|
|
|
915
916
|
function _openVoicePickerForAgent(agent, draft, onDone) {
|
|
916
917
|
let _allVoices = [];
|
|
917
|
-
let _filterText = '';
|
|
918
918
|
let _previewProc = null;
|
|
919
919
|
let _previewVoiceId = null;
|
|
920
920
|
let _vpClosed = false;
|
|
@@ -959,29 +959,19 @@ ${_tl('bmadDesc')}
|
|
|
959
959
|
});
|
|
960
960
|
vpModal.setFront();
|
|
961
961
|
|
|
962
|
-
// Search
|
|
963
|
-
blessed.text({
|
|
964
|
-
parent: vpModal, top: 1, left: 2,
|
|
965
|
-
content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
|
|
966
|
-
});
|
|
967
|
-
const vpSearch = blessed.textbox({
|
|
968
|
-
parent: vpModal, top: 1, left: 11, width: 40, height: 1,
|
|
969
|
-
inputOnFocus: true, keys: true,
|
|
970
|
-
style: { fg: COLORS.valueFg, bg: '#1a3a5c', focus: { bg: '#245a80' } },
|
|
971
|
-
});
|
|
972
|
-
|
|
973
962
|
// Column header
|
|
974
|
-
const COL_N =
|
|
975
|
-
const COL_G =
|
|
963
|
+
const COL_N = 30;
|
|
964
|
+
const COL_G = 4;
|
|
976
965
|
blessed.text({
|
|
977
|
-
parent: vpModal, top:
|
|
978
|
-
content: `{bright-cyan-fg}${'Name'.padEnd(COL_N)}
|
|
966
|
+
parent: vpModal, top: 1, left: 6, tags: true,
|
|
967
|
+
content: `{bright-cyan-fg}${'Name'.padEnd(COL_N)}{/bright-cyan-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {bright-cyan-fg}Provider{/bright-cyan-fg}`,
|
|
979
968
|
style: { bg: COLORS.contentBg },
|
|
980
969
|
});
|
|
981
970
|
|
|
982
971
|
const vpList = blessed.list({
|
|
983
|
-
parent: vpModal, top:
|
|
972
|
+
parent: vpModal, top: 2, left: 2, right: 2, bottom: 5,
|
|
984
973
|
keys: true, vi: true, mouse: true,
|
|
974
|
+
tags: true,
|
|
985
975
|
border: { type: 'line' },
|
|
986
976
|
scrollbar: { ch: '│', style: { fg: COLORS.borderFg } },
|
|
987
977
|
style: {
|
|
@@ -1004,16 +994,10 @@ ${_tl('bmadDesc')}
|
|
|
1004
994
|
|
|
1005
995
|
blessed.text({
|
|
1006
996
|
parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
|
|
1007
|
-
content: '{#455a64-fg}[
|
|
997
|
+
content: '{#455a64-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [Esc] Cancel{/#455a64-fg}',
|
|
1008
998
|
style: { bg: COLORS.contentBg },
|
|
1009
999
|
});
|
|
1010
1000
|
|
|
1011
|
-
function _getFiltered() {
|
|
1012
|
-
if (!_filterText) return _allVoices;
|
|
1013
|
-
const f = _filterText.toLowerCase();
|
|
1014
|
-
return _allVoices.filter(v => v.toLowerCase().includes(f));
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
1001
|
function _buildVoiceItems(voices) {
|
|
1018
1002
|
return voices.map(v => {
|
|
1019
1003
|
const isActive = v === draft.voice;
|
|
@@ -1023,7 +1007,8 @@ ${_tl('bmadDesc')}
|
|
|
1023
1007
|
const name = meta.displayName.length > COL_N
|
|
1024
1008
|
? meta.displayName.slice(0, COL_N - 1) + '…'
|
|
1025
1009
|
: meta.displayName.padEnd(COL_N);
|
|
1026
|
-
|
|
1010
|
+
// genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
|
|
1011
|
+
return ` ${dot} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
|
|
1027
1012
|
});
|
|
1028
1013
|
}
|
|
1029
1014
|
|
|
@@ -1032,8 +1017,10 @@ ${_tl('bmadDesc')}
|
|
|
1032
1017
|
const savedIdx = vpList.selected ?? 0;
|
|
1033
1018
|
const savedScroll = vpList.childBase ?? 0;
|
|
1034
1019
|
_allVoices = scanInstalledVoices();
|
|
1035
|
-
|
|
1036
|
-
|
|
1020
|
+
// Sort by display name so the first-letter quick jump is intuitive
|
|
1021
|
+
_allVoices.sort((a, b) => getVoiceMeta(a).displayName.localeCompare(
|
|
1022
|
+
getVoiceMeta(b).displayName, undefined, { sensitivity: 'base' }));
|
|
1023
|
+
const items = _buildVoiceItems(_allVoices);
|
|
1037
1024
|
vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
|
|
1038
1025
|
vpList.select(Math.min(savedIdx, items.length - 1));
|
|
1039
1026
|
vpList.childBase = Math.min(savedScroll, Math.max(0, items.length - (vpList.height - 2)));
|
|
@@ -1104,25 +1091,41 @@ ${_tl('bmadDesc')}
|
|
|
1104
1091
|
piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1105
1092
|
}
|
|
1106
1093
|
|
|
1107
|
-
vpSearch.on('keypress', () => {
|
|
1108
|
-
setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
|
|
1109
|
-
});
|
|
1110
|
-
vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
|
|
1111
|
-
vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
|
|
1112
1094
|
vpList.key(['enter'], () => {
|
|
1113
|
-
const
|
|
1114
|
-
const sel = filtered[vpList.selected];
|
|
1095
|
+
const sel = _allVoices[vpList.selected];
|
|
1115
1096
|
if (sel) { draft.voice = sel; _closeVP(); }
|
|
1116
1097
|
});
|
|
1117
1098
|
vpList.key(['space'], () => {
|
|
1118
|
-
const
|
|
1119
|
-
const sel = filtered[vpList.selected];
|
|
1099
|
+
const sel = _allVoices[vpList.selected];
|
|
1120
1100
|
if (sel) _previewVoice(sel);
|
|
1121
1101
|
});
|
|
1122
1102
|
vpList.key(['escape', 'q'], _closeVP);
|
|
1123
1103
|
|
|
1104
|
+
// PageUp / PageDown / Home / End navigation
|
|
1105
|
+
const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
|
|
1106
|
+
vpList.key(['pageup'], () => { vpList.up(_pageSize()); screen.render(); });
|
|
1107
|
+
vpList.key(['pagedown'], () => { vpList.down(_pageSize()); screen.render(); });
|
|
1108
|
+
vpList.key(['home'], () => { vpList.select(0); screen.render(); });
|
|
1109
|
+
vpList.key(['end'], () => { vpList.select(Math.max(0, _allVoices.length - 1)); screen.render(); });
|
|
1110
|
+
|
|
1111
|
+
// First-letter quick jump: typing 'a' jumps to the first voice starting
|
|
1112
|
+
// with A. Block keys reserved by the list widget (vi nav, cancel) so
|
|
1113
|
+
// they don't get swallowed: q (cancel), j/k/g/h/l (vi navigation).
|
|
1114
|
+
const _vpJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'q']);
|
|
1115
|
+
vpList.on('keypress', (ch, key) => {
|
|
1116
|
+
if (!ch || key?.ctrl || key?.meta) return;
|
|
1117
|
+
if (!/^[a-zA-Z]$/.test(ch)) return;
|
|
1118
|
+
const target = ch.toLowerCase();
|
|
1119
|
+
if (_vpJumpBlocked.has(target)) return;
|
|
1120
|
+
const idx = _allVoices.findIndex(v => {
|
|
1121
|
+
const name = getVoiceMeta(v).displayName.toLowerCase();
|
|
1122
|
+
return name.startsWith(target);
|
|
1123
|
+
});
|
|
1124
|
+
if (idx >= 0) { vpList.select(idx); screen.render(); }
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1124
1127
|
_refreshVP();
|
|
1125
|
-
const activeIdx =
|
|
1128
|
+
const activeIdx = _allVoices.indexOf(draft.voice);
|
|
1126
1129
|
if (activeIdx >= 0) vpList.select(activeIdx);
|
|
1127
1130
|
vpList.focus();
|
|
1128
1131
|
screen.render();
|