agentvibes 4.2.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentvibes/bmad/bmad-voices.md +69 -69
- package/.agentvibes/config.json +12 -0
- package/.claude/activation-instructions +54 -54
- package/.claude/audio/tracks/README.md +52 -52
- package/.claude/commands/agent-vibes/add.md +21 -21
- package/.claude/commands/agent-vibes/agent-vibes.md +101 -101
- package/.claude/commands/agent-vibes/agent.md +79 -79
- package/.claude/commands/agent-vibes/background-music.md +111 -111
- package/.claude/commands/agent-vibes/bmad.md +198 -198
- package/.claude/commands/agent-vibes/clean.md +18 -18
- package/.claude/commands/agent-vibes/cleanup.md +18 -18
- package/.claude/commands/agent-vibes/commands.json +145 -145
- package/.claude/commands/agent-vibes/effects.md +97 -97
- package/.claude/commands/agent-vibes/get.md +9 -9
- package/.claude/commands/agent-vibes/hide.md +91 -91
- package/.claude/commands/agent-vibes/language.md +23 -23
- package/.claude/commands/agent-vibes/learn.md +67 -67
- package/.claude/commands/agent-vibes/list.md +13 -13
- package/.claude/commands/agent-vibes/mute.md +37 -37
- package/.claude/commands/agent-vibes/preview.md +17 -17
- package/.claude/commands/agent-vibes/provider.md +68 -68
- package/.claude/commands/agent-vibes/replay-target.md +14 -14
- package/.claude/commands/agent-vibes/sample.md +12 -12
- package/.claude/commands/agent-vibes/set-favorite-voice.md +84 -84
- package/.claude/commands/agent-vibes/set-pretext.md +65 -65
- package/.claude/commands/agent-vibes/set-speed.md +41 -41
- package/.claude/commands/agent-vibes/show.md +84 -84
- package/.claude/commands/agent-vibes/switch.md +87 -87
- package/.claude/commands/agent-vibes/target-voice.md +26 -26
- package/.claude/commands/agent-vibes/target.md +30 -30
- package/.claude/commands/agent-vibes/translate.md +68 -68
- package/.claude/commands/agent-vibes/unmute.md +45 -45
- package/.claude/commands/agent-vibes/verbosity.md +89 -89
- package/.claude/commands/agent-vibes/whoami.md +7 -7
- package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
- package/.claude/commands/agent-vibes-rdp.md +24 -24
- package/.claude/config/agentvibes.json +1 -0
- package/.claude/config/audio-effects.cfg +2 -2
- package/.claude/config/audio-effects.cfg.sample +52 -52
- package/.claude/config/background-music-volume.txt +1 -0
- package/.claude/config/intro-text.txt +1 -0
- package/.claude/config/piper-speech-rate.txt +4 -0
- package/.claude/config/piper-target-speech-rate.txt +1 -0
- package/.claude/config/reverb-level.txt +1 -0
- package/.claude/config/tts-speech-rate.txt +4 -0
- package/.claude/config/tts-target-speech-rate.txt +1 -0
- package/.claude/docs/TERMUX_SETUP.md +408 -408
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/README-TTS-QUEUE.md +135 -135
- package/.claude/hooks/audio-cache-utils.sh +246 -246
- package/.claude/hooks/audio-processor.sh +433 -433
- package/.claude/hooks/background-music-manager.sh +404 -404
- package/.claude/hooks/bmad-speak-enhanced.sh +165 -165
- package/.claude/hooks/bmad-speak.sh +269 -269
- package/.claude/hooks/bmad-tts-injector.sh +568 -568
- package/.claude/hooks/bmad-voice-manager.sh +928 -928
- package/.claude/hooks/clawdbot-receiver-SECURE.sh +129 -129
- package/.claude/hooks/clawdbot-receiver.sh +107 -107
- package/.claude/hooks/clean-audio-cache.sh +22 -22
- package/.claude/hooks/cleanup-cache.sh +106 -106
- package/.claude/hooks/configure-rdp-mode.sh +137 -137
- package/.claude/hooks/download-extra-voices.sh +244 -244
- package/.claude/hooks/effects-manager.sh +268 -268
- package/.claude/hooks/github-star-reminder.sh +154 -154
- package/.claude/hooks/language-manager.sh +362 -362
- package/.claude/hooks/learn-manager.sh +492 -492
- package/.claude/hooks/macos-voice-manager.sh +205 -205
- package/.claude/hooks/migrate-background-music.sh +125 -125
- package/.claude/hooks/migrate-to-agentvibes.sh +161 -161
- package/.claude/hooks/optimize-background-music.sh +87 -87
- package/.claude/hooks/path-resolver.sh +60 -60
- package/.claude/hooks/personality-manager.sh +448 -448
- package/.claude/hooks/piper-download-voices.sh +225 -225
- package/.claude/hooks/piper-installer.sh +292 -292
- package/.claude/hooks/piper-multispeaker-registry.sh +171 -171
- package/.claude/hooks/piper-voice-manager.sh +24 -3
- package/.claude/hooks/play-tts-agentvibes-receiver-for-voiceless-connections.sh +90 -90
- package/.claude/hooks/play-tts-enhanced.sh +105 -105
- package/.claude/hooks/play-tts-macos.sh +368 -368
- package/.claude/hooks/play-tts-piper.sh +679 -679
- package/.claude/hooks/play-tts-soprano.sh +356 -356
- package/.claude/hooks/play-tts-ssh-remote.sh +167 -167
- package/.claude/hooks/play-tts-termux-ssh.sh +169 -169
- package/.claude/hooks/play-tts.sh +301 -301
- package/.claude/hooks/prepare-release.sh +54 -54
- package/.claude/hooks/provider-commands.sh +617 -617
- package/.claude/hooks/provider-manager.sh +399 -399
- package/.claude/hooks/replay-target-audio.sh +95 -95
- package/.claude/hooks/requirements.txt +6 -6
- package/.claude/hooks/sentiment-manager.sh +201 -201
- package/.claude/hooks/session-start-tts.sh +81 -81
- package/.claude/hooks/soprano-gradio-synth.py +139 -139
- package/.claude/hooks/speed-manager.sh +291 -291
- package/.claude/hooks/stop-tts.sh +84 -84
- package/.claude/hooks/termux-installer.sh +261 -261
- package/.claude/hooks/translate-manager.sh +341 -341
- package/.claude/hooks/translator.py +237 -237
- package/.claude/hooks/tts-queue-worker.sh +145 -145
- package/.claude/hooks/tts-queue.sh +165 -165
- package/.claude/hooks/verbosity-manager.sh +178 -178
- package/.claude/hooks/voice-manager.sh +548 -548
- package/.claude/hooks-windows/audio-cache-utils.ps1 +119 -119
- package/.claude/hooks-windows/background-music-manager.ps1 +348 -0
- package/.claude/hooks-windows/clean-audio-cache.ps1 +53 -0
- package/.claude/hooks-windows/download-extra-voices.ps1 +185 -0
- package/.claude/hooks-windows/effects-manager.ps1 +294 -0
- package/.claude/hooks-windows/language-manager.ps1 +193 -0
- package/.claude/hooks-windows/learn-manager.ps1 +241 -0
- package/.claude/hooks-windows/personality-manager.ps1 +266 -0
- package/.claude/hooks-windows/play-tts-piper.ps1 +209 -0
- package/.claude/hooks-windows/play-tts-sapi.ps1 +108 -0
- package/.claude/hooks-windows/play-tts-soprano.ps1 +159 -158
- package/.claude/hooks-windows/play-tts-windows-piper.ps1 +50 -5
- package/.claude/hooks-windows/play-tts-windows-sapi.ps1 +108 -108
- package/.claude/hooks-windows/play-tts.ps1 +344 -266
- package/.claude/hooks-windows/provider-manager.ps1 +29 -10
- package/.claude/hooks-windows/session-start-tts.ps1 +124 -124
- package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
- package/.claude/hooks-windows/speed-manager.ps1 +166 -0
- package/.claude/hooks-windows/verbosity-manager.ps1 +119 -0
- package/.claude/hooks-windows/voice-manager-windows.ps1 +92 -8
- package/.claude/output-styles/agent-vibes.md +202 -202
- package/.claude/personalities/angry.md +14 -14
- package/.claude/personalities/annoying.md +14 -14
- package/.claude/personalities/crass.md +14 -14
- package/.claude/personalities/dramatic.md +14 -14
- package/.claude/personalities/dry-humor.md +50 -50
- package/.claude/personalities/flirty.md +20 -20
- package/.claude/personalities/funny.md +14 -14
- package/.claude/personalities/grandpa.md +32 -32
- package/.claude/personalities/millennial.md +14 -14
- package/.claude/personalities/moody.md +14 -14
- package/.claude/personalities/normal.md +16 -16
- package/.claude/personalities/pirate.md +14 -14
- package/.claude/personalities/poetic.md +14 -14
- package/.claude/personalities/professional.md +14 -14
- package/.claude/personalities/rapper.md +55 -55
- package/.claude/personalities/robot.md +14 -14
- package/.claude/personalities/sarcastic.md +38 -38
- package/.claude/personalities/sassy.md +14 -14
- package/.claude/personalities/surfer-dude.md +14 -14
- package/.claude/personalities/zen.md +14 -14
- package/.claude/settings.json +15 -15
- package/.claude/verbosity.txt +1 -1
- package/.clawdbot/README.md +105 -105
- package/.clawdbot/skill/SKILL.md +241 -241
- package/.mcp.json +12 -0
- package/CLAUDE.md +170 -170
- package/README.md +2029 -2007
- package/RELEASE_NOTES.md +1310 -1203
- package/WINDOWS-SETUP.md +208 -208
- package/bin/agent-vibes +39 -39
- package/bin/agentvibes-voice-browser.js +1840 -1840
- package/bin/agentvibes.js +48 -2
- package/bin/mcp-server.js +121 -121
- package/bin/mcp-server.sh +206 -206
- package/bin/test-bmad-pr +78 -78
- package/mcp-server/QUICK_START.md +203 -203
- package/mcp-server/README.md +345 -345
- package/mcp-server/WINDOWS_SETUP.md +260 -260
- package/mcp-server/docs/troubleshooting-audio.md +313 -313
- package/mcp-server/examples/claude_desktop_config.json +11 -11
- package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
- package/mcp-server/examples/custom_instructions.md +169 -169
- package/mcp-server/install-deps.js +130 -130
- package/mcp-server/pyproject.toml +52 -52
- package/mcp-server/requirements.txt +2 -2
- package/mcp-server/server.py +1465 -1453
- package/mcp-server/test_server.py +395 -395
- package/mcp-server/test_windows_script_parity.py +336 -0
- package/package.json +110 -110
- package/setup-windows.ps1 +815 -815
- package/src/bmad-detector.js +71 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/app.js +824 -824
- package/src/console/audio-env.js +20 -1
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/footer-config.js +50 -50
- package/src/console/modals/modal-overlay.js +247 -247
- package/src/console/navigation.js +62 -62
- package/src/console/tabs/agents-tab.js +1684 -1516
- package/src/console/tabs/help-tab.js +261 -261
- package/src/console/tabs/install-tab.js +1007 -991
- package/src/console/tabs/music-tab.js +22 -8
- package/src/console/tabs/placeholder-tab.js +53 -53
- package/src/console/tabs/readme-tab.js +267 -267
- package/src/console/tabs/receiver-tab.js +1472 -1212
- package/src/console/tabs/settings-tab.js +208 -84
- package/src/console/tabs/voices-tab.js +100 -21
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/format-utils.js +89 -89
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +185 -185
- package/src/console/widgets/reverb-picker.js +94 -94
- package/src/console/widgets/track-picker.js +285 -285
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +5895 -5829
- package/src/services/agent-voice-store.js +423 -423
- package/src/services/config-service.js +264 -264
- package/src/services/navigation-service.js +123 -123
- package/src/services/provider-service.js +143 -132
- package/src/services/verbosity-service.js +157 -157
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -466
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/provider-validator.js +96 -12
- package/src/utils/secure-music-storage.js +412 -412
- package/templates/agentvibes-receiver.sh +482 -482
- package/templates/audio/welcome-music.mp3 +0 -0
- package/voice-assignments.json +8244 -8244
- package/.claude/config/background-music-position.txt +0 -1
|
@@ -1,277 +1,277 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Audio Format Validator - Magic Number Detection
|
|
3
|
-
* Story 4.2: Audio Format Detection (Magic Number Validation)
|
|
4
|
-
*
|
|
5
|
-
* Validates audio file format by checking file headers (magic numbers).
|
|
6
|
-
* Supported formats: MP3, WAV, OGG, M4A
|
|
7
|
-
*
|
|
8
|
-
* @module audio-format-validator
|
|
9
|
-
* @requires fs
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Supported audio formats with their magic number signatures
|
|
17
|
-
*/
|
|
18
|
-
const AUDIO_FORMATS = {
|
|
19
|
-
mp3: {
|
|
20
|
-
extension: '.mp3',
|
|
21
|
-
magicNumbers: [
|
|
22
|
-
// MPEG-1/2/2.5 Audio Frame Header
|
|
23
|
-
{ bytes: Buffer.from([0xFF, 0xFB]), offset: 0, name: 'MP3 MPEG-2 Layer III' },
|
|
24
|
-
{ bytes: Buffer.from([0xFF, 0xFA]), offset: 0, name: 'MP3 MPEG-1 Layer III' },
|
|
25
|
-
// ID3 Tag (v2.x and v2.4)
|
|
26
|
-
{ bytes: Buffer.from([0x49, 0x44, 0x33]), offset: 0, name: 'ID3v2 Tag' } // "ID3"
|
|
27
|
-
]
|
|
28
|
-
},
|
|
29
|
-
wav: {
|
|
30
|
-
extension: '.wav',
|
|
31
|
-
magicNumbers: [
|
|
32
|
-
{ bytes: Buffer.from('RIFF'), offset: 0, name: 'RIFF Header' },
|
|
33
|
-
{ bytes: Buffer.from('WAVE'), offset: 8, name: 'WAV Format' }
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
ogg: {
|
|
37
|
-
extension: '.ogg',
|
|
38
|
-
magicNumbers: [
|
|
39
|
-
{ bytes: Buffer.from('OggS'), offset: 0, name: 'OggS Header' }
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
m4a: {
|
|
43
|
-
extension: '.m4a',
|
|
44
|
-
magicNumbers: [
|
|
45
|
-
{ bytes: Buffer.from('ftypisom'), offset: 4, name: 'M4A ISO Base Media' },
|
|
46
|
-
{ bytes: Buffer.from('ftypM4A'), offset: 4, name: 'M4A Apple iTunes' }
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const SUPPORTED_EXTENSIONS = Object.values(AUDIO_FORMATS).map(f => f.extension);
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Story 4.2: Detect audio format by checking magic numbers in file header
|
|
55
|
-
*
|
|
56
|
-
* Reads the first 256 bytes of the file to check magic number signatures.
|
|
57
|
-
* Does NOT require the file to have the correct extension matching format.
|
|
58
|
-
*
|
|
59
|
-
* @param {string} filePath - Path to audio file (must already be validated with isPathSafe)
|
|
60
|
-
* @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null }
|
|
61
|
-
*
|
|
62
|
-
* Object properties:
|
|
63
|
-
* - isValid: true if file is valid audio format
|
|
64
|
-
* - error: null on success, error message on failure
|
|
65
|
-
* - format: detected format ('mp3', 'wav', 'ogg', 'm4a') or null
|
|
66
|
-
* - detectedFormat: human-readable format name (e.g., "MP3 MPEG-1 Layer III")
|
|
67
|
-
*/
|
|
68
|
-
function detectAudioFormat(filePath) {
|
|
69
|
-
try {
|
|
70
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
71
|
-
return {
|
|
72
|
-
isValid: false,
|
|
73
|
-
error: 'File path must be a non-empty string',
|
|
74
|
-
format: null,
|
|
75
|
-
detectedFormat: null
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check file exists and is readable
|
|
80
|
-
if (!fs.existsSync(filePath)) {
|
|
81
|
-
return {
|
|
82
|
-
isValid: false,
|
|
83
|
-
error: 'File does not exist',
|
|
84
|
-
format: null,
|
|
85
|
-
detectedFormat: null
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const stats = fs.statSync(filePath);
|
|
90
|
-
if (!stats.isFile()) {
|
|
91
|
-
return {
|
|
92
|
-
isValid: false,
|
|
93
|
-
error: 'Path must be a regular file',
|
|
94
|
-
format: null,
|
|
95
|
-
detectedFormat: null
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Check minimum file size (at least 12 bytes for WAV format check)
|
|
100
|
-
if (stats.size < 12) {
|
|
101
|
-
return {
|
|
102
|
-
isValid: false,
|
|
103
|
-
error: 'File is too small to be a valid audio file (minimum 12 bytes)',
|
|
104
|
-
format: null,
|
|
105
|
-
detectedFormat: null
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Read first 256 bytes for magic number detection
|
|
110
|
-
const buffer = Buffer.alloc(256);
|
|
111
|
-
const fd = fs.openSync(filePath, 'r');
|
|
112
|
-
try {
|
|
113
|
-
fs.readSync(fd, buffer, 0, 256, 0);
|
|
114
|
-
} finally {
|
|
115
|
-
fs.closeSync(fd);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Check each format's magic numbers
|
|
119
|
-
for (const [formatKey, formatInfo] of Object.entries(AUDIO_FORMATS)) {
|
|
120
|
-
for (const magicInfo of formatInfo.magicNumbers) {
|
|
121
|
-
// Check if magic bytes exist at the specified offset
|
|
122
|
-
const isMatch = buffer.subarray(magicInfo.offset, magicInfo.offset + magicInfo.bytes.length)
|
|
123
|
-
.equals(magicInfo.bytes);
|
|
124
|
-
|
|
125
|
-
if (isMatch) {
|
|
126
|
-
return {
|
|
127
|
-
isValid: true,
|
|
128
|
-
error: null,
|
|
129
|
-
format: formatKey,
|
|
130
|
-
detectedFormat: magicInfo.name
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// No magic numbers matched
|
|
137
|
-
return {
|
|
138
|
-
isValid: false,
|
|
139
|
-
error: `Unsupported audio format. Supported formats: ${SUPPORTED_EXTENSIONS.join(', ')}`,
|
|
140
|
-
format: null,
|
|
141
|
-
detectedFormat: null
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
} catch (err) {
|
|
145
|
-
return {
|
|
146
|
-
isValid: false,
|
|
147
|
-
error: `Error detecting audio format: ${err.message}`,
|
|
148
|
-
format: null,
|
|
149
|
-
detectedFormat: null
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Story 4.2: Validate that file extension matches detected audio format
|
|
156
|
-
*
|
|
157
|
-
* Ensures the file extension is correct for the detected format.
|
|
158
|
-
* This prevents accidental misnamed files (e.g., .txt file with MP3 content).
|
|
159
|
-
*
|
|
160
|
-
* @param {string} filePath - Path to audio file
|
|
161
|
-
* @param {string} detectedFormat - Format from detectAudioFormat() result
|
|
162
|
-
* @returns {Object} { isValid: boolean, error: string|null }
|
|
163
|
-
*/
|
|
164
|
-
function validateFileExtension(filePath, detectedFormat) {
|
|
165
|
-
try {
|
|
166
|
-
if (!filePath || !detectedFormat) {
|
|
167
|
-
return {
|
|
168
|
-
isValid: false,
|
|
169
|
-
error: 'File path and detected format are required'
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
174
|
-
const expectedExt = AUDIO_FORMATS[detectedFormat]?.extension;
|
|
175
|
-
|
|
176
|
-
if (!expectedExt) {
|
|
177
|
-
return {
|
|
178
|
-
isValid: false,
|
|
179
|
-
error: `Unknown detected format: ${detectedFormat}`
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (ext === expectedExt) {
|
|
184
|
-
return {
|
|
185
|
-
isValid: true,
|
|
186
|
-
error: null
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Extension doesn't match - return specific error
|
|
191
|
-
return {
|
|
192
|
-
isValid: false,
|
|
193
|
-
error: `File extension (${ext || 'none'}) doesn't match actual format (.${detectedFormat})`
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
} catch (err) {
|
|
197
|
-
return {
|
|
198
|
-
isValid: false,
|
|
199
|
-
error: `Error validating file extension: ${err.message}`
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Story 4.2: Comprehensive audio file validation
|
|
206
|
-
*
|
|
207
|
-
* Combines format detection and extension validation in one call.
|
|
208
|
-
* Returns false only if BOTH format is unsupported AND extension is wrong.
|
|
209
|
-
* Returns true if format is valid (even if extension differs).
|
|
210
|
-
*
|
|
211
|
-
* @param {string} filePath - Path to audio file
|
|
212
|
-
* @param {Object} options - Validation options
|
|
213
|
-
* @param {boolean} options.strictExtension - If true, reject files with wrong extension (default: false)
|
|
214
|
-
* @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null, extensionMatch: boolean }
|
|
215
|
-
*/
|
|
216
|
-
function validateAudioFile(filePath, options = {}) {
|
|
217
|
-
const { strictExtension = false } = options;
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
// Detect format first
|
|
221
|
-
const formatResult = detectAudioFormat(filePath);
|
|
222
|
-
|
|
223
|
-
if (!formatResult.isValid) {
|
|
224
|
-
return {
|
|
225
|
-
isValid: false,
|
|
226
|
-
error: formatResult.error,
|
|
227
|
-
format: null,
|
|
228
|
-
detectedFormat: null,
|
|
229
|
-
extensionMatch: false
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Check extension if strict mode enabled
|
|
234
|
-
if (strictExtension) {
|
|
235
|
-
const extResult = validateFileExtension(filePath, formatResult.format);
|
|
236
|
-
if (!extResult.isValid) {
|
|
237
|
-
return {
|
|
238
|
-
isValid: false,
|
|
239
|
-
error: extResult.error,
|
|
240
|
-
format: formatResult.format,
|
|
241
|
-
detectedFormat: formatResult.detectedFormat,
|
|
242
|
-
extensionMatch: false
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Format is valid - check if extension matches (informational)
|
|
248
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
249
|
-
const expectedExt = AUDIO_FORMATS[formatResult.format]?.extension;
|
|
250
|
-
const extensionMatch = ext === expectedExt;
|
|
251
|
-
|
|
252
|
-
return {
|
|
253
|
-
isValid: true,
|
|
254
|
-
error: null,
|
|
255
|
-
format: formatResult.format,
|
|
256
|
-
detectedFormat: formatResult.detectedFormat,
|
|
257
|
-
extensionMatch
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
} catch (err) {
|
|
261
|
-
return {
|
|
262
|
-
isValid: false,
|
|
263
|
-
error: `Unexpected validation error: ${err.message}`,
|
|
264
|
-
format: null,
|
|
265
|
-
detectedFormat: null,
|
|
266
|
-
extensionMatch: false
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export {
|
|
272
|
-
detectAudioFormat,
|
|
273
|
-
validateFileExtension,
|
|
274
|
-
validateAudioFile,
|
|
275
|
-
AUDIO_FORMATS,
|
|
276
|
-
SUPPORTED_EXTENSIONS
|
|
277
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Audio Format Validator - Magic Number Detection
|
|
3
|
+
* Story 4.2: Audio Format Detection (Magic Number Validation)
|
|
4
|
+
*
|
|
5
|
+
* Validates audio file format by checking file headers (magic numbers).
|
|
6
|
+
* Supported formats: MP3, WAV, OGG, M4A
|
|
7
|
+
*
|
|
8
|
+
* @module audio-format-validator
|
|
9
|
+
* @requires fs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Supported audio formats with their magic number signatures
|
|
17
|
+
*/
|
|
18
|
+
const AUDIO_FORMATS = {
|
|
19
|
+
mp3: {
|
|
20
|
+
extension: '.mp3',
|
|
21
|
+
magicNumbers: [
|
|
22
|
+
// MPEG-1/2/2.5 Audio Frame Header
|
|
23
|
+
{ bytes: Buffer.from([0xFF, 0xFB]), offset: 0, name: 'MP3 MPEG-2 Layer III' },
|
|
24
|
+
{ bytes: Buffer.from([0xFF, 0xFA]), offset: 0, name: 'MP3 MPEG-1 Layer III' },
|
|
25
|
+
// ID3 Tag (v2.x and v2.4)
|
|
26
|
+
{ bytes: Buffer.from([0x49, 0x44, 0x33]), offset: 0, name: 'ID3v2 Tag' } // "ID3"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
wav: {
|
|
30
|
+
extension: '.wav',
|
|
31
|
+
magicNumbers: [
|
|
32
|
+
{ bytes: Buffer.from('RIFF'), offset: 0, name: 'RIFF Header' },
|
|
33
|
+
{ bytes: Buffer.from('WAVE'), offset: 8, name: 'WAV Format' }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
ogg: {
|
|
37
|
+
extension: '.ogg',
|
|
38
|
+
magicNumbers: [
|
|
39
|
+
{ bytes: Buffer.from('OggS'), offset: 0, name: 'OggS Header' }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
m4a: {
|
|
43
|
+
extension: '.m4a',
|
|
44
|
+
magicNumbers: [
|
|
45
|
+
{ bytes: Buffer.from('ftypisom'), offset: 4, name: 'M4A ISO Base Media' },
|
|
46
|
+
{ bytes: Buffer.from('ftypM4A'), offset: 4, name: 'M4A Apple iTunes' }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const SUPPORTED_EXTENSIONS = Object.values(AUDIO_FORMATS).map(f => f.extension);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Story 4.2: Detect audio format by checking magic numbers in file header
|
|
55
|
+
*
|
|
56
|
+
* Reads the first 256 bytes of the file to check magic number signatures.
|
|
57
|
+
* Does NOT require the file to have the correct extension matching format.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} filePath - Path to audio file (must already be validated with isPathSafe)
|
|
60
|
+
* @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null }
|
|
61
|
+
*
|
|
62
|
+
* Object properties:
|
|
63
|
+
* - isValid: true if file is valid audio format
|
|
64
|
+
* - error: null on success, error message on failure
|
|
65
|
+
* - format: detected format ('mp3', 'wav', 'ogg', 'm4a') or null
|
|
66
|
+
* - detectedFormat: human-readable format name (e.g., "MP3 MPEG-1 Layer III")
|
|
67
|
+
*/
|
|
68
|
+
function detectAudioFormat(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
71
|
+
return {
|
|
72
|
+
isValid: false,
|
|
73
|
+
error: 'File path must be a non-empty string',
|
|
74
|
+
format: null,
|
|
75
|
+
detectedFormat: null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check file exists and is readable
|
|
80
|
+
if (!fs.existsSync(filePath)) {
|
|
81
|
+
return {
|
|
82
|
+
isValid: false,
|
|
83
|
+
error: 'File does not exist',
|
|
84
|
+
format: null,
|
|
85
|
+
detectedFormat: null
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const stats = fs.statSync(filePath);
|
|
90
|
+
if (!stats.isFile()) {
|
|
91
|
+
return {
|
|
92
|
+
isValid: false,
|
|
93
|
+
error: 'Path must be a regular file',
|
|
94
|
+
format: null,
|
|
95
|
+
detectedFormat: null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check minimum file size (at least 12 bytes for WAV format check)
|
|
100
|
+
if (stats.size < 12) {
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
error: 'File is too small to be a valid audio file (minimum 12 bytes)',
|
|
104
|
+
format: null,
|
|
105
|
+
detectedFormat: null
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Read first 256 bytes for magic number detection
|
|
110
|
+
const buffer = Buffer.alloc(256);
|
|
111
|
+
const fd = fs.openSync(filePath, 'r');
|
|
112
|
+
try {
|
|
113
|
+
fs.readSync(fd, buffer, 0, 256, 0);
|
|
114
|
+
} finally {
|
|
115
|
+
fs.closeSync(fd);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check each format's magic numbers
|
|
119
|
+
for (const [formatKey, formatInfo] of Object.entries(AUDIO_FORMATS)) {
|
|
120
|
+
for (const magicInfo of formatInfo.magicNumbers) {
|
|
121
|
+
// Check if magic bytes exist at the specified offset
|
|
122
|
+
const isMatch = buffer.subarray(magicInfo.offset, magicInfo.offset + magicInfo.bytes.length)
|
|
123
|
+
.equals(magicInfo.bytes);
|
|
124
|
+
|
|
125
|
+
if (isMatch) {
|
|
126
|
+
return {
|
|
127
|
+
isValid: true,
|
|
128
|
+
error: null,
|
|
129
|
+
format: formatKey,
|
|
130
|
+
detectedFormat: magicInfo.name
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// No magic numbers matched
|
|
137
|
+
return {
|
|
138
|
+
isValid: false,
|
|
139
|
+
error: `Unsupported audio format. Supported formats: ${SUPPORTED_EXTENSIONS.join(', ')}`,
|
|
140
|
+
format: null,
|
|
141
|
+
detectedFormat: null
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return {
|
|
146
|
+
isValid: false,
|
|
147
|
+
error: `Error detecting audio format: ${err.message}`,
|
|
148
|
+
format: null,
|
|
149
|
+
detectedFormat: null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Story 4.2: Validate that file extension matches detected audio format
|
|
156
|
+
*
|
|
157
|
+
* Ensures the file extension is correct for the detected format.
|
|
158
|
+
* This prevents accidental misnamed files (e.g., .txt file with MP3 content).
|
|
159
|
+
*
|
|
160
|
+
* @param {string} filePath - Path to audio file
|
|
161
|
+
* @param {string} detectedFormat - Format from detectAudioFormat() result
|
|
162
|
+
* @returns {Object} { isValid: boolean, error: string|null }
|
|
163
|
+
*/
|
|
164
|
+
function validateFileExtension(filePath, detectedFormat) {
|
|
165
|
+
try {
|
|
166
|
+
if (!filePath || !detectedFormat) {
|
|
167
|
+
return {
|
|
168
|
+
isValid: false,
|
|
169
|
+
error: 'File path and detected format are required'
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
174
|
+
const expectedExt = AUDIO_FORMATS[detectedFormat]?.extension;
|
|
175
|
+
|
|
176
|
+
if (!expectedExt) {
|
|
177
|
+
return {
|
|
178
|
+
isValid: false,
|
|
179
|
+
error: `Unknown detected format: ${detectedFormat}`
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (ext === expectedExt) {
|
|
184
|
+
return {
|
|
185
|
+
isValid: true,
|
|
186
|
+
error: null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extension doesn't match - return specific error
|
|
191
|
+
return {
|
|
192
|
+
isValid: false,
|
|
193
|
+
error: `File extension (${ext || 'none'}) doesn't match actual format (.${detectedFormat})`
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return {
|
|
198
|
+
isValid: false,
|
|
199
|
+
error: `Error validating file extension: ${err.message}`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Story 4.2: Comprehensive audio file validation
|
|
206
|
+
*
|
|
207
|
+
* Combines format detection and extension validation in one call.
|
|
208
|
+
* Returns false only if BOTH format is unsupported AND extension is wrong.
|
|
209
|
+
* Returns true if format is valid (even if extension differs).
|
|
210
|
+
*
|
|
211
|
+
* @param {string} filePath - Path to audio file
|
|
212
|
+
* @param {Object} options - Validation options
|
|
213
|
+
* @param {boolean} options.strictExtension - If true, reject files with wrong extension (default: false)
|
|
214
|
+
* @returns {Object} { isValid: boolean, error: string|null, format: string|null, detectedFormat: string|null, extensionMatch: boolean }
|
|
215
|
+
*/
|
|
216
|
+
function validateAudioFile(filePath, options = {}) {
|
|
217
|
+
const { strictExtension = false } = options;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Detect format first
|
|
221
|
+
const formatResult = detectAudioFormat(filePath);
|
|
222
|
+
|
|
223
|
+
if (!formatResult.isValid) {
|
|
224
|
+
return {
|
|
225
|
+
isValid: false,
|
|
226
|
+
error: formatResult.error,
|
|
227
|
+
format: null,
|
|
228
|
+
detectedFormat: null,
|
|
229
|
+
extensionMatch: false
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check extension if strict mode enabled
|
|
234
|
+
if (strictExtension) {
|
|
235
|
+
const extResult = validateFileExtension(filePath, formatResult.format);
|
|
236
|
+
if (!extResult.isValid) {
|
|
237
|
+
return {
|
|
238
|
+
isValid: false,
|
|
239
|
+
error: extResult.error,
|
|
240
|
+
format: formatResult.format,
|
|
241
|
+
detectedFormat: formatResult.detectedFormat,
|
|
242
|
+
extensionMatch: false
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Format is valid - check if extension matches (informational)
|
|
248
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
249
|
+
const expectedExt = AUDIO_FORMATS[formatResult.format]?.extension;
|
|
250
|
+
const extensionMatch = ext === expectedExt;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
isValid: true,
|
|
254
|
+
error: null,
|
|
255
|
+
format: formatResult.format,
|
|
256
|
+
detectedFormat: formatResult.detectedFormat,
|
|
257
|
+
extensionMatch
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
} catch (err) {
|
|
261
|
+
return {
|
|
262
|
+
isValid: false,
|
|
263
|
+
error: `Unexpected validation error: ${err.message}`,
|
|
264
|
+
format: null,
|
|
265
|
+
detectedFormat: null,
|
|
266
|
+
extensionMatch: false
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export {
|
|
272
|
+
detectAudioFormat,
|
|
273
|
+
validateFileExtension,
|
|
274
|
+
validateAudioFile,
|
|
275
|
+
AUDIO_FORMATS,
|
|
276
|
+
SUPPORTED_EXTENSIONS
|
|
277
|
+
};
|