agentvibes 4.5.7 → 4.6.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/config/audio-effects.cfg +1 -1
- package/.claude/config/tts-pretext.txt +1 -0
- package/.claude/hooks/audio-processor.sh +1 -1
- package/.claude/hooks/bmad-party-speak.sh +175 -0
- package/.claude/hooks-windows/bmad-party-speak.ps1 +207 -0
- package/.claude/hooks-windows/bmad-speak.ps1 +32 -7
- package/.claude/hooks-windows/play-tts-piper.ps1 +43 -6
- package/.claude/hooks-windows/play-tts.ps1 +57 -30
- package/.mcp.json +7 -0
- package/README.md +64 -2
- package/RELEASE_NOTES.md +42 -0
- package/bin/agent-vibes +1 -1
- package/bin/agentvibes-voice-browser.js +1 -1
- package/bin/mcp-server.js +1 -1
- package/bin/test-bmad-pr +1 -1
- package/package.json +110 -110
- package/src/console/tabs/agents-tab.js +240 -34
- package/src/console/tabs/install-tab.js +1 -0
- package/src/console/tabs/voices-tab.js +38 -5
- package/src/console/widgets/track-picker.js +50 -18
- package/src/installer.js +97 -3
- package/templates/agentvibes-receiver.sh +1 -1
|
@@ -17,7 +17,7 @@ bmad-agent-tech-writer||agent_vibes_arabic_v2_loop.mp3|0.70
|
|
|
17
17
|
# BMAD Agents - each with unique audio personality|||
|
|
18
18
|
|||
|
|
19
19
|
# PM John - upbeat, driving energy|||
|
|
20
|
-
John|gain -1 equalizer 3000 1q +2|agentvibes_soft_flamenco_loop.mp3|0.
|
|
20
|
+
John|gain -1 equalizer 3000 1q +2|agentvibes_soft_flamenco_loop.mp3|0.20
|
|
21
21
|
|||
|
|
22
22
|
# Architect Winston - deep, authoritative|||
|
|
23
23
|
Winston|reverb 40 50 90 gain -2|agentvibes_soft_flamenco_loop.mp3|0.25
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AgentVibes
|
|
@@ -310,7 +310,7 @@ mix_background() {
|
|
|
310
310
|
fi
|
|
311
311
|
|
|
312
312
|
ffmpeg -y -i "$voice" -ss "$start_pos" -stream_loop -1 -i "$background" \
|
|
313
|
-
-filter_complex "[1:a]volume=${volume},afade=t=in:st=0:d=0.3,afade=t=out:st=${bg_fade_out_adjusted}:d=2[bg];[0:a]adelay=${voice_delay_ms}|${voice_delay_ms}[v];[v][bg]amix=inputs=2:duration=longest[out]" \
|
|
313
|
+
-filter_complex "[1:a]volume=${volume},afade=t=in:st=0:d=0.3,afade=t=out:st=${bg_fade_out_adjusted}:d=2[bg];[0:a]adelay=${voice_delay_ms}|${voice_delay_ms},volume=1.5[v];[v][bg]amix=inputs=2:duration=longest:normalize=0[out]" \
|
|
314
314
|
-map "[out]" $audio_settings -t "$total_duration" "$output" 2>/dev/null || {
|
|
315
315
|
echo "Warning: Background mixing failed, using voice only" >&2
|
|
316
316
|
cp "$voice" "$output"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# File: ~/.claude/hooks/bmad-party-speak.sh
|
|
4
|
+
#
|
|
5
|
+
# AgentVibes PostToolUse Hook - BMAD Party Mode TTS (Linux / macOS / WSL)
|
|
6
|
+
#
|
|
7
|
+
# Fires after every Agent tool call. Detects BMAD party mode agents by
|
|
8
|
+
# fingerprinting the prompt, extracts the agent display name, maps it to
|
|
9
|
+
# the canonical agent ID via the manifest, then calls bmad-speak.sh.
|
|
10
|
+
# Uses flock for cross-process audio serialization (no overlapping speech).
|
|
11
|
+
#
|
|
12
|
+
# Installed globally so it works in any BMAD project.
|
|
13
|
+
# Uses CLAUDE_PROJECT_DIR env var to locate the project manifest at runtime.
|
|
14
|
+
#
|
|
15
|
+
# Input: JSON on stdin (Claude Code PostToolUse payload)
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
LOCK_FILE="/tmp/agentvibes-party-queue.lock"
|
|
22
|
+
|
|
23
|
+
# --- Read stdin ---
|
|
24
|
+
raw="$(cat)"
|
|
25
|
+
[[ -z "$raw" ]] && exit 0
|
|
26
|
+
|
|
27
|
+
# --- Parse all needed fields in one python3 call (fixes M5: 3x subprocess, echo safety) ---
|
|
28
|
+
# Outputs: TOOL_NAME|DISPLAY_NAME|RESPONSE_TEXT (newlines in response encoded as \n literals)
|
|
29
|
+
parsed="$(printf '%s' "$raw" | python3 - <<'PYEOF'
|
|
30
|
+
import sys, json, re
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
d = json.load(sys.stdin)
|
|
34
|
+
except Exception:
|
|
35
|
+
print("|||")
|
|
36
|
+
sys.exit(0)
|
|
37
|
+
|
|
38
|
+
tool_name = d.get('tool_name', '')
|
|
39
|
+
prompt = d.get('tool_input', {}).get('prompt', '')
|
|
40
|
+
|
|
41
|
+
# Extract display name — safe alternative to grep -oP (fixes C2: macOS BSD grep no -P)
|
|
42
|
+
display_name = ''
|
|
43
|
+
m = re.search(r'You are ([A-Za-z]+)\s*\(', prompt)
|
|
44
|
+
if m:
|
|
45
|
+
display_name = m.group(1)
|
|
46
|
+
|
|
47
|
+
# Extract response text
|
|
48
|
+
response_text = ''
|
|
49
|
+
for item in d.get('tool_response', {}).get('content', []):
|
|
50
|
+
if item.get('type') == 'text':
|
|
51
|
+
response_text = item['text']
|
|
52
|
+
break
|
|
53
|
+
|
|
54
|
+
# Strip leading icon + bold name header (e.g. "📊 **Mary:** " or garbled prefix)
|
|
55
|
+
response_text = response_text.strip()
|
|
56
|
+
response_text = re.sub(r'^\S*\s*\*\*[^:]+:\*\*\s*', '', response_text).strip()
|
|
57
|
+
|
|
58
|
+
# Encode newlines so we can pass multi-line text through a single shell variable (fixes m3)
|
|
59
|
+
response_text = response_text.replace('\n', '\\n')
|
|
60
|
+
|
|
61
|
+
print(f"{tool_name}|{display_name}|{response_text}")
|
|
62
|
+
PYEOF
|
|
63
|
+
)" 2>/dev/null || true
|
|
64
|
+
|
|
65
|
+
[[ -z "$parsed" ]] && exit 0
|
|
66
|
+
|
|
67
|
+
tool_name="${parsed%%|*}"
|
|
68
|
+
rest="${parsed#*|}"
|
|
69
|
+
display_name="${rest%%|*}"
|
|
70
|
+
response_text="${rest#*|}"
|
|
71
|
+
|
|
72
|
+
# Decode \n back to newlines for TTS
|
|
73
|
+
response_text="${response_text//\\n/ }"
|
|
74
|
+
|
|
75
|
+
# --- Only handle Agent tool ---
|
|
76
|
+
[[ "$tool_name" != "Agent" ]] && exit 0
|
|
77
|
+
|
|
78
|
+
# --- Fingerprint: only fire for BMAD party mode agents (safe string match, no pipe) ---
|
|
79
|
+
[[ "$raw" == *"BMAD agent in a collaborative roundtable"* ]] || exit 0
|
|
80
|
+
|
|
81
|
+
[[ -z "$display_name" ]] && exit 0
|
|
82
|
+
[[ -z "$response_text" ]] && exit 0
|
|
83
|
+
|
|
84
|
+
# --- Resolve project root ---
|
|
85
|
+
project_root="${CLAUDE_PROJECT_DIR:-}"
|
|
86
|
+
|
|
87
|
+
# --- Find bmad-speak.sh (prefer project-local, fall back to global) ---
|
|
88
|
+
bmad_speak=""
|
|
89
|
+
if [[ -n "$project_root" && -f "$project_root/.claude/hooks/bmad-speak.sh" ]]; then
|
|
90
|
+
bmad_speak="$project_root/.claude/hooks/bmad-speak.sh"
|
|
91
|
+
elif [[ -f "$SCRIPT_DIR/bmad-speak.sh" ]]; then
|
|
92
|
+
bmad_speak="$SCRIPT_DIR/bmad-speak.sh"
|
|
93
|
+
fi
|
|
94
|
+
[[ -z "$bmad_speak" ]] && exit 0
|
|
95
|
+
|
|
96
|
+
# --- Look up canonical agent ID from project manifest via python3 (fixes M4: awk CSV comma) ---
|
|
97
|
+
agent_id="$display_name" # fallback
|
|
98
|
+
if [[ -n "$project_root" && -f "$project_root/_bmad/_config/agent-manifest.csv" ]]; then
|
|
99
|
+
manifest="$project_root/_bmad/_config/agent-manifest.csv"
|
|
100
|
+
matched="$(python3 - "$manifest" "$display_name" <<'PYEOF'
|
|
101
|
+
import sys, csv
|
|
102
|
+
manifest_path, target = sys.argv[1], sys.argv[2].lower()
|
|
103
|
+
try:
|
|
104
|
+
with open(manifest_path, newline='', encoding='utf-8') as f:
|
|
105
|
+
for row in csv.DictReader(f):
|
|
106
|
+
if row.get('displayName', '').lower() == target:
|
|
107
|
+
print(row.get('name', ''))
|
|
108
|
+
break
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
PYEOF
|
|
112
|
+
)" 2>/dev/null || true
|
|
113
|
+
[[ -n "$matched" ]] && agent_id="$matched"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# --- Apply verbosity truncation ---
|
|
117
|
+
verbosity="medium"
|
|
118
|
+
# Guard project_root empty to avoid /.claude/... path (fixes M1)
|
|
119
|
+
if [[ -n "$project_root" && -f "$project_root/.claude/tts-verbosity.txt" ]]; then
|
|
120
|
+
v="$(tr -d '[:space:]' < "$project_root/.claude/tts-verbosity.txt")"
|
|
121
|
+
[[ -n "$v" ]] && verbosity="$v"
|
|
122
|
+
elif [[ -f "$HOME/.claude/tts-verbosity.txt" ]]; then
|
|
123
|
+
v="$(tr -d '[:space:]' < "$HOME/.claude/tts-verbosity.txt")"
|
|
124
|
+
[[ -n "$v" ]] && verbosity="$v"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
case "$verbosity" in
|
|
128
|
+
low)
|
|
129
|
+
# First sentence — fall back to full text if no punctuation (fixes m1)
|
|
130
|
+
first="$(printf '%s' "$response_text" | python3 -c "
|
|
131
|
+
import sys, re
|
|
132
|
+
t = sys.stdin.read()
|
|
133
|
+
m = re.match(r'^.*?[.!?]', t)
|
|
134
|
+
print(m.group(0) if m else t)
|
|
135
|
+
" 2>/dev/null || printf '%s' "$response_text")"
|
|
136
|
+
[[ -n "$first" ]] && response_text="$first"
|
|
137
|
+
;;
|
|
138
|
+
medium)
|
|
139
|
+
# First 2 sentences — fall back to full text if no punctuation (fixes m1)
|
|
140
|
+
two="$(printf '%s' "$response_text" | python3 -c "
|
|
141
|
+
import sys, re
|
|
142
|
+
t = sys.stdin.read()
|
|
143
|
+
parts = re.findall(r'.*?[.!?]', t)
|
|
144
|
+
print(' '.join(parts[:2]) if parts else t)
|
|
145
|
+
" 2>/dev/null || printf '%s' "$response_text")"
|
|
146
|
+
[[ -n "$two" ]] && response_text="$two"
|
|
147
|
+
;;
|
|
148
|
+
# high = full text
|
|
149
|
+
esac
|
|
150
|
+
|
|
151
|
+
[[ -z "$response_text" ]] && exit 0
|
|
152
|
+
|
|
153
|
+
# --- Acquire queue lock (flock: cross-process, auto-releases on crash) ---
|
|
154
|
+
exec 9>"$LOCK_FILE"
|
|
155
|
+
if command -v flock &>/dev/null; then
|
|
156
|
+
flock -w 60 9
|
|
157
|
+
"$bmad_speak" "$agent_id" "$response_text" || true
|
|
158
|
+
flock -u 9
|
|
159
|
+
else
|
|
160
|
+
# macOS fallback: atomic mkdir polling lock
|
|
161
|
+
LOCK_DIR="/tmp/agentvibes-party-queue.lock.d"
|
|
162
|
+
# Register trap BEFORE acquiring lock so SIGTERM can't orphan it (fixes M3)
|
|
163
|
+
trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
|
|
164
|
+
WAITED=0
|
|
165
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
166
|
+
sleep 0.5
|
|
167
|
+
WAITED=$((WAITED + 1))
|
|
168
|
+
if [[ $WAITED -ge 120 ]]; then
|
|
169
|
+
echo "[AgentVibes] Party mode TTS queue timeout for agent: $agent_id" >&2
|
|
170
|
+
exit 0
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
"$bmad_speak" "$agent_id" "$response_text" || true
|
|
174
|
+
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
175
|
+
fi
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#
|
|
2
|
+
# File: ~/.claude/hooks-windows/bmad-party-speak.ps1
|
|
3
|
+
#
|
|
4
|
+
# AgentVibes PostToolUse Hook - BMAD Party Mode TTS
|
|
5
|
+
#
|
|
6
|
+
# Fires after every Agent tool call. Detects BMAD party mode agents by
|
|
7
|
+
# fingerprinting the prompt, extracts the agent display name, maps it to
|
|
8
|
+
# the canonical agent ID via the manifest, then calls bmad-speak.ps1.
|
|
9
|
+
#
|
|
10
|
+
# Installed globally so it works in any BMAD project.
|
|
11
|
+
# Uses $env:CLAUDE_PROJECT_DIR to locate the project manifest at runtime.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
# --- Read stdin safely ---
|
|
16
|
+
$raw = [Console]::In.ReadToEnd()
|
|
17
|
+
if (-not $raw -or $raw.Trim() -eq "") { exit 0 }
|
|
18
|
+
|
|
19
|
+
$data = $raw | ConvertFrom-Json
|
|
20
|
+
if (-not $data) { exit 0 }
|
|
21
|
+
|
|
22
|
+
# --- Only handle Agent tool ---
|
|
23
|
+
if ($data.tool_name -ne "Agent") { exit 0 }
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# --- Extract prompt ---
|
|
27
|
+
$prompt = $data.tool_input.prompt
|
|
28
|
+
if (-not $prompt) { exit 0 }
|
|
29
|
+
|
|
30
|
+
# --- Fingerprint: only fire for BMAD party mode agents ---
|
|
31
|
+
if ($prompt -notmatch "BMAD agent in a collaborative roundtable") { exit 0 }
|
|
32
|
+
|
|
33
|
+
# --- Extract display name from "You are {Name} (" ---
|
|
34
|
+
if ($prompt -notmatch "You are ([A-Za-z]+)\s*\(") { exit 0 }
|
|
35
|
+
$DisplayName = $Matches[1]
|
|
36
|
+
|
|
37
|
+
# --- Extract response text ---
|
|
38
|
+
$content = $data.tool_response.content
|
|
39
|
+
if (-not $content -or $content.Count -eq 0) { exit 0 }
|
|
40
|
+
$ResponseText = ($content | Where-Object { $_.type -eq "text" } | Select-Object -First 1).text
|
|
41
|
+
if (-not $ResponseText) { exit 0 }
|
|
42
|
+
|
|
43
|
+
# Strip leading icon + bold name header e.g. "📊 **Mary:**" or garbled "≡ƒôè **Mary:**"
|
|
44
|
+
# Trim first so leading newlines don't defeat the ^ anchor (fixes M7)
|
|
45
|
+
$ResponseText = $ResponseText.Trim()
|
|
46
|
+
$ResponseText = $ResponseText -replace '^\S*\s*\*\*[^:]+:\*\*\s*', ''
|
|
47
|
+
$ResponseText = $ResponseText.Trim()
|
|
48
|
+
if (-not $ResponseText) { exit 0 }
|
|
49
|
+
|
|
50
|
+
# --- Resolve paths ---
|
|
51
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
52
|
+
$ProjectRoot = $env:CLAUDE_PROJECT_DIR
|
|
53
|
+
|
|
54
|
+
# --- Find bmad-speak.ps1 (prefer project-local, fall back to global) ---
|
|
55
|
+
$BmadSpeak = $null
|
|
56
|
+
if ($ProjectRoot) {
|
|
57
|
+
$local = Join-Path $ProjectRoot ".claude\hooks-windows\bmad-speak.ps1"
|
|
58
|
+
if (Test-Path $local) { $BmadSpeak = $local }
|
|
59
|
+
}
|
|
60
|
+
if (-not $BmadSpeak) {
|
|
61
|
+
$global = Join-Path $ScriptDir "bmad-speak.ps1"
|
|
62
|
+
if (Test-Path $global) { $BmadSpeak = $global }
|
|
63
|
+
}
|
|
64
|
+
if (-not $BmadSpeak) { exit 0 }
|
|
65
|
+
|
|
66
|
+
# --- Look up canonical agent ID from project manifest (fixes m2: UTF8 encoding) ---
|
|
67
|
+
$AgentId = $DisplayName # fallback
|
|
68
|
+
if ($ProjectRoot) {
|
|
69
|
+
$ManifestFile = Join-Path $ProjectRoot "_bmad\_config\agent-manifest.csv"
|
|
70
|
+
if (Test-Path $ManifestFile) {
|
|
71
|
+
$rows = Import-Csv $ManifestFile -Encoding UTF8
|
|
72
|
+
foreach ($row in $rows) {
|
|
73
|
+
if ($row.displayName -ieq $DisplayName) {
|
|
74
|
+
$AgentId = $row.name
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- Apply verbosity truncation ---
|
|
82
|
+
$Verbosity = "medium"
|
|
83
|
+
$verbosityPaths = @(
|
|
84
|
+
(Join-Path $env:USERPROFILE ".claude\tts-verbosity.txt")
|
|
85
|
+
)
|
|
86
|
+
if ($ProjectRoot) {
|
|
87
|
+
$verbosityPaths = @((Join-Path $ProjectRoot ".claude\tts-verbosity.txt")) + $verbosityPaths
|
|
88
|
+
}
|
|
89
|
+
foreach ($p in $verbosityPaths) {
|
|
90
|
+
if (Test-Path $p) {
|
|
91
|
+
$v = (Get-Content $p -Raw -ErrorAction SilentlyContinue).Trim()
|
|
92
|
+
if ($v) { $Verbosity = $v; break }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
switch ($Verbosity) {
|
|
97
|
+
"low" {
|
|
98
|
+
$sentences = [regex]::Split($ResponseText, '(?<=[.!?])\s+')
|
|
99
|
+
# Fall back to full text if no sentence-ending punctuation (fixes m1)
|
|
100
|
+
if ($sentences.Count -gt 0 -and $sentences[0]) { $ResponseText = $sentences[0] }
|
|
101
|
+
}
|
|
102
|
+
"medium" {
|
|
103
|
+
$sentences = [regex]::Split($ResponseText, '(?<=[.!?])\s+')
|
|
104
|
+
# Fall back to full text if no sentence-ending punctuation (fixes m1)
|
|
105
|
+
$truncated = ($sentences | Select-Object -First 2) -join " "
|
|
106
|
+
if ($truncated) { $ResponseText = $truncated }
|
|
107
|
+
}
|
|
108
|
+
# "high" = full text
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# --- Pre-synthesize WAV before acquiring mutex so synthesis overlaps with previous agent's playback ---
|
|
112
|
+
$PreSynthWav = $null
|
|
113
|
+
try {
|
|
114
|
+
# Resolve agent voice from voice map
|
|
115
|
+
$VoiceMapLocal = if ($ProjectRoot) { Join-Path $ProjectRoot ".agentvibes\bmad-voice-map.json" } else { $null }
|
|
116
|
+
$VoiceMapGlobal = Join-Path $env:USERPROFILE ".agentvibes\bmad-voice-map.json"
|
|
117
|
+
$VoiceMapFile = if ($VoiceMapLocal -and (Test-Path $VoiceMapLocal)) { $VoiceMapLocal }
|
|
118
|
+
elseif (Test-Path $VoiceMapGlobal) { $VoiceMapGlobal }
|
|
119
|
+
else { $null }
|
|
120
|
+
|
|
121
|
+
$AgentVoiceName = $null
|
|
122
|
+
$SpeakerId = $null
|
|
123
|
+
if ($VoiceMapFile) {
|
|
124
|
+
$vm = Get-Content $VoiceMapFile -Raw | ConvertFrom-Json
|
|
125
|
+
$profile = $vm.agents.$AgentId
|
|
126
|
+
if ($profile -and $profile.voice) {
|
|
127
|
+
$raw = $profile.voice
|
|
128
|
+
if ($raw -match '::') {
|
|
129
|
+
$parts = $raw -split '::'
|
|
130
|
+
$AgentVoiceName = $parts[0]
|
|
131
|
+
if ($parts[1] -match '-(\d+)$') { $SpeakerId = $Matches[1] }
|
|
132
|
+
} else {
|
|
133
|
+
$AgentVoiceName = $raw
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Locate piper
|
|
139
|
+
$PiperExe = "$env:LOCALAPPDATA\Programs\Piper\piper.exe"
|
|
140
|
+
if (-not (Test-Path $PiperExe)) {
|
|
141
|
+
$found = Get-Command piper.exe -ErrorAction SilentlyContinue
|
|
142
|
+
if ($found) { $PiperExe = $found.Source }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Test-Path $PiperExe) {
|
|
146
|
+
$VoicesDir = "$env:USERPROFILE\.claude\piper-voices"
|
|
147
|
+
# Fall back to first available voice if agent voice not found
|
|
148
|
+
if (-not $AgentVoiceName) {
|
|
149
|
+
$first = Get-ChildItem $VoicesDir -Filter "*.onnx" -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
150
|
+
if ($first) { $AgentVoiceName = $first.BaseName }
|
|
151
|
+
}
|
|
152
|
+
if ($AgentVoiceName -and ($AgentVoiceName -match '^[a-zA-Z0-9_\-\.]+$')) {
|
|
153
|
+
$VoiceModel = Join-Path $VoicesDir "$AgentVoiceName.onnx"
|
|
154
|
+
if (Test-Path $VoiceModel) {
|
|
155
|
+
$AudioDir = "$env:USERPROFILE\.claude\audio"
|
|
156
|
+
if (-not (Test-Path $AudioDir)) { New-Item -ItemType Directory -Path $AudioDir -Force | Out-Null }
|
|
157
|
+
$PreSynthWav = Join-Path $AudioDir "tts-presynth-$([System.IO.Path]::GetRandomFileName() -replace '\..*').wav"
|
|
158
|
+
$piperArgs = @("--model", $VoiceModel, "--output-file", $PreSynthWav)
|
|
159
|
+
if ($SpeakerId) { $piperArgs += @("--speaker", $SpeakerId) }
|
|
160
|
+
$ResponseText | & $PiperExe @piperArgs 2>$null
|
|
161
|
+
if (-not (Test-Path $PreSynthWav) -or (Get-Item $PreSynthWav).Length -eq 0) {
|
|
162
|
+
$PreSynthWav = $null
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
$PreSynthWav = $null # degrade gracefully — will synthesize inside mutex instead
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# --- Speak with queue serialization (named mutex, cross-process) ---
|
|
172
|
+
$mutex = New-Object System.Threading.Mutex($false, "AgentVibesPartyModeTTSQueue")
|
|
173
|
+
try {
|
|
174
|
+
$acquired = $false
|
|
175
|
+
try {
|
|
176
|
+
# WaitOne throws AbandonedMutexException if prior process crashed while holding it.
|
|
177
|
+
# That exception means we DID acquire the mutex — treat it as success (fixes M2).
|
|
178
|
+
$acquired = $mutex.WaitOne(60000)
|
|
179
|
+
} catch [System.Threading.AbandonedMutexException] {
|
|
180
|
+
$acquired = $true # abandoned = we now own it
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if ($acquired) {
|
|
184
|
+
try {
|
|
185
|
+
# Pass pre-synthesized WAV path so play-tts.ps1 skips synthesis (reduces gap between agents)
|
|
186
|
+
if ($PreSynthWav) { $env:AGENTVIBES_PRESYNTHESIZED_WAV = $PreSynthWav }
|
|
187
|
+
# Pass positional args directly after -File (spaces handled by quoting via array)
|
|
188
|
+
& powershell -NoProfile -ExecutionPolicy Bypass -File $BmadSpeak $AgentId $ResponseText
|
|
189
|
+
} finally {
|
|
190
|
+
$env:AGENTVIBES_PRESYNTHESIZED_WAV = ""
|
|
191
|
+
if ($PreSynthWav -and (Test-Path $PreSynthWav)) {
|
|
192
|
+
Remove-Item $PreSynthWav -Force -ErrorAction SilentlyContinue
|
|
193
|
+
}
|
|
194
|
+
$mutex.ReleaseMutex()
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
# Timed out — log to stderr so it's visible in hook error output (fixes M6)
|
|
198
|
+
[Console]::Error.WriteLine("[AgentVibes] Party mode TTS queue timeout for agent: $AgentId")
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
$mutex.Close()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
} catch {
|
|
205
|
+
# Silently exit — never block Claude
|
|
206
|
+
exit 0
|
|
207
|
+
}
|
|
@@ -50,6 +50,7 @@ $VoiceMapGlobal = Join-Path $env:USERPROFILE ".agentvibes\bmad-voice-map.json"
|
|
|
50
50
|
$VoiceMapFile = if (Test-Path $VoiceMapLocal) { $VoiceMapLocal } else { $VoiceMapGlobal }
|
|
51
51
|
|
|
52
52
|
$AgentVoice = ""
|
|
53
|
+
$AgentPretext = ""
|
|
53
54
|
$AgentPersonality = ""
|
|
54
55
|
$AgentBgEnabled = $false
|
|
55
56
|
$AgentBgTrack = ""
|
|
@@ -72,24 +73,35 @@ if (Test-Path $_BgVolFile) {
|
|
|
72
73
|
$AgentBgVolume = "0.20"
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
# Resolve agent ID and display name/title from manifest (needed for default pretext)
|
|
77
|
+
$AgentDisplayName = ""
|
|
78
|
+
$AgentTitle = ""
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
if (Test-Path $ManifestFile) {
|
|
81
|
+
try {
|
|
80
82
|
$ManifestRows = Import-Csv $ManifestFile -Encoding UTF8
|
|
81
83
|
foreach ($row in $ManifestRows) {
|
|
82
84
|
$id = ($row.PSObject.Properties | Select-Object -First 1).Value -replace '^"|"$', ''
|
|
83
85
|
$display = ($row.PSObject.Properties | Select-Object -Skip 1 -First 1).Value -replace '^"|"$', ''
|
|
86
|
+
$title = ($row.PSObject.Properties | Select-Object -Skip 2 -First 1).Value -replace '^"|"$', ''
|
|
84
87
|
if ($id -ieq $AgentNameOrId -or $display -like "$AgentNameOrId*") {
|
|
85
|
-
$AgentId
|
|
88
|
+
$AgentId = $id
|
|
89
|
+
$AgentDisplayName = $display
|
|
90
|
+
$AgentTitle = $title
|
|
86
91
|
break
|
|
87
92
|
}
|
|
88
93
|
}
|
|
94
|
+
} catch { }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Test-Path $VoiceMapFile) {
|
|
98
|
+
try {
|
|
99
|
+
$VoiceMap = Get-Content $VoiceMapFile -Raw | ConvertFrom-Json
|
|
89
100
|
|
|
90
101
|
if ($AgentId -and $VoiceMap.agents.$AgentId) {
|
|
91
102
|
$Profile = $VoiceMap.agents.$AgentId
|
|
92
103
|
if ($Profile.voice) { $AgentVoice = $Profile.voice }
|
|
104
|
+
if ($Profile.pretext) { $AgentPretext = $Profile.pretext }
|
|
93
105
|
if ($Profile.personality) { $AgentPersonality = $Profile.personality }
|
|
94
106
|
if ($Profile.backgroundMusic) {
|
|
95
107
|
$AgentBgEnabled = [bool]$Profile.backgroundMusic.enabled
|
|
@@ -105,6 +117,16 @@ if (Test-Path $VoiceMapFile) {
|
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
119
|
|
|
120
|
+
# Fall back to default pretext if none stored: "DisplayName, Title here."
|
|
121
|
+
# Matches AgentVoiceStore.getDefaultPretext() in agent-voice-store.js
|
|
122
|
+
if (-not $AgentPretext -and $AgentDisplayName) {
|
|
123
|
+
if ($AgentTitle) {
|
|
124
|
+
$AgentPretext = "$AgentDisplayName, $AgentTitle here."
|
|
125
|
+
} else {
|
|
126
|
+
$AgentPretext = "$AgentDisplayName here."
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
108
130
|
# ---------------------------------------------------------------------------
|
|
109
131
|
# Locate play-tts.ps1 — prefer project-local, fall back to global
|
|
110
132
|
$PlayTtsLocal = Join-Path $ProjectRoot ".claude\hooks-windows\play-tts.ps1"
|
|
@@ -163,11 +185,14 @@ if ($AgentBgEnabled -and $AgentBgTrack) {
|
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
try {
|
|
188
|
+
# Prepend pretext if configured (e.g. "As your UX designer")
|
|
189
|
+
$SpeakText = if ($AgentPretext) { "$AgentPretext. $Dialogue" } else { $Dialogue }
|
|
190
|
+
|
|
166
191
|
# Speak with agent's voice (or global voice if none configured)
|
|
167
192
|
if ($AgentVoice) {
|
|
168
|
-
& powershell -NoProfile -ExecutionPolicy Bypass -File $PlayTtsScript $
|
|
193
|
+
& powershell -NoProfile -ExecutionPolicy Bypass -File $PlayTtsScript $SpeakText $AgentVoice
|
|
169
194
|
} else {
|
|
170
|
-
& powershell -NoProfile -ExecutionPolicy Bypass -File $PlayTtsScript $
|
|
195
|
+
& powershell -NoProfile -ExecutionPolicy Bypass -File $PlayTtsScript $SpeakText
|
|
171
196
|
}
|
|
172
197
|
} finally {
|
|
173
198
|
# Restore personality
|
|
@@ -70,15 +70,52 @@ elseif (Test-Path $VoiceFile) {
|
|
|
70
70
|
$VoiceName = (Get-Content $VoiceFile -Raw).Trim()
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
# Strip display name suffix (e.g. "en_US-libritts-high::
|
|
74
|
-
# and
|
|
73
|
+
# Strip display name suffix (e.g. "en_US-libritts-high::Holly-7" -> "en_US-libritts-high")
|
|
74
|
+
# and resolve the real Piper speaker index.
|
|
75
|
+
# IMPORTANT: The trailing number in a speaker name (e.g. "Holly-7") is a disambiguation
|
|
76
|
+
# suffix, NOT the speaker index. Real index must be looked up from voice-assignments.json.
|
|
75
77
|
if ($VoiceName -match '::') {
|
|
76
78
|
$parts = $VoiceName -split '::'
|
|
77
79
|
$VoiceName = $parts[0]
|
|
78
|
-
if ($parts.Length -ge 2
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
$SpeakerName = if ($parts.Length -ge 2) { $parts[1] } else { "" }
|
|
81
|
+
Remove-Item env:PIPER_SPEAKER -ErrorAction SilentlyContinue
|
|
82
|
+
|
|
83
|
+
if ($SpeakerName) {
|
|
84
|
+
# Primary: look up in voice-assignments.json catalog (libritts_speakers keyed by speaker index)
|
|
85
|
+
# Derive project root from this script's location: .claude/hooks-windows/ -> project root
|
|
86
|
+
$PiperScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
87
|
+
$PiperProjectRoot = Split-Path -Parent (Split-Path -Parent $PiperScriptRoot)
|
|
88
|
+
$VoiceAssignmentsPath = Join-Path $PiperProjectRoot "voice-assignments.json"
|
|
89
|
+
# Fallback: global AgentVibes install if not found in project
|
|
90
|
+
if (-not (Test-Path $VoiceAssignmentsPath)) {
|
|
91
|
+
$VoiceAssignmentsPath = Join-Path $env:USERPROFILE "AgentVibes\voice-assignments.json"
|
|
92
|
+
}
|
|
93
|
+
$SpeakerResolved = $false
|
|
94
|
+
if (Test-Path $VoiceAssignmentsPath) {
|
|
95
|
+
try {
|
|
96
|
+
$vaData = Get-Content $VoiceAssignmentsPath -Raw | ConvertFrom-Json
|
|
97
|
+
foreach ($prop in $vaData.libritts_speakers.PSObject.Properties) {
|
|
98
|
+
if ($prop.Value.voice_name -eq $SpeakerName) {
|
|
99
|
+
$env:PIPER_SPEAKER = $prop.Name
|
|
100
|
+
$SpeakerResolved = $true
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch { }
|
|
105
|
+
}
|
|
106
|
+
# Fallback: check patched speaker_id_map in the .onnx.json
|
|
107
|
+
if (-not $SpeakerResolved) {
|
|
108
|
+
$OnnxJsonPath = "$VoicesDir\$VoiceName.onnx.json"
|
|
109
|
+
if (Test-Path $OnnxJsonPath) {
|
|
110
|
+
try {
|
|
111
|
+
$onnxData = Get-Content $OnnxJsonPath -Raw | ConvertFrom-Json
|
|
112
|
+
$speakerIdMap = $onnxData.speaker_id_map
|
|
113
|
+
if ($speakerIdMap -and $speakerIdMap.PSObject.Properties[$SpeakerName]) {
|
|
114
|
+
$env:PIPER_SPEAKER = [string]$speakerIdMap.PSObject.Properties[$SpeakerName].Value
|
|
115
|
+
}
|
|
116
|
+
} catch { }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
82
119
|
}
|
|
83
120
|
} else {
|
|
84
121
|
# No multi-speaker syntax — clear any stale speaker env var
|
|
@@ -128,58 +128,85 @@ if ($BgEnabled -or $HasReverb) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
# Check for pre-synthesized WAV (party mode optimization — synthesis done before mutex acquisition)
|
|
132
|
+
$PreSynthWav = $env:AGENTVIBES_PRESYNTHESIZED_WAV
|
|
133
|
+
$UsePreSynth = $PreSynthWav -and (Test-Path $PreSynthWav) -and
|
|
134
|
+
(Get-Item $PreSynthWav -ErrorAction SilentlyContinue).Length -gt 0
|
|
135
|
+
|
|
131
136
|
# If background music or reverb enabled and ffmpeg available, tell provider to skip playback
|
|
132
137
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
133
138
|
$env:AGENTVIBES_NO_PLAY = "1"
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
# Call the provider script
|
|
141
|
+
# Call the provider script (skip if using pre-synthesized audio)
|
|
137
142
|
# When post-processing (reverb/music), capture output preserving InformationRecord colors.
|
|
138
143
|
# Otherwise call directly so Write-Host colors pass through to the terminal.
|
|
139
144
|
$NeedsPostProcess = ($BgEnabled -or $HasReverb) -and $HasFfmpeg
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
if ($UsePreSynth) {
|
|
146
|
+
Write-Host "[SYNTH] Using pre-synthesized audio..." -ForegroundColor Cyan
|
|
147
|
+
# If no post-processing needed, play the pre-synth file directly and exit
|
|
148
|
+
if (-not $NeedsPostProcess) {
|
|
149
|
+
$player = $null
|
|
150
|
+
try {
|
|
151
|
+
$player = New-Object System.Media.SoundPlayer $PreSynthWav
|
|
152
|
+
$player.PlaySync()
|
|
153
|
+
} catch {
|
|
154
|
+
Write-Host "[WARNING] Pre-synth playback failed: $_" -ForegroundColor Yellow
|
|
155
|
+
} finally {
|
|
156
|
+
if ($player) { $player.Dispose() }
|
|
146
157
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
159
|
+
exit 0
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
try {
|
|
163
|
+
if ($NeedsPostProcess) {
|
|
164
|
+
if ($VoiceOverride) {
|
|
165
|
+
$providerOutput = & $ProviderScript $Text $VoiceOverride 6>&1 2>&1
|
|
166
|
+
} else {
|
|
167
|
+
$providerOutput = & $ProviderScript $Text 6>&1 2>&1
|
|
168
|
+
}
|
|
169
|
+
# Re-emit preserving colors from InformationRecords (Write-Host output)
|
|
170
|
+
foreach ($item in $providerOutput) {
|
|
171
|
+
if ($item -is [System.Management.Automation.InformationRecord]) {
|
|
172
|
+
$msg = $item.MessageData
|
|
173
|
+
if ($msg -is [System.Management.Automation.HostInformationMessage]) {
|
|
174
|
+
Write-Host $msg.Message -ForegroundColor $msg.ForegroundColor -NoNewline:$msg.NoNewLine
|
|
175
|
+
if (-not $msg.NoNewLine) { Write-Host }
|
|
176
|
+
} else {
|
|
177
|
+
Write-Host "$item"
|
|
178
|
+
}
|
|
154
179
|
} else {
|
|
155
180
|
Write-Host "$item"
|
|
156
181
|
}
|
|
157
|
-
} else {
|
|
158
|
-
Write-Host "$item"
|
|
159
182
|
}
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
if ($VoiceOverride) {
|
|
163
|
-
& $ProviderScript $Text $VoiceOverride
|
|
164
183
|
} else {
|
|
165
|
-
|
|
184
|
+
if ($VoiceOverride) {
|
|
185
|
+
& $ProviderScript $Text $VoiceOverride
|
|
186
|
+
} else {
|
|
187
|
+
& $ProviderScript $Text
|
|
188
|
+
}
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
catch {
|
|
192
|
+
Write-Host "[ERROR] TTS Error: $_" -ForegroundColor Red
|
|
193
|
+
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
194
|
+
exit 1
|
|
195
|
+
}
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
# Apply reverb and/or mix with background music
|
|
176
199
|
if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
177
200
|
Remove-Item env:AGENTVIBES_NO_PLAY -ErrorAction SilentlyContinue
|
|
178
201
|
|
|
179
|
-
# Find the
|
|
202
|
+
# Find the WAV to post-process: use pre-synthesized file if available, else most recent
|
|
180
203
|
$AudioDir = "$ClaudeDir\audio"
|
|
181
|
-
$RecentWav =
|
|
182
|
-
|
|
204
|
+
$RecentWav = if ($UsePreSynth) {
|
|
205
|
+
Get-Item $PreSynthWav -ErrorAction SilentlyContinue
|
|
206
|
+
} else {
|
|
207
|
+
Get-ChildItem -Path $AudioDir -Filter "tts-*.wav" -ErrorAction SilentlyContinue |
|
|
208
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
209
|
+
}
|
|
183
210
|
|
|
184
211
|
if ($RecentWav -and $RecentWav.Length -gt 0) {
|
|
185
212
|
$voicePath = $RecentWav.FullName
|
|
@@ -282,7 +309,7 @@ if (($BgEnabled -or $HasReverb) -and $HasFfmpeg) {
|
|
|
282
309
|
$fadeOutStart = $totalDuration - 2
|
|
283
310
|
|
|
284
311
|
# Filter: music fades in 0.5s, voice delayed 2s, music fades out last 2s
|
|
285
|
-
$filter = "[0:a]volume=${BgVolume},afade=t=in:d=0.5,afade=t=out:st=${fadeOutStart}:d=2[bg];[1:a]adelay=
|
|
312
|
+
$filter = "[0:a]volume=${BgVolume},afade=t=in:d=0.5,afade=t=out:st=${fadeOutStart}:d=2[bg];[1:a]adelay=1000|1000,volume=1.5,apad=pad_dur=2[voice];[bg][voice]amix=inputs=2:duration=longest:dropout_transition=2:normalize=0[out]"
|
|
286
313
|
|
|
287
314
|
# Run ffmpeg - use Start-Process to avoid stderr issues with $ErrorActionPreference
|
|
288
315
|
$ffmpegArgs = "-y -stream_loop -1 -i `"$BgTrackPath`" -i `"$voicePath`" -filter_complex `"$filter`" -map `"[out]`" -t $totalDuration `"$MixedFile`""
|