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.
Files changed (160) hide show
  1. package/.agentvibes/config.json +15 -1
  2. package/.agentvibes/install-manifest.json +342 -0
  3. package/.claude/audio/ui/CREDITS.txt +16 -0
  4. package/.claude/audio/ui/bling-success.wav +0 -0
  5. package/.claude/commands/agent-vibes/receiver.md +64 -0
  6. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  7. package/.claude/commands/agent-vibes-rdp.md +24 -24
  8. package/.claude/config/audio-effects.cfg +5 -4
  9. package/.claude/config/audio-effects.cfg.sample +52 -52
  10. package/.claude/config/background-music-enabled.txt +1 -1
  11. package/.claude/config/background-music-position.txt +27 -0
  12. package/.claude/config/background-music-volume.txt +1 -0
  13. package/.claude/config/background-music.cfg +1 -0
  14. package/.claude/config/background-music.txt +1 -0
  15. package/.claude/config/language.txt +1 -0
  16. package/.claude/config/personality.txt +1 -0
  17. package/.claude/config/reverb-level.txt +1 -0
  18. package/.claude/config/tts-speech-rate.txt +1 -0
  19. package/.claude/config/tts-verbosity.txt +1 -0
  20. package/.claude/docs/TERMUX_SETUP.md +408 -408
  21. package/.claude/github-star-reminder.txt +1 -1
  22. package/.claude/hooks/audio-cache-utils.sh +0 -0
  23. package/.claude/hooks/audio-processor.sh +57 -18
  24. package/.claude/hooks/background-music-manager.sh +0 -0
  25. package/.claude/hooks/bmad-party-speak.sh +0 -0
  26. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  27. package/.claude/hooks/bmad-speak.sh +0 -0
  28. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  29. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  30. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  31. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  32. package/.claude/hooks/clean-audio-cache.sh +0 -0
  33. package/.claude/hooks/cleanup-cache.sh +0 -0
  34. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  35. package/.claude/hooks/download-extra-voices.sh +0 -0
  36. package/.claude/hooks/effects-manager.sh +0 -0
  37. package/.claude/hooks/github-star-reminder.sh +0 -0
  38. package/.claude/hooks/kokoro-installer.sh +117 -0
  39. package/.claude/hooks/kokoro-server.py +219 -0
  40. package/.claude/hooks/kokoro-tts.py +141 -0
  41. package/.claude/hooks/language-manager.sh +0 -0
  42. package/.claude/hooks/learn-manager.sh +0 -0
  43. package/.claude/hooks/macos-voice-manager.sh +0 -0
  44. package/.claude/hooks/migrate-background-music.sh +0 -0
  45. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  46. package/.claude/hooks/optimize-background-music.sh +0 -0
  47. package/.claude/hooks/path-resolver.sh +0 -0
  48. package/.claude/hooks/personality-manager.sh +0 -0
  49. package/.claude/hooks/piper-download-voices.sh +39 -16
  50. package/.claude/hooks/piper-installer.sh +11 -6
  51. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  52. package/.claude/hooks/piper-voice-manager.sh +6 -6
  53. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +3 -1
  54. package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
  55. package/.claude/hooks/play-tts-elevenlabs.sh +360 -0
  56. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  57. package/.claude/hooks/play-tts-kokoro.sh +127 -0
  58. package/.claude/hooks/play-tts-macos.sh +0 -0
  59. package/.claude/hooks/play-tts-piper.sh +0 -0
  60. package/.claude/hooks/play-tts-soprano.sh +0 -0
  61. package/.claude/hooks/play-tts-ssh-remote.sh +9 -2
  62. package/.claude/hooks/play-tts-termux-ssh.sh +3 -1
  63. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  64. package/.claude/hooks/play-tts.sh +78 -2
  65. package/.claude/hooks/prepare-release.sh +0 -0
  66. package/.claude/hooks/provider-commands.sh +71 -22
  67. package/.claude/hooks/provider-manager.sh +15 -7
  68. package/.claude/hooks/replay-target-audio.sh +0 -0
  69. package/.claude/hooks/requirements.txt +6 -6
  70. package/.claude/hooks/sentiment-manager.sh +0 -0
  71. package/.claude/hooks/session-start-tts.sh +0 -0
  72. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  73. package/.claude/hooks/speed-manager.sh +0 -0
  74. package/.claude/hooks/stop-tts.sh +0 -0
  75. package/.claude/hooks/termux-installer.sh +0 -0
  76. package/.claude/hooks/translate-manager.sh +0 -0
  77. package/.claude/hooks/translator.py +237 -237
  78. package/.claude/hooks/tts-queue-worker.sh +0 -0
  79. package/.claude/hooks/tts-queue.sh +0 -0
  80. package/.claude/hooks/verbosity-manager.sh +0 -0
  81. package/.claude/hooks/voice-manager.sh +0 -0
  82. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  83. package/.claude/hooks-windows/kokoro-server.py +219 -0
  84. package/.claude/hooks-windows/kokoro-tts.py +107 -0
  85. package/.claude/hooks-windows/play-tts-kokoro.ps1 +191 -0
  86. package/.claude/hooks-windows/play-tts.ps1 +29 -1
  87. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  88. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  89. package/.claude/piper-voices-dir.txt +1 -0
  90. package/.claude/verbosity.txt +1 -1
  91. package/.clawdbot/README.md +105 -105
  92. package/.mcp.json +6 -19
  93. package/README.md +127 -2116
  94. package/RELEASE_NOTES.md +73 -0
  95. package/WINDOWS-SETUP.md +208 -208
  96. package/bin/agent-vibes +39 -39
  97. package/bin/agentvibes-voice-browser.js +0 -0
  98. package/bin/agentvibes.js +0 -0
  99. package/bin/mcp-server.js +121 -121
  100. package/bin/mcp-server.sh +0 -0
  101. package/bin/test-bmad-pr +78 -78
  102. package/mcp-server/QUICK_START.md +203 -203
  103. package/mcp-server/README.md +345 -345
  104. package/mcp-server/WINDOWS_SETUP.md +0 -0
  105. package/mcp-server/examples/claude_desktop_config.json +11 -11
  106. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  107. package/mcp-server/examples/custom_instructions.md +169 -169
  108. package/mcp-server/install-deps.js +0 -0
  109. package/mcp-server/server.py +1807 -1807
  110. package/mcp-server/test_server.py +0 -0
  111. package/package.json +4 -3
  112. package/src/cli/list-personalities.js +110 -110
  113. package/src/cli/list-voices.js +114 -114
  114. package/src/commands/bmad-voices.js +394 -394
  115. package/src/commands/install-mcp.js +3 -3
  116. package/src/console/app.js +25 -9
  117. package/src/console/audio-env.js +85 -1
  118. package/src/console/brand-colors.js +13 -13
  119. package/src/console/constants/personalities.js +44 -44
  120. package/src/console/navigation.js +4 -0
  121. package/src/console/tabs/agents-tab.js +12 -6
  122. package/src/console/tabs/help-tab.js +314 -314
  123. package/src/console/tabs/music-tab.js +7 -25
  124. package/src/console/tabs/readme-tab.js +272 -272
  125. package/src/console/tabs/setup-tab.js +1704 -120
  126. package/src/console/tabs/voices-tab.js +72 -88
  127. package/src/console/widgets/destroy-list.js +25 -25
  128. package/src/console/widgets/format-utils.js +14 -2
  129. package/src/console/widgets/help-bar.js +55 -0
  130. package/src/console/widgets/notice.js +55 -55
  131. package/src/console/widgets/reverb-picker.js +429 -41
  132. package/src/console/widgets/track-picker.js +60 -51
  133. package/src/i18n/de.js +2 -1
  134. package/src/i18n/en.js +1 -0
  135. package/src/i18n/es.js +2 -1
  136. package/src/i18n/fr.js +2 -1
  137. package/src/i18n/hi.js +2 -1
  138. package/src/i18n/ja.js +2 -1
  139. package/src/i18n/ko.js +2 -1
  140. package/src/i18n/pt.js +2 -1
  141. package/src/i18n/strings.js +9 -9
  142. package/src/i18n/zh-CN.js +2 -1
  143. package/src/installer/language-screen.js +31 -31
  144. package/src/installer/music-file-input.js +304 -304
  145. package/src/installer.js +95 -7
  146. package/src/services/config-service.js +264 -264
  147. package/src/services/language-service.js +47 -47
  148. package/src/services/provider-service.js +35 -0
  149. package/src/services/provider-voice-catalog.js +126 -0
  150. package/src/services/tts-engine-service.js +51 -2
  151. package/src/utils/audio-duration-validator.js +53 -10
  152. package/src/utils/audio-format-validator.js +277 -277
  153. package/src/utils/dependency-checker.js +469 -469
  154. package/src/utils/file-ownership-verifier.js +358 -358
  155. package/src/utils/music-file-validator.js +285 -285
  156. package/src/utils/preview-list-prompt.js +144 -136
  157. package/src/utils/provider-validator.js +70 -0
  158. package/src/utils/secure-music-storage.js +412 -412
  159. package/templates/agentvibes-receiver.sh +235 -231
  160. 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
- if [[ "$1" == "--yes" ]] || [[ "$1" == "-y" ]]; then
52
- AUTO_YES=true
53
- fi
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
- "en_US-ryan-high" # Default, expressive male (30MB) - BMAD: John (pm)
58
- "en_US-amy-medium" # Warm female (13MB) - BMAD: Mary (analyst)
59
- "en_US-hfc_female-medium" # Professional female (13MB) - BMAD: Amelia (dev)
60
- "en_US-lessac-medium" # Clear female (13MB) - BMAD: Murat (tea) - NOTE: Female voice
61
- "en_US-danny-low" # Calm male (13MB) - BMAD: Winston (architect)
62
- "en_US-bryce-medium" # Professional male (13MB) - BMAD: Bob (sm)
63
- "en_US-kathleen-low" # Clear female (13MB) - BMAD: Paige (tech-writer)
64
- "en_US-kusal-medium" # Male voice (13MB) - BMAD: Saif (frame-expert)
65
- "en_US-kristin-medium" # Female voice (13MB) - BMAD: Sally (ux-designer)
66
- "en_US-libritts_r-medium" # Premium male (57MB) - BMAD: BMad Master (NOTE: Changed from -high to -medium, -high doesn't exist)
67
- "en_US-libritts-high" # Premium quality (57MB)
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
- [[ -n "$SSH_PORT" ]] && SSH_ARGS+=(-p "$SSH_PORT")
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