agentvibes 5.12.0 → 5.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/install-manifest.json +111 -91
- 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 +61 -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 +2 -2
- 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 +136 -18
- 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
|
@@ -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
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
#
|
|
32
32
|
# ---
|
|
33
33
|
#
|
|
34
|
-
# @fileoverview TTS Provider Router with Translation
|
|
34
|
+
# @fileoverview TTS Provider Router with Translation Support
|
|
35
35
|
# @context Routes TTS requests to active provider (Piper or macOS) with optional translation
|
|
36
|
-
# @architecture Provider abstraction layer - single entry point for all TTS, handles translation
|
|
37
|
-
# @dependencies provider-manager.sh, play-tts-piper.sh, translator.py, translate-manager.sh
|
|
36
|
+
# @architecture Provider abstraction layer - single entry point for all TTS, handles translation
|
|
37
|
+
# @dependencies provider-manager.sh, play-tts-piper.sh, translator.py, translate-manager.sh
|
|
38
38
|
# @entrypoints Called by hooks, slash commands, personality-manager.sh, and all TTS features
|
|
39
39
|
# @patterns Provider pattern - delegates to provider-specific implementations, auto-detects provider from voice name
|
|
40
|
-
# @related provider-manager.sh, play-tts-piper.sh,
|
|
40
|
+
# @related provider-manager.sh, play-tts-piper.sh, translate-manager.sh
|
|
41
41
|
#
|
|
42
42
|
# **IMPORTANT: This script should be called inline (NOT in background) in Bash tool**
|
|
43
43
|
# Wait for TTS playback to complete before continuing.
|
|
@@ -113,6 +113,12 @@ elif [[ -f "$GLOBAL_MUTE_FILE" ]]; then
|
|
|
113
113
|
exit 0
|
|
114
114
|
fi
|
|
115
115
|
|
|
116
|
+
# Resolve a working Python interpreter once (translator and transport config
|
|
117
|
+
# both need it). Sourced AFTER the mute exits so muted calls
|
|
118
|
+
# pay nothing. Windows git-bash frequently has no python3 on PATH — see
|
|
119
|
+
# python-resolver.sh; $PYTHON_BIN is empty when none is usable.
|
|
120
|
+
source "$SCRIPT_DIR/python-resolver.sh"
|
|
121
|
+
|
|
116
122
|
# Parse named flags (e.g. --llm) before positional arguments.
|
|
117
123
|
# This allows callers to pass: play-tts.sh --llm claude-code "text to speak"
|
|
118
124
|
# Named args are extracted; remaining positional args are shifted into $1/$2/$3.
|
|
@@ -241,6 +247,53 @@ if [[ -z "$_PRETEXT" ]]; then
|
|
|
241
247
|
fi
|
|
242
248
|
done
|
|
243
249
|
fi
|
|
250
|
+
# Dynamic session self-ID: a per-LLM PRETEXT containing the {{session}} token
|
|
251
|
+
# expands to "<LLM> on <Project> in <Terminal>" so multi-session users can tell
|
|
252
|
+
# which tab just spoke. Announced once per session (best-effort — see the key
|
|
253
|
+
# note below); on later utterances the token is dropped so it doesn't prefix
|
|
254
|
+
# every line. Opt-in — only fires when the pretext actually contains the token.
|
|
255
|
+
if [[ "$_PRETEXT" == *"{{session}}"* ]]; then
|
|
256
|
+
# Session key. There is no true Claude-session id available to a plain Bash
|
|
257
|
+
# command, so this is best-effort: a terminal-session id if the emulator
|
|
258
|
+
# exports one (stable for the tab's life), else the parent PID. Worst case the
|
|
259
|
+
# self-ID repeats or is skipped — cosmetic, and only when a pretext opts in.
|
|
260
|
+
_SID_RAW="${WT_SESSION:-${TERM_SESSION_ID:-${TMUX:-$PPID}}}"
|
|
261
|
+
_SID_KEY="$(printf '%s' "${LLM_PROVIDER:-}|$(basename "${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}")|$_SID_RAW" | tr -c 'A-Za-z0-9' '_')"
|
|
262
|
+
# Announce-marker dir: an OWNED subdir, per the project's secure-temp rule.
|
|
263
|
+
# If the dir exists but isn't ours (pre-created symlink attack on a shared
|
|
264
|
+
# host), skip the marker entirely and just strip the token — never chmod or
|
|
265
|
+
# write through someone else's path.
|
|
266
|
+
_ANN_BASE="${XDG_RUNTIME_DIR:-/tmp/agentvibes-$(id -u 2>/dev/null || echo 0)}"
|
|
267
|
+
_ANN_DIR="$_ANN_BASE/agentvibes-session"
|
|
268
|
+
_ANN_OK=0
|
|
269
|
+
if mkdir -p "$_ANN_DIR" 2>/dev/null && chmod 700 "$_ANN_DIR" 2>/dev/null; then
|
|
270
|
+
_ANN_OWNER="$(stat -c '%u' "$_ANN_DIR" 2>/dev/null || stat -f '%u' "$_ANN_DIR" 2>/dev/null || echo '')"
|
|
271
|
+
[[ -z "$_ANN_OWNER" || "$_ANN_OWNER" == "$(id -u 2>/dev/null || echo 0)" ]] && _ANN_OK=1
|
|
272
|
+
fi
|
|
273
|
+
_ANN_FILE="$_ANN_DIR/announced-$_SID_KEY"
|
|
274
|
+
# First utterance → replacement is the self-ID; later utterances → empty (drop).
|
|
275
|
+
if [[ "$_ANN_OK" == "1" && -f "$_ANN_FILE" ]]; then
|
|
276
|
+
_SESSION_ID=""
|
|
277
|
+
else
|
|
278
|
+
_SESSION_ID="$(bash "$SCRIPT_DIR/agentvibes-session-id.sh" "${LLM_PROVIDER:-claude-code}" "${CLAUDE_PROJECT_DIR:-$PROJECT_ROOT}" 2>/dev/null || true)"
|
|
279
|
+
if [[ "$_ANN_OK" == "1" ]]; then
|
|
280
|
+
touch "$_ANN_FILE" 2>/dev/null || true
|
|
281
|
+
# Bound growth: drop announce-markers older than a day (find is optional).
|
|
282
|
+
find "$_ANN_DIR" -maxdepth 1 -name 'announced-*' -mtime +1 -delete 2>/dev/null || true
|
|
283
|
+
fi
|
|
284
|
+
fi
|
|
285
|
+
if [[ -n "$_SESSION_ID" ]]; then
|
|
286
|
+
_PRETEXT="${_PRETEXT//"{{session}}"/$_SESSION_ID}"
|
|
287
|
+
else
|
|
288
|
+
# Empty replacement: also swallow one adjacent ", " so "Hey {{session}}, ready"
|
|
289
|
+
# becomes "Hey ready", not "Hey , ready"; then drop any bare token.
|
|
290
|
+
_PRETEXT="${_PRETEXT//"{{session}}, "/}"
|
|
291
|
+
_PRETEXT="${_PRETEXT//", {{session}}"/}"
|
|
292
|
+
_PRETEXT="${_PRETEXT//"{{session}}"/}"
|
|
293
|
+
fi
|
|
294
|
+
# Final tidy: collapse doubled/edge commas and runs of whitespace.
|
|
295
|
+
_PRETEXT="$(printf '%s' "$_PRETEXT" | sed -E 's/,[[:space:]]*,/,/g; s/^[[:space:]]*,[[:space:]]*//; s/[[:space:]]*,[[:space:]]*$//; s/[[:space:]]{2,}/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
|
|
296
|
+
fi
|
|
244
297
|
if [[ -n "$_PRETEXT" ]]; then
|
|
245
298
|
TEXT="${_PRETEXT}, ${TEXT}"
|
|
246
299
|
fi
|
|
@@ -332,7 +385,7 @@ case "$ACTIVE_PROVIDER" in
|
|
|
332
385
|
if [[ "$VOICE_OVERRIDE" =~ ^(af|am|bf|bm|jf|jm|kf|km|zf|zm|ff|fm|hf|hm|if|im|pf|pm|ef|em|nf|nm)_[a-zA-Z0-9]+$ ]]; then
|
|
333
386
|
# Kokoro-shaped voice override (e.g. af_heart) must win over the LLM row's
|
|
334
387
|
# engine column, else it is synthesized as a Piper voice, 404s downloading
|
|
335
|
-
# a model that doesn't exist, and stays silent.
|
|
388
|
+
# a model that doesn't exist, and stays silent.
|
|
336
389
|
ACTIVE_PROVIDER="kokoro"
|
|
337
390
|
elif [[ -n "$_LLM_ENGINE" ]]; then
|
|
338
391
|
ACTIVE_PROVIDER="$_LLM_ENGINE"
|
|
@@ -356,8 +409,8 @@ fi
|
|
|
356
409
|
_TRANSPORT_CFG="$HOME/.agentvibes/transport-config.json"
|
|
357
410
|
if [[ "$ACTIVE_PROVIDER" != "ssh-remote" && "$ACTIVE_PROVIDER" != "agentvibes-receiver" && "$ACTIVE_PROVIDER" != "termux-ssh" ]] \
|
|
358
411
|
&& [[ -n "$LLM_PROVIDER" && "$LLM_PROVIDER" != "default" ]] \
|
|
359
|
-
&& [[ -f "$_TRANSPORT_CFG" ]] &&
|
|
360
|
-
_LLM_SSH_MODE=$(AGENTVIBES_CFG="$_TRANSPORT_CFG" AGENTVIBES_KEY="$LLM_PROVIDER"
|
|
412
|
+
&& [[ -f "$_TRANSPORT_CFG" ]] && [[ -n "$PYTHON_BIN" ]]; then
|
|
413
|
+
_LLM_SSH_MODE=$(AGENTVIBES_CFG="$_TRANSPORT_CFG" AGENTVIBES_KEY="$LLM_PROVIDER" "$PYTHON_BIN" - <<'PYEOF'
|
|
361
414
|
import json, os, sys
|
|
362
415
|
try:
|
|
363
416
|
d = json.load(open(os.environ['AGENTVIBES_CFG'], encoding='utf-8'))
|
|
@@ -368,7 +421,7 @@ PYEOF
|
|
|
368
421
|
)
|
|
369
422
|
if [[ "$_LLM_SSH_MODE" == "remote" ]]; then
|
|
370
423
|
# Redirect this LLM's audio through ssh-remote using its own SSH config
|
|
371
|
-
_llm_remote_data=$(AGENTVIBES_CFG="$_TRANSPORT_CFG" AGENTVIBES_KEY="$LLM_PROVIDER"
|
|
424
|
+
_llm_remote_data=$(AGENTVIBES_CFG="$_TRANSPORT_CFG" AGENTVIBES_KEY="$LLM_PROVIDER" "$PYTHON_BIN" - <<'PYEOF'
|
|
372
425
|
import json, os, sys
|
|
373
426
|
try:
|
|
374
427
|
d = json.load(open(os.environ['AGENTVIBES_CFG'], encoding='utf-8'))
|
|
@@ -536,50 +589,8 @@ speak_text() {
|
|
|
536
589
|
esac
|
|
537
590
|
}
|
|
538
591
|
|
|
539
|
-
# Note:
|
|
540
|
-
#
|
|
541
|
-
|
|
542
|
-
# @function handle_learning_mode
|
|
543
|
-
# @intent Speak in both main language and target language for learning
|
|
544
|
-
# @why Issue #51 - Auto-translate and speak twice for immersive language learning
|
|
545
|
-
# @returns 0 if learning mode handled, 1 if not in learning mode
|
|
546
|
-
handle_learning_mode() {
|
|
547
|
-
# Source learn-manager for learning mode functions
|
|
548
|
-
source "$SCRIPT_DIR/learn-manager.sh" 2>/dev/null || return 1
|
|
549
|
-
|
|
550
|
-
# Check if learning mode is enabled
|
|
551
|
-
if ! is_learn_mode_enabled 2>/dev/null; then
|
|
552
|
-
return 1
|
|
553
|
-
fi
|
|
554
|
-
|
|
555
|
-
local target_lang
|
|
556
|
-
target_lang=$(get_target_language 2>/dev/null || echo "")
|
|
557
|
-
local target_voice
|
|
558
|
-
target_voice=$(get_target_voice 2>/dev/null || echo "")
|
|
559
|
-
|
|
560
|
-
# Need both target language and voice for learning mode
|
|
561
|
-
if [[ -z "$target_lang" ]] || [[ -z "$target_voice" ]]; then
|
|
562
|
-
return 1
|
|
563
|
-
fi
|
|
564
|
-
|
|
565
|
-
# 1. Speak in main language (current voice)
|
|
566
|
-
speak_text "$TEXT" "$VOICE_OVERRIDE" "$ACTIVE_PROVIDER"
|
|
567
|
-
|
|
568
|
-
# 2. Auto-translate to target language
|
|
569
|
-
local translated
|
|
570
|
-
# SECURITY: Add timeout to prevent hanging (#134)
|
|
571
|
-
translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$target_lang" 2>/dev/null) || translated="$TEXT"
|
|
572
|
-
|
|
573
|
-
# Small pause between languages
|
|
574
|
-
sleep 0.5
|
|
575
|
-
|
|
576
|
-
# 3. Speak translated text with target voice
|
|
577
|
-
local target_provider
|
|
578
|
-
target_provider=$(detect_voice_provider "$target_voice")
|
|
579
|
-
speak_text "$translated" "$target_voice" "$target_provider"
|
|
580
|
-
|
|
581
|
-
return 0
|
|
582
|
-
}
|
|
592
|
+
# Note: translate-manager.sh is sourced inside its handler function to avoid
|
|
593
|
+
# triggering its main handler
|
|
583
594
|
|
|
584
595
|
# @function handle_translation_mode
|
|
585
596
|
# @intent Translate and speak in target language (non-learning mode)
|
|
@@ -604,7 +615,7 @@ handle_translation_mode() {
|
|
|
604
615
|
# Translate text
|
|
605
616
|
local translated
|
|
606
617
|
# SECURITY: Add timeout to prevent hanging (#134)
|
|
607
|
-
translated=$(timeout 5 python3 "$SCRIPT_DIR/translator.py" "$TEXT" "$translate_to" 2>/dev/null) || translated="$TEXT"
|
|
618
|
+
translated=$(timeout 5 "${PYTHON_BIN:-python3}" "$SCRIPT_DIR/translator.py" "$TEXT" "$translate_to" 2>/dev/null) || translated="$TEXT"
|
|
608
619
|
|
|
609
620
|
# Get voice for target language if no override specified
|
|
610
621
|
local voice_to_use="$VOICE_OVERRIDE"
|
|
@@ -625,14 +636,8 @@ handle_translation_mode() {
|
|
|
625
636
|
}
|
|
626
637
|
|
|
627
638
|
# Mode priority:
|
|
628
|
-
# 1.
|
|
629
|
-
# 2.
|
|
630
|
-
# 3. Normal mode (speaks as-is)
|
|
631
|
-
|
|
632
|
-
# Try learning mode first (Issue #51)
|
|
633
|
-
if handle_learning_mode; then
|
|
634
|
-
exit 0
|
|
635
|
-
fi
|
|
639
|
+
# 1. Translation mode (speaks translated only)
|
|
640
|
+
# 2. Normal mode (speaks as-is)
|
|
636
641
|
|
|
637
642
|
# Try translation mode (Issue #50)
|
|
638
643
|
if handle_translation_mode; then
|