agentvibes 5.7.7 → 5.9.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 (138) hide show
  1. package/.agentvibes/config.json +12 -5
  2. package/.agentvibes/install-manifest.json +188 -300
  3. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  4. package/.claude/commands/agent-vibes-rdp.md +24 -24
  5. package/.claude/config/audio-effects.cfg +3 -2
  6. package/.claude/config/audio-effects.cfg.sample +52 -52
  7. package/.claude/config/background-music-enabled.txt +1 -0
  8. package/.claude/config/background-music-position.txt +1 -1
  9. package/.claude/config/language.txt +1 -0
  10. package/.claude/docs/TERMUX_SETUP.md +408 -408
  11. package/.claude/github-star-reminder.txt +1 -1
  12. package/.claude/hooks/audio-cache-utils.sh +0 -0
  13. package/.claude/hooks/audio-processor.sh +0 -0
  14. package/.claude/hooks/background-music-manager.sh +0 -0
  15. package/.claude/hooks/bmad-party-speak.sh +0 -0
  16. package/.claude/hooks/bmad-speak-enhanced.sh +0 -0
  17. package/.claude/hooks/bmad-speak.sh +0 -0
  18. package/.claude/hooks/bmad-tts-injector.sh +0 -0
  19. package/.claude/hooks/bmad-voice-manager.sh +0 -0
  20. package/.claude/hooks/clawdbot-receiver-SECURE.sh +0 -0
  21. package/.claude/hooks/clawdbot-receiver.sh +0 -0
  22. package/.claude/hooks/clean-audio-cache.sh +0 -0
  23. package/.claude/hooks/cleanup-cache.sh +0 -0
  24. package/.claude/hooks/configure-rdp-mode.sh +0 -0
  25. package/.claude/hooks/download-extra-voices.sh +0 -0
  26. package/.claude/hooks/effects-manager.sh +0 -0
  27. package/.claude/hooks/github-star-reminder.sh +0 -0
  28. package/.claude/hooks/language-manager.sh +0 -0
  29. package/.claude/hooks/learn-manager.sh +0 -0
  30. package/.claude/hooks/macos-voice-manager.sh +0 -0
  31. package/.claude/hooks/migrate-background-music.sh +0 -0
  32. package/.claude/hooks/migrate-to-agentvibes.sh +0 -0
  33. package/.claude/hooks/optimize-background-music.sh +0 -0
  34. package/.claude/hooks/path-resolver.sh +0 -0
  35. package/.claude/hooks/personality-manager.sh +0 -0
  36. package/.claude/hooks/piper-download-voices.sh +0 -0
  37. package/.claude/hooks/piper-installer.sh +0 -0
  38. package/.claude/hooks/piper-multispeaker-registry.sh +0 -0
  39. package/.claude/hooks/piper-voice-manager.sh +0 -0
  40. package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +0 -0
  41. package/.claude/hooks/play-tts-agentvibes-receiver.sh +1 -0
  42. package/.claude/hooks/play-tts-enhanced.sh +0 -0
  43. package/.claude/hooks/play-tts-macos.sh +0 -0
  44. package/.claude/hooks/play-tts-piper.sh +0 -0
  45. package/.claude/hooks/play-tts-soprano.sh +0 -0
  46. package/.claude/hooks/play-tts-ssh-remote.sh +2 -2
  47. package/.claude/hooks/play-tts-termux-ssh.sh +0 -0
  48. package/.claude/hooks/play-tts-windows-receiver.sh +0 -0
  49. package/.claude/hooks/play-tts.sh +0 -0
  50. package/.claude/hooks/prepare-release.sh +0 -0
  51. package/.claude/hooks/provider-commands.sh +0 -0
  52. package/.claude/hooks/provider-manager.sh +0 -0
  53. package/.claude/hooks/replay-target-audio.sh +0 -0
  54. package/.claude/hooks/requirements.txt +6 -6
  55. package/.claude/hooks/sentiment-manager.sh +0 -0
  56. package/.claude/hooks/session-start-tts.sh +0 -0
  57. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  58. package/.claude/hooks/speed-manager.sh +0 -0
  59. package/.claude/hooks/stop-tts.sh +0 -0
  60. package/.claude/hooks/termux-installer.sh +0 -0
  61. package/.claude/hooks/translate-manager.sh +0 -0
  62. package/.claude/hooks/translator.py +237 -237
  63. package/.claude/hooks/tts-queue-worker.sh +0 -0
  64. package/.claude/hooks/tts-queue.sh +0 -0
  65. package/.claude/hooks/verbosity-manager.sh +0 -0
  66. package/.claude/hooks/voice-manager.sh +0 -0
  67. package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
  68. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +119 -0
  69. package/.claude/hooks-windows/bmad-speak.ps1 +9 -38
  70. package/.claude/hooks-windows/play-tts-soprano.ps1 +13 -2
  71. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  72. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +153 -0
  73. package/.claude/piper-voices-dir.txt +1 -1
  74. package/.claude/verbosity.txt +1 -1
  75. package/.clawdbot/README.md +105 -105
  76. package/.mcp.json +5 -14
  77. package/README.md +33 -2
  78. package/RELEASE_NOTES.md +80 -0
  79. package/WINDOWS-SETUP.md +208 -208
  80. package/bin/agent-vibes +39 -39
  81. package/bin/agentvibes-voice-browser.js +0 -0
  82. package/bin/agentvibes.js +0 -0
  83. package/bin/mcp-server.js +121 -121
  84. package/bin/mcp-server.sh +0 -0
  85. package/bin/test-bmad-pr +78 -78
  86. package/mcp-server/QUICK_START.md +203 -203
  87. package/mcp-server/README.md +345 -345
  88. package/mcp-server/WINDOWS_SETUP.md +0 -0
  89. package/mcp-server/examples/claude_desktop_config.json +11 -11
  90. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  91. package/mcp-server/examples/custom_instructions.md +169 -169
  92. package/mcp-server/install-deps.js +0 -0
  93. package/mcp-server/server.py +1797 -1797
  94. package/mcp-server/test_server.py +0 -0
  95. package/package.json +1 -1
  96. package/src/cli/list-personalities.js +110 -110
  97. package/src/cli/list-voices.js +114 -114
  98. package/src/commands/bmad-voices.js +394 -394
  99. package/src/commands/install-mcp.js +476 -476
  100. package/src/console/audio-env.js +4 -1
  101. package/src/console/brand-colors.js +13 -13
  102. package/src/console/constants/personalities.js +44 -44
  103. package/src/console/tabs/agents-tab.js +84 -61
  104. package/src/console/tabs/help-tab.js +314 -314
  105. package/src/console/tabs/music-tab.js +3 -2
  106. package/src/console/tabs/readme-tab.js +272 -272
  107. package/src/console/tabs/setup-tab.js +285 -41
  108. package/src/console/tabs/voices-tab.js +13 -1
  109. package/src/console/widgets/destroy-list.js +25 -25
  110. package/src/console/widgets/notice.js +55 -55
  111. package/src/i18n/de.js +202 -202
  112. package/src/i18n/es.js +202 -202
  113. package/src/i18n/fr.js +202 -202
  114. package/src/i18n/hi.js +202 -202
  115. package/src/i18n/ja.js +202 -202
  116. package/src/i18n/ko.js +202 -202
  117. package/src/i18n/pt.js +202 -202
  118. package/src/i18n/strings.js +54 -54
  119. package/src/i18n/zh-CN.js +202 -202
  120. package/src/installer/language-screen.js +31 -31
  121. package/src/installer/music-file-input.js +304 -304
  122. package/src/installer.js +0 -0
  123. package/src/services/config-service.js +264 -264
  124. package/src/services/language-service.js +47 -47
  125. package/src/services/provider-service.js +143 -143
  126. package/src/utils/audio-duration-validator.js +298 -298
  127. package/src/utils/audio-format-validator.js +277 -277
  128. package/src/utils/dependency-checker.js +469 -469
  129. package/src/utils/file-ownership-verifier.js +358 -358
  130. package/src/utils/list-formatter.js +194 -194
  131. package/src/utils/music-file-validator.js +285 -285
  132. package/src/utils/preview-list-prompt.js +136 -136
  133. package/src/utils/secure-music-storage.js +412 -412
  134. package/templates/agentvibes-receiver.sh +231 -231
  135. package/templates/audio/welcome-music.mp3 +0 -0
  136. package/.claude/hooks/bmad-party-manager.sh +0 -225
  137. package/.claude/hooks/stop.sh +0 -38
  138. /package/.claude/audio/tracks/{CelestialVelvet.mp3 → celestial_velvet.mp3} +0 -0
@@ -1,298 +1,298 @@
1
- /**
2
- * Audio Duration Validator - Validate audio file length for background music
3
- * Story 4.7: TTS Integration - Background Music Mixing
4
- *
5
- * Validates that custom background music files have appropriate duration:
6
- * - Recommended: 30-90 seconds (ideal for looping without repetition fatigue)
7
- * - Warning: < 30 seconds (may sound repetitive)
8
- * - Warning: > 120 seconds (may use excess memory, slow processing)
9
- *
10
- * @module audio-duration-validator
11
- * @requires child_process
12
- * @requires fs
13
- */
14
-
15
- import { spawn } from 'node:child_process';
16
- import fs from 'node:fs';
17
-
18
- /**
19
- * Recommended duration limits for background music
20
- *
21
- * Rationale:
22
- * - MIN_RECOMMENDED (30s): Research shows loops under 30s cause listener fatigue and annoyance
23
- * - MAX_RECOMMENDED (90s): Optimal range for music loops - long enough for variety, short enough to prevent memory issues
24
- * - MAX_WARNING (120s): Above 2 minutes, ffmpeg stream_loop becomes less efficient (memory overhead)
25
- * - MAX_ALLOWED (300s): Hard limit at 5 minutes to prevent excessive memory usage during TTS processing
26
- *
27
- * Source: Based on UX best practices for background audio in voice assistants and
28
- * technical constraints of ffmpeg stream looping with -stream_loop -1 flag
29
- */
30
- export const DURATION_LIMITS = {
31
- MIN_RECOMMENDED: 30, // 30 seconds - below this gets repetitive
32
- MAX_RECOMMENDED: 90, // 90 seconds - sweet spot for variety
33
- MAX_WARNING: 120, // 2 minutes - above this warn user
34
- MAX_ALLOWED: 300 // 5 minutes - hard limit to prevent issues
35
- };
36
-
37
- /**
38
- * Get audio file duration using ffprobe
39
- *
40
- * @param {string} filePath - Path to audio file
41
- * @returns {Promise<Object>} Result object:
42
- * - success: boolean
43
- * - duration: number|null - Duration in seconds
44
- * - error: string|null - Error message if failed
45
- *
46
- * @example
47
- * const result = await getAudioDuration('/path/to/music.mp3');
48
- * // => { success: true, duration: 45.5, error: null }
49
- */
50
- export async function getAudioDuration(filePath) {
51
- return new Promise((resolve) => {
52
- try {
53
- // Verify file exists
54
- if (!fs.existsSync(filePath)) {
55
- resolve({
56
- success: false,
57
- duration: null,
58
- error: 'File does not exist'
59
- });
60
- return;
61
- }
62
-
63
- // Check if ffprobe is available
64
- const checkFFprobe = spawn('which', ['ffprobe']);
65
- let ffprobeExists = false;
66
-
67
- checkFFprobe.on('close', (code) => {
68
- ffprobeExists = (code === 0);
69
-
70
- if (!ffprobeExists) {
71
- resolve({
72
- success: false,
73
- duration: null,
74
- error: 'ffprobe not found. Please install ffmpeg: sudo apt install ffmpeg (Linux) or brew install ffmpeg (macOS)'
75
- });
76
- return;
77
- }
78
-
79
- // Use ffprobe to get duration (part of ffmpeg)
80
- // SECURITY: Use spawn with args array to prevent command injection
81
- const ffprobe = spawn('ffprobe', [
82
- '-v', 'error',
83
- '-show_entries', 'format=duration',
84
- '-of', 'default=noprint_wrappers=1:nokey=1',
85
- filePath
86
- ]);
87
-
88
- let stdoutChunks = [];
89
- let stderrChunks = [];
90
-
91
- ffprobe.stdout.on('data', (data) => {
92
- stdoutChunks.push(data);
93
- });
94
-
95
- ffprobe.stderr.on('data', (data) => {
96
- stderrChunks.push(data);
97
- });
98
-
99
- ffprobe.on('close', (code) => {
100
- const stdout = Buffer.concat(stdoutChunks).toString();
101
- const stderr = Buffer.concat(stderrChunks).toString();
102
-
103
- if (code !== 0) {
104
- resolve({
105
- success: false,
106
- duration: null,
107
- error: `ffprobe exited with code ${code}: ${stderr || 'Unknown error'}`
108
- });
109
- return;
110
- }
111
-
112
- const duration = parseFloat(stdout.trim());
113
-
114
- if (isNaN(duration) || duration <= 0) {
115
- resolve({
116
- success: false,
117
- duration: null,
118
- error: 'Invalid duration value from ffprobe'
119
- });
120
- return;
121
- }
122
-
123
- resolve({
124
- success: true,
125
- duration,
126
- error: null
127
- });
128
- });
129
-
130
- ffprobe.on('error', (err) => {
131
- resolve({
132
- success: false,
133
- duration: null,
134
- error: `Failed to spawn ffprobe: ${err.message}. Please install ffmpeg.`
135
- });
136
- });
137
- });
138
-
139
- } catch (err) {
140
- resolve({
141
- success: false,
142
- duration: null,
143
- error: `Unexpected error: ${err.message}`
144
- });
145
- }
146
- });
147
- }
148
-
149
- /**
150
- * Validate audio duration against recommended limits
151
- *
152
- * Story 4.7 AC-11: Validate audio length and advise user
153
- *
154
- * @param {number} duration - Duration in seconds
155
- * @returns {Object} Validation result:
156
- * - isValid: boolean - True if within acceptable limits
157
- * - level: string - 'ok' | 'warning' | 'error'
158
- * - message: string - User-friendly message
159
- * - recommendation: string|null - Recommendation for user
160
- *
161
- * @example
162
- * const validation = validateDuration(45);
163
- * // => { isValid: true, level: 'ok', message: 'Duration is ideal...', recommendation: null }
164
- */
165
- export function validateDuration(duration) {
166
- if (typeof duration !== 'number' || isNaN(duration) || duration <= 0) {
167
- return {
168
- isValid: false,
169
- level: 'error',
170
- message: 'Invalid duration value',
171
- recommendation: null
172
- };
173
- }
174
-
175
- // Hard limit exceeded
176
- if (duration > DURATION_LIMITS.MAX_ALLOWED) {
177
- return {
178
- isValid: false,
179
- level: 'error',
180
- message: `Audio is too long (${formatDuration(duration)}). Maximum allowed: ${DURATION_LIMITS.MAX_ALLOWED} seconds (${formatDuration(DURATION_LIMITS.MAX_ALLOWED)})`,
181
- recommendation: 'Please use a shorter audio file or trim this one to under 5 minutes. Long background music can cause memory issues and slow TTS processing.'
182
- };
183
- }
184
-
185
- // Warning: too long but acceptable
186
- if (duration > DURATION_LIMITS.MAX_WARNING) {
187
- return {
188
- isValid: true,
189
- level: 'warning',
190
- message: `Audio is quite long (${formatDuration(duration)})`,
191
- recommendation: `Consider using a shorter track (30-90 seconds recommended). Longer tracks use more memory and may slow down TTS processing.`
192
- };
193
- }
194
-
195
- // Ideal range
196
- if (duration >= DURATION_LIMITS.MIN_RECOMMENDED && duration <= DURATION_LIMITS.MAX_RECOMMENDED) {
197
- return {
198
- isValid: true,
199
- level: 'ok',
200
- message: `Duration is ideal (${formatDuration(duration)})`,
201
- recommendation: null
202
- };
203
- }
204
-
205
- // Warning: too short but acceptable
206
- if (duration < DURATION_LIMITS.MIN_RECOMMENDED) {
207
- return {
208
- isValid: true,
209
- level: 'warning',
210
- message: `Audio is short (${formatDuration(duration)})`,
211
- recommendation: `Consider using a longer track (30-90 seconds recommended). Short tracks will loop frequently and may sound repetitive during longer TTS messages.`
212
- };
213
- }
214
-
215
- // Between recommended max and warning limit - acceptable
216
- return {
217
- isValid: true,
218
- level: 'ok',
219
- message: `Duration is acceptable (${formatDuration(duration)})`,
220
- recommendation: null
221
- };
222
- }
223
-
224
- /**
225
- * Format duration in seconds to human-readable string
226
- *
227
- * @param {number} seconds - Duration in seconds
228
- * @returns {string} Formatted duration (e.g., "1:23", "2:45", "45s")
229
- *
230
- * @example
231
- * formatDuration(45) // => "45s"
232
- * formatDuration(90) // => "1:30"
233
- * formatDuration(195) // => "3:15"
234
- */
235
- export function formatDuration(seconds) {
236
- if (seconds < 60) {
237
- return `${Math.round(seconds)}s`;
238
- }
239
-
240
- const minutes = Math.floor(seconds / 60);
241
- const remainingSeconds = Math.round(seconds % 60);
242
- return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
243
- }
244
-
245
- /**
246
- * Get duration validation for audio file (convenience function)
247
- *
248
- * Combines getAudioDuration() and validateDuration() for one-step validation
249
- *
250
- * @param {string} filePath - Path to audio file
251
- * @returns {Promise<Object>} Combined result:
252
- * - success: boolean
253
- * - duration: number|null
254
- * - isValid: boolean
255
- * - level: string
256
- * - message: string
257
- * - recommendation: string|null
258
- * - error: string|null
259
- *
260
- * @example
261
- * const result = await validateAudioDuration('/path/to/music.mp3');
262
- * if (result.success && result.level === 'warning') {
263
- * console.log(result.message);
264
- * console.log(result.recommendation);
265
- * }
266
- */
267
- export async function validateAudioDuration(filePath) {
268
- const durationResult = await getAudioDuration(filePath);
269
-
270
- if (!durationResult.success) {
271
- return {
272
- success: false,
273
- duration: null,
274
- isValid: false,
275
- level: 'error',
276
- message: durationResult.error,
277
- recommendation: null,
278
- error: durationResult.error
279
- };
280
- }
281
-
282
- const validation = validateDuration(durationResult.duration);
283
-
284
- return {
285
- success: true,
286
- duration: durationResult.duration,
287
- ...validation,
288
- error: null
289
- };
290
- }
291
-
292
- export default {
293
- getAudioDuration,
294
- validateDuration,
295
- validateAudioDuration,
296
- formatDuration,
297
- DURATION_LIMITS
298
- };
1
+ /**
2
+ * Audio Duration Validator - Validate audio file length for background music
3
+ * Story 4.7: TTS Integration - Background Music Mixing
4
+ *
5
+ * Validates that custom background music files have appropriate duration:
6
+ * - Recommended: 30-90 seconds (ideal for looping without repetition fatigue)
7
+ * - Warning: < 30 seconds (may sound repetitive)
8
+ * - Warning: > 120 seconds (may use excess memory, slow processing)
9
+ *
10
+ * @module audio-duration-validator
11
+ * @requires child_process
12
+ * @requires fs
13
+ */
14
+
15
+ import { spawn } from 'node:child_process';
16
+ import fs from 'node:fs';
17
+
18
+ /**
19
+ * Recommended duration limits for background music
20
+ *
21
+ * Rationale:
22
+ * - MIN_RECOMMENDED (30s): Research shows loops under 30s cause listener fatigue and annoyance
23
+ * - MAX_RECOMMENDED (90s): Optimal range for music loops - long enough for variety, short enough to prevent memory issues
24
+ * - MAX_WARNING (120s): Above 2 minutes, ffmpeg stream_loop becomes less efficient (memory overhead)
25
+ * - MAX_ALLOWED (300s): Hard limit at 5 minutes to prevent excessive memory usage during TTS processing
26
+ *
27
+ * Source: Based on UX best practices for background audio in voice assistants and
28
+ * technical constraints of ffmpeg stream looping with -stream_loop -1 flag
29
+ */
30
+ export const DURATION_LIMITS = {
31
+ MIN_RECOMMENDED: 30, // 30 seconds - below this gets repetitive
32
+ MAX_RECOMMENDED: 90, // 90 seconds - sweet spot for variety
33
+ MAX_WARNING: 120, // 2 minutes - above this warn user
34
+ MAX_ALLOWED: 300 // 5 minutes - hard limit to prevent issues
35
+ };
36
+
37
+ /**
38
+ * Get audio file duration using ffprobe
39
+ *
40
+ * @param {string} filePath - Path to audio file
41
+ * @returns {Promise<Object>} Result object:
42
+ * - success: boolean
43
+ * - duration: number|null - Duration in seconds
44
+ * - error: string|null - Error message if failed
45
+ *
46
+ * @example
47
+ * const result = await getAudioDuration('/path/to/music.mp3');
48
+ * // => { success: true, duration: 45.5, error: null }
49
+ */
50
+ export async function getAudioDuration(filePath) {
51
+ return new Promise((resolve) => {
52
+ try {
53
+ // Verify file exists
54
+ if (!fs.existsSync(filePath)) {
55
+ resolve({
56
+ success: false,
57
+ duration: null,
58
+ error: 'File does not exist'
59
+ });
60
+ return;
61
+ }
62
+
63
+ // Check if ffprobe is available
64
+ const checkFFprobe = spawn('which', ['ffprobe']);
65
+ let ffprobeExists = false;
66
+
67
+ checkFFprobe.on('close', (code) => {
68
+ ffprobeExists = (code === 0);
69
+
70
+ if (!ffprobeExists) {
71
+ resolve({
72
+ success: false,
73
+ duration: null,
74
+ error: 'ffprobe not found. Please install ffmpeg: sudo apt install ffmpeg (Linux) or brew install ffmpeg (macOS)'
75
+ });
76
+ return;
77
+ }
78
+
79
+ // Use ffprobe to get duration (part of ffmpeg)
80
+ // SECURITY: Use spawn with args array to prevent command injection
81
+ const ffprobe = spawn('ffprobe', [
82
+ '-v', 'error',
83
+ '-show_entries', 'format=duration',
84
+ '-of', 'default=noprint_wrappers=1:nokey=1',
85
+ filePath
86
+ ]);
87
+
88
+ let stdoutChunks = [];
89
+ let stderrChunks = [];
90
+
91
+ ffprobe.stdout.on('data', (data) => {
92
+ stdoutChunks.push(data);
93
+ });
94
+
95
+ ffprobe.stderr.on('data', (data) => {
96
+ stderrChunks.push(data);
97
+ });
98
+
99
+ ffprobe.on('close', (code) => {
100
+ const stdout = Buffer.concat(stdoutChunks).toString();
101
+ const stderr = Buffer.concat(stderrChunks).toString();
102
+
103
+ if (code !== 0) {
104
+ resolve({
105
+ success: false,
106
+ duration: null,
107
+ error: `ffprobe exited with code ${code}: ${stderr || 'Unknown error'}`
108
+ });
109
+ return;
110
+ }
111
+
112
+ const duration = parseFloat(stdout.trim());
113
+
114
+ if (isNaN(duration) || duration <= 0) {
115
+ resolve({
116
+ success: false,
117
+ duration: null,
118
+ error: 'Invalid duration value from ffprobe'
119
+ });
120
+ return;
121
+ }
122
+
123
+ resolve({
124
+ success: true,
125
+ duration,
126
+ error: null
127
+ });
128
+ });
129
+
130
+ ffprobe.on('error', (err) => {
131
+ resolve({
132
+ success: false,
133
+ duration: null,
134
+ error: `Failed to spawn ffprobe: ${err.message}. Please install ffmpeg.`
135
+ });
136
+ });
137
+ });
138
+
139
+ } catch (err) {
140
+ resolve({
141
+ success: false,
142
+ duration: null,
143
+ error: `Unexpected error: ${err.message}`
144
+ });
145
+ }
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Validate audio duration against recommended limits
151
+ *
152
+ * Story 4.7 AC-11: Validate audio length and advise user
153
+ *
154
+ * @param {number} duration - Duration in seconds
155
+ * @returns {Object} Validation result:
156
+ * - isValid: boolean - True if within acceptable limits
157
+ * - level: string - 'ok' | 'warning' | 'error'
158
+ * - message: string - User-friendly message
159
+ * - recommendation: string|null - Recommendation for user
160
+ *
161
+ * @example
162
+ * const validation = validateDuration(45);
163
+ * // => { isValid: true, level: 'ok', message: 'Duration is ideal...', recommendation: null }
164
+ */
165
+ export function validateDuration(duration) {
166
+ if (typeof duration !== 'number' || isNaN(duration) || duration <= 0) {
167
+ return {
168
+ isValid: false,
169
+ level: 'error',
170
+ message: 'Invalid duration value',
171
+ recommendation: null
172
+ };
173
+ }
174
+
175
+ // Hard limit exceeded
176
+ if (duration > DURATION_LIMITS.MAX_ALLOWED) {
177
+ return {
178
+ isValid: false,
179
+ level: 'error',
180
+ message: `Audio is too long (${formatDuration(duration)}). Maximum allowed: ${DURATION_LIMITS.MAX_ALLOWED} seconds (${formatDuration(DURATION_LIMITS.MAX_ALLOWED)})`,
181
+ recommendation: 'Please use a shorter audio file or trim this one to under 5 minutes. Long background music can cause memory issues and slow TTS processing.'
182
+ };
183
+ }
184
+
185
+ // Warning: too long but acceptable
186
+ if (duration > DURATION_LIMITS.MAX_WARNING) {
187
+ return {
188
+ isValid: true,
189
+ level: 'warning',
190
+ message: `Audio is quite long (${formatDuration(duration)})`,
191
+ recommendation: `Consider using a shorter track (30-90 seconds recommended). Longer tracks use more memory and may slow down TTS processing.`
192
+ };
193
+ }
194
+
195
+ // Ideal range
196
+ if (duration >= DURATION_LIMITS.MIN_RECOMMENDED && duration <= DURATION_LIMITS.MAX_RECOMMENDED) {
197
+ return {
198
+ isValid: true,
199
+ level: 'ok',
200
+ message: `Duration is ideal (${formatDuration(duration)})`,
201
+ recommendation: null
202
+ };
203
+ }
204
+
205
+ // Warning: too short but acceptable
206
+ if (duration < DURATION_LIMITS.MIN_RECOMMENDED) {
207
+ return {
208
+ isValid: true,
209
+ level: 'warning',
210
+ message: `Audio is short (${formatDuration(duration)})`,
211
+ recommendation: `Consider using a longer track (30-90 seconds recommended). Short tracks will loop frequently and may sound repetitive during longer TTS messages.`
212
+ };
213
+ }
214
+
215
+ // Between recommended max and warning limit - acceptable
216
+ return {
217
+ isValid: true,
218
+ level: 'ok',
219
+ message: `Duration is acceptable (${formatDuration(duration)})`,
220
+ recommendation: null
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Format duration in seconds to human-readable string
226
+ *
227
+ * @param {number} seconds - Duration in seconds
228
+ * @returns {string} Formatted duration (e.g., "1:23", "2:45", "45s")
229
+ *
230
+ * @example
231
+ * formatDuration(45) // => "45s"
232
+ * formatDuration(90) // => "1:30"
233
+ * formatDuration(195) // => "3:15"
234
+ */
235
+ export function formatDuration(seconds) {
236
+ if (seconds < 60) {
237
+ return `${Math.round(seconds)}s`;
238
+ }
239
+
240
+ const minutes = Math.floor(seconds / 60);
241
+ const remainingSeconds = Math.round(seconds % 60);
242
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
243
+ }
244
+
245
+ /**
246
+ * Get duration validation for audio file (convenience function)
247
+ *
248
+ * Combines getAudioDuration() and validateDuration() for one-step validation
249
+ *
250
+ * @param {string} filePath - Path to audio file
251
+ * @returns {Promise<Object>} Combined result:
252
+ * - success: boolean
253
+ * - duration: number|null
254
+ * - isValid: boolean
255
+ * - level: string
256
+ * - message: string
257
+ * - recommendation: string|null
258
+ * - error: string|null
259
+ *
260
+ * @example
261
+ * const result = await validateAudioDuration('/path/to/music.mp3');
262
+ * if (result.success && result.level === 'warning') {
263
+ * console.log(result.message);
264
+ * console.log(result.recommendation);
265
+ * }
266
+ */
267
+ export async function validateAudioDuration(filePath) {
268
+ const durationResult = await getAudioDuration(filePath);
269
+
270
+ if (!durationResult.success) {
271
+ return {
272
+ success: false,
273
+ duration: null,
274
+ isValid: false,
275
+ level: 'error',
276
+ message: durationResult.error,
277
+ recommendation: null,
278
+ error: durationResult.error
279
+ };
280
+ }
281
+
282
+ const validation = validateDuration(durationResult.duration);
283
+
284
+ return {
285
+ success: true,
286
+ duration: durationResult.duration,
287
+ ...validation,
288
+ error: null
289
+ };
290
+ }
291
+
292
+ export default {
293
+ getAudioDuration,
294
+ validateDuration,
295
+ validateAudioDuration,
296
+ formatDuration,
297
+ DURATION_LIMITS
298
+ };