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
@@ -1 +1 @@
1
- 20260525
1
+ 20260625
File without changes
@@ -60,6 +60,9 @@ fi
60
60
  # CLAUDE_PROJECT_DIR takes priority so TUI preview writes to the project dir and
61
61
  # audio-processor.sh (running from the package hooks dir) still finds the flag.
62
62
  is_background_music_enabled() {
63
+ # Suppress music entirely when a test run is in progress
64
+ [[ -f "${HOME}/.agentvibes-tests-running" ]] && return 1
65
+
63
66
  local enabled="" _f
64
67
  # 1. Project dir (set by TUI preview via CLAUDE_PROJECT_DIR env var)
65
68
  if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
@@ -103,6 +106,47 @@ if ! command -v sox &> /dev/null; then
103
106
  exit 0
104
107
  fi
105
108
 
109
+ # @function _resolve_one_effect
110
+ # @intent Translate a single preset name to its sox effect string
111
+ # @param $1 Preset name (e.g. 'light', 'echo-short', 'off', 'off-reverb')
112
+ # @returns Sox effect string (empty for off/unknown-off variants)
113
+ _resolve_one_effect() {
114
+ case "${1:-}" in
115
+ light) printf '%s' "reverb 20 50 50" ;;
116
+ medium) printf '%s' "reverb 40 50 70" ;;
117
+ heavy) printf '%s' "reverb 70 50 100" ;;
118
+ cathedral) printf '%s' "reverb 90 30 100" ;;
119
+ chorus-light) printf '%s' "chorus 0.7 0.9 55 0.4 0.25 2 -t" ;;
120
+ chorus-deep) printf '%s' "chorus 0.8 0.9 55 0.4 0.25 2 -t chorus 0.8 0.9 44 0.4 0.2 2.3 -t" ;;
121
+ echo-short) printf '%s' "echo 0.8 0.6 60 0.4" ;;
122
+ echo-long) printf '%s' "echo 0.8 0.7 100 0.5 200 0.3" ;;
123
+ warm) printf '%s' "bass 5 reverb 30 50 60" ;;
124
+ radio) printf '%s' "highpass 300 treble 5 gain -3 overdrive 10 gain -3" ;;
125
+ off*|'') ;; # empty output — no effects
126
+ *) printf '%s' "$1" ;; # raw sox string passthrough
127
+ esac
128
+ }
129
+
130
+ # @function _resolve_effect_value
131
+ # @intent Translate a preset value (single or '+'-combined) to sox effects string
132
+ # @param $1 Effect value, e.g. 'light', 'light+echo-short', 'off'
133
+ # @returns Full sox effects argument string (may be empty)
134
+ _resolve_effect_value() {
135
+ local _val="${1:-}"
136
+ if [[ "$_val" == *"+"* ]]; then
137
+ local _combined="" _part _resolved
138
+ IFS='+' read -ra _parts <<< "$_val"
139
+ for _part in "${_parts[@]}"; do
140
+ _part="${_part// /}"
141
+ _resolved=$(_resolve_one_effect "$_part")
142
+ [[ -n "$_resolved" ]] && _combined="${_combined:+$_combined }$_resolved"
143
+ done
144
+ printf '%s' "$_combined"
145
+ else
146
+ _resolve_one_effect "$_val"
147
+ fi
148
+ }
149
+
106
150
  # @function get_agent_config
107
151
  # @intent Parse audio-effects.cfg for agent-specific settings
108
152
  # @param $1 Agent name
@@ -404,28 +448,15 @@ main() {
404
448
  local _rest
405
449
  IFS='|' read -r _ sox_effects background_file bg_volume _rest <<< "$config"
406
450
 
407
- # Translate reverb preset names (stored by the console Configure UI) to sox effects strings.
408
- # LLM per-agent rows store human-readable names like "light"; sox needs the raw effect string.
409
- # Raw sox strings (used by BMAD agent rows) pass through unchanged.
410
- case "${sox_effects:-}" in
411
- light) sox_effects="reverb 20 50 50" ;;
412
- medium) sox_effects="reverb 40 50 70" ;;
413
- heavy) sox_effects="reverb 70 50 100" ;;
414
- cathedral) sox_effects="reverb 90 30 100" ;;
415
- off) sox_effects="" ;;
416
- esac
451
+ # Translate preset names (single or '+'-combined) to sox effects strings.
452
+ # LLM per-agent rows store human-readable names like 'light' or 'light+echo-short';
453
+ # sox needs the raw effect string. Raw sox strings (BMAD agent rows) pass through.
454
+ sox_effects=$(_resolve_effect_value "${sox_effects:-}")
417
455
 
418
456
  # Per-invocation reverb override (set by play-tts-enhanced.sh for profile-based reverb).
419
457
  # Using an env var avoids permanently mutating audio-effects.cfg — process-scoped and race-free.
420
458
  if [[ -n "${AGENTVIBES_REVERB_OVERRIDE:-}" ]]; then
421
- case "$AGENTVIBES_REVERB_OVERRIDE" in
422
- light) sox_effects="reverb 20 50 50" ;;
423
- medium) sox_effects="reverb 40 50 70" ;;
424
- heavy) sox_effects="reverb 70 50 100" ;;
425
- cathedral) sox_effects="reverb 90 30 100" ;;
426
- off) sox_effects="" ;;
427
- *) sox_effects="$AGENTVIBES_REVERB_OVERRIDE" ;; # raw sox string passthrough
428
- esac
459
+ sox_effects=$(_resolve_effect_value "$AGENTVIBES_REVERB_OVERRIDE")
429
460
  fi
430
461
 
431
462
  # Per-agent background music override from bmad-speak.sh profile JSON (takes priority over cfg).
@@ -447,6 +478,14 @@ main() {
447
478
  fi
448
479
  fi
449
480
 
481
+ # When audio is explicitly enabled during test runs (AGENTVIBES_TEST_AUDIO=true
482
+ # passed to scripts/run-tests.sh), use a calmer test track so spooky/goa-trance
483
+ # music doesn't play. run-tests.sh sets AGENTVIBES_TEST_TRACK=flamenco by default;
484
+ # the user may override it with any other track filename.
485
+ if [[ -n "${AGENTVIBES_TEST_TRACK:-}" ]]; then
486
+ background_file="$AGENTVIBES_TEST_TRACK"
487
+ fi
488
+
450
489
  # SECURITY: Use secure temp directory per CLAUDE.md guidelines
451
490
  # Prefer XDG_RUNTIME_DIR (user-owned, restricted permissions)
452
491
  # Fall back to user-specific directory in /tmp
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/kokoro-installer.sh
4
+ #
5
+ # AgentVibes - Kokoro TTS Installer
6
+ # Installs kokoro-onnx and dependencies for local neural TTS.
7
+ # Website: https://agentvibes.org
8
+ #
9
+ # Licensed under the Apache License, Version 2.0
10
+ #
11
+ # Usage:
12
+ # ./kokoro-installer.sh # interactive
13
+ # ./kokoro-installer.sh --check # check only, no install
14
+ # ./kokoro-installer.sh --non-interactive # install without prompts
15
+ #
16
+
17
+ set -euo pipefail
18
+ export LC_ALL=C
19
+
20
+ NON_INTERACTIVE=false
21
+ CHECK_ONLY=false
22
+
23
+ for arg in "$@"; do
24
+ case "$arg" in
25
+ --non-interactive|--yes|-y) NON_INTERACTIVE=true ;;
26
+ --check) CHECK_ONLY=true ;;
27
+ esac
28
+ done
29
+
30
+ REQUIRED_PACKAGES=("kokoro" "soundfile" "numpy")
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Check current installation state (use find_spec to avoid slow torch import)
34
+ check_kokoro() {
35
+ python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>/dev/null && \
36
+ python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('soundfile') else 1)" 2>/dev/null && \
37
+ python3 -c "import importlib.util; exit(0 if importlib.util.find_spec('numpy') else 1)" 2>/dev/null
38
+ }
39
+
40
+ if check_kokoro; then
41
+ echo "✅ Kokoro TTS is already installed"
42
+ if [[ "$CHECK_ONLY" == true ]]; then exit 0; fi
43
+ echo ""
44
+ echo "Voice examples:"
45
+ echo " af_heart af_nova af_sky (American Female)"
46
+ echo " am_adam am_michael (American Male)"
47
+ echo " bf_emma bf_isabella (British Female)"
48
+ echo " bm_george bm_lewis (British Male)"
49
+ exit 0
50
+ fi
51
+
52
+ if [[ "$CHECK_ONLY" == true ]]; then
53
+ echo "❌ Kokoro TTS is not installed"
54
+ exit 1
55
+ fi
56
+
57
+ # ---------------------------------------------------------------------------
58
+ echo "🎙️ Kokoro TTS Installer"
59
+ echo " High-quality local neural TTS — 60+ voices, 8+ languages"
60
+ echo " No API key needed. Runs 100% offline."
61
+ echo " Model: kokoro-onnx (~82MB download on first use)"
62
+ echo ""
63
+
64
+ if [[ "$NON_INTERACTIVE" != true ]]; then
65
+ read -p "Install Kokoro TTS? [Y/n]: " -r REPLY
66
+ REPLY="${REPLY:-y}"
67
+ if [[ ! "$REPLY" =~ ^[Yy] ]]; then
68
+ echo "Skipping Kokoro installation."
69
+ exit 0
70
+ fi
71
+ fi
72
+
73
+ # Check Python is available
74
+ if ! command -v python3 &>/dev/null; then
75
+ echo "❌ Python 3 is required but not found in PATH" >&2
76
+ echo " Install with: sudo apt install python3 python3-pip" >&2
77
+ exit 1
78
+ fi
79
+
80
+ echo ""
81
+ echo "Installing: ${REQUIRED_PACKAGES[*]}"
82
+ echo ""
83
+
84
+ # Try pip install with PEP 668 fallback to pipx/user install
85
+ _install_pkgs() {
86
+ local pkgs=("$@")
87
+ if pip install "${pkgs[@]}" 2>/dev/null; then return 0; fi
88
+ if pip install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
89
+ if pip3 install "${pkgs[@]}" 2>/dev/null; then return 0; fi
90
+ if pip3 install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
91
+ if python3 -m pip install "${pkgs[@]}" 2>/dev/null; then return 0; fi
92
+ if python3 -m pip install --user "${pkgs[@]}" 2>/dev/null; then return 0; fi
93
+ return 1
94
+ }
95
+
96
+ if _install_pkgs "${REQUIRED_PACKAGES[@]}"; then
97
+ echo ""
98
+ if check_kokoro; then
99
+ echo "✅ Kokoro TTS installed successfully!"
100
+ echo ""
101
+ echo "Voice examples:"
102
+ echo " af_heart af_nova af_sky (American Female)"
103
+ echo " am_adam am_michael (American Male)"
104
+ echo " bf_emma bf_isabella (British Female)"
105
+ echo " bm_george bm_lewis (British Male)"
106
+ echo ""
107
+ echo "Switch to Kokoro: /agent-vibes:provider switch kokoro"
108
+ else
109
+ echo "⚠️ Packages installed but import check failed." >&2
110
+ echo " Try: python3 -c 'import kokoro'" >&2
111
+ exit 1
112
+ fi
113
+ else
114
+ echo "❌ Installation failed." >&2
115
+ echo " Try manually: pip install kokoro-onnx soundfile numpy" >&2
116
+ exit 1
117
+ fi
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # File: .claude/hooks/kokoro-server.py
4
+ #
5
+ # AgentVibes - Kokoro TTS persistent daemon
6
+ # Loads the Kokoro model ONCE (on GPU when available) and serves synthesis over
7
+ # a localhost HTTP socket, so each request skips the ~10s model-load + CUDA-init
8
+ # cost that a fresh `python kokoro-tts.py` invocation pays every time.
9
+ #
10
+ # This is the difference between ~31s/message (cold process per call) and
11
+ # ~2-3s/message (warm resident model).
12
+ #
13
+ # Endpoints (bound to 127.0.0.1 only — never exposed off-host):
14
+ # GET /health -> 200 {"ok":true} once the model is loaded and warmed
15
+ # POST /synth -> body {text, voice, speed, output}; writes a WAV to `output`
16
+ # and returns {"ok":true,"path":output} or {"ok":false,"error":...}
17
+ #
18
+ # Usage: python kokoro-server.py [port] (default port 7855)
19
+ #
20
+ # Licensed under the Apache License, Version 2.0
21
+ #
22
+ import json
23
+ import os
24
+ import sys
25
+ import tempfile
26
+ import threading
27
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
28
+
29
+ # A bad argv[1] (non-numeric) must not crash the daemon before it can bind;
30
+ # fall back to the standard kokoro-server port.
31
+ try:
32
+ PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 7855
33
+ except (ValueError, IndexError):
34
+ PORT = 7855
35
+
36
+ # Language code from the voice prefix (af/am=American, bf/bm=British, etc.)
37
+ LANG_MAP = {
38
+ 'af': 'a', 'am': 'a', 'bf': 'b', 'bm': 'b', 'jf': 'j', 'jm': 'j',
39
+ 'kf': 'k', 'km': 'k', 'zf': 'z', 'zm': 'z', 'ff': 'f', 'fm': 'f',
40
+ 'hf': 'h', 'hm': 'h', 'if': 'i', 'im': 'i', 'pf': 'p', 'pm': 'p',
41
+ 'ef': 'e', 'em': 'e', 'nf': 'n', 'nm': 'n',
42
+ }
43
+
44
+ # Heavy imports up front so the model is resident before we accept connections.
45
+ import numpy as np
46
+ import soundfile as sf
47
+ from kokoro import KPipeline
48
+
49
+ _pipelines = {}
50
+ _lock = threading.Lock() # guards pipeline creation only
51
+ _infer_lock = threading.Lock() # serializes inference (torch is not thread-safe)
52
+
53
+
54
+ def get_pipeline(lang_code):
55
+ """Return a cached KPipeline for the language, creating it once on demand."""
56
+ with _lock:
57
+ pipe = _pipelines.get(lang_code)
58
+ if pipe is None:
59
+ pipe = KPipeline(lang_code=lang_code)
60
+ _pipelines[lang_code] = pipe
61
+ return pipe
62
+
63
+
64
+ def synth(text, voice, speed, output):
65
+ """Synthesize `text` with `voice` to the `output` WAV path. Returns the path."""
66
+ prefix = voice[:2].lower() if len(voice) >= 2 else 'af'
67
+ lang_code = LANG_MAP.get(prefix, 'a')
68
+ pipeline = get_pipeline(lang_code)
69
+
70
+ chunks = []
71
+ # The daemon shares one KPipeline across ThreadingHTTPServer worker threads
72
+ # and torch inference is not thread-safe — run one inference at a time. This
73
+ # lock is acquired after get_pipeline() has released _lock, so no deadlock.
74
+ with _infer_lock:
75
+ for result in pipeline(text, voice=voice, speed=speed):
76
+ audio = getattr(result, "audio", None)
77
+ if audio is None:
78
+ try:
79
+ audio = result[-1]
80
+ except (TypeError, IndexError, KeyError):
81
+ audio = None
82
+ if audio is None:
83
+ continue
84
+ if hasattr(audio, "detach"):
85
+ audio = audio.detach().cpu().numpy()
86
+ audio = np.asarray(audio).reshape(-1)
87
+ if audio.size > 0:
88
+ chunks.append(audio)
89
+
90
+ if not chunks:
91
+ raise RuntimeError("Kokoro returned no audio samples")
92
+
93
+ sf.write(output, np.concatenate(chunks), 24000) # kokoro is always 24 kHz
94
+ return output
95
+
96
+
97
+ def _allowed_output_dirs():
98
+ """Directories a /synth request is permitted to write a WAV into.
99
+
100
+ Mirrors the shell hooks' audio-cache resolution (project-local first, then
101
+ the global ~/.claude/audio) plus TEMP, which is where the daemon writes its
102
+ own transient/warmup files.
103
+ """
104
+ dirs = []
105
+ proj = os.environ.get("CLAUDE_PROJECT_DIR")
106
+ if proj:
107
+ dirs.append(os.path.join(proj, ".claude", "audio"))
108
+ home = os.environ.get("HOME") or os.path.expanduser("~")
109
+ dirs.append(os.path.join(home, ".claude", "audio"))
110
+ dirs.append(os.environ.get("TEMP", "/tmp"))
111
+ return dirs
112
+
113
+
114
+ def is_output_within_cache(output):
115
+ """True if `output` resolves inside an allowed audio dir (blocks traversal).
116
+
117
+ The daemon is shared across projects on a fixed port, so it cannot rely on
118
+ its own CLAUDE_PROJECT_DIR to know every project's audio dir. Accept the
119
+ home/TEMP locations plus any file that sits directly inside a ".claude/audio"
120
+ directory (the only place the providers ever write). realpath has already
121
+ collapsed any "..", so true traversal (e.g. /etc/x.wav) is still rejected.
122
+ """
123
+ tgt = os.path.realpath(output)
124
+ for base in _allowed_output_dirs():
125
+ base = os.path.realpath(base)
126
+ if tgt == base or tgt.startswith(base + os.sep):
127
+ return True
128
+ # Project-local audio dir for ANY project — the file must live directly in
129
+ # a ".claude/audio" directory.
130
+ parent = os.path.dirname(tgt).replace("\\", "/").lower()
131
+ if parent.endswith("/.claude/audio"):
132
+ return True
133
+ return False
134
+
135
+
136
+ class Handler(BaseHTTPRequestHandler):
137
+ # Silence default per-request stderr logging
138
+ def log_message(self, *args):
139
+ pass
140
+
141
+ def _send(self, code, obj):
142
+ data = json.dumps(obj).encode("utf-8")
143
+ self.send_response(code)
144
+ self.send_header("Content-Type", "application/json")
145
+ self.send_header("Content-Length", str(len(data)))
146
+ self.end_headers()
147
+ try:
148
+ self.wfile.write(data)
149
+ except OSError:
150
+ # Client closed the socket early (health probes do this) — WinError
151
+ # 10053/10054 etc. Nothing to send to; ignore.
152
+ pass
153
+
154
+ def do_GET(self):
155
+ if self.path == "/health":
156
+ self._send(200, {"ok": True})
157
+ else:
158
+ self._send(404, {"ok": False, "error": "not found"})
159
+
160
+ def do_POST(self):
161
+ if self.path != "/synth":
162
+ self._send(404, {"ok": False, "error": "not found"})
163
+ return
164
+ try:
165
+ length = int(self.headers.get("Content-Length", 0))
166
+ body = json.loads(self.rfile.read(length) or b"{}")
167
+ text = body["text"]
168
+ voice = body.get("voice", "af_heart")
169
+ speed = float(body.get("speed", 1.0))
170
+ output = body["output"]
171
+ # Output must be an absolute .wav path (the provider always supplies one)
172
+ if not isinstance(output, str) or not output.lower().endswith(".wav"):
173
+ raise ValueError("output must be a .wav path")
174
+ # Path-traversal guard: the resolved target must stay inside an
175
+ # allowed audio directory — reject before any file is written.
176
+ if not is_output_within_cache(output):
177
+ self._send(400, {"ok": False, "error": "output path outside allowed audio directory"})
178
+ return
179
+ synth(text, voice, speed, output)
180
+ self._send(200, {"ok": True, "path": output})
181
+ except Exception as e: # noqa: BLE001 - report any failure to the client
182
+ self._send(500, {"ok": False, "error": str(e)})
183
+
184
+
185
+ def main():
186
+ try:
187
+ import torch
188
+ device = "cuda" if torch.cuda.is_available() else "cpu"
189
+ except Exception:
190
+ device = "cpu"
191
+
192
+ # Bind first so a second accidental launch fails fast (address in use) and
193
+ # exits instead of loading a second copy of the model.
194
+ try:
195
+ server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
196
+ except OSError as e:
197
+ print(f"kokoro-server: port {PORT} unavailable ({e}); another instance is likely running", file=sys.stderr)
198
+ sys.exit(0)
199
+
200
+ # Warm the default English pipeline + trigger CUDA kernel compilation so the
201
+ # very first real /synth request is already fast.
202
+ warm_fd, warm_out = tempfile.mkstemp(suffix=".wav")
203
+ os.close(warm_fd) # synth/soundfile reopens by path; we only need the name
204
+ try:
205
+ synth("ready", "af_heart", 1.0, warm_out)
206
+ except Exception as e: # noqa: BLE001
207
+ print(f"kokoro-server: warmup failed (continuing): {e}", file=sys.stderr)
208
+ finally:
209
+ try:
210
+ os.remove(warm_out)
211
+ except OSError:
212
+ pass
213
+
214
+ print(f"kokoro-server ready on 127.0.0.1:{PORT} device={device}", flush=True)
215
+ server.serve_forever()
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AgentVibes — Kokoro TTS synthesizer
4
+ Synthesizes text to a WAV file using the kokoro-onnx package.
5
+ Prints the output WAV path to stdout.
6
+
7
+ Usage: kokoro-tts.py <text> <voice> <output_path> [speed]
8
+
9
+ Voices (examples):
10
+ af_heart af_nova af_sky af_bella af_sarah af_nicole
11
+ am_adam am_michael
12
+ bf_emma bf_isabella
13
+ bm_george bm_lewis
14
+
15
+ Install: pip install kokoro-onnx soundfile numpy
16
+ """
17
+
18
+ import sys
19
+ import os
20
+ import re
21
+
22
+ # Kokoro/torch auto-selects CUDA whenever a GPU is visible, but many setups
23
+ # (notably Windows) fail GPU allocation with "CUDA error: unknown error /
24
+ # Memory allocation failure", which crashes synthesis after a long torch load.
25
+ # The 82M-param Kokoro model runs fine on CPU in a few seconds, so force CPU by
26
+ # default. Opt back into GPU with AGENTVIBES_KOKORO_DEVICE=cuda. This MUST run
27
+ # before torch/kokoro are imported so torch.cuda.is_available() sees no device.
28
+ _KOKORO_DEVICE = os.environ.get('AGENTVIBES_KOKORO_DEVICE', 'cpu').strip().lower()
29
+ if _KOKORO_DEVICE != 'cuda':
30
+ os.environ['CUDA_VISIBLE_DEVICES'] = ''
31
+
32
+ def main():
33
+ # --download-only: fetch voice .pt file from HuggingFace without synthesis
34
+ if len(sys.argv) >= 2 and sys.argv[1] == '--download-only':
35
+ voice = sys.argv[2] if len(sys.argv) > 2 else 'af_heart'
36
+ # Validate before the id flows into a HuggingFace filename — blocks path
37
+ # traversal / arbitrary repo paths in voices/<voice>.pt.
38
+ if not re.match(r'^[a-z]{2}_[a-z0-9_]+$', voice):
39
+ print(f"❌ Invalid voice id: {voice}", file=sys.stderr)
40
+ sys.exit(4)
41
+ try:
42
+ from huggingface_hub import hf_hub_download
43
+ local = hf_hub_download(repo_id='hexgrad/Kokoro-82M', filename=f'voices/{voice}.pt')
44
+ print(local)
45
+ except Exception as e:
46
+ print(f"❌ Download failed: {e}", file=sys.stderr)
47
+ sys.exit(4)
48
+ sys.exit(0)
49
+
50
+ if len(sys.argv) < 4:
51
+ print("Usage: kokoro-tts.py <text> <voice> <output_path> [speed]", file=sys.stderr)
52
+ sys.exit(1)
53
+
54
+ text = sys.argv[1]
55
+ voice = sys.argv[2]
56
+ output_path = sys.argv[3]
57
+ try:
58
+ speed = float(sys.argv[4]) if len(sys.argv) > 4 else 1.0
59
+ except ValueError:
60
+ speed = 1.0
61
+
62
+ # Determine language code from voice prefix
63
+ # af/am = American English ('a'), bf/bm = British English ('b')
64
+ # jf/jm = Japanese ('j'), kf/km = Korean ('k'), etc.
65
+ prefix = voice[:2].lower() if len(voice) >= 2 else 'af'
66
+ lang_map = {
67
+ 'af': 'a', 'am': 'a', # American English
68
+ 'bf': 'b', 'bm': 'b', # British English
69
+ 'jf': 'j', 'jm': 'j', # Japanese
70
+ 'kf': 'k', 'km': 'k', # Korean
71
+ 'zf': 'z', 'zm': 'z', # Mandarin
72
+ 'ff': 'f', 'fm': 'f', # French
73
+ 'hf': 'h', 'hm': 'h', # Hindi
74
+ 'if': 'i', 'im': 'i', # Italian
75
+ 'pf': 'p', 'pm': 'p', # Brazilian Portuguese
76
+ 'ef': 'e', 'em': 'e', # Spanish (Spain)
77
+ 'nf': 'n', 'nm': 'n', # Spanish (Latin America)
78
+ }
79
+ lang_code = lang_map.get(prefix, 'a')
80
+
81
+ try:
82
+ from kokoro import KPipeline
83
+ except ImportError:
84
+ print("❌ kokoro-onnx not installed. Run: pip install kokoro-onnx soundfile numpy", file=sys.stderr)
85
+ sys.exit(2)
86
+
87
+ try:
88
+ import numpy as np
89
+ import soundfile as sf
90
+ except ImportError:
91
+ print("❌ soundfile/numpy not installed. Run: pip install soundfile numpy", file=sys.stderr)
92
+ sys.exit(2)
93
+
94
+ try:
95
+ # Pass device explicitly when supported; fall back for older kokoro
96
+ # versions whose KPipeline has no `device` kwarg (CUDA_VISIBLE_DEVICES
97
+ # already forces CPU in that case).
98
+ try:
99
+ pipeline = KPipeline(lang_code=lang_code, device=_KOKORO_DEVICE if _KOKORO_DEVICE == 'cuda' else 'cpu')
100
+ except TypeError:
101
+ pipeline = KPipeline(lang_code=lang_code)
102
+ generator = pipeline(text, voice=voice, speed=speed)
103
+
104
+ all_samples = []
105
+ sample_rate = 24000 # kokoro always outputs 24 kHz
106
+
107
+ for result in generator:
108
+ # kokoro >=0.9 yields a KPipeline.Result whose waveform is the
109
+ # `.audio` attribute (a torch.Tensor); older builds yield a
110
+ # (graphemes, phonemes, audio) tuple. Prefer `.audio`, then fall
111
+ # back to the last tuple element.
112
+ audio = getattr(result, "audio", None)
113
+ if audio is None:
114
+ try:
115
+ audio = result[-1]
116
+ except (TypeError, IndexError, KeyError):
117
+ audio = None
118
+ if audio is None:
119
+ continue
120
+ # Tensor -> numpy, force 1-D so np.concatenate never sees a 0-d array
121
+ if hasattr(audio, "detach"):
122
+ audio = audio.detach().cpu().numpy()
123
+ audio = np.asarray(audio).reshape(-1)
124
+ if audio.size > 0:
125
+ all_samples.append(audio)
126
+
127
+ if not all_samples:
128
+ print("❌ Kokoro returned no audio samples", file=sys.stderr)
129
+ sys.exit(3)
130
+
131
+ combined = np.concatenate(all_samples)
132
+ sf.write(output_path, combined, sample_rate)
133
+ print(output_path)
134
+
135
+ except Exception as e:
136
+ print(f"❌ Kokoro synthesis failed: {e}", file=sys.stderr)
137
+ sys.exit(3)
138
+
139
+
140
+ if __name__ == '__main__':
141
+ main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes