agentvibes 5.10.1 → 5.11.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/config.json +15 -1
- package/.agentvibes/install-manifest.json +342 -0
- package/.claude/audio/ui/CREDITS.txt +16 -0
- package/.claude/audio/ui/bling-success.wav +0 -0
- package/.claude/commands/agent-vibes/receiver.md +64 -0
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/audio-effects.cfg +5 -4
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-enabled.txt +1 -1
- package/.claude/config/background-music-position.txt +27 -0
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/background-music.cfg +1 -0
- package/.claude/config/background-music.txt +1 -0
- package/.claude/config/language.txt +1 -0
- package/.claude/config/personality.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +1 -0
- package/.claude/config/tts-verbosity.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/audio-cache-utils.sh +0 -0
- package/.claude/hooks/audio-processor.sh +57 -18
- package/.claude/hooks/background-music-manager.sh +0 -0
- package/.claude/hooks/bmad-party-speak.sh +0 -0
- package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
- package/.claude/hooks/bmad-speak.sh +0 -0
- package/.claude/hooks/bmad-tts-injector.sh +0 -0
- package/.claude/hooks/bmad-voice-manager.sh +0 -0
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
- package/.claude/hooks/clawdbot-receiver.sh +0 -0
- package/.claude/hooks/clean-audio-cache.sh +0 -0
- package/.claude/hooks/cleanup-cache.sh +0 -0
- package/.claude/hooks/configure-rdp-mode.sh +0 -0
- package/.claude/hooks/download-extra-voices.sh +0 -0
- package/.claude/hooks/effects-manager.sh +0 -0
- package/.claude/hooks/github-star-reminder.sh +0 -0
- package/.claude/hooks/kokoro-installer.sh +117 -0
- package/.claude/hooks/kokoro-server.py +219 -0
- package/.claude/hooks/kokoro-tts.py +141 -0
- package/.claude/hooks/language-manager.sh +0 -0
- package/.claude/hooks/learn-manager.sh +0 -0
- package/.claude/hooks/macos-voice-manager.sh +0 -0
- package/.claude/hooks/migrate-background-music.sh +0 -0
- package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
- package/.claude/hooks/optimize-background-music.sh +0 -0
- package/.claude/hooks/path-resolver.sh +0 -0
- package/.claude/hooks/personality-manager.sh +0 -0
- package/.claude/hooks/piper-download-voices.sh +39 -16
- package/.claude/hooks/piper-installer.sh +11 -6
- package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
- package/.claude/hooks/piper-voice-manager.sh +6 -6
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
- package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
- package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
- package/.claude/hooks/play-tts-enhanced.sh +0 -0
- package/.claude/hooks/play-tts-kokoro.sh +127 -0
- package/.claude/hooks/play-tts-macos.sh +0 -0
- package/.claude/hooks/play-tts-piper.sh +0 -0
- package/.claude/hooks/play-tts-soprano.sh +0 -0
- package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
- package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
- package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
- package/.claude/hooks/play-tts.sh +78 -2
- package/.claude/hooks/prepare-release.sh +0 -0
- package/.claude/hooks/provider-commands.sh +71 -22
- package/.claude/hooks/provider-manager.sh +15 -7
- package/.claude/hooks/replay-target-audio.sh +0 -0
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +0 -0
- package/.claude/hooks/session-start-tts.sh +0 -0
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +0 -0
- package/.claude/hooks/stop-tts.sh +0 -0
- package/.claude/hooks/termux-installer.sh +0 -0
- package/.claude/hooks/translate-manager.sh +0 -0
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +0 -0
- package/.claude/hooks/tts-queue.sh +0 -0
- package/.claude/hooks/verbosity-manager.sh +0 -0
- package/.claude/hooks/voice-manager.sh +0 -0
- package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
- package/.claude/hooks-windows/kokoro-server.py +219 -0
- package/.claude/hooks-windows/kokoro-tts.py +107 -0
- package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
- package/.claude/hooks-windows/play-tts.ps1 +29 -1
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
- package/.claude/piper-voices-dir.txt +1 -0
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.mcp.json +6 -19
- package/README.md +127 -2116
- package/RELEASE_NOTES.md +73 -0
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +0 -0
- package/bin/agentvibes.js +0 -0
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +0 -0
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +0 -0
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +0 -0
- package/mcp-server/server.py +1807 -1807
- package/mcp-server/test_server.py +0 -0
- package/package.json +4 -3
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +3 -3
- package/src/console/app.js +25 -9
- package/src/console/audio-env.js +85 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/navigation.js +4 -0
- package/src/console/tabs/agents-tab.js +12 -6
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/music-tab.js +7 -25
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/tabs/setup-tab.js +1704 -120
- package/src/console/tabs/voices-tab.js +72 -88
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +14 -2
- package/src/console/widgets/help-bar.js +55 -0
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/reverb-picker.js +429 -41
- package/src/console/widgets/track-picker.js +60 -51
- package/src/i18n/de.js +2 -1
- package/src/i18n/en.js +1 -0
- package/src/i18n/es.js +2 -1
- package/src/i18n/fr.js +2 -1
- package/src/i18n/hi.js +2 -1
- package/src/i18n/ja.js +2 -1
- package/src/i18n/ko.js +2 -1
- package/src/i18n/pt.js +2 -1
- package/src/i18n/strings.js +9 -9
- package/src/i18n/zh-CN.js +2 -1
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +95 -7
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/provider-service.js +35 -0
- package/src/services/provider-voice-catalog.js +126 -0
- package/src/services/tts-engine-service.js +51 -2
- package/src/utils/audio-duration-validator.js +53 -10
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +144 -136
- package/src/utils/provider-validator.js +70 -0
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +235 -231
- package/templates/audio/welcome-music.mp3 +0 -0
|
@@ -48,25 +48,48 @@ source "$SCRIPT_DIR/piper-voice-manager.sh"
|
|
|
48
48
|
|
|
49
49
|
# Parse command line arguments
|
|
50
50
|
AUTO_YES=false
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
INCLUDE_LIBRITTS=false
|
|
52
|
+
for arg in "$@"; do
|
|
53
|
+
case "$arg" in
|
|
54
|
+
--yes|-y) AUTO_YES=true ;;
|
|
55
|
+
--libritts) INCLUDE_LIBRITTS=true ;;
|
|
56
|
+
esac
|
|
57
|
+
done
|
|
54
58
|
|
|
55
59
|
# Common voice models to download
|
|
60
|
+
# Includes all models required by voice-assignments.json (voices-tab curated voices)
|
|
61
|
+
# plus BMAD agent voices.
|
|
56
62
|
COMMON_VOICES=(
|
|
57
|
-
|
|
58
|
-
"en_US-
|
|
59
|
-
"en_US-
|
|
60
|
-
"
|
|
61
|
-
"en_US-
|
|
62
|
-
"en_US-
|
|
63
|
-
"en_US-
|
|
64
|
-
"en_US-
|
|
65
|
-
"en_US-
|
|
66
|
-
"en_US-
|
|
67
|
-
"
|
|
63
|
+
# --- Voices-tab curated voices (voice-assignments.json: curated_voices) ---
|
|
64
|
+
"en_US-ryan-high" # Ryan — expressive male (30MB)
|
|
65
|
+
"en_US-joe-medium" # Joe — natural male (13MB)
|
|
66
|
+
"en_GB-alan-medium" # Alan — British male (13MB)
|
|
67
|
+
"en_US-kusal-medium" # Marcus — male (13MB)
|
|
68
|
+
"en_US-john-medium" # John — male (13MB)
|
|
69
|
+
"en_US-lessac-high" # Katherine — clear female high-quality (30MB)
|
|
70
|
+
"en_US-ljspeech-high" # Linda — female high-quality (30MB)
|
|
71
|
+
"en_US-amy-medium" # Amy — warm female (13MB)
|
|
72
|
+
"en_US-kristin-medium" # Kristin — female (13MB)
|
|
73
|
+
"en_GB-southern_english_female-low" # Charlotte — British female (13MB)
|
|
74
|
+
# --- Additional BMAD agent voices ---
|
|
75
|
+
"en_US-hfc_female-medium" # Professional female (13MB) - BMAD: Amelia (dev)
|
|
76
|
+
"en_US-lessac-medium" # Clear female (13MB) - BMAD: Murat (tea)
|
|
77
|
+
"en_US-danny-low" # Calm male (13MB) - BMAD: Winston (architect)
|
|
78
|
+
"en_US-bryce-medium" # Professional male (13MB) - BMAD: Bob (sm)
|
|
79
|
+
"en_US-kathleen-low" # Clear female (13MB) - BMAD: Paige (tech-writer)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# LibriTTS voices — 904 named speakers, opt-in due to size (~114MB total)
|
|
83
|
+
LIBRITTS_VOICES=(
|
|
84
|
+
"en_US-libritts-high" # 904 named speakers — premium quality (57MB)
|
|
85
|
+
"en_US-libritts_r-medium" # Premium male (57MB) - BMAD: BMad Master
|
|
68
86
|
)
|
|
69
87
|
|
|
88
|
+
# Merge libritts into the download list if requested
|
|
89
|
+
if [[ "$INCLUDE_LIBRITTS" == "true" ]]; then
|
|
90
|
+
COMMON_VOICES+=("${LIBRITTS_VOICES[@]}")
|
|
91
|
+
fi
|
|
92
|
+
|
|
70
93
|
echo "🎙️ Piper Voice Model Downloader"
|
|
71
94
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
72
95
|
echo ""
|
|
@@ -155,14 +178,14 @@ for voice in "${NEED_DOWNLOAD[@]}"; do
|
|
|
155
178
|
echo "📥 Downloading: $voice..."
|
|
156
179
|
|
|
157
180
|
if download_voice "$voice"; then
|
|
158
|
-
((DOWNLOADED
|
|
181
|
+
DOWNLOADED=$((DOWNLOADED + 1))
|
|
159
182
|
voice_path="$VOICE_DIR/${voice}.onnx"
|
|
160
183
|
file_size=$(du -h "$voice_path" 2>/dev/null | cut -f1)
|
|
161
184
|
echo " ✓ Downloaded: $voice"
|
|
162
185
|
echo " 📁 Path: $voice_path"
|
|
163
186
|
echo " 📦 Size: $file_size"
|
|
164
187
|
else
|
|
165
|
-
((FAILED
|
|
188
|
+
FAILED=$((FAILED + 1))
|
|
166
189
|
echo " ✗ Failed: $voice"
|
|
167
190
|
fi
|
|
168
191
|
done
|
|
@@ -178,9 +178,9 @@ else
|
|
|
178
178
|
|
|
179
179
|
# Try to install pipx
|
|
180
180
|
if command -v apt-get &> /dev/null; then
|
|
181
|
-
# Debian/Ubuntu
|
|
182
|
-
sudo apt-get update
|
|
183
|
-
sudo apt-get install -y pipx
|
|
181
|
+
# Debian/Ubuntu — DEBIAN_FRONTEND prevents tzdata interactive prompt
|
|
182
|
+
DEBIAN_FRONTEND=noninteractive sudo -E apt-get update -qq
|
|
183
|
+
DEBIAN_FRONTEND=noninteractive sudo -E apt-get install -y pipx
|
|
184
184
|
elif command -v brew &> /dev/null; then
|
|
185
185
|
# Linux with Homebrew
|
|
186
186
|
brew install pipx
|
|
@@ -198,16 +198,21 @@ else
|
|
|
198
198
|
exit 1
|
|
199
199
|
fi
|
|
200
200
|
|
|
201
|
-
# Ensure pipx is in PATH
|
|
202
|
-
pipx ensurepath
|
|
201
|
+
# Ensure pipx is in PATH (updates shell rc file) and update current session
|
|
202
|
+
pipx ensurepath 2>/dev/null || true
|
|
203
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
203
204
|
echo ""
|
|
204
205
|
fi
|
|
205
206
|
|
|
207
|
+
# Make sure ~/.local/bin is in PATH for this session (pipx installs there)
|
|
208
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
209
|
+
INSTALL_DIR="$HOME/.local/bin"
|
|
210
|
+
|
|
206
211
|
# Install Piper TTS
|
|
207
212
|
echo "📥 Installing Piper TTS via pipx..."
|
|
208
213
|
pipx install piper-tts
|
|
209
214
|
|
|
210
|
-
if ! command -v piper &> /dev/null; then
|
|
215
|
+
if ! command -v piper &> /dev/null && [[ ! -x "$HOME/.local/bin/piper" ]]; then
|
|
211
216
|
echo ""
|
|
212
217
|
echo "❌ Installation completed but piper command not found in PATH"
|
|
213
218
|
echo ""
|
|
File without changes
|
|
@@ -62,16 +62,16 @@ PIPER_VOICES_BASE_URL="https://huggingface.co/rhasspy/piper-voices/resolve/main"
|
|
|
62
62
|
# @calledby All voice management functions (verify_voice, get_voice_path, download_voice, list_downloaded_voices)
|
|
63
63
|
# @calls mkdir, cat, dirname
|
|
64
64
|
get_voice_storage_dir() {
|
|
65
|
-
local voice_dir
|
|
65
|
+
local voice_dir=""
|
|
66
66
|
|
|
67
67
|
# Check for custom path in environment or config file
|
|
68
|
-
if [[ -n "$PIPER_VOICES_DIR" ]]; then
|
|
69
|
-
voice_dir="$PIPER_VOICES_DIR"
|
|
68
|
+
if [[ -n "${PIPER_VOICES_DIR:-}" ]]; then
|
|
69
|
+
voice_dir="${PIPER_VOICES_DIR}"
|
|
70
70
|
else
|
|
71
71
|
# Check for config file (project-local first, then global)
|
|
72
|
-
local config_file
|
|
73
|
-
if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -f "$CLAUDE_PROJECT_DIR/.claude/piper-voices-dir.txt" ]]; then
|
|
74
|
-
config_file="$CLAUDE_PROJECT_DIR/.claude/piper-voices-dir.txt"
|
|
72
|
+
local config_file=""
|
|
73
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ -f "${CLAUDE_PROJECT_DIR}/.claude/piper-voices-dir.txt" ]]; then
|
|
74
|
+
config_file="${CLAUDE_PROJECT_DIR}/.claude/piper-voices-dir.txt"
|
|
75
75
|
else
|
|
76
76
|
# Search up directory tree for .claude/
|
|
77
77
|
local current_dir="$PWD"
|
|
@@ -98,7 +98,9 @@ echo "📱 Sending to $SSH_HOST for local playback..." >&2
|
|
|
98
98
|
# Build SSH args — use explicit key/port from config if available, else rely on ~/.ssh/config
|
|
99
99
|
SSH_ARGS=()
|
|
100
100
|
[[ -n "$SSH_KEY" && -f "$SSH_KEY" ]] && SSH_ARGS+=(-i "$SSH_KEY")
|
|
101
|
-
|
|
101
|
+
# Never force "-p 22" (ssh default) -- it would override an ~/.ssh/config
|
|
102
|
+
# Host alias port (e.g. laptop-win -> 45217). Empty/22 => defer to ssh config.
|
|
103
|
+
if [[ -n "$SSH_PORT" && "$SSH_PORT" != "22" ]]; then SSH_ARGS+=(-p "$SSH_PORT"); fi
|
|
102
104
|
|
|
103
105
|
# Try receiver scripts in order — single SSH call, no separate probe
|
|
104
106
|
# SECURITY: Base64-encoded values are safe to pass as arguments (no shell metacharacters)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
play-tts-agentvibes-receiver-for-voiceless-connections.sh
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# File: .claude/hooks/play-tts-elevenlabs.sh
|
|
4
|
+
#
|
|
5
|
+
# AgentVibes - Text-to-Speech WITH personality for AI Assistants
|
|
6
|
+
# Website: https://agentvibes.org
|
|
7
|
+
# Repository: https://github.com/paulpreibisch/AgentVibes
|
|
8
|
+
#
|
|
9
|
+
# Licensed under the Apache License, Version 2.0
|
|
10
|
+
#
|
|
11
|
+
# @fileoverview ElevenLabs TTS Provider — cloud neural voices with 32+ language support
|
|
12
|
+
# @context Provider-specific implementation for ElevenLabs API integration
|
|
13
|
+
# @architecture Implements provider contract: text/voice → audio playback
|
|
14
|
+
# @dependencies ELEVENLABS_API_KEY env var, curl, ffmpeg (optional), audio player
|
|
15
|
+
# @entrypoints Called by play-tts.sh router when provider=elevenlabs
|
|
16
|
+
# @related play-tts.sh, provider-manager.sh, language-manager.sh
|
|
17
|
+
#
|
|
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 via Infisical.
|
|
20
|
+
# Default voice: Rachel (English, warm female)
|
|
21
|
+
#
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
export LC_ALL=C
|
|
25
|
+
|
|
26
|
+
TEXT="${1:-}"
|
|
27
|
+
VOICE_OVERRIDE="${2:-}"
|
|
28
|
+
|
|
29
|
+
# Resolve script dir (handles symlinks)
|
|
30
|
+
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
31
|
+
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
32
|
+
|
|
33
|
+
source "$SCRIPT_DIR/audio-cache-utils.sh"
|
|
34
|
+
source "$SCRIPT_DIR/language-manager.sh"
|
|
35
|
+
|
|
36
|
+
if [[ -z "$TEXT" ]]; then
|
|
37
|
+
echo "Usage: $0 \"text to speak\" [voice_name_or_id]" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Clamp text to 2000 chars (ElevenLabs API limit)
|
|
42
|
+
if [[ ${#TEXT} -gt 2000 ]]; then
|
|
43
|
+
TEXT="${TEXT:0:1997}..."
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Optional: fetch the key from Infisical on demand, so no key is stored on disk.
|
|
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.
|
|
99
|
+
# A malformed/truncated ELEVENLABS_API_KEY (e.g. a half-pasted key) is ignored so
|
|
100
|
+
# it can't silently shadow a good key file — a common foot-gun that yields
|
|
101
|
+
# confusing "API key fail" errors despite a valid key on disk.
|
|
102
|
+
_looks_like_el_key() {
|
|
103
|
+
[[ "$1" =~ ^sk_[A-Za-z0-9]{40,}$ ]] && return 0 # current format: sk_ + >=40 chars
|
|
104
|
+
[[ "$1" =~ ^[A-Fa-f0-9]{32}$ ]] && return 0 # legacy 32-char hex key
|
|
105
|
+
return 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
API_KEY=""
|
|
109
|
+
if [[ -n "${ELEVENLABS_API_KEY:-}" ]]; then
|
|
110
|
+
# A non-empty env var is authoritative — ElevenLabs has shipped several key
|
|
111
|
+
# formats over time, so we must not silently discard a key just because it
|
|
112
|
+
# looks unfamiliar. Warn (using ONLY the length, never the value) if the format
|
|
113
|
+
# is unexpected, but use it regardless.
|
|
114
|
+
API_KEY="$ELEVENLABS_API_KEY"
|
|
115
|
+
if ! _looks_like_el_key "$ELEVENLABS_API_KEY"; then
|
|
116
|
+
echo "⚠️ ELEVENLABS_API_KEY env var has an unexpected format (length ${#ELEVENLABS_API_KEY}); using it anyway." >&2
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
if [[ -z "$API_KEY" ]]; then
|
|
120
|
+
_key_file="${HOME}/.agentvibes/elevenlabs-key.txt"
|
|
121
|
+
if [[ -f "$_key_file" ]]; then
|
|
122
|
+
API_KEY="$(tr -d '[:space:]' < "$_key_file")"
|
|
123
|
+
fi
|
|
124
|
+
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
|
+
|
|
130
|
+
if [[ -z "$API_KEY" ]]; then
|
|
131
|
+
echo "❌ ElevenLabs API key not set." >&2
|
|
132
|
+
echo " Set it one of three ways:" >&2
|
|
133
|
+
echo " 1. export ELEVENLABS_API_KEY=your_key (add to ~/.bashrc or ~/.zshrc)" >&2
|
|
134
|
+
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
|
+
echo " Get a free key at: https://elevenlabs.io" >&2
|
|
137
|
+
exit 2
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Inline voice name → ID map (covers common built-in ElevenLabs voices)
|
|
142
|
+
declare -A VOICE_IDS
|
|
143
|
+
VOICE_IDS["Rachel"]="21m00Tcm4TlvDq8ikWAM"
|
|
144
|
+
VOICE_IDS["Adam"]="pNInz6obpgDQGcFmaJgB"
|
|
145
|
+
VOICE_IDS["Antoni"]="ErXwobaYiN019PkySvjV"
|
|
146
|
+
VOICE_IDS["Arnold"]="VR6AewLTigWG4xSOukaG"
|
|
147
|
+
VOICE_IDS["Bella"]="EXAVITQu4vr4xnSDxMaL"
|
|
148
|
+
VOICE_IDS["Callum"]="N2lVS1w4EtoT3dr4eOWO"
|
|
149
|
+
VOICE_IDS["Charlie"]="IKne3meq5aSn9XLyUdCD"
|
|
150
|
+
VOICE_IDS["Charlotte"]="XB0fDUnXU5powFXDhCwa"
|
|
151
|
+
VOICE_IDS["Clyde"]="2EiwWnXFnvU5JabPnv8n"
|
|
152
|
+
VOICE_IDS["Daniel"]="onwK4e9ZLuTAKqWW03F9"
|
|
153
|
+
VOICE_IDS["Dave"]="CYw3kZ02Hs0563khs1Fj"
|
|
154
|
+
VOICE_IDS["Dorothy"]="ThT5KcBeYPX3keUQqHPh"
|
|
155
|
+
VOICE_IDS["Domi"]="AZnzlk1XvdvUeBnXmlld"
|
|
156
|
+
VOICE_IDS["Drew"]="29vD33N1CtxCmqQRPOHJ"
|
|
157
|
+
VOICE_IDS["Emily"]="LcfcDJNUP1GQjkzn1xUU"
|
|
158
|
+
VOICE_IDS["Ethan"]="g5CIjZEefAph4nQFvHAz"
|
|
159
|
+
VOICE_IDS["Fin"]="D38z5RcWu1voky8WS1ja"
|
|
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]}"
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Resolve voice ID from override or config
|
|
186
|
+
VOICE_ID=""
|
|
187
|
+
|
|
188
|
+
if [[ -n "$VOICE_OVERRIDE" ]]; then
|
|
189
|
+
if [[ -n "${VOICE_IDS[$VOICE_OVERRIDE]:-}" ]]; then
|
|
190
|
+
VOICE_ID="${VOICE_IDS[$VOICE_OVERRIDE]}"
|
|
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
|
|
196
|
+
VOICE_ID="$DEFAULT_VOICE_ID"
|
|
197
|
+
fi
|
|
198
|
+
else
|
|
199
|
+
# Check voice manager for configured voice
|
|
200
|
+
VOICE_NAME=""
|
|
201
|
+
if [[ -f "$SCRIPT_DIR/voice-manager.sh" ]]; then
|
|
202
|
+
VOICE_NAME="$("$SCRIPT_DIR/voice-manager.sh" get 2>/dev/null || true)"
|
|
203
|
+
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
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
# Language/model selection
|
|
214
|
+
CURRENT_LANGUAGE="$(get_language_code 2>/dev/null || echo 'english')"
|
|
215
|
+
|
|
216
|
+
case "$CURRENT_LANGUAGE" in
|
|
217
|
+
english) LANGUAGE_CODE="en"; MODEL_ID="eleven_turbo_v2_5" ;;
|
|
218
|
+
spanish) LANGUAGE_CODE="es"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
219
|
+
french) LANGUAGE_CODE="fr"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
220
|
+
german) LANGUAGE_CODE="de"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
221
|
+
italian) LANGUAGE_CODE="it"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
222
|
+
portuguese) LANGUAGE_CODE="pt"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
223
|
+
chinese) LANGUAGE_CODE="zh"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
224
|
+
japanese) LANGUAGE_CODE="ja"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
225
|
+
korean) LANGUAGE_CODE="ko"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
226
|
+
russian) LANGUAGE_CODE="ru"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
227
|
+
polish) LANGUAGE_CODE="pl"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
228
|
+
dutch) LANGUAGE_CODE="nl"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
229
|
+
arabic) LANGUAGE_CODE="ar"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
230
|
+
hindi) LANGUAGE_CODE="hi"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
231
|
+
turkish) LANGUAGE_CODE="tr"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
232
|
+
swedish) LANGUAGE_CODE="sv"; MODEL_ID="eleven_multilingual_v2" ;;
|
|
233
|
+
*) LANGUAGE_CODE="en"; MODEL_ID="eleven_turbo_v2_5" ;;
|
|
234
|
+
esac
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Audio output directory
|
|
238
|
+
AUDIO_DIR="$(get_audio_cache_dir 2>/dev/null || echo "${HOME}/.claude/audio")"
|
|
239
|
+
mkdir -p "$AUDIO_DIR"
|
|
240
|
+
chmod 700 "$AUDIO_DIR"
|
|
241
|
+
|
|
242
|
+
TEMP_FILE="${AUDIO_DIR}/tts-elevenlabs-$(date +%s%N | head -c 18).mp3"
|
|
243
|
+
BODY_FILE="${AUDIO_DIR}/tts-el-body-$(date +%s%N | head -c 18).json"
|
|
244
|
+
trap 'rm -f "${TEMP_FILE:-}" "${BODY_FILE:-}" 2>/dev/null || true' EXIT
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Build the JSON request body with python so all escaping is handled correctly
|
|
248
|
+
# (text is passed via env, never interpolated into a shell-quoted payload).
|
|
249
|
+
if ! TTS_TEXT="$TEXT" TTS_MODEL="$MODEL_ID" TTS_LANG="$LANGUAGE_CODE" python3 -c '
|
|
250
|
+
import os, json
|
|
251
|
+
print(json.dumps({
|
|
252
|
+
"text": os.environ["TTS_TEXT"],
|
|
253
|
+
"model_id": os.environ["TTS_MODEL"],
|
|
254
|
+
"language_code": os.environ["TTS_LANG"],
|
|
255
|
+
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75},
|
|
256
|
+
}))' > "$BODY_FILE" 2>/dev/null; then
|
|
257
|
+
echo "❌ Failed to build ElevenLabs request body (python3 required)" >&2
|
|
258
|
+
exit 3
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Test mode: never make a billable request to the ElevenLabs API (and never play
|
|
263
|
+
# audio). Short-circuit BEFORE the network call. Mirrors the sibling providers'
|
|
264
|
+
# AGENTVIBES_TEST_MODE handling; the EXIT trap cleans up the temp files.
|
|
265
|
+
if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
|
|
266
|
+
exit 0
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Call ElevenLabs API
|
|
271
|
+
# Security: API key passed via a curl config read from stdin (-K -), so it never
|
|
272
|
+
# appears in argv (where `ps` could expose it) nor in any temp file.
|
|
273
|
+
HTTP_STATUS=$(printf 'header = "xi-api-key: %s"\n' "$API_KEY" | curl -s -o "$TEMP_FILE" -w "%{http_code}" \
|
|
274
|
+
-X POST "https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}" \
|
|
275
|
+
-K - \
|
|
276
|
+
-H "Content-Type: application/json" \
|
|
277
|
+
--max-time 30 \
|
|
278
|
+
--data-binary "@${BODY_FILE}" 2>&1)
|
|
279
|
+
rm -f "$BODY_FILE" 2>/dev/null || true
|
|
280
|
+
|
|
281
|
+
if [[ "$HTTP_STATUS" != "200" ]]; then
|
|
282
|
+
echo "❌ ElevenLabs API error (HTTP $HTTP_STATUS)" >&2
|
|
283
|
+
if [[ -f "$TEMP_FILE" ]]; then
|
|
284
|
+
cat "$TEMP_FILE" >&2
|
|
285
|
+
fi
|
|
286
|
+
exit 3
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
if [[ ! -f "$TEMP_FILE" || ! -s "$TEMP_FILE" ]]; then
|
|
290
|
+
echo "❌ ElevenLabs returned empty audio" >&2
|
|
291
|
+
exit 3
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Convert MP3 → WAV so the post-processor (sox effects) can read it, and add a
|
|
296
|
+
# short silence lead-in (prevents a clipped first phoneme / WSL audio static).
|
|
297
|
+
# ffmpeg is preferred; if it is absent we keep the MP3 and skip effects/music.
|
|
298
|
+
if command -v ffmpeg &>/dev/null; then
|
|
299
|
+
_WAV="${AUDIO_DIR}/tts-el-$(date +%s%N | head -c 18).wav"
|
|
300
|
+
trap 'rm -f "${TEMP_FILE:-}" "${_WAV:-}" 2>/dev/null || true' EXIT
|
|
301
|
+
if ffmpeg -f lavfi -i "anullsrc=r=44100:cl=mono:d=0.15" -i "$TEMP_FILE" \
|
|
302
|
+
-filter_complex "[0:a][1:a]concat=n=2:v=0:a=1[out]" \
|
|
303
|
+
-map "[out]" -ar 44100 -ac 1 -y "$_WAV" 2>/dev/null \
|
|
304
|
+
&& [[ -s "$_WAV" ]]; then
|
|
305
|
+
rm -f "$TEMP_FILE"
|
|
306
|
+
TEMP_FILE="$_WAV"
|
|
307
|
+
fi
|
|
308
|
+
fi
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
# Post-process: apply audio effects (reverb/echo/chorus via sox) and mix
|
|
312
|
+
# background music (via ffmpeg) — exactly like the Piper provider does. Honors
|
|
313
|
+
# the per-LLM row (AGENTVIBES_LLM_KEY) and any per-call AGENTVIBES_REVERB_OVERRIDE.
|
|
314
|
+
# Fails soft: if audio-processor.sh or its tools (sox/ffmpeg) are missing, the
|
|
315
|
+
# unprocessed audio is played instead. NOTE: reverb/echo/chorus REQUIRE sox to be
|
|
316
|
+
# installed; without it, effects are silently skipped (background music still works
|
|
317
|
+
# via ffmpeg).
|
|
318
|
+
#
|
|
319
|
+
# Only post-process when invoked via the orchestrator (which exports
|
|
320
|
+
# AGENTVIBES_LLM_KEY) or with an explicit per-call effect override. A bare direct
|
|
321
|
+
# call — e.g. the voice-picker audition — gets the clean, dry voice so the user
|
|
322
|
+
# hears the voice itself, not the default LLM row's effects/music.
|
|
323
|
+
if [[ -f "$SCRIPT_DIR/audio-processor.sh" \
|
|
324
|
+
&& ( -n "${AGENTVIBES_LLM_KEY:-}" || -n "${AGENTVIBES_REVERB_OVERRIDE:-}" ) ]]; then
|
|
325
|
+
_PROCESSED="${AUDIO_DIR}/tts-el-proc-$(date +%s%N | head -c 18).wav"
|
|
326
|
+
trap 'rm -f "${TEMP_FILE:-}" "${_PROCESSED:-}" 2>/dev/null || true' EXIT
|
|
327
|
+
_AGENT_KEY="${AGENTVIBES_LLM_KEY:-default}"
|
|
328
|
+
if _PROC_OUT="$(bash "$SCRIPT_DIR/audio-processor.sh" "$TEMP_FILE" "$_AGENT_KEY" "$_PROCESSED" 2>/dev/null)"; then
|
|
329
|
+
_PF="${_PROC_OUT%%|*}"
|
|
330
|
+
if [[ -n "$_PF" && -f "$_PF" && -s "$_PF" && "$_PF" != "$TEMP_FILE" ]]; then
|
|
331
|
+
rm -f "$TEMP_FILE"
|
|
332
|
+
TEMP_FILE="$_PF"
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
fi
|
|
336
|
+
|
|
337
|
+
# Play audio (skip in test mode or no-playback mode — matches the Piper provider)
|
|
338
|
+
# AGENTVIBES_NO_PLAYBACK: Set to "true" to generate audio without playing (for post-processing)
|
|
339
|
+
# Native Windows (git-bash, OS=Windows_NT — not WSL) has none of the Linux
|
|
340
|
+
# players, and ffplay spawned from the TUI's Node process is unreliable. Use the
|
|
341
|
+
# built-in PowerShell WPF MediaPlayer (same as audio-env.js) — no ffmpeg/PATH
|
|
342
|
+
# dependency. Other platforms try the usual players.
|
|
343
|
+
if [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]] && [[ "${AGENTVIBES_NO_PLAYBACK:-false}" != "true" ]]; then
|
|
344
|
+
_PS_BIN="$(command -v powershell 2>/dev/null || command -v powershell.exe 2>/dev/null || true)"
|
|
345
|
+
if [[ "${OS:-}" == "Windows_NT" && -n "$_PS_BIN" ]]; then
|
|
346
|
+
_WINPATH="$(cygpath -w "$TEMP_FILE" 2>/dev/null || echo "$TEMP_FILE")"
|
|
347
|
+
_WINPATH="${_WINPATH//\'/\'\'}" # escape single quotes for the PowerShell literal
|
|
348
|
+
"$_PS_BIN" -NoProfile -Command "Add-Type -AssemblyName PresentationCore; \$p = New-Object System.Windows.Media.MediaPlayer; \$p.Open([uri]'${_WINPATH}'); \$p.Play(); Start-Sleep -Milliseconds 300; \$last=[TimeSpan]::Zero; \$stall=0; \$n=0; while (\$n -lt 100) { Start-Sleep -Milliseconds 100; \$pos=\$p.Position; if (\$p.NaturalDuration.HasTimeSpan -and \$pos -ge \$p.NaturalDuration.TimeSpan) { break }; if (\$pos -eq \$last) { \$stall++ } else { \$stall=0 }; if (\$stall -ge 8 -and \$pos -gt [TimeSpan]::Zero) { break }; \$last=\$pos; \$n++ }; \$p.Stop(); \$p.Close()" 2>/dev/null || true
|
|
349
|
+
else
|
|
350
|
+
(paplay "$TEMP_FILE" 2>/dev/null \
|
|
351
|
+
|| aplay "$TEMP_FILE" 2>/dev/null \
|
|
352
|
+
|| mpg123 -q "$TEMP_FILE" 2>/dev/null \
|
|
353
|
+
|| ffplay -nodisp -autoexit -loglevel error "$TEMP_FILE" 2>/dev/null \
|
|
354
|
+
|| true) &
|
|
355
|
+
wait $!
|
|
356
|
+
fi
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# Cancel trap so file persists in cache (for replay)
|
|
360
|
+
trap '' EXIT
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# File: .claude/hooks/play-tts-kokoro.sh
|
|
4
|
+
#
|
|
5
|
+
# AgentVibes - Text-to-Speech WITH personality for AI Assistants
|
|
6
|
+
# Website: https://agentvibes.org
|
|
7
|
+
# Repository: https://github.com/paulpreibisch/AgentVibes
|
|
8
|
+
#
|
|
9
|
+
# Licensed under the Apache License, Version 2.0
|
|
10
|
+
#
|
|
11
|
+
# @fileoverview Kokoro TTS Provider — 82M-param local neural TTS, 60+ voices, 8+ languages
|
|
12
|
+
# @context Provides high-quality local TTS via kokoro-onnx (MIT license, no API key needed)
|
|
13
|
+
# @architecture Implements provider contract: text/voice → audio playback
|
|
14
|
+
# @dependencies kokoro-onnx, soundfile, numpy (pip), ffmpeg (optional), audio player
|
|
15
|
+
# @entrypoints Called by play-tts.sh router when provider=kokoro
|
|
16
|
+
# @related play-tts.sh, kokoro-installer.sh, kokoro-tts.py
|
|
17
|
+
#
|
|
18
|
+
# Default voice: af_heart (American Female, warm)
|
|
19
|
+
# Install: /home/user/.claude/hooks/kokoro-installer.sh
|
|
20
|
+
# Or manually: pip install kokoro-onnx soundfile numpy
|
|
21
|
+
#
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
export LC_ALL=C
|
|
25
|
+
|
|
26
|
+
TEXT="${1:-}"
|
|
27
|
+
VOICE_OVERRIDE="${2:-}"
|
|
28
|
+
|
|
29
|
+
# Resolve script dir (handles symlinks)
|
|
30
|
+
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
|
31
|
+
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
32
|
+
|
|
33
|
+
source "$SCRIPT_DIR/audio-cache-utils.sh"
|
|
34
|
+
|
|
35
|
+
if [[ -z "$TEXT" ]]; then
|
|
36
|
+
echo "Usage: $0 \"text to speak\" [voice]" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Check kokoro is installed (use find_spec to avoid slow torch import)
|
|
41
|
+
if ! python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null; then
|
|
42
|
+
echo "❌ Kokoro TTS not installed." >&2
|
|
43
|
+
echo " Install with: ${SCRIPT_DIR}/kokoro-installer.sh" >&2
|
|
44
|
+
echo " Or manually: pip install kokoro-onnx soundfile numpy" >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Resolve voice
|
|
50
|
+
DEFAULT_VOICE="af_heart"
|
|
51
|
+
|
|
52
|
+
if [[ -n "$VOICE_OVERRIDE" ]]; then
|
|
53
|
+
# Accept kokoro-style voice IDs (2-letter prefix + underscore + name)
|
|
54
|
+
# e.g. af_heart, am_adam, bf_emma, bm_george
|
|
55
|
+
if [[ "$VOICE_OVERRIDE" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
|
|
56
|
+
VOICE="$VOICE_OVERRIDE"
|
|
57
|
+
else
|
|
58
|
+
echo "⚠️ Unrecognized kokoro voice '$VOICE_OVERRIDE', using $DEFAULT_VOICE" >&2
|
|
59
|
+
VOICE="$DEFAULT_VOICE"
|
|
60
|
+
fi
|
|
61
|
+
else
|
|
62
|
+
# Check voice manager
|
|
63
|
+
if [[ -f "$SCRIPT_DIR/voice-manager.sh" ]]; then
|
|
64
|
+
_CONFIGURED="$("$SCRIPT_DIR/voice-manager.sh" get 2>/dev/null || true)"
|
|
65
|
+
if [[ "$_CONFIGURED" =~ ^[a-z]{2}_[a-z0-9_]+$ ]]; then
|
|
66
|
+
VOICE="$_CONFIGURED"
|
|
67
|
+
else
|
|
68
|
+
VOICE="$DEFAULT_VOICE"
|
|
69
|
+
fi
|
|
70
|
+
else
|
|
71
|
+
VOICE="$DEFAULT_VOICE"
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Speed from config (optional, defaults to 1.0)
|
|
76
|
+
# SECURITY: Validate speed is numeric before passing to python (avoids traceback)
|
|
77
|
+
SPEED="${KOKORO_SPEED:-1.0}"
|
|
78
|
+
[[ "$SPEED" =~ ^[0-9]+(\.[0-9]+)?$ ]] || SPEED="1.0"
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Synthesis
|
|
82
|
+
AUDIO_DIR="$(get_audio_cache_dir 2>/dev/null || echo "${HOME}/.claude/audio")"
|
|
83
|
+
mkdir -p "$AUDIO_DIR"
|
|
84
|
+
chmod 700 "$AUDIO_DIR"
|
|
85
|
+
|
|
86
|
+
TEMP_WAV="${AUDIO_DIR}/tts-kokoro-$(date +%s%N | head -c 18).wav"
|
|
87
|
+
trap 'rm -f "${TEMP_WAV:-}" 2>/dev/null || true' EXIT
|
|
88
|
+
|
|
89
|
+
SYNTH_SCRIPT="${SCRIPT_DIR}/kokoro-tts.py"
|
|
90
|
+
if [[ ! -f "$SYNTH_SCRIPT" ]]; then
|
|
91
|
+
echo "❌ kokoro-tts.py not found at $SYNTH_SCRIPT" >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Skip the (slow) synthesis + playback entirely in test mode — nothing to play
|
|
96
|
+
# and no need to spin up the kokoro model. Mirrors the sibling Piper provider.
|
|
97
|
+
if [[ "${AGENTVIBES_TEST_MODE:-false}" == "true" ]]; then
|
|
98
|
+
trap '' EXIT
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Run synthesis — output path printed to stdout
|
|
103
|
+
RESULT=$(python3 "$SYNTH_SCRIPT" "$TEXT" "$VOICE" "$TEMP_WAV" "$SPEED" 2>&1) || {
|
|
104
|
+
echo "❌ Kokoro synthesis failed: $RESULT" >&2
|
|
105
|
+
exit 3
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if [[ ! -f "$TEMP_WAV" || ! -s "$TEMP_WAV" ]]; then
|
|
109
|
+
echo "❌ Kokoro synthesis produced no audio" >&2
|
|
110
|
+
exit 3
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Play audio — try players in order (WAV-capable)
|
|
115
|
+
# Skip playback in test mode or no-playback mode (matches the Piper provider).
|
|
116
|
+
# AGENTVIBES_NO_PLAYBACK: Set to "true" to generate audio without playing (for post-processing)
|
|
117
|
+
if [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]] && [[ "${AGENTVIBES_NO_PLAYBACK:-false}" != "true" ]]; then
|
|
118
|
+
(aplay -q "$TEMP_WAV" 2>/dev/null \
|
|
119
|
+
|| paplay "$TEMP_WAV" 2>/dev/null \
|
|
120
|
+
|| ffplay -nodisp -autoexit -loglevel quiet "$TEMP_WAV" 2>/dev/null \
|
|
121
|
+
|| mpg123 -q "$TEMP_WAV" 2>/dev/null \
|
|
122
|
+
|| true) &
|
|
123
|
+
wait $!
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Cancel trap so file persists in cache
|
|
127
|
+
trap '' EXIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|