agentvibes 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/.agentvibes/bmad/bmad-voices.md +69 -69
  2. package/.agentvibes/config.json +12 -0
  3. package/.claude/activation-instructions +54 -54
  4. package/.claude/audio/tracks/README.md +52 -52
  5. package/.claude/commands/agent-vibes/add.md +21 -21
  6. package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
  7. package/.claude/commands/agent-vibes/agent.md +79 -79
  8. package/.claude/commands/agent-vibes/background-music.md +111 -111
  9. package/.claude/commands/agent-vibes/bmad.md +198 -198
  10. package/.claude/commands/agent-vibes/clean.md +18 -18
  11. package/.claude/commands/agent-vibes/cleanup.md +18 -18
  12. package/.claude/commands/agent-vibes/commands.json +145 -145
  13. package/.claude/commands/agent-vibes/effects.md +97 -97
  14. package/.claude/commands/agent-vibes/get.md +9 -9
  15. package/.claude/commands/agent-vibes/hide.md +91 -91
  16. package/.claude/commands/agent-vibes/language.md +23 -23
  17. package/.claude/commands/agent-vibes/learn.md +67 -67
  18. package/.claude/commands/agent-vibes/list.md +13 -13
  19. package/.claude/commands/agent-vibes/mute.md +37 -37
  20. package/.claude/commands/agent-vibes/preview.md +17 -17
  21. package/.claude/commands/agent-vibes/provider.md +68 -68
  22. package/.claude/commands/agent-vibes/replay-target.md +14 -14
  23. package/.claude/commands/agent-vibes/sample.md +12 -12
  24. package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
  25. package/.claude/commands/agent-vibes/set-pretext.md +65 -65
  26. package/.claude/commands/agent-vibes/set-speed.md +41 -41
  27. package/.claude/commands/agent-vibes/show.md +84 -84
  28. package/.claude/commands/agent-vibes/switch.md +87 -87
  29. package/.claude/commands/agent-vibes/target-voice.md +26 -26
  30. package/.claude/commands/agent-vibes/target.md +30 -30
  31. package/.claude/commands/agent-vibes/translate.md +68 -68
  32. package/.claude/commands/agent-vibes/unmute.md +45 -45
  33. package/.claude/commands/agent-vibes/verbosity.md +89 -89
  34. package/.claude/commands/agent-vibes/whoami.md +7 -7
  35. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  36. package/.claude/commands/agent-vibes-rdp.md +24 -24
  37. package/.claude/config/agentvibes.json +1 -0
  38. package/.claude/config/audio-effects.cfg +2 -2
  39. package/.claude/config/audio-effects.cfg.sample +52 -52
  40. package/.claude/config/background-music-volume.txt +1 -0
  41. package/.claude/config/intro-text.txt +1 -0
  42. package/.claude/config/piper-speech-rate.txt +4 -0
  43. package/.claude/config/piper-target-speech-rate.txt +1 -0
  44. package/.claude/config/reverb-level.txt +1 -0
  45. package/.claude/config/tts-speech-rate.txt +4 -0
  46. package/.claude/config/tts-target-speech-rate.txt +1 -0
  47. package/.claude/docs/TERMUX_SETUP.md +408 -408
  48. package/.claude/github-star-reminder.txt +1 -1
  49. package/.claude/hooks/README-TTS-QUEUE.md +135 -135
  50. package/.claude/hooks/audio-cache-utils.sh +246 -246
  51. package/.claude/hooks/audio-processor.sh +433 -433
  52. package/.claude/hooks/background-music-manager.sh +404 -404
  53. package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
  54. package/.claude/hooks/bmad-speak.sh +269 -269
  55. package/.claude/hooks/bmad-tts-injector.sh +568 -568
  56. package/.claude/hooks/bmad-voice-manager.sh +928 -928
  57. package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
  58. package/.claude/hooks/clawdbot-receiver.sh +107 -107
  59. package/.claude/hooks/clean-audio-cache.sh +22 -22
  60. package/.claude/hooks/cleanup-cache.sh +106 -106
  61. package/.claude/hooks/configure-rdp-mode.sh +137 -137
  62. package/.claude/hooks/download-extra-voices.sh +244 -244
  63. package/.claude/hooks/effects-manager.sh +268 -268
  64. package/.claude/hooks/github-star-reminder.sh +154 -154
  65. package/.claude/hooks/language-manager.sh +362 -362
  66. package/.claude/hooks/learn-manager.sh +492 -492
  67. package/.claude/hooks/macos-voice-manager.sh +205 -205
  68. package/.claude/hooks/migrate-background-music.sh +125 -125
  69. package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
  70. package/.claude/hooks/optimize-background-music.sh +87 -87
  71. package/.claude/hooks/path-resolver.sh +60 -60
  72. package/.claude/hooks/personality-manager.sh +448 -448
  73. package/.claude/hooks/piper-download-voices.sh +225 -225
  74. package/.claude/hooks/piper-installer.sh +292 -292
  75. package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
  76. package/.claude/hooks/piper-voice-manager.sh +24 -3
  77. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
  78. package/.claude/hooks/play-tts-enhanced.sh +105 -105
  79. package/.claude/hooks/play-tts-macos.sh +368 -368
  80. package/.claude/hooks/play-tts-piper.sh +679 -679
  81. package/.claude/hooks/play-tts-soprano.sh +356 -356
  82. package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
  83. package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
  84. package/.claude/hooks/play-tts.sh +301 -301
  85. package/.claude/hooks/prepare-release.sh +54 -54
  86. package/.claude/hooks/provider-commands.sh +617 -617
  87. package/.claude/hooks/provider-manager.sh +399 -399
  88. package/.claude/hooks/replay-target-audio.sh +95 -95
  89. package/.claude/hooks/requirements.txt +6 -6
  90. package/.claude/hooks/sentiment-manager.sh +201 -201
  91. package/.claude/hooks/session-start-tts.sh +81 -81
  92. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  93. package/.claude/hooks/speed-manager.sh +291 -291
  94. package/.claude/hooks/stop-tts.sh +84 -84
  95. package/.claude/hooks/termux-installer.sh +261 -261
  96. package/.claude/hooks/translate-manager.sh +341 -341
  97. package/.claude/hooks/translator.py +237 -237
  98. package/.claude/hooks/tts-queue-worker.sh +145 -145
  99. package/.claude/hooks/tts-queue.sh +165 -165
  100. package/.claude/hooks/verbosity-manager.sh +178 -178
  101. package/.claude/hooks/voice-manager.sh +548 -548
  102. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  103. package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
  104. package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
  105. package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
  106. package/.claude/hooks-windows/effects-manager.ps1 +294 -0
  107. package/.claude/hooks-windows/language-manager.ps1 +193 -0
  108. package/.claude/hooks-windows/learn-manager.ps1 +241 -0
  109. package/.claude/hooks-windows/personality-manager.ps1 +266 -0
  110. package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
  111. package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
  112. package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
  113. package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
  114. package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
  115. package/.claude/hooks-windows/play-tts.ps1 +344 -266
  116. package/.claude/hooks-windows/provider-manager.ps1 +29 -10
  117. package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
  118. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  119. package/.claude/hooks-windows/speed-manager.ps1 +166 -0
  120. package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
  121. package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
  122. package/.claude/output-styles/agent-vibes.md +202 -202
  123. package/.claude/personalities/angry.md +14 -14
  124. package/.claude/personalities/annoying.md +14 -14
  125. package/.claude/personalities/crass.md +14 -14
  126. package/.claude/personalities/dramatic.md +14 -14
  127. package/.claude/personalities/dry-humor.md +50 -50
  128. package/.claude/personalities/flirty.md +20 -20
  129. package/.claude/personalities/funny.md +14 -14
  130. package/.claude/personalities/grandpa.md +32 -32
  131. package/.claude/personalities/millennial.md +14 -14
  132. package/.claude/personalities/moody.md +14 -14
  133. package/.claude/personalities/normal.md +16 -16
  134. package/.claude/personalities/pirate.md +14 -14
  135. package/.claude/personalities/poetic.md +14 -14
  136. package/.claude/personalities/professional.md +14 -14
  137. package/.claude/personalities/rapper.md +55 -55
  138. package/.claude/personalities/robot.md +14 -14
  139. package/.claude/personalities/sarcastic.md +38 -38
  140. package/.claude/personalities/sassy.md +14 -14
  141. package/.claude/personalities/surfer-dude.md +14 -14
  142. package/.claude/personalities/zen.md +14 -14
  143. package/.claude/settings.json +15 -15
  144. package/.claude/verbosity.txt +1 -1
  145. package/.clawdbot/README.md +105 -105
  146. package/.clawdbot/skill/SKILL.md +241 -241
  147. package/.mcp.json +12 -0
  148. package/CLAUDE.md +170 -170
  149. package/README.md +2029 -2007
  150. package/RELEASE_NOTES.md +1310 -1203
  151. package/WINDOWS-SETUP.md +208 -208
  152. package/bin/agent-vibes +39 -39
  153. package/bin/agentvibes-voice-browser.js +1840 -1840
  154. package/bin/agentvibes.js +48 -2
  155. package/bin/mcp-server.js +121 -121
  156. package/bin/mcp-server.sh +206 -206
  157. package/bin/test-bmad-pr +78 -78
  158. package/mcp-server/QUICK_START.md +203 -203
  159. package/mcp-server/README.md +345 -345
  160. package/mcp-server/WINDOWS_SETUP.md +260 -260
  161. package/mcp-server/docs/troubleshooting-audio.md +313 -313
  162. package/mcp-server/examples/claude_desktop_config.json +11 -11
  163. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  164. package/mcp-server/examples/custom_instructions.md +169 -169
  165. package/mcp-server/install-deps.js +130 -130
  166. package/mcp-server/pyproject.toml +52 -52
  167. package/mcp-server/requirements.txt +2 -2
  168. package/mcp-server/server.py +1465 -1453
  169. package/mcp-server/test_server.py +395 -395
  170. package/mcp-server/test_windows_script_parity.py +336 -0
  171. package/package.json +110 -110
  172. package/setup-windows.ps1 +815 -815
  173. package/src/bmad-detector.js +71 -71
  174. package/src/cli/list-personalities.js +110 -110
  175. package/src/cli/list-voices.js +114 -114
  176. package/src/commands/bmad-voices.js +394 -394
  177. package/src/commands/install-mcp.js +476 -476
  178. package/src/console/app.js +824 -824
  179. package/src/console/audio-env.js +20 -1
  180. package/src/console/brand-colors.js +13 -13
  181. package/src/console/constants/personalities.js +44 -44
  182. package/src/console/footer-config.js +50 -50
  183. package/src/console/modals/modal-overlay.js +247 -247
  184. package/src/console/navigation.js +62 -62
  185. package/src/console/tabs/agents-tab.js +1684 -1516
  186. package/src/console/tabs/help-tab.js +261 -261
  187. package/src/console/tabs/install-tab.js +1007 -991
  188. package/src/console/tabs/music-tab.js +22 -8
  189. package/src/console/tabs/placeholder-tab.js +53 -53
  190. package/src/console/tabs/readme-tab.js +267 -267
  191. package/src/console/tabs/receiver-tab.js +1472 -1212
  192. package/src/console/tabs/settings-tab.js +152 -79
  193. package/src/console/tabs/voices-tab.js +100 -21
  194. package/src/console/widgets/destroy-list.js +25 -25
  195. package/src/console/widgets/format-utils.js +89 -89
  196. package/src/console/widgets/notice.js +55 -55
  197. package/src/console/widgets/personality-picker.js +185 -185
  198. package/src/console/widgets/reverb-picker.js +94 -94
  199. package/src/console/widgets/track-picker.js +285 -285
  200. package/src/installer/music-file-input.js +304 -304
  201. package/src/installer.js +5882 -5829
  202. package/src/services/agent-voice-store.js +423 -423
  203. package/src/services/config-service.js +264 -264
  204. package/src/services/navigation-service.js +123 -123
  205. package/src/services/provider-service.js +132 -132
  206. package/src/services/verbosity-service.js +157 -157
  207. package/src/utils/audio-duration-validator.js +298 -298
  208. package/src/utils/audio-format-validator.js +277 -277
  209. package/src/utils/dependency-checker.js +469 -466
  210. package/src/utils/file-ownership-verifier.js +358 -358
  211. package/src/utils/list-formatter.js +194 -194
  212. package/src/utils/music-file-validator.js +285 -285
  213. package/src/utils/preview-list-prompt.js +136 -136
  214. package/src/utils/provider-validator.js +96 -12
  215. package/src/utils/secure-music-storage.js +412 -412
  216. package/templates/agentvibes-receiver.sh +482 -482
  217. package/templates/audio/welcome-music.mp3 +0 -0
  218. package/voice-assignments.json +8244 -8244
  219. package/.claude/config/background-music-position.txt +0 -1
@@ -1,433 +1,433 @@
1
- #!/usr/bin/env bash
2
- #
3
- # File: .claude/hooks/audio-processor.sh
4
- #
5
- # AgentVibes - Audio Effects and Background Mixing Processor
6
- # Website: https://agentvibes.org
7
- # Repository: https://github.com/paulpreibisch/AgentVibes
8
- #
9
- # Co-created by Paul Preibisch with Claude AI
10
- # Copyright (c) 2025 Paul Preibisch
11
- #
12
- # Licensed under the Apache License, Version 2.0 (the "License");
13
- # you may not use this file except in compliance with the License.
14
- #
15
- # ---
16
- #
17
- # @fileoverview Audio post-processor for TTS with effects and background mixing
18
- # @context Applies sox effects and mixes background audio for enhanced TTS experience
19
- # @architecture Post-processing hook called after TTS generation, before playback
20
- # @dependencies sox, ffmpeg
21
- # @entrypoints Called by play-tts-piper.sh after audio generation
22
- # @patterns Pipeline pattern: input.wav → effects → mix → output.wav
23
- #
24
-
25
- set -euo pipefail
26
-
27
- # Fix locale warnings
28
- export LC_ALL=C
29
-
30
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
- PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
32
-
33
- # Input parameters
34
- INPUT_FILE="${1:-}"
35
- AGENT_NAME="${2:-default}"
36
- OUTPUT_FILE="${3:-}"
37
- AGENT_PROFILE_FILE="${4:-}" # Optional: path to per-agent profile JSON (from bmad-speak.sh)
38
-
39
- # Config and directories (resolve to absolute paths)
40
- CONFIG_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/audio-effects.cfg"
41
- BACKGROUNDS_DIR="$(cd "$SCRIPT_DIR/../audio" && pwd)/tracks"
42
- ENABLED_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/background-music-enabled.txt"
43
- GLOBAL_ENABLED_FILE="$HOME/.claude/config/background-music-enabled.txt"
44
-
45
- # Check if background music is enabled (project-local, then global fallback)
46
- is_background_music_enabled() {
47
- local enabled=""
48
- if [[ -f "$ENABLED_FILE" ]]; then
49
- enabled=$(cat "$ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
50
- elif [[ -f "$GLOBAL_ENABLED_FILE" ]]; then
51
- enabled=$(cat "$GLOBAL_ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
52
- else
53
- return 1 # Disabled by default
54
- fi
55
-
56
- # Return 0 (true) if enabled, 1 (false) otherwise
57
- [[ "$enabled" == "true" ]]
58
- }
59
-
60
- # Validate inputs
61
- if [[ -z "$INPUT_FILE" ]] || [[ ! -f "$INPUT_FILE" ]]; then
62
- echo "Error: Input file required and must exist" >&2
63
- echo "Usage: $0 <input.wav> [agent_name] [output.wav]" >&2
64
- exit 1
65
- fi
66
-
67
- # Default output to input location with -processed suffix
68
- if [[ -z "$OUTPUT_FILE" ]]; then
69
- OUTPUT_FILE="${INPUT_FILE%.wav}-processed.wav"
70
- fi
71
-
72
- # Check for required tools
73
- if ! command -v sox &> /dev/null; then
74
- echo "Warning: sox not installed, skipping effects" >&2
75
- cp "$INPUT_FILE" "$OUTPUT_FILE"
76
- echo "$OUTPUT_FILE"
77
- exit 0
78
- fi
79
-
80
- # @function get_agent_config
81
- # @intent Parse audio-effects.cfg for agent-specific settings
82
- # @param $1 Agent name
83
- # @returns Pipe-separated config line or default
84
- get_agent_config() {
85
- local agent="$1"
86
-
87
- if [[ ! -f "$CONFIG_FILE" ]]; then
88
- echo "default|gain -8||0.0"
89
- return
90
- fi
91
-
92
- # Try exact match first (use awk for safe literal matching)
93
- local config
94
- config=$(awk -F'|' -v agent="$agent" 'tolower($1) == tolower(agent)' "$CONFIG_FILE" 2>/dev/null | head -1)
95
-
96
- # Fall back to default
97
- if [[ -z "$config" ]]; then
98
- config=$(grep "^default|" "$CONFIG_FILE" 2>/dev/null | head -1)
99
- fi
100
-
101
- # Return config or empty default
102
- if [[ -n "$config" ]]; then
103
- echo "$config"
104
- else
105
- echo "default|gain -8||0.0"
106
- fi
107
- }
108
-
109
- # @function apply_sox_effects
110
- # @intent Apply sox effect chain to audio file
111
- # @param $1 Input file
112
- # @param $2 Output file
113
- # @param $3 Sox effects string
114
- apply_sox_effects() {
115
- local input="$1"
116
- local output="$2"
117
- local effects="$3"
118
-
119
- if [[ -z "$effects" ]]; then
120
- cp "$input" "$output"
121
- return 0
122
- fi
123
-
124
- # Validate effects contain only allowed sox effect names and numeric params
125
- local allowed_effects="gain|reverb|echo|chorus|flanger|phaser|tremolo|overdrive|bass|treble|equalizer|highpass|lowpass|bandpass|vol|speed|tempo|pitch|rate|pad|silence|trim|fade|norm|loudness|compand|contrast|delay|repeat|stat|remix"
126
- for word in $effects; do
127
- if ! [[ "$word" =~ ^-?[0-9]*\.?[0-9]+$ ]] && ! echo "$word" | grep -qiE "^($allowed_effects)$"; then
128
- echo "Warning: Invalid sox effect '$word', skipping effects" >&2
129
- cp "$input" "$output"
130
- return 0
131
- fi
132
- done
133
-
134
- # Apply effects - note: effects string is intentionally unquoted to allow word splitting
135
- # shellcheck disable=SC2086
136
- sox "$input" "$output" $effects 2>/dev/null || {
137
- echo "Warning: Sox effects failed, using original" >&2
138
- cp "$input" "$output"
139
- }
140
- }
141
-
142
- # Position tracking file for continuous playback
143
- POSITION_FILE="$SCRIPT_DIR/../config/background-music-position.txt"
144
- # Lock file for position file — prevents race conditions in party mode with concurrent agents
145
- POSITION_LOCK="/tmp/agentvibes-bgpos-$(id -u).lock"
146
-
147
- # @function get_background_position
148
- # @intent Get saved position for a background track (caller must hold POSITION_LOCK)
149
- # @param $1 Background file path
150
- # @returns Position in seconds (or 0 if not found)
151
- get_background_position() {
152
- local bg_file="$1"
153
- local bg_name
154
- bg_name=$(basename "$bg_file")
155
-
156
- if [[ -f "$POSITION_FILE" ]]; then
157
- awk -F: -v name="$bg_name" '$1 == name {print $2}' "$POSITION_FILE" 2>/dev/null | tr -d '[:space:]' | tail -1
158
- else
159
- echo "0"
160
- fi
161
- }
162
-
163
- # @function save_background_position
164
- # @intent Save position for a background track (caller must hold POSITION_LOCK)
165
- # @param $1 Background file path
166
- # @param $2 New position in seconds
167
- save_background_position() {
168
- local bg_file="$1"
169
- local position="$2"
170
- local bg_name
171
- bg_name=$(basename "$bg_file")
172
-
173
- mkdir -p "$(dirname "$POSITION_FILE")"
174
-
175
- # Remove old entry and add new one (atomic update via temp file + mv)
176
- local tmp_pos
177
- tmp_pos=$(mktemp "${POSITION_FILE}.XXXXXX")
178
- if [[ -f "$POSITION_FILE" ]]; then
179
- # SECURITY: Use grep -F for fixed string matching (#134)
180
- grep -vF "${bg_name}:" "$POSITION_FILE" > "$tmp_pos" 2>/dev/null || true
181
- fi
182
- echo "${bg_name}:${position}" >> "$tmp_pos"
183
- mv "$tmp_pos" "$POSITION_FILE"
184
- }
185
-
186
- # @function mix_background
187
- # @intent Mix background audio with voice at specified volume, continuing from last position
188
- # @param $1 Voice file (foreground)
189
- # @param $2 Background file
190
- # @param $3 Background volume (0.0-1.0)
191
- # @param $4 Output file
192
- mix_background() {
193
- local voice="$1"
194
- local background="$2"
195
- local volume="$3"
196
- local output="$4"
197
-
198
- if [[ -z "$background" ]] || [[ ! -f "$background" ]]; then
199
- cp "$voice" "$output"
200
- return 0
201
- fi
202
-
203
- if ! command -v ffmpeg &> /dev/null; then
204
- echo "Warning: ffmpeg not installed, skipping background mix" >&2
205
- cp "$voice" "$output"
206
- return 0
207
- fi
208
-
209
- # Get voice duration
210
- local duration
211
- duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$voice" 2>/dev/null)
212
-
213
- if [[ -z "$duration" ]]; then
214
- cp "$voice" "$output"
215
- return 0
216
- fi
217
-
218
- # Get background track duration
219
- local bg_duration
220
- bg_duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$background" 2>/dev/null)
221
- bg_duration=${bg_duration:-0}
222
-
223
- # Read the start position and pre-compute the new position atomically under flock.
224
- # This prevents party-mode race conditions where concurrent agents both read the
225
- # same position, compute independently, and overwrite each other's updates.
226
- local start_pos
227
- local new_pos
228
- local total_duration
229
- {
230
- flock -x 200
231
-
232
- # Get saved position for this track (continuous playback)
233
- start_pos=$(get_background_position "$background")
234
-
235
- # Validate start_pos: if too small (floating point error) or invalid, reset to 0
236
- if command -v bc &> /dev/null; then
237
- if ! [[ "$start_pos" =~ ^[0-9]+\.?[0-9]*$ ]] || (( $(echo "$start_pos < 0.001" | bc -l) )); then
238
- start_pos="0"
239
- fi
240
- else
241
- # Without bc, just check if it's a valid number
242
- if ! [[ "$start_pos" =~ ^[0-9]+\.?[0-9]*$ ]]; then
243
- start_pos="0"
244
- fi
245
- fi
246
-
247
- # If position exceeds track length, wrap around
248
- if command -v bc &> /dev/null && [[ -n "$bg_duration" ]]; then
249
- if (( $(echo "$start_pos >= $bg_duration" | bc -l) )); then
250
- start_pos=$(echo "$start_pos % $bg_duration" | bc -l)
251
- fi
252
- fi
253
-
254
- # Extend total duration by 2 seconds for background music fade out
255
- if command -v bc &> /dev/null; then
256
- total_duration=$(echo "$duration + 2" | bc -l)
257
- else
258
- total_duration=$(awk "BEGIN {print $duration + 2}")
259
- fi
260
-
261
- # Calculate new position after this clip (including fade out time)
262
- if command -v bc &> /dev/null; then
263
- new_pos=$(echo "$start_pos + $total_duration" | bc -l)
264
- # Wrap around if needed
265
- if [[ -n "$bg_duration" ]] && (( $(echo "$new_pos >= $bg_duration" | bc -l) )); then
266
- new_pos=$(echo "$new_pos % $bg_duration" | bc -l)
267
- fi
268
- else
269
- new_pos="0"
270
- fi
271
-
272
- # Claim the new position immediately so concurrent agents advance past it
273
- save_background_position "$background" "$new_pos"
274
- } 200>"$POSITION_LOCK"
275
-
276
- # Mix: Seek to position in background, apply volume and fades
277
- # Background fades in at start (0.3s), continues under speech, then fades out over 2s after speech ends
278
- # -ss before -i seeks efficiently without decoding
279
- local bg_fade_out_start
280
- if command -v bc &> /dev/null; then
281
- bg_fade_out_start=$(echo "$duration" | bc -l)
282
- else
283
- bg_fade_out_start="$duration"
284
- fi
285
-
286
- # Auto-detect remote sessions (SSH/RDP) and enable compression
287
- if [[ -z "${AGENTVIBES_RDP_MODE:-}" ]]; then
288
- if [[ -n "${SSH_CLIENT:-}" ]] || [[ -n "${SSH_TTY:-}" ]] || [[ "${DISPLAY:-}" =~ ^localhost:.* ]]; then
289
- export AGENTVIBES_RDP_MODE=true
290
- fi
291
- fi
292
-
293
- # RDP-optimized audio settings: mono 22kHz for lower bandwidth
294
- # Automatically enabled for remote desktop/SSH environments
295
- local audio_settings=""
296
- if [[ "${AGENTVIBES_RDP_MODE:-false}" == "true" ]]; then
297
- audio_settings="-ac 1 -ar 22050 -b:a 64k"
298
- fi
299
-
300
- # Add 2 seconds of background music intro before voice starts
301
- # Background: fades in (0.3s), plays solo (2s), then voice joins, fades out at end (2s)
302
- # Voice: delayed by 2000ms (2s), no fade-in (full volume from first word)
303
- local voice_delay_ms="2000" # adelay takes milliseconds
304
- local voice_delay_sec="2.0"
305
- local bg_fade_out_adjusted
306
- if command -v bc &> /dev/null; then
307
- bg_fade_out_adjusted=$(echo "$duration + $voice_delay_sec" | bc -l)
308
- else
309
- bg_fade_out_adjusted=$(echo "$duration + 2" | bc)
310
- fi
311
-
312
- ffmpeg -y -i "$voice" -ss "$start_pos" -stream_loop -1 -i "$background" \
313
- -filter_complex "[1:a]volume=${volume},afade=t=in:st=0:d=0.3,afade=t=out:st=${bg_fade_out_adjusted}:d=2[bg];[0:a]adelay=${voice_delay_ms}|${voice_delay_ms}[v];[v][bg]amix=inputs=2:duration=longest[out]" \
314
- -map "[out]" $audio_settings -t "$total_duration" "$output" 2>/dev/null || {
315
- echo "Warning: Background mixing failed, using voice only" >&2
316
- cp "$voice" "$output"
317
- return
318
- }
319
- }
320
-
321
- # Main processing
322
- main() {
323
- echo "🎛️ Processing audio for agent: $AGENT_NAME" >&2
324
-
325
- # Get agent config
326
- local config
327
- config=$(get_agent_config "$AGENT_NAME")
328
-
329
- # Parse config (format: NAME|EFFECTS|BACKGROUND|VOLUME)
330
- IFS='|' read -r _ sox_effects background_file bg_volume <<< "$config"
331
-
332
- # Per-agent background music override from bmad-speak.sh profile JSON (takes priority over cfg).
333
- # The profile file is a PID-scoped temp file written by bmad-speak.sh; no env var leakage.
334
- if [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
335
- # SECURITY: Pass profile path via env var to avoid shell injection in node -e string
336
- local _prof_track _prof_vol _prof_enabled
337
- _prof_track=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(p.backgroundMusic?.track??'')}catch{process.stdout.write('')}" 2>/dev/null || true)
338
- _prof_vol=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.volume??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
339
- _prof_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
340
- if [[ "$_prof_enabled" == "true" ]] && [[ -n "$_prof_track" ]]; then
341
- background_file="$_prof_track"
342
- # Convert percentage volume (0-100) to decimal (0.0-1.0) for ffmpeg
343
- if [[ "$_prof_vol" =~ ^[0-9]+$ ]]; then
344
- bg_volume=$(awk "BEGIN{printf \"%.2f\", ${_prof_vol}/100}")
345
- else
346
- bg_volume="0.70"
347
- fi
348
- fi
349
- fi
350
-
351
- # SECURITY: Use secure temp directory per CLAUDE.md guidelines
352
- # Prefer XDG_RUNTIME_DIR (user-owned, restricted permissions)
353
- # Fall back to user-specific directory in /tmp
354
- local TEMP_DIR
355
- if [[ -d "/data/data/com.termux" ]]; then
356
- # On Termux - use Termux temp
357
- TEMP_DIR="${TMPDIR:-${PREFIX:-/data/data/com.termux/files/usr}/tmp}/agentvibes-audio-$$"
358
- elif [[ -n "${XDG_RUNTIME_DIR:-}" ]] && [[ -d "$XDG_RUNTIME_DIR" ]]; then
359
- # Preferred: XDG_RUNTIME_DIR (user-owned, 700 permissions)
360
- TEMP_DIR="$XDG_RUNTIME_DIR/agentvibes-audio"
361
- else
362
- # Fallback: user-specific directory in /tmp
363
- TEMP_DIR="/tmp/agentvibes-audio-${USER:-$(id -un)}"
364
- fi
365
-
366
- # Create temp directory with restrictive permissions
367
- mkdir -p "$TEMP_DIR"
368
- chmod 700 "$TEMP_DIR"
369
-
370
- # SECURITY: Verify ownership of temp directory
371
- # SECURITY: Handle stat failure explicitly (#134)
372
- local _dir_uid
373
- _dir_uid=$(stat -c '%u' "$TEMP_DIR" 2>/dev/null || stat -f '%u' "$TEMP_DIR" 2>/dev/null)
374
- if [[ -z "$_dir_uid" ]] || [[ "$_dir_uid" != "$(id -u)" ]]; then
375
- echo "Error: Temp directory not owned by current user: $TEMP_DIR" >&2
376
- exit 1
377
- fi
378
-
379
- # SECURITY: Use mktemp for unpredictable filenames
380
- local temp_effects
381
- local temp_final
382
- temp_effects=$(mktemp "$TEMP_DIR/effects-XXXXXX.wav")
383
- temp_final=$(mktemp "$TEMP_DIR/final-XXXXXX.wav")
384
-
385
- # Clean up on exit - use a cleanup function to avoid trap injection
386
- _cleanup_effects="$temp_effects"
387
- _cleanup_final="$temp_final"
388
- cleanup() { rm -f "$_cleanup_effects" "$_cleanup_final"; }
389
- trap cleanup EXIT
390
-
391
- # Step 1: Apply sox effects
392
- if [[ -n "$sox_effects" ]]; then
393
- echo " → Applying effects: $sox_effects" >&2
394
- apply_sox_effects "$INPUT_FILE" "$temp_effects" "$sox_effects"
395
- else
396
- cp "$INPUT_FILE" "$temp_effects"
397
- fi
398
-
399
- # Step 2: Mix background if configured AND enabled
400
- local background_path=""
401
- if [[ -n "$background_file" ]]; then
402
- background_path="$BACKGROUNDS_DIR/$background_file"
403
- fi
404
-
405
- # Per-agent profile enables music independently of the global flag.
406
- local _bg_allowed=false
407
- if is_background_music_enabled; then
408
- _bg_allowed=true
409
- elif [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
410
- # A valid agent profile with enabled=true overrides the global off switch.
411
- local _check_enabled
412
- _check_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
413
- [[ "$_check_enabled" == "true" ]] && _bg_allowed=true
414
- fi
415
-
416
- local used_background=""
417
- if [[ "$_bg_allowed" == "true" ]] && [[ -n "$background_path" ]] && [[ -f "$background_path" ]] && [[ "${bg_volume:-0}" != "0" ]] && [[ "${bg_volume:-0}" != "0.0" ]]; then
418
- echo " → Mixing background: $background_file at ${bg_volume} volume" >&2
419
- mix_background "$temp_effects" "$background_path" "$bg_volume" "$temp_final"
420
- used_background="$background_path" # Return full path instead of just filename
421
- else
422
- cp "$temp_effects" "$temp_final"
423
- fi
424
-
425
- # Move to final output
426
- mv "$temp_final" "$OUTPUT_FILE"
427
-
428
- # Return the output file path (stdout for caller to capture)
429
- # Format: OUTPUT_FILE|BACKGROUND_FILE_PATH (background is empty if not used)
430
- echo "$OUTPUT_FILE|$used_background"
431
- }
432
-
433
- main
1
+ #!/usr/bin/env bash
2
+ #
3
+ # File: .claude/hooks/audio-processor.sh
4
+ #
5
+ # AgentVibes - Audio Effects and Background Mixing Processor
6
+ # Website: https://agentvibes.org
7
+ # Repository: https://github.com/paulpreibisch/AgentVibes
8
+ #
9
+ # Co-created by Paul Preibisch with Claude AI
10
+ # Copyright (c) 2025 Paul Preibisch
11
+ #
12
+ # Licensed under the Apache License, Version 2.0 (the "License");
13
+ # you may not use this file except in compliance with the License.
14
+ #
15
+ # ---
16
+ #
17
+ # @fileoverview Audio post-processor for TTS with effects and background mixing
18
+ # @context Applies sox effects and mixes background audio for enhanced TTS experience
19
+ # @architecture Post-processing hook called after TTS generation, before playback
20
+ # @dependencies sox, ffmpeg
21
+ # @entrypoints Called by play-tts-piper.sh after audio generation
22
+ # @patterns Pipeline pattern: input.wav → effects → mix → output.wav
23
+ #
24
+
25
+ set -euo pipefail
26
+
27
+ # Fix locale warnings
28
+ export LC_ALL=C
29
+
30
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
32
+
33
+ # Input parameters
34
+ INPUT_FILE="${1:-}"
35
+ AGENT_NAME="${2:-default}"
36
+ OUTPUT_FILE="${3:-}"
37
+ AGENT_PROFILE_FILE="${4:-}" # Optional: path to per-agent profile JSON (from bmad-speak.sh)
38
+
39
+ # Config and directories (resolve to absolute paths)
40
+ CONFIG_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/audio-effects.cfg"
41
+ BACKGROUNDS_DIR="$(cd "$SCRIPT_DIR/../audio" && pwd)/tracks"
42
+ ENABLED_FILE="$(cd "$SCRIPT_DIR/.." && pwd)/config/background-music-enabled.txt"
43
+ GLOBAL_ENABLED_FILE="$HOME/.claude/config/background-music-enabled.txt"
44
+
45
+ # Check if background music is enabled (project-local, then global fallback)
46
+ is_background_music_enabled() {
47
+ local enabled=""
48
+ if [[ -f "$ENABLED_FILE" ]]; then
49
+ enabled=$(cat "$ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
50
+ elif [[ -f "$GLOBAL_ENABLED_FILE" ]]; then
51
+ enabled=$(cat "$GLOBAL_ENABLED_FILE" 2>/dev/null | tr -d '[:space:]')
52
+ else
53
+ return 1 # Disabled by default
54
+ fi
55
+
56
+ # Return 0 (true) if enabled, 1 (false) otherwise
57
+ [[ "$enabled" == "true" ]]
58
+ }
59
+
60
+ # Validate inputs
61
+ if [[ -z "$INPUT_FILE" ]] || [[ ! -f "$INPUT_FILE" ]]; then
62
+ echo "Error: Input file required and must exist" >&2
63
+ echo "Usage: $0 <input.wav> [agent_name] [output.wav]" >&2
64
+ exit 1
65
+ fi
66
+
67
+ # Default output to input location with -processed suffix
68
+ if [[ -z "$OUTPUT_FILE" ]]; then
69
+ OUTPUT_FILE="${INPUT_FILE%.wav}-processed.wav"
70
+ fi
71
+
72
+ # Check for required tools
73
+ if ! command -v sox &> /dev/null; then
74
+ echo "Warning: sox not installed, skipping effects" >&2
75
+ cp "$INPUT_FILE" "$OUTPUT_FILE"
76
+ echo "$OUTPUT_FILE"
77
+ exit 0
78
+ fi
79
+
80
+ # @function get_agent_config
81
+ # @intent Parse audio-effects.cfg for agent-specific settings
82
+ # @param $1 Agent name
83
+ # @returns Pipe-separated config line or default
84
+ get_agent_config() {
85
+ local agent="$1"
86
+
87
+ if [[ ! -f "$CONFIG_FILE" ]]; then
88
+ echo "default|gain -8||0.0"
89
+ return
90
+ fi
91
+
92
+ # Try exact match first (use awk for safe literal matching)
93
+ local config
94
+ config=$(awk -F'|' -v agent="$agent" 'tolower($1) == tolower(agent)' "$CONFIG_FILE" 2>/dev/null | head -1)
95
+
96
+ # Fall back to default
97
+ if [[ -z "$config" ]]; then
98
+ config=$(grep "^default|" "$CONFIG_FILE" 2>/dev/null | head -1)
99
+ fi
100
+
101
+ # Return config or empty default
102
+ if [[ -n "$config" ]]; then
103
+ echo "$config"
104
+ else
105
+ echo "default|gain -8||0.0"
106
+ fi
107
+ }
108
+
109
+ # @function apply_sox_effects
110
+ # @intent Apply sox effect chain to audio file
111
+ # @param $1 Input file
112
+ # @param $2 Output file
113
+ # @param $3 Sox effects string
114
+ apply_sox_effects() {
115
+ local input="$1"
116
+ local output="$2"
117
+ local effects="$3"
118
+
119
+ if [[ -z "$effects" ]]; then
120
+ cp "$input" "$output"
121
+ return 0
122
+ fi
123
+
124
+ # Validate effects contain only allowed sox effect names and numeric params
125
+ local allowed_effects="gain|reverb|echo|chorus|flanger|phaser|tremolo|overdrive|bass|treble|equalizer|highpass|lowpass|bandpass|vol|speed|tempo|pitch|rate|pad|silence|trim|fade|norm|loudness|compand|contrast|delay|repeat|stat|remix"
126
+ for word in $effects; do
127
+ if ! [[ "$word" =~ ^-?[0-9]*\.?[0-9]+$ ]] && ! echo "$word" | grep -qiE "^($allowed_effects)$"; then
128
+ echo "Warning: Invalid sox effect '$word', skipping effects" >&2
129
+ cp "$input" "$output"
130
+ return 0
131
+ fi
132
+ done
133
+
134
+ # Apply effects - note: effects string is intentionally unquoted to allow word splitting
135
+ # shellcheck disable=SC2086
136
+ sox "$input" "$output" $effects 2>/dev/null || {
137
+ echo "Warning: Sox effects failed, using original" >&2
138
+ cp "$input" "$output"
139
+ }
140
+ }
141
+
142
+ # Position tracking file for continuous playback
143
+ POSITION_FILE="$SCRIPT_DIR/../config/background-music-position.txt"
144
+ # Lock file for position file — prevents race conditions in party mode with concurrent agents
145
+ POSITION_LOCK="/tmp/agentvibes-bgpos-$(id -u).lock"
146
+
147
+ # @function get_background_position
148
+ # @intent Get saved position for a background track (caller must hold POSITION_LOCK)
149
+ # @param $1 Background file path
150
+ # @returns Position in seconds (or 0 if not found)
151
+ get_background_position() {
152
+ local bg_file="$1"
153
+ local bg_name
154
+ bg_name=$(basename "$bg_file")
155
+
156
+ if [[ -f "$POSITION_FILE" ]]; then
157
+ awk -F: -v name="$bg_name" '$1 == name {print $2}' "$POSITION_FILE" 2>/dev/null | tr -d '[:space:]' | tail -1
158
+ else
159
+ echo "0"
160
+ fi
161
+ }
162
+
163
+ # @function save_background_position
164
+ # @intent Save position for a background track (caller must hold POSITION_LOCK)
165
+ # @param $1 Background file path
166
+ # @param $2 New position in seconds
167
+ save_background_position() {
168
+ local bg_file="$1"
169
+ local position="$2"
170
+ local bg_name
171
+ bg_name=$(basename "$bg_file")
172
+
173
+ mkdir -p "$(dirname "$POSITION_FILE")"
174
+
175
+ # Remove old entry and add new one (atomic update via temp file + mv)
176
+ local tmp_pos
177
+ tmp_pos=$(mktemp "${POSITION_FILE}.XXXXXX")
178
+ if [[ -f "$POSITION_FILE" ]]; then
179
+ # SECURITY: Use grep -F for fixed string matching (#134)
180
+ grep -vF "${bg_name}:" "$POSITION_FILE" > "$tmp_pos" 2>/dev/null || true
181
+ fi
182
+ echo "${bg_name}:${position}" >> "$tmp_pos"
183
+ mv "$tmp_pos" "$POSITION_FILE"
184
+ }
185
+
186
+ # @function mix_background
187
+ # @intent Mix background audio with voice at specified volume, continuing from last position
188
+ # @param $1 Voice file (foreground)
189
+ # @param $2 Background file
190
+ # @param $3 Background volume (0.0-1.0)
191
+ # @param $4 Output file
192
+ mix_background() {
193
+ local voice="$1"
194
+ local background="$2"
195
+ local volume="$3"
196
+ local output="$4"
197
+
198
+ if [[ -z "$background" ]] || [[ ! -f "$background" ]]; then
199
+ cp "$voice" "$output"
200
+ return 0
201
+ fi
202
+
203
+ if ! command -v ffmpeg &> /dev/null; then
204
+ echo "Warning: ffmpeg not installed, skipping background mix" >&2
205
+ cp "$voice" "$output"
206
+ return 0
207
+ fi
208
+
209
+ # Get voice duration
210
+ local duration
211
+ duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$voice" 2>/dev/null)
212
+
213
+ if [[ -z "$duration" ]]; then
214
+ cp "$voice" "$output"
215
+ return 0
216
+ fi
217
+
218
+ # Get background track duration
219
+ local bg_duration
220
+ bg_duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$background" 2>/dev/null)
221
+ bg_duration=${bg_duration:-0}
222
+
223
+ # Read the start position and pre-compute the new position atomically under flock.
224
+ # This prevents party-mode race conditions where concurrent agents both read the
225
+ # same position, compute independently, and overwrite each other's updates.
226
+ local start_pos
227
+ local new_pos
228
+ local total_duration
229
+ {
230
+ flock -x 200
231
+
232
+ # Get saved position for this track (continuous playback)
233
+ start_pos=$(get_background_position "$background")
234
+
235
+ # Validate start_pos: if too small (floating point error) or invalid, reset to 0
236
+ if command -v bc &> /dev/null; then
237
+ if ! [[ "$start_pos" =~ ^[0-9]+\.?[0-9]*$ ]] || (( $(echo "$start_pos < 0.001" | bc -l) )); then
238
+ start_pos="0"
239
+ fi
240
+ else
241
+ # Without bc, just check if it's a valid number
242
+ if ! [[ "$start_pos" =~ ^[0-9]+\.?[0-9]*$ ]]; then
243
+ start_pos="0"
244
+ fi
245
+ fi
246
+
247
+ # If position exceeds track length, wrap around
248
+ if command -v bc &> /dev/null && [[ -n "$bg_duration" ]]; then
249
+ if (( $(echo "$start_pos >= $bg_duration" | bc -l) )); then
250
+ start_pos=$(echo "$start_pos % $bg_duration" | bc -l)
251
+ fi
252
+ fi
253
+
254
+ # Extend total duration by 2 seconds for background music fade out
255
+ if command -v bc &> /dev/null; then
256
+ total_duration=$(echo "$duration + 2" | bc -l)
257
+ else
258
+ total_duration=$(awk "BEGIN {print $duration + 2}")
259
+ fi
260
+
261
+ # Calculate new position after this clip (including fade out time)
262
+ if command -v bc &> /dev/null; then
263
+ new_pos=$(echo "$start_pos + $total_duration" | bc -l)
264
+ # Wrap around if needed
265
+ if [[ -n "$bg_duration" ]] && (( $(echo "$new_pos >= $bg_duration" | bc -l) )); then
266
+ new_pos=$(echo "$new_pos % $bg_duration" | bc -l)
267
+ fi
268
+ else
269
+ new_pos="0"
270
+ fi
271
+
272
+ # Claim the new position immediately so concurrent agents advance past it
273
+ save_background_position "$background" "$new_pos"
274
+ } 200>"$POSITION_LOCK"
275
+
276
+ # Mix: Seek to position in background, apply volume and fades
277
+ # Background fades in at start (0.3s), continues under speech, then fades out over 2s after speech ends
278
+ # -ss before -i seeks efficiently without decoding
279
+ local bg_fade_out_start
280
+ if command -v bc &> /dev/null; then
281
+ bg_fade_out_start=$(echo "$duration" | bc -l)
282
+ else
283
+ bg_fade_out_start="$duration"
284
+ fi
285
+
286
+ # Auto-detect remote sessions (SSH/RDP) and enable compression
287
+ if [[ -z "${AGENTVIBES_RDP_MODE:-}" ]]; then
288
+ if [[ -n "${SSH_CLIENT:-}" ]] || [[ -n "${SSH_TTY:-}" ]] || [[ "${DISPLAY:-}" =~ ^localhost:.* ]]; then
289
+ export AGENTVIBES_RDP_MODE=true
290
+ fi
291
+ fi
292
+
293
+ # RDP-optimized audio settings: mono 22kHz for lower bandwidth
294
+ # Automatically enabled for remote desktop/SSH environments
295
+ local audio_settings=""
296
+ if [[ "${AGENTVIBES_RDP_MODE:-false}" == "true" ]]; then
297
+ audio_settings="-ac 1 -ar 22050 -b:a 64k"
298
+ fi
299
+
300
+ # Add 2 seconds of background music intro before voice starts
301
+ # Background: fades in (0.3s), plays solo (2s), then voice joins, fades out at end (2s)
302
+ # Voice: delayed by 2000ms (2s), no fade-in (full volume from first word)
303
+ local voice_delay_ms="2000" # adelay takes milliseconds
304
+ local voice_delay_sec="2.0"
305
+ local bg_fade_out_adjusted
306
+ if command -v bc &> /dev/null; then
307
+ bg_fade_out_adjusted=$(echo "$duration + $voice_delay_sec" | bc -l)
308
+ else
309
+ bg_fade_out_adjusted=$(echo "$duration + 2" | bc)
310
+ fi
311
+
312
+ ffmpeg -y -i "$voice" -ss "$start_pos" -stream_loop -1 -i "$background" \
313
+ -filter_complex "[1:a]volume=${volume},afade=t=in:st=0:d=0.3,afade=t=out:st=${bg_fade_out_adjusted}:d=2[bg];[0:a]adelay=${voice_delay_ms}|${voice_delay_ms}[v];[v][bg]amix=inputs=2:duration=longest[out]" \
314
+ -map "[out]" $audio_settings -t "$total_duration" "$output" 2>/dev/null || {
315
+ echo "Warning: Background mixing failed, using voice only" >&2
316
+ cp "$voice" "$output"
317
+ return
318
+ }
319
+ }
320
+
321
+ # Main processing
322
+ main() {
323
+ echo "🎛️ Processing audio for agent: $AGENT_NAME" >&2
324
+
325
+ # Get agent config
326
+ local config
327
+ config=$(get_agent_config "$AGENT_NAME")
328
+
329
+ # Parse config (format: NAME|EFFECTS|BACKGROUND|VOLUME)
330
+ IFS='|' read -r _ sox_effects background_file bg_volume <<< "$config"
331
+
332
+ # Per-agent background music override from bmad-speak.sh profile JSON (takes priority over cfg).
333
+ # The profile file is a PID-scoped temp file written by bmad-speak.sh; no env var leakage.
334
+ if [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
335
+ # SECURITY: Pass profile path via env var to avoid shell injection in node -e string
336
+ local _prof_track _prof_vol _prof_enabled
337
+ _prof_track=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(p.backgroundMusic?.track??'')}catch{process.stdout.write('')}" 2>/dev/null || true)
338
+ _prof_vol=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.volume??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
339
+ _prof_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
340
+ if [[ "$_prof_enabled" == "true" ]] && [[ -n "$_prof_track" ]]; then
341
+ background_file="$_prof_track"
342
+ # Convert percentage volume (0-100) to decimal (0.0-1.0) for ffmpeg
343
+ if [[ "$_prof_vol" =~ ^[0-9]+$ ]]; then
344
+ bg_volume=$(awk "BEGIN{printf \"%.2f\", ${_prof_vol}/100}")
345
+ else
346
+ bg_volume="0.70"
347
+ fi
348
+ fi
349
+ fi
350
+
351
+ # SECURITY: Use secure temp directory per CLAUDE.md guidelines
352
+ # Prefer XDG_RUNTIME_DIR (user-owned, restricted permissions)
353
+ # Fall back to user-specific directory in /tmp
354
+ local TEMP_DIR
355
+ if [[ -d "/data/data/com.termux" ]]; then
356
+ # On Termux - use Termux temp
357
+ TEMP_DIR="${TMPDIR:-${PREFIX:-/data/data/com.termux/files/usr}/tmp}/agentvibes-audio-$$"
358
+ elif [[ -n "${XDG_RUNTIME_DIR:-}" ]] && [[ -d "$XDG_RUNTIME_DIR" ]]; then
359
+ # Preferred: XDG_RUNTIME_DIR (user-owned, 700 permissions)
360
+ TEMP_DIR="$XDG_RUNTIME_DIR/agentvibes-audio"
361
+ else
362
+ # Fallback: user-specific directory in /tmp
363
+ TEMP_DIR="/tmp/agentvibes-audio-${USER:-$(id -un)}"
364
+ fi
365
+
366
+ # Create temp directory with restrictive permissions
367
+ mkdir -p "$TEMP_DIR"
368
+ chmod 700 "$TEMP_DIR"
369
+
370
+ # SECURITY: Verify ownership of temp directory
371
+ # SECURITY: Handle stat failure explicitly (#134)
372
+ local _dir_uid
373
+ _dir_uid=$(stat -c '%u' "$TEMP_DIR" 2>/dev/null || stat -f '%u' "$TEMP_DIR" 2>/dev/null)
374
+ if [[ -z "$_dir_uid" ]] || [[ "$_dir_uid" != "$(id -u)" ]]; then
375
+ echo "Error: Temp directory not owned by current user: $TEMP_DIR" >&2
376
+ exit 1
377
+ fi
378
+
379
+ # SECURITY: Use mktemp for unpredictable filenames
380
+ local temp_effects
381
+ local temp_final
382
+ temp_effects=$(mktemp "$TEMP_DIR/effects-XXXXXX.wav")
383
+ temp_final=$(mktemp "$TEMP_DIR/final-XXXXXX.wav")
384
+
385
+ # Clean up on exit - use a cleanup function to avoid trap injection
386
+ _cleanup_effects="$temp_effects"
387
+ _cleanup_final="$temp_final"
388
+ cleanup() { rm -f "$_cleanup_effects" "$_cleanup_final"; }
389
+ trap cleanup EXIT
390
+
391
+ # Step 1: Apply sox effects
392
+ if [[ -n "$sox_effects" ]]; then
393
+ echo " → Applying effects: $sox_effects" >&2
394
+ apply_sox_effects "$INPUT_FILE" "$temp_effects" "$sox_effects"
395
+ else
396
+ cp "$INPUT_FILE" "$temp_effects"
397
+ fi
398
+
399
+ # Step 2: Mix background if configured AND enabled
400
+ local background_path=""
401
+ if [[ -n "$background_file" ]]; then
402
+ background_path="$BACKGROUNDS_DIR/$background_file"
403
+ fi
404
+
405
+ # Per-agent profile enables music independently of the global flag.
406
+ local _bg_allowed=false
407
+ if is_background_music_enabled; then
408
+ _bg_allowed=true
409
+ elif [[ -n "$AGENT_PROFILE_FILE" ]] && [[ -f "$AGENT_PROFILE_FILE" ]]; then
410
+ # A valid agent profile with enabled=true overrides the global off switch.
411
+ local _check_enabled
412
+ _check_enabled=$(_AV_PROF="$AGENT_PROFILE_FILE" node -e "try{const p=JSON.parse(require('fs').readFileSync(process.env._AV_PROF,'utf8'));process.stdout.write(String(p.backgroundMusic?.enabled??''))}catch{process.stdout.write('')}" 2>/dev/null || true)
413
+ [[ "$_check_enabled" == "true" ]] && _bg_allowed=true
414
+ fi
415
+
416
+ local used_background=""
417
+ if [[ "$_bg_allowed" == "true" ]] && [[ -n "$background_path" ]] && [[ -f "$background_path" ]] && [[ "${bg_volume:-0}" != "0" ]] && [[ "${bg_volume:-0}" != "0.0" ]]; then
418
+ echo " → Mixing background: $background_file at ${bg_volume} volume" >&2
419
+ mix_background "$temp_effects" "$background_path" "$bg_volume" "$temp_final"
420
+ used_background="$background_path" # Return full path instead of just filename
421
+ else
422
+ cp "$temp_effects" "$temp_final"
423
+ fi
424
+
425
+ # Move to final output
426
+ mv "$temp_final" "$OUTPUT_FILE"
427
+
428
+ # Return the output file path (stdout for caller to capture)
429
+ # Format: OUTPUT_FILE|BACKGROUND_FILE_PATH (background is empty if not used)
430
+ echo "$OUTPUT_FILE|$used_background"
431
+ }
432
+
433
+ main