agentvibes 5.6.7 → 5.6.8
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/config/audio-effects.cfg +5 -0
- package/.claude/config/background-music-position.txt +1 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/play-tts-piper.sh +39 -1
- package/.claude/hooks/play-tts.sh +9 -0
- package/.claude/hooks/session-start-tts.sh +48 -13
- package/.claude/hooks-windows/play-tts-piper.ps1 +26 -1
- package/.claude/hooks-windows/play-tts.ps1 +25 -1
- package/.claude/hooks-windows/session-start-tts.ps1 +28 -9
- package/.claude/settings.json +2 -2
- package/CLAUDE.md +9 -0
- package/README.md +11 -3
- package/RELEASE_NOTES.md +32 -0
- package/package.json +1 -1
- package/src/console/app.js +6 -0
- package/src/console/widgets/format-utils.js +11 -2
- package/src/installer.js +38 -37
- package/src/services/llm-provider-service.js +28 -9
- package/src/utils/voice-names.js +2 -0
|
@@ -1 +1,6 @@
|
|
|
1
1
|
default||agentvibes_soft_flamenco_loop.mp3|0.30
|
|
2
|
+
llm:default|light||0.15|en_US-lessac-high||piper
|
|
3
|
+
llm:claude-code|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Claude Code here|piper
|
|
4
|
+
llm:copilot|light|agent_vibes_bossa_nova_v2_loop.mp3|0.15|en_US-libritts-high::Anna-11|Copilot here|piper
|
|
5
|
+
llm:codex|light|agent_vibes_chillwave_v2_loop.mp3|0.15|en_US-lessac-high|Codex here|piper
|
|
6
|
+
llm:hermes|light|agent_vibes_bachata_v1_loop.mp3|0.15|en_US-libritts-high::Leo-8|Hermes here|piper
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20260509
|
|
@@ -91,6 +91,7 @@ DEFAULT_VOICE="en_US-lessac-medium"
|
|
|
91
91
|
# @returns Sets $VOICE_MODEL global variable
|
|
92
92
|
# @sideeffects None
|
|
93
93
|
VOICE_MODEL=""
|
|
94
|
+
FILE_VOICE=""
|
|
94
95
|
|
|
95
96
|
# Get current language setting
|
|
96
97
|
CURRENT_LANGUAGE=$(get_language_code)
|
|
@@ -206,6 +207,15 @@ else
|
|
|
206
207
|
fi
|
|
207
208
|
fi
|
|
208
209
|
|
|
210
|
+
# Preserve full display name (with ::SpeakerName) before any stripping for logging
|
|
211
|
+
if [[ -n "$VOICE_OVERRIDE" ]]; then
|
|
212
|
+
DISPLAY_VOICE_NAME="$VOICE_OVERRIDE"
|
|
213
|
+
elif [[ -n "$FILE_VOICE" ]]; then
|
|
214
|
+
DISPLAY_VOICE_NAME="$FILE_VOICE"
|
|
215
|
+
else
|
|
216
|
+
DISPLAY_VOICE_NAME="$VOICE_MODEL"
|
|
217
|
+
fi
|
|
218
|
+
|
|
209
219
|
# @function validate_inputs
|
|
210
220
|
# @intent Check required parameters
|
|
211
221
|
# @why Fail fast with clear errors if inputs missing
|
|
@@ -215,6 +225,10 @@ if [[ -z "$TEXT" ]]; then
|
|
|
215
225
|
exit 1
|
|
216
226
|
fi
|
|
217
227
|
|
|
228
|
+
# Augment PATH for non-interactive shells (pipx installs to ~/.local/bin which
|
|
229
|
+
# interactive shells get via .bashrc/.zshrc, but Bash tool calls skip profile)
|
|
230
|
+
export PATH="$HOME/.local/bin:$HOME/.local/share/pipx/venvs/piper-tts/bin:$PATH"
|
|
231
|
+
|
|
218
232
|
# Check if Piper is installed
|
|
219
233
|
if ! command -v piper &> /dev/null; then
|
|
220
234
|
echo "❌ Error: Piper TTS not installed"
|
|
@@ -614,7 +628,31 @@ if [[ -n "$BACKGROUND_MUSIC" ]]; then
|
|
|
614
628
|
MUSIC_FILENAME=$(basename "$BACKGROUND_MUSIC")
|
|
615
629
|
echo -e "${WHITE}🎵 Background music:${NC} ${PURPLE}$MUSIC_FILENAME${NC}"
|
|
616
630
|
fi
|
|
617
|
-
|
|
631
|
+
# Build friendly label: "model::Mike-13 [Mike Nash]"
|
|
632
|
+
_SURNAME_POOL=("Bell" "Carter" "Davis" "Ellis" "Foster" "Gray" "Hayes" "Irving" "Jones" "Knox" "Lane" "Mason" "Nash" "Owens" "Pierce" "Quinn")
|
|
633
|
+
_VOICE_DISPLAY_LABEL="$DISPLAY_VOICE_NAME"
|
|
634
|
+
if [[ "$DISPLAY_VOICE_NAME" == *"::"* ]]; then
|
|
635
|
+
_SP="${DISPLAY_VOICE_NAME#*::}"
|
|
636
|
+
# Skip 16Speakers names (underscore = already first_last format)
|
|
637
|
+
if [[ "$_SP" != *"_"* ]]; then
|
|
638
|
+
_FRIENDLY=""
|
|
639
|
+
if [[ "$_SP" =~ ^(.+)-([0-9]+)$ ]]; then
|
|
640
|
+
if [[ ${BASH_REMATCH[2]} -ge 2 ]]; then
|
|
641
|
+
_IDX=$(( (${BASH_REMATCH[2]} - 1) % 16 ))
|
|
642
|
+
_FRIENDLY="${BASH_REMATCH[1]} ${_SURNAME_POOL[$_IDX]}"
|
|
643
|
+
else
|
|
644
|
+
# n=1: strip suffix, use Bell — matches uniquifyVoiceName JS behaviour
|
|
645
|
+
_FRIENDLY="${BASH_REMATCH[1]} ${_SURNAME_POOL[0]}"
|
|
646
|
+
fi
|
|
647
|
+
elif [[ "$_SP" =~ [[:space:]] ]]; then
|
|
648
|
+
_FRIENDLY="$_SP"
|
|
649
|
+
else
|
|
650
|
+
_FRIENDLY="$_SP ${_SURNAME_POOL[0]}"
|
|
651
|
+
fi
|
|
652
|
+
[[ "$_FRIENDLY" != "$_SP" ]] && _VOICE_DISPLAY_LABEL="$DISPLAY_VOICE_NAME [$_FRIENDLY]"
|
|
653
|
+
fi
|
|
654
|
+
fi
|
|
655
|
+
echo -e "${WHITE}🎤 Voice used:${NC} ${BLUE}$_VOICE_DISPLAY_LABEL${NC} ${WHITE}(Piper TTS)${NC}"
|
|
618
656
|
|
|
619
657
|
# Show personality if configured
|
|
620
658
|
PERSONALITY=$(cat "$PROJECT_ROOT/.claude/tts-personality.txt" 2>/dev/null || cat "$HOME/.claude/tts-personality.txt" 2>/dev/null || echo "")
|
|
@@ -115,6 +115,15 @@ while [[ $# -gt 0 ]]; do
|
|
|
115
115
|
LLM_PROVIDER="${2:-}"
|
|
116
116
|
shift 2
|
|
117
117
|
;;
|
|
118
|
+
--project-dir)
|
|
119
|
+
# Always prefer the explicitly-injected project dir over any stale
|
|
120
|
+
# CLAUDE_PROJECT_DIR in the environment (fixes silent override by env).
|
|
121
|
+
# Validate the path exists before trusting it.
|
|
122
|
+
if [[ -n "${2:-}" && -d "${2}" ]]; then
|
|
123
|
+
export CLAUDE_PROJECT_DIR="${2}"
|
|
124
|
+
fi
|
|
125
|
+
shift 2
|
|
126
|
+
;;
|
|
118
127
|
*)
|
|
119
128
|
_POSITIONAL_ARGS+=("$1")
|
|
120
129
|
shift
|
|
@@ -10,18 +10,49 @@ set -euo pipefail
|
|
|
10
10
|
# Fix locale warnings
|
|
11
11
|
export LC_ALL=C
|
|
12
12
|
|
|
13
|
-
# Get script directory
|
|
13
|
+
# Get script directory (resolve symlinks so $SCRIPT_DIR is the real hooks dir)
|
|
14
14
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
15
|
|
|
16
|
+
# Resolve absolute path to play-tts.sh from this script's own location.
|
|
17
|
+
# Using an absolute path in the injected protocol ensures the correct
|
|
18
|
+
# play-tts.sh is called regardless of the working directory when Claude
|
|
19
|
+
# runs the command — fixes "wrong voice in fresh folder" regression.
|
|
20
|
+
PLAY_TTS_PATH="$SCRIPT_DIR/play-tts.sh"
|
|
21
|
+
|
|
16
22
|
# Check if AgentVibes is installed
|
|
17
|
-
if [[ ! -f "$
|
|
23
|
+
if [[ ! -f "$PLAY_TTS_PATH" ]]; then
|
|
18
24
|
# AgentVibes not installed, don't inject anything
|
|
19
25
|
exit 0
|
|
20
26
|
fi
|
|
21
27
|
|
|
28
|
+
# Capture project dir NOW while Claude Code has set CLAUDE_PROJECT_DIR.
|
|
29
|
+
# Bash tool calls (how Claude actually runs play-tts.sh) do not automatically
|
|
30
|
+
# receive CLAUDE_PROJECT_DIR, so we bake it into the injected protocol command
|
|
31
|
+
# via --project-dir so the correct per-project config is always found.
|
|
32
|
+
CAPTURED_PROJECT_DIR=""
|
|
33
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" && -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then
|
|
34
|
+
CAPTURED_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
|
|
35
|
+
_PROJECT_CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude"
|
|
36
|
+
else
|
|
37
|
+
# Fallback: script lives inside .claude/hooks/, so parent IS .claude/
|
|
38
|
+
_PROJECT_CLAUDE_DIR="$(dirname "$SCRIPT_DIR")"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Build --project-dir flag to embed in TTS commands.
|
|
42
|
+
# Sanitize: strip any embedded quotes that would break shell quoting.
|
|
43
|
+
PROJECT_DIR_FLAG=""
|
|
44
|
+
if [[ -n "$CAPTURED_PROJECT_DIR" ]]; then
|
|
45
|
+
_SAFE_PROJECT_DIR="${CAPTURED_PROJECT_DIR//\"/}"
|
|
46
|
+
PROJECT_DIR_FLAG=" --project-dir \"$_SAFE_PROJECT_DIR\""
|
|
47
|
+
fi
|
|
48
|
+
|
|
22
49
|
# Check for sentiment (priority) or personality (fallback)
|
|
23
|
-
SENTIMENT=$(cat
|
|
24
|
-
|
|
50
|
+
SENTIMENT=$(cat "$_PROJECT_CLAUDE_DIR/tts-sentiment.txt" 2>/dev/null \
|
|
51
|
+
|| cat "$HOME/.claude/tts-sentiment.txt" 2>/dev/null \
|
|
52
|
+
|| echo "")
|
|
53
|
+
PERSONALITY=$(cat "$_PROJECT_CLAUDE_DIR/tts-personality.txt" 2>/dev/null \
|
|
54
|
+
|| cat "$HOME/.claude/tts-personality.txt" 2>/dev/null \
|
|
55
|
+
|| echo "normal")
|
|
25
56
|
|
|
26
57
|
# Determine style
|
|
27
58
|
if [[ -n "$SENTIMENT" ]]; then
|
|
@@ -31,26 +62,30 @@ else
|
|
|
31
62
|
fi
|
|
32
63
|
|
|
33
64
|
# Get verbosity level (Issue #32)
|
|
34
|
-
VERBOSITY=$(cat
|
|
65
|
+
VERBOSITY=$(cat "$_PROJECT_CLAUDE_DIR/tts-verbosity.txt" 2>/dev/null \
|
|
66
|
+
|| cat "$HOME/.claude/tts-verbosity.txt" 2>/dev/null \
|
|
67
|
+
|| echo "low")
|
|
35
68
|
|
|
36
|
-
# Output optimized TTS protocol
|
|
37
|
-
|
|
69
|
+
# Output optimized TTS protocol.
|
|
70
|
+
# Note: heredoc uses double-quotes (<<EOF not <<'EOF') so $PLAY_TTS_PATH expands.
|
|
71
|
+
# Backticks must be escaped (\`) to avoid command substitution.
|
|
72
|
+
cat << EOF
|
|
38
73
|
|
|
39
74
|
# AgentVibes TTS Protocol
|
|
40
75
|
|
|
41
76
|
**CRITICAL: You MUST execute TTS at TWO points for EVERY user interaction:**
|
|
42
77
|
|
|
43
|
-
1. **Acknowledgment** - Start of task:
|
|
44
|
-
2. **Completion** - End of task:
|
|
78
|
+
1. **Acknowledgment** - Start of task: \`Bash: $PLAY_TTS_PATH "[action]" --llm claude-code$PROJECT_DIR_FLAG\`
|
|
79
|
+
2. **Completion** - End of task: \`Bash: $PLAY_TTS_PATH "[result + key details]" --llm claude-code$PROJECT_DIR_FLAG\`
|
|
45
80
|
|
|
46
81
|
**IMPORTANT: Run TTS commands inline (NOT in background). Wait for each TTS call to complete before continuing.**
|
|
47
82
|
|
|
48
83
|
**Example:**
|
|
49
|
-
|
|
50
|
-
[Bash:
|
|
84
|
+
\`\`\`
|
|
85
|
+
[Bash: $PLAY_TTS_PATH "Checking git status" --llm claude-code$PROJECT_DIR_FLAG]
|
|
51
86
|
[work...]
|
|
52
|
-
[Bash:
|
|
53
|
-
|
|
87
|
+
[Bash: $PLAY_TTS_PATH "Repository is clean, no changes" --llm claude-code$PROJECT_DIR_FLAG]
|
|
88
|
+
\`\`\`
|
|
54
89
|
|
|
55
90
|
EOF
|
|
56
91
|
|
|
@@ -75,6 +75,7 @@ elseif (Test-Path $VoiceFile) {
|
|
|
75
75
|
# IMPORTANT: The trailing number in a speaker name (e.g. "Holly-7") is a disambiguation
|
|
76
76
|
# suffix, NOT the speaker index. Real index must be looked up from voice-assignments.json.
|
|
77
77
|
$SpeakerId = $null
|
|
78
|
+
$DisplayVoiceName = $VoiceName # preserve full name (e.g. "model::SpeakerName") for logging
|
|
78
79
|
|
|
79
80
|
if ($VoiceName -match '::') {
|
|
80
81
|
$parts = $VoiceName -split '::'
|
|
@@ -227,7 +228,31 @@ try {
|
|
|
227
228
|
|
|
228
229
|
# Display results
|
|
229
230
|
Write-Host "[OK] Saved to: $AudioFile" -ForegroundColor Green
|
|
230
|
-
|
|
231
|
+
|
|
232
|
+
# Build friendly label: "model::Mike-13 [Mike Nash]"
|
|
233
|
+
$SURNAME_POOL = @('Bell','Carter','Davis','Ellis','Foster','Gray','Hayes','Irving','Jones','Knox','Lane','Mason','Nash','Owens','Pierce','Quinn')
|
|
234
|
+
$VoiceDisplayLabel = $DisplayVoiceName
|
|
235
|
+
if ($DisplayVoiceName -match '::(.+)$') {
|
|
236
|
+
$sp = $Matches[1]
|
|
237
|
+
# Skip 16Speakers names (contain underscore — already first_last format)
|
|
238
|
+
if ($sp -notmatch '_') {
|
|
239
|
+
$friendly = $null
|
|
240
|
+
if ($sp -match '^(.+)-(\d+)$') {
|
|
241
|
+
if ([int]$Matches[2] -ge 2) {
|
|
242
|
+
$friendly = "$($Matches[1]) $($SURNAME_POOL[([int]$Matches[2] - 1) % $SURNAME_POOL.Length])"
|
|
243
|
+
} else {
|
|
244
|
+
# n=1: strip suffix, use Bell — matches uniquifyVoiceName JS behaviour
|
|
245
|
+
$friendly = "$($Matches[1]) $($SURNAME_POOL[0])"
|
|
246
|
+
}
|
|
247
|
+
} elseif ($sp -match '\s') {
|
|
248
|
+
$friendly = $sp
|
|
249
|
+
} else {
|
|
250
|
+
$friendly = "$sp $($SURNAME_POOL[0])"
|
|
251
|
+
}
|
|
252
|
+
if ($null -ne $friendly -and $friendly -ne $sp) { $VoiceDisplayLabel = "$DisplayVoiceName [$friendly]" }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
Write-Host "[VOICE] Voice used: $VoiceDisplayLabel (Piper)" -ForegroundColor Green
|
|
231
256
|
|
|
232
257
|
# Apply audio effects (reverb, background music) if processor script exists.
|
|
233
258
|
# SKIP when AGENTVIBES_NO_PLAY is set — that means the parent play-tts.ps1
|
|
@@ -16,7 +16,13 @@ param(
|
|
|
16
16
|
# When provided, the router looks up an `llm:<name>` row in audio-effects.cfg
|
|
17
17
|
# to apply LLM-specific voice, pretext, reverb, and engine settings.
|
|
18
18
|
[Parameter(Mandatory = $false)]
|
|
19
|
-
[string]$llm = ""
|
|
19
|
+
[string]$llm = "",
|
|
20
|
+
|
|
21
|
+
# Project directory override. session-start-tts.ps1 bakes the session's
|
|
22
|
+
# CLAUDE_PROJECT_DIR value here so per-project config is found even when
|
|
23
|
+
# Bash tool calls do not propagate CLAUDE_PROJECT_DIR to child processes.
|
|
24
|
+
[Parameter(Mandatory = $false)]
|
|
25
|
+
[string]$ProjectDir = ""
|
|
20
26
|
)
|
|
21
27
|
|
|
22
28
|
# Text-file handoff: the SSH receiver watcher writes long/special-char text to
|
|
@@ -31,6 +37,16 @@ if ($Text -eq "__from_file__" -and $env:AGENTVIBES_TEXT_FILE) {
|
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
# If -ProjectDir was passed (by session-start-tts.ps1), promote it to the
|
|
41
|
+
# CLAUDE_PROJECT_DIR env var so the per-LLM config lookup below finds it.
|
|
42
|
+
# This ensures per-project audio settings work even when Bash tool calls
|
|
43
|
+
# don't automatically inherit CLAUDE_PROJECT_DIR from Claude Code.
|
|
44
|
+
if ($ProjectDir -and (Test-Path $ProjectDir)) {
|
|
45
|
+
# Always prefer the explicitly-injected project dir; validates path exists
|
|
46
|
+
# before trusting it (fixes both the stale-env-var override bug and path injection).
|
|
47
|
+
$env:CLAUDE_PROJECT_DIR = $ProjectDir
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
# Configuration paths
|
|
35
51
|
# First check if we're running from a project directory with .claude
|
|
36
52
|
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
@@ -187,6 +203,14 @@ if ($llm -and $llm -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_-]*$') {
|
|
|
187
203
|
}
|
|
188
204
|
|
|
189
205
|
# --- Default fallback --------------------------------------------------------
|
|
206
|
+
# When no -llm flag is passed (e.g. hooks invoked by Claude Code without the
|
|
207
|
+
# flag), check AGENTVIBES_LLM_KEY first — it is set by the hook infrastructure
|
|
208
|
+
# as "llm:<name>" and carries the active LLM identity. Strip the "llm:" prefix
|
|
209
|
+
# to get the bare key used for config lookup.
|
|
210
|
+
if (-not $llm -and $env:AGENTVIBES_LLM_KEY -match '^llm:([a-zA-Z0-9][a-zA-Z0-9_-]*)$') {
|
|
211
|
+
$llm = $Matches[1]
|
|
212
|
+
}
|
|
213
|
+
|
|
190
214
|
# An empty $llm routes through the "default" pseudo-LLM. Users who configure
|
|
191
215
|
# an `llm:default` row in audio-effects.cfg get consistent audio settings for
|
|
192
216
|
# every LLM that doesn't pass its own -llm flag — a convenient global override
|
|
@@ -9,18 +9,37 @@
|
|
|
9
9
|
|
|
10
10
|
$ErrorActionPreference = "Stop"
|
|
11
11
|
|
|
12
|
-
# Get script directory
|
|
12
|
+
# Get script directory and resolve absolute path to play-tts.ps1.
|
|
13
|
+
# Using an absolute path in the injected protocol ensures the correct play-tts.ps1
|
|
14
|
+
# is called regardless of the working directory when Claude runs the command.
|
|
13
15
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
16
|
+
$PlayTtsPath = Join-Path $ScriptDir "play-tts.ps1"
|
|
14
17
|
|
|
15
18
|
# Check if AgentVibes is installed
|
|
16
|
-
if (-not (Test-Path
|
|
19
|
+
if (-not (Test-Path $PlayTtsPath)) {
|
|
17
20
|
# AgentVibes not installed, don't inject anything
|
|
18
21
|
exit 0
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
# Capture project dir NOW while Claude Code has set it correctly.
|
|
25
|
+
# Bash tool calls (how Claude actually runs play-tts.ps1) do not
|
|
26
|
+
# automatically receive CLAUDE_PROJECT_DIR, so we bake the value
|
|
27
|
+
# into the injected protocol command via the -ProjectDir parameter.
|
|
28
|
+
$CapturedProjectDir = ""
|
|
29
|
+
if ($env:CLAUDE_PROJECT_DIR -and (Test-Path "$env:CLAUDE_PROJECT_DIR\.claude")) {
|
|
30
|
+
$CapturedProjectDir = $env:CLAUDE_PROJECT_DIR
|
|
31
|
+
$ProjectClaudeDir = "$env:CLAUDE_PROJECT_DIR\.claude"
|
|
32
|
+
} else {
|
|
33
|
+
$ProjectClaudeDir = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptDir)) ".claude"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Build the -ProjectDir flag to inject into TTS commands (empty string = omit flag).
|
|
37
|
+
# Sanitize: strip any embedded quotes that would break PowerShell argument quoting.
|
|
38
|
+
$ProjectDirFlag = ""
|
|
39
|
+
if ($CapturedProjectDir) {
|
|
40
|
+
$SafeProjectDir = $CapturedProjectDir -replace '"', ''
|
|
41
|
+
$ProjectDirFlag = " -ProjectDir `"$SafeProjectDir`""
|
|
42
|
+
}
|
|
24
43
|
|
|
25
44
|
# Check for sentiment (priority) or personality (fallback)
|
|
26
45
|
$Sentiment = ""
|
|
@@ -65,14 +84,14 @@ Write-Output @"
|
|
|
65
84
|
|
|
66
85
|
**CRITICAL: You MUST execute TTS at TWO points for EVERY user interaction:**
|
|
67
86
|
|
|
68
|
-
1. **Acknowledgment** - Start of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "
|
|
69
|
-
2. **Completion** - End of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "
|
|
87
|
+
1. **Acknowledgment** - Start of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "[action]" -llm claude-code$ProjectDirFlag``
|
|
88
|
+
2. **Completion** - End of task: ``Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "[result + key details]" -llm claude-code$ProjectDirFlag``
|
|
70
89
|
|
|
71
90
|
**Example:**
|
|
72
91
|
``````
|
|
73
|
-
[Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "
|
|
92
|
+
[Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "Checking git status" -llm claude-code$ProjectDirFlag]
|
|
74
93
|
[work...]
|
|
75
|
-
[Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "
|
|
94
|
+
[Bash: powershell -NoProfile -ExecutionPolicy Bypass -File "$PlayTtsPath" "Repository is clean, no changes" -llm claude-code$ProjectDirFlag]
|
|
76
95
|
``````
|
|
77
96
|
|
|
78
97
|
"@
|
package/.claude/settings.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"$
|
|
9
|
+
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"$HOME\\.claude\\hooks-windows\\session-start-tts.ps1\""
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"hooks": [
|
|
17
17
|
{
|
|
18
18
|
"type": "command",
|
|
19
|
-
"command": "bash \"$
|
|
19
|
+
"command": "bash \"$HOME/.claude/hooks/session-start-tts.sh\""
|
|
20
20
|
}
|
|
21
21
|
]
|
|
22
22
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -28,6 +28,15 @@ This project follows **BMAD (BMM - Business Model Methodology)** for all story d
|
|
|
28
28
|
4. **Update sprint-status.yaml** automatically via `/dev-story`
|
|
29
29
|
5. **Code review included** - Built into `/dev-story` workflow
|
|
30
30
|
|
|
31
|
+
### ✅ Non-Destructive Configuration Rule (MANDATORY)
|
|
32
|
+
All code that reads, writes, or modifies user configuration MUST be non-destructive:
|
|
33
|
+
1. **Never delete or overwrite** existing user `.claude/` or `~/.claude/` config files (settings, voices, personalities, audio-effects.cfg) unless the user explicitly requested it
|
|
34
|
+
2. **Copy new files; never remove existing ones** — installer adds missing files only
|
|
35
|
+
3. **Write hooks only when absent** — `configureSessionStartHook` and similar functions check for existing hooks before writing
|
|
36
|
+
4. **Preserve custom entries** — e.g. `audio-effects.cfg` user rows must survive an `agentvibes update`
|
|
37
|
+
5. **Creating directories is fine** — `mkdir -p` / `{ recursive: true }` is always safe
|
|
38
|
+
6. Any function that could overwrite user data must have a test asserting idempotency
|
|
39
|
+
|
|
31
40
|
### ✅ Git Workflow (ONLY Outside BMAD)
|
|
32
41
|
For changes outside story development:
|
|
33
42
|
1. Describe changes before acting
|
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.6.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.6.8
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -40,9 +40,17 @@ Whether you're coding in Claude Code, chatting in Claude Desktop, using Warp Ter
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
## 🌟 NEW IN v5.6.
|
|
43
|
+
## 🌟 NEW IN v5.6.8 — WSL Voice Routing Fixed + Session Lifecycle Reliability
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
**WSL users:** AgentVibes was playing `en_US-lessac-medium` regardless of your configured voice. Fixed — Piper is now found in non-interactive shells by explicitly prepending `~/.local/bin` to `PATH` before the binary check.
|
|
46
|
+
|
|
47
|
+
**Per-project routing:** The session-start hook now bakes `--project-dir` into every injected TTS command, so your configured voice and music play correctly in Bash tool calls even when `CLAUDE_PROJECT_DIR` isn't in the environment.
|
|
48
|
+
|
|
49
|
+
`play-tts-piper.sh` and `play-tts-piper.ps1` are now included in `agentvibes install`'s critical hooks deployment — updated versions propagate automatically.
|
|
50
|
+
|
|
51
|
+
## v5.6.7 — Windows Preview Fixed
|
|
52
|
+
|
|
53
|
+
The Preview button in LLM audio configuration now works correctly on Windows.
|
|
46
54
|
|
|
47
55
|
## v5.6.6 — Preview Button Works in WSL + Comprehensive Windows Test Suite
|
|
48
56
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# AgentVibes Release Notes
|
|
2
2
|
|
|
3
|
+
## 🐧 v5.6.8 — WSL Voice Routing Fixed + Session Lifecycle Reliability
|
|
4
|
+
|
|
5
|
+
**Released:** 2026-05-09
|
|
6
|
+
|
|
7
|
+
### 🐛 WSL: Configured Voice Now Plays (Not Lessac Fallback)
|
|
8
|
+
|
|
9
|
+
In WSL sessions, AgentVibes was playing `en_US-lessac-medium` regardless of what voice you configured. The root cause: `pipx` installs Piper to `~/.local/bin/`, which interactive shells get via `.bashrc`/`.zshrc`, but Claude Code's Bash tool calls run non-interactively and skip profile sourcing — `command -v piper` failed, falling back to the default voice.
|
|
10
|
+
|
|
11
|
+
**Fix:** `play-tts-piper.sh` now prepends `~/.local/bin` and the pipx Piper venv bin to `PATH` before the binary check, so Piper is found regardless of shell mode.
|
|
12
|
+
|
|
13
|
+
### 🐛 Per-Project Voice/Music Lost When `CLAUDE_PROJECT_DIR` Not in Bash Environment
|
|
14
|
+
|
|
15
|
+
When Claude Code runs a Bash tool call, `CLAUDE_PROJECT_DIR` is not passed in the environment. The TTS hooks couldn't find per-project config and fell back to global defaults — wrong voice, wrong music, no pretext.
|
|
16
|
+
|
|
17
|
+
**Fix:** `session-start-tts.sh` (and `.ps1`) now bakes the project directory into the injected hook command as `--project-dir`. `play-tts.sh` reads this flag before any config lookup, so per-project routing is reliable in every Bash tool call.
|
|
18
|
+
|
|
19
|
+
### 🐛 `play-tts-piper.sh` and `play-tts-piper.ps1` Not Deployed by `agentvibes install`
|
|
20
|
+
|
|
21
|
+
These hooks were missing from `CRITICAL_HOOKS` / `CRITICAL_HOOKS_WINDOWS`, so `agentvibes install` never propagated updated versions to `~/.claude/hooks/`.
|
|
22
|
+
|
|
23
|
+
**Fix:** Both are now in the critical hooks list and always deployed on install/update.
|
|
24
|
+
|
|
25
|
+
### 🐛 Voice Display Name Bugs
|
|
26
|
+
|
|
27
|
+
- `uniquifyVoiceName("Mary-1")` returned `"Mary-1 Bell"` instead of `"Mary Bell"`.
|
|
28
|
+
- 16Speakers names like `Rose_Ibex` were incorrectly getting a surname appended (`"Rose Ibex Bell"`).
|
|
29
|
+
- `🎤 Voice used:` line was missing from WSL bash output.
|
|
30
|
+
|
|
31
|
+
All three fixed. A new test file (`test/unit/voice-names.test.js`, 16 tests) covers these cases.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
3
35
|
## 🪟 v5.6.7 — Windows Preview Fixed
|
|
4
36
|
|
|
5
37
|
**Released:** 2026-05-08
|
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.6.
|
|
4
|
+
"version": "5.6.8",
|
|
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": [
|
package/src/console/app.js
CHANGED
|
@@ -30,6 +30,7 @@ import { createReceiverTab } from './tabs/receiver-tab.js';
|
|
|
30
30
|
import { createAgentsTab } from './tabs/agents-tab.js';
|
|
31
31
|
import { ConfigService } from '../services/config-service.js';
|
|
32
32
|
import { ProviderService } from '../services/provider-service.js';
|
|
33
|
+
import { seedAllLlmDefaultsSync } from '../services/llm-provider-service.js';
|
|
33
34
|
|
|
34
35
|
const _dir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
36
|
const _pkg = JSON.parse(readFileSync(path.join(_dir, '../../package.json'), 'utf8'));
|
|
@@ -893,6 +894,11 @@ export class AgentVibesConsole {
|
|
|
893
894
|
* @returns {Promise<AgentVibesConsole>}
|
|
894
895
|
*/
|
|
895
896
|
export async function launchConsole(opts = {}) {
|
|
897
|
+
// Seed per-LLM piper defaults into the project config on every TUI launch.
|
|
898
|
+
// This guarantees play-tts.ps1 always finds a piper engine row for each LLM
|
|
899
|
+
// and never silently falls back to the global windows-sapi provider.
|
|
900
|
+
seedAllLlmDefaultsSync(process.cwd());
|
|
901
|
+
|
|
896
902
|
const app = new AgentVibesConsole(opts);
|
|
897
903
|
await app.init();
|
|
898
904
|
return app;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* circular imports between widgets and tabs.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { uniquifyVoiceName } from '../../utils/voice-names.js';
|
|
9
|
+
|
|
8
10
|
const TRACK_NAMES = Object.freeze({
|
|
9
11
|
'agentvibes_soft_flamenco_loop.mp3': '🎻 Soft Flamenco',
|
|
10
12
|
'agent_vibes_bachata_v1_loop.mp3': '🎺 Bachata',
|
|
@@ -60,8 +62,15 @@ export function formatVoiceName(voice) {
|
|
|
60
62
|
|
|
61
63
|
let name;
|
|
62
64
|
if (voice.includes('::')) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const speakerPart = voice.split('::')[1];
|
|
66
|
+
if (speakerPart.includes('_')) {
|
|
67
|
+
// 16Speakers format (Rose_Ibex) — already a complete name, just normalise display
|
|
68
|
+
name = speakerPart.replace(/_/g, ' ');
|
|
69
|
+
} else {
|
|
70
|
+
// LibriTTS / single-word names: append deterministic surname
|
|
71
|
+
// "Mary" → "Mary Bell", "Mary-7" → "Mary Hayes"
|
|
72
|
+
name = uniquifyVoiceName(speakerPart);
|
|
73
|
+
}
|
|
65
74
|
} else {
|
|
66
75
|
const parts = voice.split('-');
|
|
67
76
|
const QUALITIES = new Set(['high', 'medium', 'low']);
|
package/src/installer.js
CHANGED
|
@@ -4054,17 +4054,18 @@ async function configureSessionStartHook(targetDir, spinner) {
|
|
|
4054
4054
|
existingSettings.hooks = {};
|
|
4055
4055
|
}
|
|
4056
4056
|
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4057
|
+
// Windows uses SessionStart; Linux/macOS/WSL uses UserPromptSubmit.
|
|
4058
|
+
// Both point to the global $HOME path so updates to the hook script
|
|
4059
|
+
// take effect immediately without needing per-project reinstalls.
|
|
4060
|
+
const hookKey = isNativeWindows() ? 'SessionStart' : 'UserPromptSubmit';
|
|
4061
|
+
const hookCommand = isNativeWindows()
|
|
4062
|
+
? 'powershell -NoProfile -ExecutionPolicy Bypass -File "$HOME\\.claude\\hooks-windows\\session-start-tts.ps1"'
|
|
4063
|
+
: 'bash "$HOME/.claude/hooks/session-start-tts.sh"';
|
|
4064
|
+
|
|
4065
|
+
if (!existingSettings.hooks[hookKey]) {
|
|
4066
|
+
existingSettings.hooks[hookKey] = [{
|
|
4067
|
+
hooks: [{ type: 'command', command: hookCommand }]
|
|
4068
|
+
}];
|
|
4068
4069
|
|
|
4069
4070
|
if (!existingSettings.$schema) {
|
|
4070
4071
|
existingSettings.$schema = templateSettings.$schema;
|
|
@@ -5045,8 +5046,8 @@ async function updateCommandFiles(targetDir, spinner) {
|
|
|
5045
5046
|
* These hooks contain bug fixes (e.g. markdown stripping) that must propagate
|
|
5046
5047
|
* on every `npx agentvibes update` regardless of target directory.
|
|
5047
5048
|
*/
|
|
5048
|
-
const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'session-start-tts.sh', 'bmad-party-speak.sh'];
|
|
5049
|
-
const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1'];
|
|
5049
|
+
const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'play-tts-piper.sh', 'audio-processor.sh', 'session-start-tts.sh', 'bmad-party-speak.sh'];
|
|
5050
|
+
const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'play-tts-piper.ps1', 'audio-processor.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1'];
|
|
5050
5051
|
|
|
5051
5052
|
/**
|
|
5052
5053
|
* Update critical hooks in the global ~/.claude/hooks/ directory if it exists.
|
|
@@ -5056,45 +5057,38 @@ const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'session-start-tts.ps1', 'bmad-s
|
|
|
5056
5057
|
* @returns {Promise<number>} Number of hooks updated
|
|
5057
5058
|
*/
|
|
5058
5059
|
async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
|
|
5059
|
-
const
|
|
5060
|
+
const homeDir = homeDirOverride || os.homedir();
|
|
5061
|
+
const globalHooksDir = path.join(homeDir, '.claude', 'hooks');
|
|
5060
5062
|
let updated = 0;
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
return 0; // global hooks dir not present — nothing to do
|
|
5065
|
-
}
|
|
5063
|
+
|
|
5064
|
+
// Always ensure the global hooks dir exists so registered $HOME hooks resolve.
|
|
5065
|
+
await fs.mkdir(globalHooksDir, { recursive: true });
|
|
5066
5066
|
|
|
5067
5067
|
for (const hook of CRITICAL_HOOKS) {
|
|
5068
5068
|
const destPath = path.join(globalHooksDir, hook);
|
|
5069
5069
|
const srcPath = path.join(srcHooksDir, hook);
|
|
5070
5070
|
try {
|
|
5071
|
-
await fs.access(destPath); // only update if already installed
|
|
5072
5071
|
await fs.copyFile(srcPath, destPath);
|
|
5073
5072
|
await fs.chmod(destPath, 0o750);
|
|
5074
5073
|
updated++;
|
|
5075
5074
|
} catch {
|
|
5076
|
-
//
|
|
5075
|
+
// src missing — skip silently
|
|
5077
5076
|
}
|
|
5078
5077
|
}
|
|
5079
5078
|
|
|
5080
|
-
// Also
|
|
5081
|
-
const globalHooksWindowsDir = path.join(
|
|
5079
|
+
// Also ensure Windows global hooks-windows dir and scripts are up to date.
|
|
5080
|
+
const globalHooksWindowsDir = path.join(homeDir, '.claude', 'hooks-windows');
|
|
5082
5081
|
const srcHooksWindowsDir = path.join(path.dirname(srcHooksDir), 'hooks-windows');
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
} catch {
|
|
5093
|
-
// file not in global dir or src missing — skip silently
|
|
5094
|
-
}
|
|
5082
|
+
await fs.mkdir(globalHooksWindowsDir, { recursive: true });
|
|
5083
|
+
for (const hook of CRITICAL_HOOKS_WINDOWS) {
|
|
5084
|
+
const destPath = path.join(globalHooksWindowsDir, hook);
|
|
5085
|
+
const srcPath = path.join(srcHooksWindowsDir, hook);
|
|
5086
|
+
try {
|
|
5087
|
+
await fs.copyFile(srcPath, destPath);
|
|
5088
|
+
updated++;
|
|
5089
|
+
} catch {
|
|
5090
|
+
// src missing — skip silently
|
|
5095
5091
|
}
|
|
5096
|
-
} catch {
|
|
5097
|
-
// hooks-windows dir not present — nothing to do
|
|
5098
5092
|
}
|
|
5099
5093
|
|
|
5100
5094
|
return updated;
|
|
@@ -5337,6 +5331,13 @@ async function install(options = {}) {
|
|
|
5337
5331
|
await copyBackgroundMusicFiles(targetDir, silentSpinner);
|
|
5338
5332
|
await copyConfigFiles(targetDir, silentSpinner);
|
|
5339
5333
|
await copyCodexFiles(targetDir, silentSpinner);
|
|
5334
|
+
|
|
5335
|
+
// Populate global ~/.claude/hooks[/-windows]/ so $HOME hook paths resolve
|
|
5336
|
+
// on first install (not just on update).
|
|
5337
|
+
const hooksSubdirInstall = isNativeWindows() ? 'hooks-windows' : 'hooks';
|
|
5338
|
+
const srcHooksDirInstall = path.join(__dirname, '..', '.claude', hooksSubdirInstall);
|
|
5339
|
+
await updateGlobalHooks(srcHooksDirInstall);
|
|
5340
|
+
|
|
5340
5341
|
await configureSessionStartHook(targetDir, silentSpinner);
|
|
5341
5342
|
await configurePartyModeHook(targetDir, silentSpinner);
|
|
5342
5343
|
await installPluginManifest(targetDir, silentSpinner);
|
|
@@ -92,8 +92,17 @@ const DEFAULT_LLM_CONFIGS = {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
function ensureDefaultLlmConfigSync(llmKey, targetDir) {
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Check only the project dir — a global config row must not prevent seeding
|
|
96
|
+
// per-project defaults (otherwise new installs silently inherit a different
|
|
97
|
+
// project's voice/pretext and the per-LLM piper engine is never written,
|
|
98
|
+
// causing play-tts.ps1 to fall through to the global SAPI provider).
|
|
99
|
+
const resolvedDir = targetDir || process.env.INIT_CWD || process.cwd();
|
|
100
|
+
const projectCfgPath = path.join(resolvedDir, '.claude', 'config', 'audio-effects.cfg');
|
|
101
|
+
const cfgKey = `llm:${llmKey}`;
|
|
102
|
+
try {
|
|
103
|
+
const content = fsSync.readFileSync(projectCfgPath, 'utf8');
|
|
104
|
+
if (content.split('\n').some(line => line.startsWith(cfgKey + '|'))) return;
|
|
105
|
+
} catch { /* file not found — continue to seed */ }
|
|
97
106
|
|
|
98
107
|
const defaults = DEFAULT_LLM_CONFIGS[llmKey];
|
|
99
108
|
if (!defaults) return;
|
|
@@ -101,6 +110,17 @@ function ensureDefaultLlmConfigSync(llmKey, targetDir) {
|
|
|
101
110
|
saveLlmConfigSync(llmKey, defaults, targetDir);
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Seed piper defaults for every LLM in DEFAULT_LLM_CONFIGS into the project dir.
|
|
115
|
+
* Called at install time so play-tts.ps1 always finds a per-LLM piper row in the
|
|
116
|
+
* project config and never silently falls back to the global windows-sapi provider.
|
|
117
|
+
*/
|
|
118
|
+
export function seedAllLlmDefaultsSync(targetDir) {
|
|
119
|
+
for (const llmKey of Object.keys(DEFAULT_LLM_CONFIGS)) {
|
|
120
|
+
ensureDefaultLlmConfigSync(llmKey, targetDir);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
104
124
|
// ── Provider install-checks ─────────────────────────────────────────────────
|
|
105
125
|
|
|
106
126
|
export async function checkClaudeInstalled(targetDir) {
|
|
@@ -902,13 +922,12 @@ export function saveLlmConfigSync(llmKey, config, targetDir) {
|
|
|
902
922
|
const sanitize = (v) => (v || '').replace(/[\|\n\r\x00]/g, '');
|
|
903
923
|
const cfgLine = `${cfgKey}|${sanitize(config.effects)}|${sanitize(config.bgTrack)}|${sanitize(config.bgVolume)}|${sanitize(config.voice)}|${sanitize(config.pretext)}|${sanitize(config.ttsEngine)}`;
|
|
904
924
|
const resolvedTargetDir = targetDir || process.env.INIT_CWD || process.cwd();
|
|
905
|
-
// When targetDir is explicitly passed, write
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
const cfgPath =
|
|
909
|
-
(
|
|
910
|
-
|
|
911
|
-
: resolveCfgPath(resolvedTargetDir));
|
|
925
|
+
// When targetDir is explicitly passed, always write to the project dir — never follow
|
|
926
|
+
// config.sourcePath back to the global ~/.claude/config (it may have been loaded from there
|
|
927
|
+
// as a "default seed" for a new project, and writing back would pollute other projects).
|
|
928
|
+
const cfgPath = targetDir
|
|
929
|
+
? path.join(resolvedTargetDir, '.claude', 'config', 'audio-effects.cfg')
|
|
930
|
+
: (config.sourcePath || resolveCfgPath(resolvedTargetDir));
|
|
912
931
|
|
|
913
932
|
try {
|
|
914
933
|
let content = '';
|
package/src/utils/voice-names.js
CHANGED
|
@@ -35,6 +35,8 @@ export function uniquifyVoiceName(rawName) {
|
|
|
35
35
|
const idx = (n - 1) % SURNAME_POOL.length;
|
|
36
36
|
return `${base} ${SURNAME_POOL[idx]}`;
|
|
37
37
|
}
|
|
38
|
+
// n=1 (or 0): strip the suffix — "Mary-1" → "Mary Bell" is cleaner than "Mary-1 Bell"
|
|
39
|
+
return `${base} ${SURNAME_POOL[0]}`;
|
|
38
40
|
}
|
|
39
41
|
if (/\s/.test(rawName)) return rawName;
|
|
40
42
|
return `${rawName} ${SURNAME_POOL[0]}`;
|