@vue-skuilder/express 0.1.20 → 0.1.22
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/assets/courseValidateDocUpdate.js +2 -1
- package/dist/assets/courseValidateDocUpdate.js +2 -1
- package/dist/attachment-preprocessing/normalize-local.d.ts +3 -0
- package/dist/attachment-preprocessing/normalize-local.d.ts.map +1 -0
- package/dist/attachment-preprocessing/normalize-local.js +169 -0
- package/dist/attachment-preprocessing/normalize-local.js.map +1 -0
- package/package.json +4 -4
- package/src/attachment-preprocessing/normalize-local.ts +228 -0
|
@@ -50,7 +50,8 @@ function(newDoc, oldDoc, userCtx, secObj) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Special case for design documents - only admins can modify (handled above)
|
|
53
|
-
|
|
53
|
+
// Use indexOf instead of startsWith for CouchDB SpiderMonkey compatibility (ES5)
|
|
54
|
+
if (newDoc._id.indexOf('_design/') === 0) {
|
|
54
55
|
throw({forbidden: "Only admins can modify design documents"});
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -50,7 +50,8 @@ function(newDoc, oldDoc, userCtx, secObj) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Special case for design documents - only admins can modify (handled above)
|
|
53
|
-
|
|
53
|
+
// Use indexOf instead of startsWith for CouchDB SpiderMonkey compatibility (ES5)
|
|
54
|
+
if (newDoc._id.indexOf('_design/') === 0) {
|
|
54
55
|
throw({forbidden: "Only admins can modify design documents"});
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-local.d.ts","sourceRoot":"","sources":["../../src/attachment-preprocessing/normalize-local.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { exec as execCallback } from 'child_process';
|
|
6
|
+
const exec = promisify(execCallback);
|
|
7
|
+
import FFMPEGstatic from 'ffmpeg-static';
|
|
8
|
+
if (!FFMPEGstatic) {
|
|
9
|
+
error('FFMPEGstatic executable not found');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
// string | null here - but we know it's a string from the above check
|
|
13
|
+
const FFMPEG = FFMPEGstatic;
|
|
14
|
+
function log(s) {
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.log(s);
|
|
17
|
+
}
|
|
18
|
+
function error(s) {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.error(s);
|
|
21
|
+
}
|
|
22
|
+
function showUsage() {
|
|
23
|
+
log(`
|
|
24
|
+
Usage: tsx normalize-local.ts <input-directory> [-o <output-directory>]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-o <output-directory> Directory to write normalized files (created if needed)
|
|
28
|
+
Default: same as input directory
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
tsx normalize-local.ts .
|
|
32
|
+
tsx normalize-local.ts /path/to/wavs -o /path/to/output
|
|
33
|
+
tsx normalize-local.ts . -o ./normalized
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
log(`FFMPEG path: ${FFMPEG}`);
|
|
37
|
+
/**
|
|
38
|
+
* Normalizes a single wav file to mp3 with loudnorm
|
|
39
|
+
* Same spec as the attachment preprocessing: I=-16:TP=-1.5:LRA=11
|
|
40
|
+
*/
|
|
41
|
+
async function normalizeFile(inputPath, outputDir) {
|
|
42
|
+
const tmpDir = fs.mkdtempSync(`audioNormalize-local-`);
|
|
43
|
+
const baseName = path.basename(inputPath, '.wav');
|
|
44
|
+
const targetDir = outputDir || path.dirname(inputPath);
|
|
45
|
+
const outputPath = path.join(targetDir, `${baseName}.normalized.mp3`);
|
|
46
|
+
const PADDED = path.join(tmpDir, 'padded.wav');
|
|
47
|
+
const PADDED_NORMALIZED = path.join(tmpDir, 'paddedNormalized.wav');
|
|
48
|
+
const NORMALIZED = path.join(tmpDir, 'normalized.mp3');
|
|
49
|
+
try {
|
|
50
|
+
log(`[${baseName}] Processing...`);
|
|
51
|
+
// Pad with 10s of silence
|
|
52
|
+
log(`[${baseName}] Padding with silence...`);
|
|
53
|
+
await exec(`"${FFMPEG}" -i "${inputPath}" -af "adelay=10000|10000" "${PADDED}"`);
|
|
54
|
+
// Analyze loudness
|
|
55
|
+
log(`[${baseName}] Analyzing loudness...`);
|
|
56
|
+
const info = await exec(`"${FFMPEG}" -i "${PADDED}" -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -`);
|
|
57
|
+
const data = JSON.parse(info.stderr.substring(info.stderr.indexOf('{')));
|
|
58
|
+
log(`[${baseName}] Input loudness: I=${data.input_i} LUFS, TP=${data.input_tp} dBTP, LRA=${data.input_lra} LU`);
|
|
59
|
+
// Normalize the padded file
|
|
60
|
+
log(`[${baseName}] Normalizing...`);
|
|
61
|
+
await exec(`"${FFMPEG}" -i "${PADDED}" -af ` +
|
|
62
|
+
`loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=${data.input_i}:` +
|
|
63
|
+
`measured_LRA=${data.input_lra}:measured_TP=${data.input_tp}:` +
|
|
64
|
+
`measured_thresh=${data.input_thresh}:offset=${data.target_offset}:linear=true:` +
|
|
65
|
+
`print_format=summary -ar 48k "${PADDED_NORMALIZED}"`);
|
|
66
|
+
// Cut off the padded part and convert to mp3
|
|
67
|
+
log(`[${baseName}] Cutting padding and encoding to mp3...`);
|
|
68
|
+
await exec(`"${FFMPEG}" -i "${PADDED_NORMALIZED}" -ss 00:00:10.000 -acodec libmp3lame -b:a 192k "${NORMALIZED}"`);
|
|
69
|
+
// Copy to output location
|
|
70
|
+
fs.copyFileSync(NORMALIZED, outputPath);
|
|
71
|
+
log(`[${baseName}] ✓ Saved to: ${outputPath}`);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
error(`[${baseName}] Error: ${e}`);
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
// Cleanup temp directory
|
|
79
|
+
const files = fs.readdirSync(tmpDir);
|
|
80
|
+
files.forEach((file) => {
|
|
81
|
+
fs.unlinkSync(path.join(tmpDir, file));
|
|
82
|
+
});
|
|
83
|
+
fs.rmdirSync(tmpDir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function main() {
|
|
87
|
+
// Check FFMPEG availability
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(FFMPEG)) {
|
|
90
|
+
error(`FFMPEG executable not found at path: ${FFMPEG}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const result = await exec(`"${FFMPEG}" -version`);
|
|
94
|
+
const version = result.stdout.split('\n')[0];
|
|
95
|
+
log(`FFMPEG version: ${version}`);
|
|
96
|
+
// Verify loudnorm filter availability
|
|
97
|
+
const filters = await exec(`"${FFMPEG}" -filters | grep loudnorm`);
|
|
98
|
+
if (!filters.stdout.includes('loudnorm')) {
|
|
99
|
+
throw new Error('loudnorm filter not available');
|
|
100
|
+
}
|
|
101
|
+
log('loudnorm filter: available\n');
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
error(`FFMPEG check failed: ${e}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
// Parse command line arguments
|
|
108
|
+
const args = process.argv.slice(2);
|
|
109
|
+
let targetDir = '.';
|
|
110
|
+
let outputDir;
|
|
111
|
+
// Check for help flag
|
|
112
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
113
|
+
showUsage();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
for (let i = 0; i < args.length; i++) {
|
|
117
|
+
if (args[i] === '-o' && args[i + 1]) {
|
|
118
|
+
outputDir = args[i + 1];
|
|
119
|
+
i++; // Skip next arg
|
|
120
|
+
}
|
|
121
|
+
else if (!args[i].startsWith('-')) {
|
|
122
|
+
targetDir = args[i];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const absoluteDir = path.resolve(targetDir);
|
|
126
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
127
|
+
error(`Directory not found: ${absoluteDir}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
131
|
+
error(`Not a directory: ${absoluteDir}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
// Create output directory if specified
|
|
135
|
+
let absoluteOutputDir;
|
|
136
|
+
if (outputDir) {
|
|
137
|
+
absoluteOutputDir = path.resolve(outputDir);
|
|
138
|
+
if (!fs.existsSync(absoluteOutputDir)) {
|
|
139
|
+
fs.mkdirSync(absoluteOutputDir, { recursive: true });
|
|
140
|
+
log(`Created output directory: ${absoluteOutputDir}`);
|
|
141
|
+
}
|
|
142
|
+
else if (!fs.statSync(absoluteOutputDir).isDirectory()) {
|
|
143
|
+
error(`Output path exists but is not a directory: ${absoluteOutputDir}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
log(`Output directory: ${absoluteOutputDir}\n`);
|
|
147
|
+
}
|
|
148
|
+
log(`Scanning directory: ${absoluteDir}\n`);
|
|
149
|
+
// Find all .wav files
|
|
150
|
+
const files = fs.readdirSync(absoluteDir);
|
|
151
|
+
const wavFiles = files.filter((f) => f.toLowerCase().endsWith('.wav'));
|
|
152
|
+
if (wavFiles.length === 0) {
|
|
153
|
+
log('No .wav files found in directory');
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
log(`Found ${wavFiles.length} .wav file(s)\n`);
|
|
157
|
+
// Process each file
|
|
158
|
+
for (const wavFile of wavFiles) {
|
|
159
|
+
const fullPath = path.join(absoluteDir, wavFile);
|
|
160
|
+
await normalizeFile(fullPath, absoluteOutputDir);
|
|
161
|
+
log(''); // blank line between files
|
|
162
|
+
}
|
|
163
|
+
log(`\nComplete! Processed ${wavFiles.length} file(s)`);
|
|
164
|
+
}
|
|
165
|
+
main().catch((error) => {
|
|
166
|
+
error('Fatal error:', error);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=normalize-local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-local.js","sourceRoot":"","sources":["../../src/attachment-preprocessing/normalize-local.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.22",
|
|
7
7
|
"description": "an API",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"type": "module",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"author": "Colin Kennedy",
|
|
36
36
|
"license": "GPL-3.0-or-later",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vue-skuilder/common": "^0.1.
|
|
39
|
-
"@vue-skuilder/db": "^0.1.
|
|
38
|
+
"@vue-skuilder/common": "^0.1.22",
|
|
39
|
+
"@vue-skuilder/db": "^0.1.22",
|
|
40
40
|
"axios": "^1.12.0",
|
|
41
41
|
"cookie-parser": "^1.4.7",
|
|
42
42
|
"cors": "^2.8.5",
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
"typescript-eslint": "^8.48.1",
|
|
78
78
|
"vitest": "^4.0.15"
|
|
79
79
|
},
|
|
80
|
-
"stableVersion": "0.1.
|
|
80
|
+
"stableVersion": "0.1.22"
|
|
81
81
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { exec as execCallback } from 'child_process';
|
|
7
|
+
const exec = promisify(execCallback);
|
|
8
|
+
|
|
9
|
+
import FFMPEGstatic from 'ffmpeg-static';
|
|
10
|
+
|
|
11
|
+
if (!FFMPEGstatic) {
|
|
12
|
+
error('FFMPEGstatic executable not found');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// string | null here - but we know it's a string from the above check
|
|
17
|
+
const FFMPEG = FFMPEGstatic as unknown as string;
|
|
18
|
+
|
|
19
|
+
function log(s: string) {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.log(s);
|
|
22
|
+
}
|
|
23
|
+
function error(s: string) {
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.error(s);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function showUsage() {
|
|
29
|
+
log(`
|
|
30
|
+
Usage: tsx normalize-local.ts <input-directory> [-o <output-directory>]
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
-o <output-directory> Directory to write normalized files (created if needed)
|
|
34
|
+
Default: same as input directory
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
tsx normalize-local.ts .
|
|
38
|
+
tsx normalize-local.ts /path/to/wavs -o /path/to/output
|
|
39
|
+
tsx normalize-local.ts . -o ./normalized
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
log(`FFMPEG path: ${FFMPEG}`);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* From FFMPEG's loudnorm output - loudness data on a media file
|
|
47
|
+
*/
|
|
48
|
+
interface LoudnessData {
|
|
49
|
+
input_i: string;
|
|
50
|
+
input_tp: string;
|
|
51
|
+
input_lra: string;
|
|
52
|
+
input_thresh: string;
|
|
53
|
+
output_i: string;
|
|
54
|
+
output_tp: string;
|
|
55
|
+
output_lra: string;
|
|
56
|
+
output_thresh: string;
|
|
57
|
+
normalization_type: string;
|
|
58
|
+
target_offset: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalizes a single wav file to mp3 with loudnorm
|
|
63
|
+
* Same spec as the attachment preprocessing: I=-16:TP=-1.5:LRA=11
|
|
64
|
+
*/
|
|
65
|
+
async function normalizeFile(
|
|
66
|
+
inputPath: string,
|
|
67
|
+
outputDir?: string
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const tmpDir = fs.mkdtempSync(`audioNormalize-local-`);
|
|
70
|
+
const baseName = path.basename(inputPath, '.wav');
|
|
71
|
+
const targetDir = outputDir || path.dirname(inputPath);
|
|
72
|
+
const outputPath = path.join(targetDir, `${baseName}.normalized.mp3`);
|
|
73
|
+
|
|
74
|
+
const PADDED = path.join(tmpDir, 'padded.wav');
|
|
75
|
+
const PADDED_NORMALIZED = path.join(tmpDir, 'paddedNormalized.wav');
|
|
76
|
+
const NORMALIZED = path.join(tmpDir, 'normalized.mp3');
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
log(`[${baseName}] Processing...`);
|
|
80
|
+
|
|
81
|
+
// Pad with 10s of silence
|
|
82
|
+
log(`[${baseName}] Padding with silence...`);
|
|
83
|
+
await exec(
|
|
84
|
+
`"${FFMPEG}" -i "${inputPath}" -af "adelay=10000|10000" "${PADDED}"`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Analyze loudness
|
|
88
|
+
log(`[${baseName}] Analyzing loudness...`);
|
|
89
|
+
const info = await exec(
|
|
90
|
+
`"${FFMPEG}" -i "${PADDED}" -af loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json -f null -`
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const data: LoudnessData = JSON.parse(
|
|
94
|
+
info.stderr.substring(info.stderr.indexOf('{'))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
log(
|
|
98
|
+
`[${baseName}] Input loudness: I=${data.input_i} LUFS, TP=${data.input_tp} dBTP, LRA=${data.input_lra} LU`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Normalize the padded file
|
|
102
|
+
log(`[${baseName}] Normalizing...`);
|
|
103
|
+
await exec(
|
|
104
|
+
`"${FFMPEG}" -i "${PADDED}" -af ` +
|
|
105
|
+
`loudnorm=I=-16:TP=-1.5:LRA=11:measured_I=${data.input_i}:` +
|
|
106
|
+
`measured_LRA=${data.input_lra}:measured_TP=${data.input_tp}:` +
|
|
107
|
+
`measured_thresh=${data.input_thresh}:offset=${data.target_offset}:linear=true:` +
|
|
108
|
+
`print_format=summary -ar 48k "${PADDED_NORMALIZED}"`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Cut off the padded part and convert to mp3
|
|
112
|
+
log(`[${baseName}] Cutting padding and encoding to mp3...`);
|
|
113
|
+
await exec(
|
|
114
|
+
`"${FFMPEG}" -i "${PADDED_NORMALIZED}" -ss 00:00:10.000 -acodec libmp3lame -b:a 192k "${NORMALIZED}"`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Copy to output location
|
|
118
|
+
fs.copyFileSync(NORMALIZED, outputPath);
|
|
119
|
+
log(`[${baseName}] ✓ Saved to: ${outputPath}`);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
error(`[${baseName}] Error: ${e}`);
|
|
122
|
+
throw e;
|
|
123
|
+
} finally {
|
|
124
|
+
// Cleanup temp directory
|
|
125
|
+
const files = fs.readdirSync(tmpDir);
|
|
126
|
+
files.forEach((file) => {
|
|
127
|
+
fs.unlinkSync(path.join(tmpDir, file));
|
|
128
|
+
});
|
|
129
|
+
fs.rmdirSync(tmpDir);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function main() {
|
|
134
|
+
// Check FFMPEG availability
|
|
135
|
+
try {
|
|
136
|
+
if (!fs.existsSync(FFMPEG)) {
|
|
137
|
+
error(`FFMPEG executable not found at path: ${FFMPEG}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = await exec(`"${FFMPEG}" -version`);
|
|
142
|
+
const version = result.stdout.split('\n')[0];
|
|
143
|
+
log(`FFMPEG version: ${version}`);
|
|
144
|
+
|
|
145
|
+
// Verify loudnorm filter availability
|
|
146
|
+
const filters = await exec(`"${FFMPEG}" -filters | grep loudnorm`);
|
|
147
|
+
if (!filters.stdout.includes('loudnorm')) {
|
|
148
|
+
throw new Error('loudnorm filter not available');
|
|
149
|
+
}
|
|
150
|
+
log('loudnorm filter: available\n');
|
|
151
|
+
} catch (e) {
|
|
152
|
+
error(`FFMPEG check failed: ${e}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Parse command line arguments
|
|
157
|
+
const args = process.argv.slice(2);
|
|
158
|
+
let targetDir = '.';
|
|
159
|
+
let outputDir: string | undefined;
|
|
160
|
+
|
|
161
|
+
// Check for help flag
|
|
162
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
163
|
+
showUsage();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < args.length; i++) {
|
|
168
|
+
if (args[i] === '-o' && args[i + 1]) {
|
|
169
|
+
outputDir = args[i + 1];
|
|
170
|
+
i++; // Skip next arg
|
|
171
|
+
} else if (!args[i].startsWith('-')) {
|
|
172
|
+
targetDir = args[i];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const absoluteDir = path.resolve(targetDir);
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
179
|
+
error(`Directory not found: ${absoluteDir}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!fs.statSync(absoluteDir).isDirectory()) {
|
|
184
|
+
error(`Not a directory: ${absoluteDir}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create output directory if specified
|
|
189
|
+
let absoluteOutputDir: string | undefined;
|
|
190
|
+
if (outputDir) {
|
|
191
|
+
absoluteOutputDir = path.resolve(outputDir);
|
|
192
|
+
if (!fs.existsSync(absoluteOutputDir)) {
|
|
193
|
+
fs.mkdirSync(absoluteOutputDir, { recursive: true });
|
|
194
|
+
log(`Created output directory: ${absoluteOutputDir}`);
|
|
195
|
+
} else if (!fs.statSync(absoluteOutputDir).isDirectory()) {
|
|
196
|
+
error(`Output path exists but is not a directory: ${absoluteOutputDir}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
log(`Output directory: ${absoluteOutputDir}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
log(`Scanning directory: ${absoluteDir}\n`);
|
|
203
|
+
|
|
204
|
+
// Find all .wav files
|
|
205
|
+
const files = fs.readdirSync(absoluteDir);
|
|
206
|
+
const wavFiles = files.filter((f) => f.toLowerCase().endsWith('.wav'));
|
|
207
|
+
|
|
208
|
+
if (wavFiles.length === 0) {
|
|
209
|
+
log('No .wav files found in directory');
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
log(`Found ${wavFiles.length} .wav file(s)\n`);
|
|
214
|
+
|
|
215
|
+
// Process each file
|
|
216
|
+
for (const wavFile of wavFiles) {
|
|
217
|
+
const fullPath = path.join(absoluteDir, wavFile);
|
|
218
|
+
await normalizeFile(fullPath, absoluteOutputDir);
|
|
219
|
+
log(''); // blank line between files
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
log(`\nComplete! Processed ${wavFiles.length} file(s)`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
main().catch((error) => {
|
|
226
|
+
error('Fatal error:', error);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|