agentvibes 5.6.9 → 5.7.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 (95) hide show
  1. package/.agentvibes/config.json +3 -38
  2. package/.claude/commands/agent-vibes/provider.md +0 -0
  3. package/.claude/config/audio-effects.cfg +1 -1
  4. package/.claude/config/background-music-position.txt +6 -8
  5. package/.claude/config/reverb-level.txt +0 -0
  6. package/.claude/github-star-reminder.txt +1 -1
  7. package/.claude/hooks/bmad-tts-injector.sh +49 -21
  8. package/.claude/hooks/provider-commands.sh +16 -4
  9. package/.claude/hooks/provider-manager.sh +38 -0
  10. package/.claude/hooks/stop.sh +2 -27
  11. package/.claude/hooks/voice-manager.sh +50 -2
  12. package/.claude/hooks-windows/play-tts.ps1 +34 -1
  13. package/.claude/hooks-windows/tts-watcher.ps1 +122 -0
  14. package/.claude/piper-voices-dir.txt +1 -1
  15. package/.mcp.json +13 -33
  16. package/README.md +6 -8
  17. package/RELEASE_NOTES.md +32 -0
  18. package/bin/agent-vibes +39 -39
  19. package/package.json +1 -1
  20. package/src/bmad-detector.js +85 -71
  21. package/src/cli/list-personalities.js +110 -110
  22. package/src/cli/list-voices.js +114 -114
  23. package/src/commands/bmad-voices.js +394 -394
  24. package/src/commands/install-mcp.js +476 -476
  25. package/src/console/brand-colors.js +13 -13
  26. package/src/console/constants/personalities.js +44 -44
  27. package/src/console/tabs/help-tab.js +314 -314
  28. package/src/console/tabs/readme-tab.js +272 -272
  29. package/src/console/widgets/destroy-list.js +25 -25
  30. package/src/console/widgets/notice.js +55 -55
  31. package/src/console/widgets/personality-picker.js +213 -213
  32. package/src/i18n/de.js +202 -202
  33. package/src/i18n/es.js +202 -202
  34. package/src/i18n/fr.js +202 -202
  35. package/src/i18n/hi.js +202 -202
  36. package/src/i18n/ja.js +202 -202
  37. package/src/i18n/ko.js +202 -202
  38. package/src/i18n/pt.js +202 -202
  39. package/src/i18n/strings.js +54 -54
  40. package/src/i18n/zh-CN.js +202 -202
  41. package/src/installer/language-screen.js +31 -31
  42. package/src/installer/music-file-input.js +304 -304
  43. package/src/installer.js +70 -7
  44. package/src/services/agent-voice-store.js +59 -12
  45. package/src/services/config-service.js +264 -264
  46. package/src/services/language-service.js +47 -47
  47. package/src/services/provider-service.js +143 -143
  48. package/src/utils/audio-duration-validator.js +298 -298
  49. package/src/utils/audio-format-validator.js +277 -277
  50. package/src/utils/dependency-checker.js +469 -469
  51. package/src/utils/file-ownership-verifier.js +358 -358
  52. package/src/utils/list-formatter.js +194 -194
  53. package/src/utils/music-file-validator.js +285 -285
  54. package/src/utils/preview-list-prompt.js +136 -136
  55. package/src/utils/secure-music-storage.js +412 -412
  56. package/.agentvibes/LITE-MODE.md +0 -236
  57. package/.agentvibes/README.md +0 -136
  58. package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +0 -141
  59. package/.agentvibes/backups/agents/analyst_20260204_144958.md +0 -78
  60. package/.agentvibes/backups/agents/architect_20260204_144958.md +0 -72
  61. package/.agentvibes/backups/agents/dev_20260204_144958.md +0 -74
  62. package/.agentvibes/backups/agents/pm_20260204_144958.md +0 -72
  63. package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +0 -64
  64. package/.agentvibes/backups/agents/sm_20260204_144958.md +0 -87
  65. package/.agentvibes/backups/agents/tea_20260204_144958.md +0 -79
  66. package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +0 -82
  67. package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +0 -80
  68. package/.agentvibes/config/README-personality-defaults.md +0 -162
  69. package/.agentvibes/config/agentvibes.json +0 -1
  70. package/.agentvibes/config/mode.txt +0 -1
  71. package/.agentvibes/config/personality-voice-defaults.default.json +0 -21
  72. package/.agentvibes/config/save-audio.txt +0 -1
  73. package/.agentvibes/config/voice-metadata.json +0 -160
  74. package/.agentvibes/hooks/help.sh +0 -191
  75. package/.agentvibes/hooks/post-tool-use-lite.sh +0 -111
  76. package/.agentvibes/hooks/save-audio-manager.sh +0 -162
  77. package/.agentvibes/hooks/session-start-full-optimized.sh +0 -102
  78. package/.agentvibes/hooks/session-start-full.sh +0 -142
  79. package/.agentvibes/hooks/session-start-lite-v2.sh +0 -34
  80. package/.agentvibes/hooks/session-start-lite.sh +0 -29
  81. package/.agentvibes/hooks/stop-lite.sh +0 -115
  82. package/.agentvibes/hooks/switch-mode.sh +0 -215
  83. package/.agentvibes/output-styles/audio-summary.md +0 -30
  84. package/.claude/audio/voice-samples/piper/alan.wav +0 -0
  85. package/.claude/audio/voice-samples/piper/amy.wav +0 -0
  86. package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
  87. package/.claude/audio/voice-samples/piper/joe.wav +0 -0
  88. package/.claude/audio/voice-samples/piper/john.wav +0 -0
  89. package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
  90. package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
  91. package/.claude/audio/voice-samples/piper/linda.wav +0 -0
  92. package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
  93. package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
  94. package/.claude/hooks/post-response.sh +0 -41
  95. package/bin/ensure-soprano-running.sh +0 -43
@@ -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
+ };