agentvibes 5.12.0 → 5.13.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/commands/agent-vibes/commands.json +0 -20
- package/.claude/commands/agent-vibes/unmute.md +6 -2
- package/.claude/config/audio-effects.cfg +6 -6
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/agentvibes-session-id.sh +69 -0
- package/.claude/hooks/bmad-party-speak.sh +20 -4
- package/.claude/hooks/bmad-speak.sh +60 -2
- package/.claude/hooks/bmad-tts-injector.sh +20 -1
- package/.claude/hooks/bmad-voice-manager.sh +25 -3
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +21 -2
- package/.claude/hooks/clawdbot-receiver.sh +19 -1
- package/.claude/hooks/elevenlabs-voices.sh +62 -0
- package/.claude/hooks/kokoro-installer.sh +20 -10
- package/.claude/hooks/language-manager.sh +10 -3
- package/.claude/hooks/party-set-room.sh +71 -0
- package/.claude/hooks/party-stage-roster.py +328 -0
- package/.claude/hooks/personality-manager.sh +19 -2
- package/.claude/hooks/piper-voice-manager.sh +3 -2
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +24 -5
- package/.claude/hooks/play-tts-elevenlabs.sh +38 -118
- package/.claude/hooks/play-tts-kokoro.sh +33 -6
- package/.claude/hooks/play-tts-soprano.sh +3 -2
- package/.claude/hooks/play-tts-ssh-remote.sh +37 -29
- package/.claude/hooks/play-tts-termux-ssh.sh +5 -4
- package/.claude/hooks/play-tts.sh +66 -61
- package/.claude/hooks/provider-catalog.json +352 -0
- package/.claude/hooks/provider-catalog.sh +161 -0
- package/.claude/hooks/provider-commands.sh +2 -1
- package/.claude/hooks/provider-manager.sh +47 -9
- package/.claude/hooks/python-resolver.sh +117 -0
- package/.claude/hooks/session-id.sh +56 -0
- package/.claude/hooks/session-start-tts.sh +39 -0
- package/.claude/hooks/speed-manager.sh +1 -1
- package/.claude/hooks/translate-manager.sh +3 -2
- package/.claude/hooks/translator.py +1 -1
- package/.claude/hooks/voice-manager.sh +242 -10
- package/.claude/hooks-windows/language-manager.ps1 +7 -1
- package/.claude/hooks-windows/personality-manager.ps1 +16 -1
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +20 -4
- package/.claude/hooks-windows/play-tts.ps1 +32 -3
- package/.claude/hooks-windows/provider-catalog.ps1 +140 -0
- package/.claude/hooks-windows/provider-manager.ps1 +63 -8
- package/.claude/hooks-windows/tts-watcher.ps1 +33 -12
- package/.claude/hooks-windows/voice-manager-windows.ps1 +49 -0
- package/.mcp.json +0 -7
- package/README.md +12 -3
- package/RELEASE_NOTES.md +43 -0
- package/mcp-server/server.py +146 -49
- package/mcp-server/test_mcp_correctness.py +20 -2
- package/mcp-server/test_windows_script_parity.py +0 -2
- package/package.json +1 -1
- package/src/cli/list-voices.js +218 -114
- package/src/console/bling.js +71 -0
- package/src/console/music-preview.js +79 -0
- package/src/console/tabs/music-tab.js +16 -39
- package/src/console/tabs/settings-tab.js +195 -13
- package/src/console/tabs/setup-tab.js +9 -34
- package/src/console/tabs/voices-tab.js +83 -14
- package/src/console/widgets/track-picker.js +82 -0
- package/src/installer.js +124 -10
- package/src/services/provider-catalog.js +412 -0
- package/src/services/provider-voice-catalog.js +52 -73
- package/src/services/tts-engine-service.js +29 -0
- package/src/utils/provider-validator.js +62 -12
- package/.claude/commands/agent-vibes/language.md +0 -23
- package/.claude/commands/agent-vibes/learn.md +0 -67
- package/.claude/commands/agent-vibes/replay-target.md +0 -14
- package/.claude/commands/agent-vibes/target-voice.md +0 -26
- package/.claude/commands/agent-vibes/target.md +0 -30
- package/.claude/hooks/learn-manager.sh +0 -492
- package/.claude/hooks/replay-target-audio.sh +0 -95
- package/.claude/hooks-windows/learn-manager.ps1 +0 -241
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
# @related play-tts.sh, provider-manager.sh, language-manager.sh
|
|
17
17
|
#
|
|
18
18
|
# Voice can be a name (e.g. "Rachel") or a raw ElevenLabs voice ID.
|
|
19
|
-
# Set ELEVENLABS_API_KEY in your shell profile or
|
|
19
|
+
# Set ELEVENLABS_API_KEY in your shell profile, or store it in
|
|
20
|
+
# ~/.agentvibes/elevenlabs-key.txt (chmod 600).
|
|
20
21
|
# Default voice: Rachel (English, warm female)
|
|
21
22
|
#
|
|
22
23
|
|
|
@@ -31,6 +32,7 @@ SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
|
31
32
|
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
32
33
|
|
|
33
34
|
source "$SCRIPT_DIR/audio-cache-utils.sh"
|
|
35
|
+
source "$SCRIPT_DIR/python-resolver.sh"
|
|
34
36
|
source "$SCRIPT_DIR/language-manager.sh"
|
|
35
37
|
|
|
36
38
|
if [[ -z "$TEXT" ]]; then
|
|
@@ -44,58 +46,7 @@ if [[ ${#TEXT} -gt 2000 ]]; then
|
|
|
44
46
|
fi
|
|
45
47
|
|
|
46
48
|
# ---------------------------------------------------------------------------
|
|
47
|
-
#
|
|
48
|
-
# Opt-in via ~/.agentvibes/infisical.env, which sets (env vars take precedence):
|
|
49
|
-
# INFISICAL_SECRET_NAME name of the secret in Infisical (e.g. ELEVEN_LABS)
|
|
50
|
-
# INFISICAL_PROJECT_ID project/workspace id
|
|
51
|
-
# INFISICAL_ENV environment slug (default: prod)
|
|
52
|
-
# INFISICAL_DOMAIN API base URL (default: http://127.0.0.1:8200/api)
|
|
53
|
-
# INFISICAL_BOOTSTRAP universal-auth creds file (default: ~/.palace-bootstrap)
|
|
54
|
-
# Requires the infisical CLI + python3. Fails soft: prints nothing and returns
|
|
55
|
-
# non-zero when unconfigured or unreachable, so the normal error path still runs.
|
|
56
|
-
# The fetched value is never printed.
|
|
57
|
-
_fetch_key_from_infisical() {
|
|
58
|
-
local cfg="${HOME}/.agentvibes/infisical.env"
|
|
59
|
-
if [[ -f "$cfg" ]]; then
|
|
60
|
-
set -o allexport; source "$cfg"; set +o allexport
|
|
61
|
-
fi
|
|
62
|
-
|
|
63
|
-
local secret_name="${INFISICAL_SECRET_NAME:-}"
|
|
64
|
-
local project_id="${INFISICAL_PROJECT_ID:-}"
|
|
65
|
-
local env_name="${INFISICAL_ENV:-prod}"
|
|
66
|
-
local domain="${INFISICAL_DOMAIN:-http://127.0.0.1:8200/api}"
|
|
67
|
-
local bootstrap="${INFISICAL_BOOTSTRAP:-${HOME}/.palace-bootstrap}"
|
|
68
|
-
|
|
69
|
-
[[ -n "$secret_name" && -n "$project_id" ]] || return 1
|
|
70
|
-
command -v infisical >/dev/null 2>&1 || return 1
|
|
71
|
-
command -v python3 >/dev/null 2>&1 || return 1
|
|
72
|
-
|
|
73
|
-
# Obtain an access token: reuse an exported INFISICAL_TOKEN, else exchange the
|
|
74
|
-
# universal-auth client credentials from the bootstrap file (creds via env only).
|
|
75
|
-
local token="${INFISICAL_TOKEN:-}"
|
|
76
|
-
if [[ -z "$token" ]]; then
|
|
77
|
-
[[ -f "$bootstrap" ]] || return 1
|
|
78
|
-
set -o allexport; source "$bootstrap"; set +o allexport
|
|
79
|
-
[[ -n "${INFISICAL_UNIVERSAL_AUTH_CLIENT_ID:-}" && -n "${INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET:-}" ]] || return 1
|
|
80
|
-
token="$(_AV_DOMAIN="$domain" python3 -c '
|
|
81
|
-
import os, json, urllib.request
|
|
82
|
-
d = os.environ["_AV_DOMAIN"].rstrip("/")
|
|
83
|
-
req = urllib.request.Request(d + "/v1/auth/universal-auth/login",
|
|
84
|
-
data=json.dumps({"clientId": os.environ["INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"],
|
|
85
|
-
"clientSecret": os.environ["INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET"]}).encode(),
|
|
86
|
-
headers={"Content-Type": "application/json"})
|
|
87
|
-
print(json.loads(urllib.request.urlopen(req, timeout=10).read())["accessToken"])
|
|
88
|
-
' 2>/dev/null || true)"
|
|
89
|
-
fi
|
|
90
|
-
[[ -n "$token" ]] || return 1
|
|
91
|
-
|
|
92
|
-
INFISICAL_TOKEN="$token" infisical secrets get "$secret_name" \
|
|
93
|
-
--projectId "$project_id" --env "$env_name" --domain "$domain" \
|
|
94
|
-
--plain 2>/dev/null | head -1
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
# ---------------------------------------------------------------------------
|
|
98
|
-
# API key — env var (only if it looks valid), then key file, then Infisical.
|
|
49
|
+
# API key — env var (only if it looks valid), then key file.
|
|
99
50
|
# A malformed/truncated ELEVENLABS_API_KEY (e.g. a half-pasted key) is ignored so
|
|
100
51
|
# it can't silently shadow a good key file — a common foot-gun that yields
|
|
101
52
|
# confusing "API key fail" errors despite a valid key on disk.
|
|
@@ -122,91 +73,55 @@ if [[ -z "$API_KEY" ]]; then
|
|
|
122
73
|
API_KEY="$(tr -d '[:space:]' < "$_key_file")"
|
|
123
74
|
fi
|
|
124
75
|
fi
|
|
125
|
-
if [[ -z "$API_KEY" ]]; then
|
|
126
|
-
API_KEY="$(_fetch_key_from_infisical || true)"
|
|
127
|
-
API_KEY="$(printf '%s' "$API_KEY" | tr -d '[:space:]')"
|
|
128
|
-
fi
|
|
129
76
|
|
|
130
77
|
if [[ -z "$API_KEY" ]]; then
|
|
131
78
|
echo "❌ ElevenLabs API key not set." >&2
|
|
132
|
-
echo " Set it one of
|
|
79
|
+
echo " Set it one of two ways:" >&2
|
|
133
80
|
echo " 1. export ELEVENLABS_API_KEY=your_key (add to ~/.bashrc or ~/.zshrc)" >&2
|
|
134
81
|
echo " 2. echo 'your_key' > ~/.agentvibes/elevenlabs-key.txt && chmod 600 ~/.agentvibes/elevenlabs-key.txt" >&2
|
|
135
|
-
echo " 3. Configure Infisical in ~/.agentvibes/infisical.env (INFISICAL_SECRET_NAME + INFISICAL_PROJECT_ID)" >&2
|
|
136
82
|
echo " Get a free key at: https://elevenlabs.io" >&2
|
|
137
83
|
exit 2
|
|
138
84
|
fi
|
|
139
85
|
|
|
140
86
|
# ---------------------------------------------------------------------------
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
VOICE_IDS["Freya"]="jsCqWAovK2LkecY7zXl4"
|
|
161
|
-
VOICE_IDS["Gigi"]="jBpfuIE2acCO8z3wKNLl"
|
|
162
|
-
VOICE_IDS["Giovanni"]="zcAOhNBS3c14rBihAFp1"
|
|
163
|
-
VOICE_IDS["Glinda"]="z9fAnlkpzviPz146aGWa"
|
|
164
|
-
VOICE_IDS["Grace"]="oWAxZDx7w5VEj9dCyTzz"
|
|
165
|
-
VOICE_IDS["Harry"]="SOYHLrjzK2X1ezoPC6cr"
|
|
166
|
-
VOICE_IDS["James"]="ZQe5CZNOzWyzPSCn5a3c"
|
|
167
|
-
VOICE_IDS["Jessie"]="t0jbNlBVZ17f02VDIeMI"
|
|
168
|
-
VOICE_IDS["Josh"]="TxGEqnHWrfWFTfGW9XjX"
|
|
169
|
-
VOICE_IDS["Liam"]="TX3LPaxmHKxFdv7VOQHJ"
|
|
170
|
-
VOICE_IDS["Lily"]="pFZP5JQG7iQjIQuC4Bku"
|
|
171
|
-
VOICE_IDS["Matilda"]="XrExE9yKIg1WjnnlVkGX"
|
|
172
|
-
VOICE_IDS["Michael"]="flq6f7yk4E4fJM5XTYuZ"
|
|
173
|
-
VOICE_IDS["Mimi"]="zrHiDhphv9ZnVXBqCLjz"
|
|
174
|
-
VOICE_IDS["Nicole"]="piTKgcLEGmPE4e6mEKli"
|
|
175
|
-
VOICE_IDS["Patrick"]="ODq5zmih8GrVes37Dizd"
|
|
176
|
-
VOICE_IDS["Paul"]="5Q0t7uMcjvnagumLfvZi"
|
|
177
|
-
VOICE_IDS["Sam"]="yoZ06aMxZJJ28mfd3POQ"
|
|
178
|
-
VOICE_IDS["Sarah"]="EXAVITQu4vr4xnSDxMaL"
|
|
179
|
-
VOICE_IDS["Serena"]="pMsXgVXv3BLzUgSXRplE"
|
|
180
|
-
VOICE_IDS["Thomas"]="GBv7mTt0atIp3Br8iCZE"
|
|
181
|
-
|
|
182
|
-
DEFAULT_VOICE_ID="${VOICE_IDS[Rachel]}"
|
|
87
|
+
# ElevenLabs voice catalog — single source of truth (shared with voice-manager.sh).
|
|
88
|
+
# Provides ELEVENLABS_DEFAULT_VOICE, elevenlabs_resolve_voice(), elevenlabs_voice_names().
|
|
89
|
+
if [[ -f "$SCRIPT_DIR/elevenlabs-voices.sh" ]]; then
|
|
90
|
+
# shellcheck source=/dev/null
|
|
91
|
+
source "$SCRIPT_DIR/elevenlabs-voices.sh"
|
|
92
|
+
else
|
|
93
|
+
# Minimal fallback so speech still works if the catalog file is missing.
|
|
94
|
+
# bash-3.2-safe: no associative array, case-statement resolver.
|
|
95
|
+
ELEVENLABS_DEFAULT_VOICE="Sarah"
|
|
96
|
+
elevenlabs_resolve_voice() {
|
|
97
|
+
local q="${1:-}"; [[ -z "$q" ]] && return 1
|
|
98
|
+
case "$(printf '%s' "$q" | tr '[:upper:]' '[:lower:]')" in
|
|
99
|
+
sarah) printf %s "EXAVITQu4vr4xnSDxMaL"; return 0 ;;
|
|
100
|
+
esac
|
|
101
|
+
[[ "$q" =~ ^[A-Za-z0-9]{20}$ ]] && { printf %s "$q"; return 0; }
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
fi
|
|
105
|
+
DEFAULT_VOICE_ID="$(elevenlabs_resolve_voice "$ELEVENLABS_DEFAULT_VOICE")"
|
|
183
106
|
|
|
184
107
|
# ---------------------------------------------------------------------------
|
|
185
108
|
# Resolve voice ID from override or config
|
|
186
109
|
VOICE_ID=""
|
|
187
110
|
|
|
188
111
|
if [[ -n "$VOICE_OVERRIDE" ]]; then
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
elif [[ "$VOICE_OVERRIDE" =~ ^[a-zA-Z0-9]{10,40}$ ]]; then
|
|
192
|
-
# Looks like a raw voice ID
|
|
193
|
-
VOICE_ID="$VOICE_OVERRIDE"
|
|
194
|
-
else
|
|
195
|
-
echo "⚠️ Unknown ElevenLabs voice '$VOICE_OVERRIDE', using Rachel" >&2
|
|
112
|
+
VOICE_ID="$(elevenlabs_resolve_voice "$VOICE_OVERRIDE")" || {
|
|
113
|
+
echo "[WARN] Unknown ElevenLabs voice '$VOICE_OVERRIDE', using $ELEVENLABS_DEFAULT_VOICE" >&2
|
|
196
114
|
VOICE_ID="$DEFAULT_VOICE_ID"
|
|
197
|
-
|
|
115
|
+
}
|
|
198
116
|
else
|
|
199
|
-
#
|
|
117
|
+
# Config path: read the voice saved by /agent-vibes:switch (a raw voice_id or
|
|
118
|
+
# a friendly name) and resolve it the same way — this is what hook-driven
|
|
119
|
+
# speech uses, so a switched voice actually plays (not a silent default).
|
|
200
120
|
VOICE_NAME=""
|
|
201
121
|
if [[ -f "$SCRIPT_DIR/voice-manager.sh" ]]; then
|
|
202
122
|
VOICE_NAME="$("$SCRIPT_DIR/voice-manager.sh" get 2>/dev/null || true)"
|
|
203
123
|
fi
|
|
204
|
-
|
|
205
|
-
if [[ -n "$VOICE_NAME" && -n "${VOICE_IDS[$VOICE_NAME]:-}" ]]; then
|
|
206
|
-
VOICE_ID="${VOICE_IDS[$VOICE_NAME]}"
|
|
207
|
-
else
|
|
208
|
-
VOICE_ID="$DEFAULT_VOICE_ID"
|
|
209
|
-
fi
|
|
124
|
+
VOICE_ID="$(elevenlabs_resolve_voice "$VOICE_NAME")" || VOICE_ID="$DEFAULT_VOICE_ID"
|
|
210
125
|
fi
|
|
211
126
|
|
|
212
127
|
# ---------------------------------------------------------------------------
|
|
@@ -246,7 +161,12 @@ trap 'rm -f "${TEMP_FILE:-}" "${BODY_FILE:-}" 2>/dev/null || true' EXIT
|
|
|
246
161
|
# ---------------------------------------------------------------------------
|
|
247
162
|
# Build the JSON request body with python so all escaping is handled correctly
|
|
248
163
|
# (text is passed via env, never interpolated into a shell-quoted payload).
|
|
249
|
-
if
|
|
164
|
+
if [[ -z "$PYTHON_BIN" ]]; then
|
|
165
|
+
echo "❌ ElevenLabs needs Python 3, but none was found." >&2
|
|
166
|
+
echo " Install Python 3, or set AGENTVIBES_PYTHON=/path/to/python." >&2
|
|
167
|
+
exit 3
|
|
168
|
+
fi
|
|
169
|
+
if ! TTS_TEXT="$TEXT" TTS_MODEL="$MODEL_ID" TTS_LANG="$LANGUAGE_CODE" "$PYTHON_BIN" -c '
|
|
250
170
|
import os, json
|
|
251
171
|
print(json.dumps({
|
|
252
172
|
"text": os.environ["TTS_TEXT"],
|
|
@@ -254,7 +174,7 @@ print(json.dumps({
|
|
|
254
174
|
"language_code": os.environ["TTS_LANG"],
|
|
255
175
|
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75},
|
|
256
176
|
}))' > "$BODY_FILE" 2>/dev/null; then
|
|
257
|
-
echo "❌ Failed to build ElevenLabs request body (
|
|
177
|
+
echo "❌ Failed to build ElevenLabs request body ($PYTHON_BIN required)" >&2
|
|
258
178
|
exit 3
|
|
259
179
|
fi
|
|
260
180
|
|
|
@@ -31,28 +31,55 @@ SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
|
31
31
|
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
32
32
|
|
|
33
33
|
source "$SCRIPT_DIR/audio-cache-utils.sh"
|
|
34
|
+
source "$SCRIPT_DIR/python-resolver.sh"
|
|
34
35
|
|
|
35
36
|
if [[ -z "$TEXT" ]]; then
|
|
36
37
|
echo "Usage: $0 \"text to speak\" [voice]" >&2
|
|
37
38
|
exit 1
|
|
38
39
|
fi
|
|
39
40
|
|
|
41
|
+
# Distinguish "no Python at all" from "Python present but kokoro module missing"
|
|
42
|
+
# — the old check conflated them, so a Windows box where python3 simply wasn't on
|
|
43
|
+
# PATH wrongly reported "Kokoro not installed" (see python-resolver.sh).
|
|
44
|
+
if [[ -z "$PYTHON_BIN" ]]; then
|
|
45
|
+
echo "❌ No Python 3 interpreter found." >&2
|
|
46
|
+
echo " Kokoro needs Python 3. Install it (Windows: https://python.org, or 'py')," >&2
|
|
47
|
+
echo " or set AGENTVIBES_PYTHON=/path/to/python.exe if it's installed elsewhere." >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
40
50
|
# Check kokoro is installed (use find_spec to avoid slow torch import)
|
|
41
|
-
if !
|
|
42
|
-
echo "❌ Kokoro TTS not installed
|
|
51
|
+
if ! "$PYTHON_BIN" -c "import importlib.util; exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null; then
|
|
52
|
+
echo "❌ Kokoro TTS module not installed for: $PYTHON_BIN" >&2
|
|
43
53
|
echo " Install with: ${SCRIPT_DIR}/kokoro-installer.sh" >&2
|
|
44
|
-
echo " Or manually: pip install kokoro
|
|
54
|
+
echo " Or manually: \"$PYTHON_BIN\" -m pip install kokoro soundfile numpy" >&2
|
|
45
55
|
exit 2
|
|
46
56
|
fi
|
|
47
57
|
|
|
48
58
|
# ---------------------------------------------------------------------------
|
|
49
59
|
# Resolve voice
|
|
60
|
+
# Default voice sourced from the generated Provider Catalog (SSOT) when present.
|
|
61
|
+
# FAIL-SAFE: keep the literal fallback for installed-tree skew (mirrors play-tts.sh
|
|
62
|
+
# PLAN_OK) — synth never breaks on a missing catalog.
|
|
50
63
|
DEFAULT_VOICE="af_heart"
|
|
64
|
+
if [[ -f "$SCRIPT_DIR/provider-catalog.sh" ]]; then
|
|
65
|
+
# shellcheck source=/dev/null
|
|
66
|
+
source "$SCRIPT_DIR/provider-catalog.sh" 2>/dev/null || true
|
|
67
|
+
if type catalog_default_voice >/dev/null 2>&1; then
|
|
68
|
+
_CATALOG_DEFAULT="$(catalog_default_voice kokoro 2>/dev/null || true)"
|
|
69
|
+
[[ -n "$_CATALOG_DEFAULT" ]] && DEFAULT_VOICE="$_CATALOG_DEFAULT"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Synth-time shape check (LENIENT per design §3.3): a voice from a NEWER kokoro
|
|
74
|
+
# model that the shipped catalog doesn't know must still degrade audibly, not
|
|
75
|
+
# brick — so this stays a shape check + loud-warning fallback, NOT membership.
|
|
76
|
+
# One literal, reused at both call sites below.
|
|
77
|
+
KOKORO_SHAPE_RE='^[a-z]{2}_[a-z0-9_]+$'
|
|
51
78
|
|
|
52
79
|
if [[ -n "$VOICE_OVERRIDE" ]]; then
|
|
53
80
|
# Accept kokoro-style voice IDs (2-letter prefix + underscore + name)
|
|
54
81
|
# e.g. af_heart, am_adam, bf_emma, bm_george
|
|
55
|
-
if [[ "$VOICE_OVERRIDE" =~
|
|
82
|
+
if [[ "$VOICE_OVERRIDE" =~ $KOKORO_SHAPE_RE ]]; then
|
|
56
83
|
VOICE="$VOICE_OVERRIDE"
|
|
57
84
|
else
|
|
58
85
|
echo "⚠️ Unrecognized kokoro voice '$VOICE_OVERRIDE', using $DEFAULT_VOICE" >&2
|
|
@@ -62,7 +89,7 @@ else
|
|
|
62
89
|
# Check voice manager
|
|
63
90
|
if [[ -f "$SCRIPT_DIR/voice-manager.sh" ]]; then
|
|
64
91
|
_CONFIGURED="$("$SCRIPT_DIR/voice-manager.sh" get 2>/dev/null || true)"
|
|
65
|
-
if [[ "$_CONFIGURED" =~
|
|
92
|
+
if [[ "$_CONFIGURED" =~ $KOKORO_SHAPE_RE ]]; then
|
|
66
93
|
VOICE="$_CONFIGURED"
|
|
67
94
|
else
|
|
68
95
|
VOICE="$DEFAULT_VOICE"
|
|
@@ -103,7 +130,7 @@ if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
|
|
|
103
130
|
fi
|
|
104
131
|
|
|
105
132
|
# Run synthesis — output path printed to stdout
|
|
106
|
-
RESULT=$(
|
|
133
|
+
RESULT=$("$PYTHON_BIN" "$SYNTH_SCRIPT" "$TEXT" "$VOICE" "$TEMP_WAV" "$SPEED" 2>&1) || {
|
|
107
134
|
echo "❌ Kokoro synthesis failed: $RESULT" >&2
|
|
108
135
|
exit 3
|
|
109
136
|
}
|
|
@@ -54,6 +54,7 @@ VOICE_OVERRIDE="$2" # Ignored — Soprano has a single voice, kept for provider
|
|
|
54
54
|
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
55
55
|
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
56
56
|
source "$SCRIPT_DIR/audio-cache-utils.sh"
|
|
57
|
+
source "$SCRIPT_DIR/python-resolver.sh"
|
|
57
58
|
|
|
58
59
|
SOPRANO_PORT="${SOPRANO_PORT:-7860}"
|
|
59
60
|
SOPRANO_DEVICE="${SOPRANO_DEVICE:-auto}"
|
|
@@ -135,7 +136,7 @@ SYNTH_MODE=""
|
|
|
135
136
|
if check_webui_server; then
|
|
136
137
|
# Gradio WebUI mode — use Python helper for SSE protocol
|
|
137
138
|
SYNTH_MODE="webui"
|
|
138
|
-
|
|
139
|
+
"$PYTHON_BIN" "$SCRIPT_DIR/soprano-gradio-synth.py" "$TEXT" "$TEMP_FILE" "$SOPRANO_PORT" 2>/dev/null
|
|
139
140
|
elif check_api_server; then
|
|
140
141
|
# OpenAI-compatible API mode — direct curl
|
|
141
142
|
SYNTH_MODE="api"
|
|
@@ -151,7 +152,7 @@ fi
|
|
|
151
152
|
|
|
152
153
|
if [[ ! -f "$TEMP_FILE" ]] || [[ ! -s "$TEMP_FILE" ]]; then
|
|
153
154
|
echo "❌ Failed to synthesize speech with Soprano ($SYNTH_MODE mode)"
|
|
154
|
-
[[ "$SYNTH_MODE" == "webui" ]] && echo " Try:
|
|
155
|
+
[[ "$SYNTH_MODE" == "webui" ]] && echo " Try: $PYTHON_BIN $SCRIPT_DIR/soprano-gradio-synth.py \"test\" /tmp/test.wav $SOPRANO_PORT"
|
|
155
156
|
exit 4
|
|
156
157
|
fi
|
|
157
158
|
|
|
@@ -47,7 +47,9 @@ if [[ -n "${AGENTVIBES_MUSIC_STOP:-}" ]]; then
|
|
|
47
47
|
elif [[ -n "${AGENTVIBES_MUSIC_ONLY:-}" ]]; then
|
|
48
48
|
# Only a bare .mp3 filename is allowed (no path separators, no leading dash) —
|
|
49
49
|
# the receiver resolves it against its own tracks dir, so reject path-like input.
|
|
50
|
-
|
|
50
|
+
# Spaces ARE allowed (many track names contain them, e.g. "Late Night Hip Hop.mp3");
|
|
51
|
+
# the name travels as a jq --arg-escaped JSON string, so a space is not injectable.
|
|
52
|
+
if [[ "$AGENTVIBES_MUSIC_ONLY" =~ ^[A-Za-z0-9._][A-Za-z0-9._\ -]*\.mp3$ ]]; then
|
|
51
53
|
PAYLOAD_KIND="music"
|
|
52
54
|
MUSIC_ONLY_TRACK="$AGENTVIBES_MUSIC_ONLY"
|
|
53
55
|
else
|
|
@@ -66,13 +68,26 @@ fi
|
|
|
66
68
|
|
|
67
69
|
# Get script directory and project root
|
|
68
70
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
71
|
+
source "$SCRIPT_DIR/python-resolver.sh"
|
|
72
|
+
source "$SCRIPT_DIR/session-id.sh"
|
|
69
73
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
70
74
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
75
|
+
# Canonical routing session id. The receiver multiplexes forwarded TTS by this
|
|
76
|
+
# id, so it MUST derive from CLAUDE_PROJECT_DIR (the real user project injected
|
|
77
|
+
# via --project-dir), NOT basename(PROJECT_ROOT) — for a global install that
|
|
78
|
+
# basename collapses to the HOME dir name and merges every project into one
|
|
79
|
+
# session. Fallback (CLAUDE_PROJECT_DIR unset) is PROJECT_ROOT, preserving the
|
|
80
|
+
# legacy value for the no-project case.
|
|
81
|
+
SESSION_ID="$(av_session_id "$PROJECT_ROOT")"
|
|
82
|
+
# PROJECT_NAME is kept for receiver back-compat (the receiver reads "project");
|
|
83
|
+
# set it to the SAME corrected session id so both fields agree.
|
|
84
|
+
PROJECT_NAME="$SESSION_ID"
|
|
85
|
+
# Absolute path, forwarded so the receiver can show/learn the real remote folder.
|
|
86
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
87
|
+
PROJECT_PATH="$CLAUDE_PROJECT_DIR"
|
|
88
|
+
else
|
|
89
|
+
PROJECT_PATH="$PROJECT_ROOT"
|
|
90
|
+
fi
|
|
76
91
|
|
|
77
92
|
# ---------------------------------------------------------------------------
|
|
78
93
|
# Get SSH connection details from config
|
|
@@ -93,10 +108,10 @@ fi
|
|
|
93
108
|
# Priority 2: ~/.agentvibes/transport-config.json (ssh-remote section)
|
|
94
109
|
if [[ -z "$SSH_HOST" ]]; then
|
|
95
110
|
_TRANSPORT_CFG="$HOME/.agentvibes/transport-config.json"
|
|
96
|
-
if [[ -f "$_TRANSPORT_CFG" ]] &&
|
|
97
|
-
SSH_HOST=$(
|
|
98
|
-
SSH_KEY=$(
|
|
99
|
-
SSH_PORT=$(
|
|
111
|
+
if [[ -f "$_TRANSPORT_CFG" ]] && [[ -n "$PYTHON_BIN" ]]; then
|
|
112
|
+
SSH_HOST=$("$PYTHON_BIN" -c "import json,sys; d=json.load(open('$_TRANSPORT_CFG')); p=d.get('ssh-remote',{}); print(p.get('host',''))" 2>/dev/null || echo "")
|
|
113
|
+
SSH_KEY=$("$PYTHON_BIN" -c "import json,sys; d=json.load(open('$_TRANSPORT_CFG')); p=d.get('ssh-remote',{}); print(p.get('sshKey',''))" 2>/dev/null || echo "")
|
|
114
|
+
SSH_PORT=$("$PYTHON_BIN" -c "import json,sys; d=json.load(open('$_TRANSPORT_CFG')); p=d.get('ssh-remote',{}); print(p.get('port',''))" 2>/dev/null || echo "")
|
|
100
115
|
fi
|
|
101
116
|
fi
|
|
102
117
|
|
|
@@ -104,8 +119,8 @@ fi
|
|
|
104
119
|
# (config keyed by LLM name rather than 'ssh-remote', e.g. 'claude-code')
|
|
105
120
|
if [[ -z "$SSH_HOST" ]]; then
|
|
106
121
|
_TRANSPORT_CFG="$HOME/.agentvibes/transport-config.json"
|
|
107
|
-
if [[ -f "$_TRANSPORT_CFG" ]] &&
|
|
108
|
-
_remote_data=$(
|
|
122
|
+
if [[ -f "$_TRANSPORT_CFG" ]] && [[ -n "$PYTHON_BIN" ]]; then
|
|
123
|
+
_remote_data=$("$PYTHON_BIN" -c "
|
|
109
124
|
import json, sys
|
|
110
125
|
try:
|
|
111
126
|
d = json.load(open('$_TRANSPORT_CFG'))
|
|
@@ -156,10 +171,11 @@ if [[ -n "$SSH_PORT" ]] && [[ ! "$SSH_PORT" =~ ^[0-9]+$ ]]; then
|
|
|
156
171
|
fi
|
|
157
172
|
|
|
158
173
|
# SECURITY: Validate VOICE
|
|
159
|
-
# Allow letters, digits, underscore, hyphen, period, colon (for :: multi-speaker separator),
|
|
174
|
+
# Allow letters, digits, underscore, hyphen, period, colon (for :: multi-speaker separator),
|
|
175
|
+
# slash, and SPACES (Windows SAPI voice names like "Microsoft David Desktop").
|
|
160
176
|
# Voice is passed to the remote via base64-encoded JSON (jq --arg safely escapes it),
|
|
161
|
-
# so shell metacharacters are the only real risk.
|
|
162
|
-
if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_
|
|
177
|
+
# so shell metacharacters are the only real risk — a space is not injectable.
|
|
178
|
+
if [[ ! "$VOICE" =~ ^[a-zA-Z0-9_.:\/\ -]+$ ]]; then
|
|
163
179
|
echo "Invalid voice format: $VOICE" >&2
|
|
164
180
|
exit 1
|
|
165
181
|
fi
|
|
@@ -343,9 +359,7 @@ fi
|
|
|
343
359
|
|
|
344
360
|
# ---------------------------------------------------------------------------
|
|
345
361
|
# Resolve LANGUAGE to forward (map §D 'language | Forward on SSH', R18).
|
|
346
|
-
#
|
|
347
|
-
# learning-mode target (tts-target-language.txt when tts-learn-mode.txt=="ON")
|
|
348
|
-
# > tts-language.txt.
|
|
362
|
+
# Source: tts-language.txt (set by language-manager.sh).
|
|
349
363
|
# Forwarded as a DEFAULT only — the receiver stays authoritative and its own
|
|
350
364
|
# language config (if any) wins. language-manager.sh stores tts-language.txt at
|
|
351
365
|
# the .claude/ root; the resolver contract tests use the .claude/config/ subdir,
|
|
@@ -353,14 +367,7 @@ fi
|
|
|
353
367
|
# ---------------------------------------------------------------------------
|
|
354
368
|
LANGUAGE=""
|
|
355
369
|
for _cdir in "$_RESOLVE_PROJECT_DIR/.claude" "$HOME/.claude"; do
|
|
356
|
-
#
|
|
357
|
-
if [[ -z "$LANGUAGE" && -f "$_cdir/tts-learn-mode.txt" ]]; then
|
|
358
|
-
if [[ "$(cat "$_cdir/tts-learn-mode.txt" 2>/dev/null || true)" == "ON" ]]; then
|
|
359
|
-
[[ -f "$_cdir/tts-target-language.txt" ]] && \
|
|
360
|
-
LANGUAGE=$(cat "$_cdir/tts-target-language.txt" 2>/dev/null || true)
|
|
361
|
-
fi
|
|
362
|
-
fi
|
|
363
|
-
# Priority 2: tts-language.txt (root or config/ subdir).
|
|
370
|
+
# tts-language.txt (root or config/ subdir).
|
|
364
371
|
if [[ -z "$LANGUAGE" && -f "$_cdir/tts-language.txt" ]]; then
|
|
365
372
|
LANGUAGE=$(cat "$_cdir/tts-language.txt" 2>/dev/null || true)
|
|
366
373
|
fi
|
|
@@ -391,6 +398,7 @@ build_json_payload() {
|
|
|
391
398
|
--arg effects "$SOX_EFFECTS" \
|
|
392
399
|
--arg music "$BG_FILE" \
|
|
393
400
|
--arg volume "$BG_VOLUME" \
|
|
401
|
+
--arg session "$SESSION_ID" \
|
|
394
402
|
--arg project "$PROJECT_NAME" \
|
|
395
403
|
--arg projectPath "$PROJECT_PATH" \
|
|
396
404
|
--arg pretext "$PRETEXT" \
|
|
@@ -400,7 +408,7 @@ build_json_payload() {
|
|
|
400
408
|
--arg mute "$MUTE" \
|
|
401
409
|
--arg language "$LANGUAGE" \
|
|
402
410
|
--arg kind "$PAYLOAD_KIND" \
|
|
403
|
-
'{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, project: $project, projectPath: $projectPath, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm, mute: $mute, language: $language, kind: $kind}'
|
|
411
|
+
'{text: $text, voice: $voice, effects: $effects, music: $music, volume: $volume, session: $session, project: $project, projectPath: $projectPath, pretext: $pretext, speed: $speed, provider: $provider, llm: $llm, mute: $mute, language: $language, kind: $kind}'
|
|
404
412
|
else
|
|
405
413
|
# Manual JSON — escape backslashes, quotes, control chars
|
|
406
414
|
local escaped_text
|
|
@@ -409,8 +417,8 @@ build_json_payload() {
|
|
|
409
417
|
escaped_pretext=$(printf '%s' "$PRETEXT" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
410
418
|
local escaped_project_path
|
|
411
419
|
escaped_project_path=$(printf '%s' "$PROJECT_PATH" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
412
|
-
printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","project":"%s","projectPath":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s","mute":"%s","language":"%s","kind":"%s"}' \
|
|
413
|
-
"$escaped_text" "$VOICE" "$SOX_EFFECTS" "$BG_FILE" "$BG_VOLUME" "$PROJECT_NAME" "$escaped_project_path" "$escaped_pretext" "$SPEED" "$PROVIDER" "$LLM_NAME" "$MUTE" "$LANGUAGE" "$PAYLOAD_KIND"
|
|
420
|
+
printf '{"text":"%s","voice":"%s","effects":"%s","music":"%s","volume":"%s","session":"%s","project":"%s","projectPath":"%s","pretext":"%s","speed":"%s","provider":"%s","llm":"%s","mute":"%s","language":"%s","kind":"%s"}' \
|
|
421
|
+
"$escaped_text" "$VOICE" "$SOX_EFFECTS" "$BG_FILE" "$BG_VOLUME" "$SESSION_ID" "$PROJECT_NAME" "$escaped_project_path" "$escaped_pretext" "$SPEED" "$PROVIDER" "$LLM_NAME" "$MUTE" "$LANGUAGE" "$PAYLOAD_KIND"
|
|
414
422
|
fi
|
|
415
423
|
}
|
|
416
424
|
|
|
@@ -68,6 +68,7 @@ VOICE_OVERRIDE="$2" # Not used for termux-ssh, but kept for interface compatibi
|
|
|
68
68
|
|
|
69
69
|
# Script directory
|
|
70
70
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
71
|
+
source "$SCRIPT_DIR/python-resolver.sh"
|
|
71
72
|
|
|
72
73
|
# @function get_ssh_host
|
|
73
74
|
# @intent Determine SSH host alias for Android device
|
|
@@ -119,10 +120,10 @@ SSH_KEY=""
|
|
|
119
120
|
SSH_PORT=""
|
|
120
121
|
|
|
121
122
|
_TRANSPORT_CFG="$HOME/.agentvibes/transport-config.json"
|
|
122
|
-
if [[ -f "$_TRANSPORT_CFG" ]] &&
|
|
123
|
-
_CFG_HOST=$(
|
|
124
|
-
SSH_KEY=$(
|
|
125
|
-
SSH_PORT=$(
|
|
123
|
+
if [[ -f "$_TRANSPORT_CFG" ]] && [[ -n "$PYTHON_BIN" ]]; then
|
|
124
|
+
_CFG_HOST=$("$PYTHON_BIN" -c "import json; d=json.load(open('$_TRANSPORT_CFG')); p=d.get('termux-ssh',{}); print(p.get('host',''))" 2>/dev/null || echo "")
|
|
125
|
+
SSH_KEY=$("$PYTHON_BIN" -c "import json; d=json.load(open('$_TRANSPORT_CFG')); p=d.get('termux-ssh',{}); print(p.get('sshKey',''))" 2>/dev/null || echo "")
|
|
126
|
+
SSH_PORT=$("$PYTHON_BIN" -c "import json; d=json.load(open('$_TRANSPORT_CFG')); p=d.get('termux-ssh',{}); print(p.get('port',''))" 2>/dev/null || echo "")
|
|
126
127
|
fi
|
|
127
128
|
|
|
128
129
|
if [[ -n "${_CFG_HOST:-}" ]]; then
|