@doedja/scenecut 1.0.0 → 1.0.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/README.md +280 -280
- package/bin/cli.js +292 -292
- package/dist/decoder/ffmpeg-decoder.d.ts.map +1 -1
- package/dist/decoder/ffmpeg-decoder.js +3 -1
- package/dist/decoder/ffmpeg-decoder.js.map +1 -1
- package/dist/detection.wasm.js +2 -2
- package/dist/detection.wasm.wasm +0 -0
- package/dist/keyframes.cjs.js +4 -1
- package/dist/keyframes.cjs.js.map +1 -1
- package/dist/keyframes.esm.js +3 -1
- package/dist/keyframes.esm.js.map +1 -1
- package/package.json +79 -77
package/bin/cli.js
CHANGED
|
@@ -1,293 +1,293 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Keyframes CLI - Simple command-line interface for scene detection
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* keyframes input.mp4
|
|
8
|
-
* keyframes input.mkv --output results.json
|
|
9
|
-
* keyframes video.mp4 --sensitivity high
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { detectSceneChanges } = require('../dist/keyframes.cjs.js');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
|
|
16
|
-
// Parse command line arguments
|
|
17
|
-
const args = process.argv.slice(2);
|
|
18
|
-
|
|
19
|
-
// Help text
|
|
20
|
-
const HELP = `
|
|
21
|
-
Scenecut - Scene change detection for videos
|
|
22
|
-
|
|
23
|
-
Usage:
|
|
24
|
-
scenecut <video-file> [options]
|
|
25
|
-
|
|
26
|
-
Examples:
|
|
27
|
-
scenecut input.mp4
|
|
28
|
-
scenecut video.mkv --output keyframes.txt --format aegisub
|
|
29
|
-
scenecut movie.mp4 --sensitivity high --format timecode
|
|
30
|
-
scenecut video.mp4 --format csv --output scenes.csv
|
|
31
|
-
|
|
32
|
-
Options:
|
|
33
|
-
--output, -o <file> Output file (default: {filename}_keyframes.txt)
|
|
34
|
-
--format, -f <format> Output format: json|csv|aegisub|timecode (default: aegisub)
|
|
35
|
-
--sensitivity, -s <level> Sensitivity: low|medium|high (default: medium)
|
|
36
|
-
--quiet, -q Suppress progress output
|
|
37
|
-
--verbose, -v Show detailed output
|
|
38
|
-
--help, -h Show this help
|
|
39
|
-
|
|
40
|
-
Formats:
|
|
41
|
-
json JSON with full metadata
|
|
42
|
-
csv CSV with frame,timestamp,timecode
|
|
43
|
-
aegisub (or txt) Aegisub keyframes format (frame numbers)
|
|
44
|
-
timecode (or tc) Simple timecode list (HH:MM:SS.mmm)
|
|
45
|
-
|
|
46
|
-
Video Formats:
|
|
47
|
-
Supports MP4, MKV, AVI, WebM, MOV, and any format FFmpeg supports
|
|
48
|
-
|
|
49
|
-
Output:
|
|
50
|
-
Results are saved to the output file and printed to stdout
|
|
51
|
-
`;
|
|
52
|
-
|
|
53
|
-
// Show help
|
|
54
|
-
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
55
|
-
console.log(HELP);
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Parse arguments
|
|
60
|
-
let videoPath = null;
|
|
61
|
-
let outputPath = null; // Will be derived from video filename if not specified
|
|
62
|
-
let outputFormat = 'aegisub'; // Default to Aegisub format
|
|
63
|
-
let sensitivity = 'medium';
|
|
64
|
-
let quiet = false;
|
|
65
|
-
let verbose = false;
|
|
66
|
-
|
|
67
|
-
for (let i = 0; i < args.length; i++) {
|
|
68
|
-
const arg = args[i];
|
|
69
|
-
|
|
70
|
-
if (arg === '--output' || arg === '-o') {
|
|
71
|
-
outputPath = args[++i];
|
|
72
|
-
} else if (arg === '--format' || arg === '-f') {
|
|
73
|
-
outputFormat = args[++i];
|
|
74
|
-
} else if (arg === '--sensitivity' || arg === '-s') {
|
|
75
|
-
sensitivity = args[++i];
|
|
76
|
-
} else if (arg === '--quiet' || arg === '-q') {
|
|
77
|
-
quiet = true;
|
|
78
|
-
} else if (arg === '--verbose' || arg === '-v') {
|
|
79
|
-
verbose = true;
|
|
80
|
-
} else if (!arg.startsWith('-')) {
|
|
81
|
-
videoPath = arg;
|
|
82
|
-
} else {
|
|
83
|
-
console.error(`Unknown option: ${arg}`);
|
|
84
|
-
console.error('Run "scenecut --help" for usage');
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Validate video path
|
|
90
|
-
if (!videoPath) {
|
|
91
|
-
console.error('Error: No video file specified');
|
|
92
|
-
console.error('Usage: scenecut <video-file>');
|
|
93
|
-
console.error('Run "scenecut --help" for more information');
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Resolve video path
|
|
98
|
-
videoPath = path.resolve(videoPath);
|
|
99
|
-
|
|
100
|
-
if (!fs.existsSync(videoPath)) {
|
|
101
|
-
console.error(`Error: Video file not found: ${videoPath}`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Generate default output path if not specified
|
|
106
|
-
if (!outputPath) {
|
|
107
|
-
const videoBasename = path.basename(videoPath, path.extname(videoPath));
|
|
108
|
-
const extension = (outputFormat === 'json') ? '.json' :
|
|
109
|
-
(outputFormat === 'csv') ? '.csv' : '.txt';
|
|
110
|
-
outputPath = `${videoBasename}_keyframes${extension}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Get file info
|
|
114
|
-
const stats = fs.statSync(videoPath);
|
|
115
|
-
const fileSize = (stats.size / (1024 * 1024)).toFixed(2);
|
|
116
|
-
|
|
117
|
-
// Main processing function
|
|
118
|
-
async function run() {
|
|
119
|
-
if (!quiet) {
|
|
120
|
-
console.log('Scenecut - Scene Detection');
|
|
121
|
-
console.log('='.repeat(60));
|
|
122
|
-
console.log(`Input: ${videoPath}`);
|
|
123
|
-
console.log(`Size: ${fileSize} MB`);
|
|
124
|
-
console.log(`Output: ${path.resolve(outputPath)}`);
|
|
125
|
-
console.log('='.repeat(60));
|
|
126
|
-
console.log();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const startTime = Date.now();
|
|
130
|
-
let lastProgressTime = startTime;
|
|
131
|
-
let lastProgressFrame = 0;
|
|
132
|
-
let sceneCount = 0;
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const results = await detectSceneChanges(videoPath, {
|
|
136
|
-
sensitivity,
|
|
137
|
-
searchRange: 'medium',
|
|
138
|
-
onProgress: (progress) => {
|
|
139
|
-
if (quiet) return;
|
|
140
|
-
|
|
141
|
-
const now = Date.now();
|
|
142
|
-
// Update every 3 seconds
|
|
143
|
-
if (now - lastProgressTime > 3000 || progress.percent === 100) {
|
|
144
|
-
const framesSinceLastUpdate = progress.currentFrame - lastProgressFrame;
|
|
145
|
-
const timeSinceLastUpdate = (now - lastProgressTime) / 1000;
|
|
146
|
-
const currentFps = framesSinceLastUpdate / timeSinceLastUpdate;
|
|
147
|
-
|
|
148
|
-
const progressBar = createProgressBar(progress.percent);
|
|
149
|
-
const etaStr = progress.eta ? ` ETA: ${formatTime(progress.eta)}` : '';
|
|
150
|
-
|
|
151
|
-
process.stdout.write(
|
|
152
|
-
`\r${progressBar} ${progress.percent.toString().padStart(3)}% ` +
|
|
153
|
-
`[${currentFps.toFixed(1)} fps]${etaStr}${' '.repeat(10)}`
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
lastProgressTime = now;
|
|
157
|
-
lastProgressFrame = progress.currentFrame;
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
onScene: (scene) => {
|
|
161
|
-
sceneCount++;
|
|
162
|
-
if (verbose && !quiet) {
|
|
163
|
-
console.log();
|
|
164
|
-
console.log(` Scene ${sceneCount}: Frame ${scene.frameNumber} at ${scene.timecode}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const endTime = Date.now();
|
|
170
|
-
const elapsed = (endTime - startTime) / 1000;
|
|
171
|
-
|
|
172
|
-
if (!quiet) {
|
|
173
|
-
console.log('\n');
|
|
174
|
-
console.log('='.repeat(60));
|
|
175
|
-
console.log('Complete!');
|
|
176
|
-
console.log('='.repeat(60));
|
|
177
|
-
console.log(`Scenes detected: ${results.scenes.length}`);
|
|
178
|
-
console.log(`Processing time: ${formatTime(elapsed)}`);
|
|
179
|
-
console.log(`Processing speed: ${(results.metadata.totalFrames / elapsed).toFixed(1)} fps`);
|
|
180
|
-
console.log('='.repeat(60));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Format output
|
|
184
|
-
let output;
|
|
185
|
-
if (outputFormat === 'csv') {
|
|
186
|
-
output = formatCSV(results);
|
|
187
|
-
if (outputPath.endsWith('.json')) {
|
|
188
|
-
outputPath = outputPath.replace('.json', '.csv');
|
|
189
|
-
}
|
|
190
|
-
} else if (outputFormat === 'aegisub' || outputFormat === 'txt') {
|
|
191
|
-
output = formatAegisub(results);
|
|
192
|
-
if (outputPath.endsWith('.json')) {
|
|
193
|
-
outputPath = outputPath.replace('.json', '.txt');
|
|
194
|
-
}
|
|
195
|
-
} else if (outputFormat === 'timecode' || outputFormat === 'tc') {
|
|
196
|
-
output = formatTimecode(results);
|
|
197
|
-
if (outputPath.endsWith('.json')) {
|
|
198
|
-
outputPath = outputPath.replace('.json', '.txt');
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
output = JSON.stringify(results, null, 2);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Save to file
|
|
205
|
-
fs.writeFileSync(outputPath, output);
|
|
206
|
-
|
|
207
|
-
if (!quiet) {
|
|
208
|
-
console.log(`Results saved to: ${path.resolve(outputPath)}`);
|
|
209
|
-
console.log();
|
|
210
|
-
|
|
211
|
-
// Print scene list
|
|
212
|
-
console.log('Scene List:');
|
|
213
|
-
results.scenes.forEach((scene, i) => {
|
|
214
|
-
console.log(` ${(i + 1).toString().padStart(3)}. Frame ${scene.frameNumber.toString().padStart(6)} at ${scene.timecode}`);
|
|
215
|
-
});
|
|
216
|
-
} else {
|
|
217
|
-
// In quiet mode, just print the output
|
|
218
|
-
console.log(output);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
process.exit(0);
|
|
222
|
-
|
|
223
|
-
} catch (error) {
|
|
224
|
-
console.error();
|
|
225
|
-
console.error('Error:', error.message);
|
|
226
|
-
if (verbose) {
|
|
227
|
-
console.error(error.stack);
|
|
228
|
-
}
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Helper functions
|
|
234
|
-
function createProgressBar(percent) {
|
|
235
|
-
const width = 30;
|
|
236
|
-
const filled = Math.round((percent / 100) * width);
|
|
237
|
-
const empty = width - filled;
|
|
238
|
-
return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function formatTime(seconds) {
|
|
242
|
-
const h = Math.floor(seconds / 3600);
|
|
243
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
244
|
-
const s = Math.floor(seconds % 60);
|
|
245
|
-
|
|
246
|
-
if (h > 0) {
|
|
247
|
-
return `${h}h ${m}m ${s}s`;
|
|
248
|
-
} else if (m > 0) {
|
|
249
|
-
return `${m}m ${s}s`;
|
|
250
|
-
} else {
|
|
251
|
-
return `${s}s`;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function formatCSV(results) {
|
|
256
|
-
let csv = 'frame,timestamp,timecode\n';
|
|
257
|
-
results.scenes.forEach(scene => {
|
|
258
|
-
csv += `${scene.frameNumber},${scene.timestamp},${scene.timecode || ''}\n`;
|
|
259
|
-
});
|
|
260
|
-
return csv;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function formatAegisub(results) {
|
|
264
|
-
// Aegisub keyframes format - simple text file with frame numbers
|
|
265
|
-
// One frame number per line
|
|
266
|
-
let output = '# keyframe format v1\n';
|
|
267
|
-
output += `fps ${results.metadata.fps}\n`;
|
|
268
|
-
results.scenes.forEach(scene => {
|
|
269
|
-
output += `${scene.frameNumber}\n`;
|
|
270
|
-
});
|
|
271
|
-
return output;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function formatTimecode(results) {
|
|
275
|
-
// Simple timecode list - one per line
|
|
276
|
-
// Can be used with various subtitle tools
|
|
277
|
-
let output = '';
|
|
278
|
-
results.scenes.forEach(scene => {
|
|
279
|
-
output += `${scene.timecode || formatTimecodeFull(scene.timestamp)}\n`;
|
|
280
|
-
});
|
|
281
|
-
return output;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function formatTimecodeFull(seconds) {
|
|
285
|
-
const h = Math.floor(seconds / 3600);
|
|
286
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
287
|
-
const s = Math.floor(seconds % 60);
|
|
288
|
-
const ms = Math.floor((seconds % 1) * 1000);
|
|
289
|
-
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Run
|
|
293
|
-
run();
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keyframes CLI - Simple command-line interface for scene detection
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* keyframes input.mp4
|
|
8
|
+
* keyframes input.mkv --output results.json
|
|
9
|
+
* keyframes video.mp4 --sensitivity high
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { detectSceneChanges } = require('../dist/keyframes.cjs.js');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
|
|
16
|
+
// Parse command line arguments
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
// Help text
|
|
20
|
+
const HELP = `
|
|
21
|
+
Scenecut - Scene change detection for videos
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
scenecut <video-file> [options]
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
scenecut input.mp4
|
|
28
|
+
scenecut video.mkv --output keyframes.txt --format aegisub
|
|
29
|
+
scenecut movie.mp4 --sensitivity high --format timecode
|
|
30
|
+
scenecut video.mp4 --format csv --output scenes.csv
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--output, -o <file> Output file (default: {filename}_keyframes.txt)
|
|
34
|
+
--format, -f <format> Output format: json|csv|aegisub|timecode (default: aegisub)
|
|
35
|
+
--sensitivity, -s <level> Sensitivity: low|medium|high (default: medium)
|
|
36
|
+
--quiet, -q Suppress progress output
|
|
37
|
+
--verbose, -v Show detailed output
|
|
38
|
+
--help, -h Show this help
|
|
39
|
+
|
|
40
|
+
Formats:
|
|
41
|
+
json JSON with full metadata
|
|
42
|
+
csv CSV with frame,timestamp,timecode
|
|
43
|
+
aegisub (or txt) Aegisub keyframes format (frame numbers)
|
|
44
|
+
timecode (or tc) Simple timecode list (HH:MM:SS.mmm)
|
|
45
|
+
|
|
46
|
+
Video Formats:
|
|
47
|
+
Supports MP4, MKV, AVI, WebM, MOV, and any format FFmpeg supports
|
|
48
|
+
|
|
49
|
+
Output:
|
|
50
|
+
Results are saved to the output file and printed to stdout
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
// Show help
|
|
54
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
55
|
+
console.log(HELP);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse arguments
|
|
60
|
+
let videoPath = null;
|
|
61
|
+
let outputPath = null; // Will be derived from video filename if not specified
|
|
62
|
+
let outputFormat = 'aegisub'; // Default to Aegisub format
|
|
63
|
+
let sensitivity = 'medium';
|
|
64
|
+
let quiet = false;
|
|
65
|
+
let verbose = false;
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
const arg = args[i];
|
|
69
|
+
|
|
70
|
+
if (arg === '--output' || arg === '-o') {
|
|
71
|
+
outputPath = args[++i];
|
|
72
|
+
} else if (arg === '--format' || arg === '-f') {
|
|
73
|
+
outputFormat = args[++i];
|
|
74
|
+
} else if (arg === '--sensitivity' || arg === '-s') {
|
|
75
|
+
sensitivity = args[++i];
|
|
76
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
77
|
+
quiet = true;
|
|
78
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
79
|
+
verbose = true;
|
|
80
|
+
} else if (!arg.startsWith('-')) {
|
|
81
|
+
videoPath = arg;
|
|
82
|
+
} else {
|
|
83
|
+
console.error(`Unknown option: ${arg}`);
|
|
84
|
+
console.error('Run "scenecut --help" for usage');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate video path
|
|
90
|
+
if (!videoPath) {
|
|
91
|
+
console.error('Error: No video file specified');
|
|
92
|
+
console.error('Usage: scenecut <video-file>');
|
|
93
|
+
console.error('Run "scenecut --help" for more information');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Resolve video path
|
|
98
|
+
videoPath = path.resolve(videoPath);
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(videoPath)) {
|
|
101
|
+
console.error(`Error: Video file not found: ${videoPath}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generate default output path if not specified
|
|
106
|
+
if (!outputPath) {
|
|
107
|
+
const videoBasename = path.basename(videoPath, path.extname(videoPath));
|
|
108
|
+
const extension = (outputFormat === 'json') ? '.json' :
|
|
109
|
+
(outputFormat === 'csv') ? '.csv' : '.txt';
|
|
110
|
+
outputPath = `${videoBasename}_keyframes${extension}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get file info
|
|
114
|
+
const stats = fs.statSync(videoPath);
|
|
115
|
+
const fileSize = (stats.size / (1024 * 1024)).toFixed(2);
|
|
116
|
+
|
|
117
|
+
// Main processing function
|
|
118
|
+
async function run() {
|
|
119
|
+
if (!quiet) {
|
|
120
|
+
console.log('Scenecut - Scene Detection');
|
|
121
|
+
console.log('='.repeat(60));
|
|
122
|
+
console.log(`Input: ${videoPath}`);
|
|
123
|
+
console.log(`Size: ${fileSize} MB`);
|
|
124
|
+
console.log(`Output: ${path.resolve(outputPath)}`);
|
|
125
|
+
console.log('='.repeat(60));
|
|
126
|
+
console.log();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const startTime = Date.now();
|
|
130
|
+
let lastProgressTime = startTime;
|
|
131
|
+
let lastProgressFrame = 0;
|
|
132
|
+
let sceneCount = 0;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const results = await detectSceneChanges(videoPath, {
|
|
136
|
+
sensitivity,
|
|
137
|
+
searchRange: 'medium',
|
|
138
|
+
onProgress: (progress) => {
|
|
139
|
+
if (quiet) return;
|
|
140
|
+
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
// Update every 3 seconds
|
|
143
|
+
if (now - lastProgressTime > 3000 || progress.percent === 100) {
|
|
144
|
+
const framesSinceLastUpdate = progress.currentFrame - lastProgressFrame;
|
|
145
|
+
const timeSinceLastUpdate = (now - lastProgressTime) / 1000;
|
|
146
|
+
const currentFps = framesSinceLastUpdate / timeSinceLastUpdate;
|
|
147
|
+
|
|
148
|
+
const progressBar = createProgressBar(progress.percent);
|
|
149
|
+
const etaStr = progress.eta ? ` ETA: ${formatTime(progress.eta)}` : '';
|
|
150
|
+
|
|
151
|
+
process.stdout.write(
|
|
152
|
+
`\r${progressBar} ${progress.percent.toString().padStart(3)}% ` +
|
|
153
|
+
`[${currentFps.toFixed(1)} fps]${etaStr}${' '.repeat(10)}`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
lastProgressTime = now;
|
|
157
|
+
lastProgressFrame = progress.currentFrame;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
onScene: (scene) => {
|
|
161
|
+
sceneCount++;
|
|
162
|
+
if (verbose && !quiet) {
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(` Scene ${sceneCount}: Frame ${scene.frameNumber} at ${scene.timecode}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const endTime = Date.now();
|
|
170
|
+
const elapsed = (endTime - startTime) / 1000;
|
|
171
|
+
|
|
172
|
+
if (!quiet) {
|
|
173
|
+
console.log('\n');
|
|
174
|
+
console.log('='.repeat(60));
|
|
175
|
+
console.log('Complete!');
|
|
176
|
+
console.log('='.repeat(60));
|
|
177
|
+
console.log(`Scenes detected: ${results.scenes.length}`);
|
|
178
|
+
console.log(`Processing time: ${formatTime(elapsed)}`);
|
|
179
|
+
console.log(`Processing speed: ${(results.metadata.totalFrames / elapsed).toFixed(1)} fps`);
|
|
180
|
+
console.log('='.repeat(60));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Format output
|
|
184
|
+
let output;
|
|
185
|
+
if (outputFormat === 'csv') {
|
|
186
|
+
output = formatCSV(results);
|
|
187
|
+
if (outputPath.endsWith('.json')) {
|
|
188
|
+
outputPath = outputPath.replace('.json', '.csv');
|
|
189
|
+
}
|
|
190
|
+
} else if (outputFormat === 'aegisub' || outputFormat === 'txt') {
|
|
191
|
+
output = formatAegisub(results);
|
|
192
|
+
if (outputPath.endsWith('.json')) {
|
|
193
|
+
outputPath = outputPath.replace('.json', '.txt');
|
|
194
|
+
}
|
|
195
|
+
} else if (outputFormat === 'timecode' || outputFormat === 'tc') {
|
|
196
|
+
output = formatTimecode(results);
|
|
197
|
+
if (outputPath.endsWith('.json')) {
|
|
198
|
+
outputPath = outputPath.replace('.json', '.txt');
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
output = JSON.stringify(results, null, 2);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Save to file
|
|
205
|
+
fs.writeFileSync(outputPath, output);
|
|
206
|
+
|
|
207
|
+
if (!quiet) {
|
|
208
|
+
console.log(`Results saved to: ${path.resolve(outputPath)}`);
|
|
209
|
+
console.log();
|
|
210
|
+
|
|
211
|
+
// Print scene list
|
|
212
|
+
console.log('Scene List:');
|
|
213
|
+
results.scenes.forEach((scene, i) => {
|
|
214
|
+
console.log(` ${(i + 1).toString().padStart(3)}. Frame ${scene.frameNumber.toString().padStart(6)} at ${scene.timecode}`);
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
// In quiet mode, just print the output
|
|
218
|
+
console.log(output);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
process.exit(0);
|
|
222
|
+
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error();
|
|
225
|
+
console.error('Error:', error.message);
|
|
226
|
+
if (verbose) {
|
|
227
|
+
console.error(error.stack);
|
|
228
|
+
}
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Helper functions
|
|
234
|
+
function createProgressBar(percent) {
|
|
235
|
+
const width = 30;
|
|
236
|
+
const filled = Math.round((percent / 100) * width);
|
|
237
|
+
const empty = width - filled;
|
|
238
|
+
return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatTime(seconds) {
|
|
242
|
+
const h = Math.floor(seconds / 3600);
|
|
243
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
244
|
+
const s = Math.floor(seconds % 60);
|
|
245
|
+
|
|
246
|
+
if (h > 0) {
|
|
247
|
+
return `${h}h ${m}m ${s}s`;
|
|
248
|
+
} else if (m > 0) {
|
|
249
|
+
return `${m}m ${s}s`;
|
|
250
|
+
} else {
|
|
251
|
+
return `${s}s`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatCSV(results) {
|
|
256
|
+
let csv = 'frame,timestamp,timecode\n';
|
|
257
|
+
results.scenes.forEach(scene => {
|
|
258
|
+
csv += `${scene.frameNumber},${scene.timestamp},${scene.timecode || ''}\n`;
|
|
259
|
+
});
|
|
260
|
+
return csv;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function formatAegisub(results) {
|
|
264
|
+
// Aegisub keyframes format - simple text file with frame numbers
|
|
265
|
+
// One frame number per line
|
|
266
|
+
let output = '# keyframe format v1\n';
|
|
267
|
+
output += `fps ${results.metadata.fps}\n`;
|
|
268
|
+
results.scenes.forEach(scene => {
|
|
269
|
+
output += `${scene.frameNumber}\n`;
|
|
270
|
+
});
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatTimecode(results) {
|
|
275
|
+
// Simple timecode list - one per line
|
|
276
|
+
// Can be used with various subtitle tools
|
|
277
|
+
let output = '';
|
|
278
|
+
results.scenes.forEach(scene => {
|
|
279
|
+
output += `${scene.timecode || formatTimecodeFull(scene.timestamp)}\n`;
|
|
280
|
+
});
|
|
281
|
+
return output;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function formatTimecodeFull(seconds) {
|
|
285
|
+
const h = Math.floor(seconds / 3600);
|
|
286
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
287
|
+
const s = Math.floor(seconds % 60);
|
|
288
|
+
const ms = Math.floor((seconds % 1) * 1000);
|
|
289
|
+
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Run
|
|
293
|
+
run();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ffmpeg-decoder.d.ts","sourceRoot":"","sources":["../../src/decoder/ffmpeg-decoder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"ffmpeg-decoder.d.ts","sourceRoot":"","sources":["../../src/decoder/ffmpeg-decoder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAuG7C,MAAM,WAAW,cAAc;IAC7B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,yCAAyC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,WAAW,CAAc;gBAErB,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB;IAU3D;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,aAAa,CAAC;IAqC3C;;OAEG;IACH,OAAO,CAAC,QAAQ;IAQhB;;;;;OAKG;IACG,aAAa,CACjB,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EAClD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GACpD,OAAO,CAAC,IAAI,CAAC;IAwEhB;;OAEG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAgD1D;;OAEG;IACH,cAAc,IAAI,WAAW;IAI7B;;OAEG;IACH,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as ffmpeg from 'fluent-ffmpeg';
|
|
7
7
|
import * as ffmpegInstaller from '@ffmpeg-installer/ffmpeg';
|
|
8
|
+
import * as ffprobeInstaller from '@ffprobe-installer/ffprobe';
|
|
8
9
|
import { FrameBuffer } from './frame-buffer';
|
|
9
|
-
// Set FFmpeg
|
|
10
|
+
// Set FFmpeg and FFprobe paths from installers
|
|
10
11
|
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
|
|
12
|
+
ffmpeg.setFfprobePath(ffprobeInstaller.path);
|
|
11
13
|
/**
|
|
12
14
|
* Ring Buffer - Fixed-size circular buffer for streaming data
|
|
13
15
|
* Eliminates repeated Buffer.concat() allocations and GC pressure
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ffmpeg-decoder.js","sourceRoot":"","sources":["../../src/decoder/ffmpeg-decoder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,eAAe,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"ffmpeg-decoder.js","sourceRoot":"","sources":["../../src/decoder/ffmpeg-decoder.ts"],"names":[],"mappings":"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"}
|