@lumy-pack/scene-sieve 0.0.3 → 0.0.5
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/dist/cli.mjs +578 -257
- package/dist/commands/Sieve.d.ts +13 -0
- package/dist/components/PhaseStep.d.ts +14 -0
- package/dist/components/ProgressBar.d.ts +7 -0
- package/dist/constants.d.ts +2 -1
- package/dist/core/extractor.d.ts +2 -2
- package/dist/core/pipeline-worker.d.ts +1 -0
- package/dist/core/run-in-worker.d.ts +9 -0
- package/dist/core/workspace.d.ts +5 -0
- package/dist/index.cjs +22 -42
- package/dist/index.mjs +23 -43
- package/dist/pipeline-worker.mjs +1049 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +5 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,86 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
9
11
|
|
|
10
12
|
// src/utils/logger.ts
|
|
11
13
|
import pc from "picocolors";
|
|
12
|
-
var debugMode = false;
|
|
13
14
|
function setDebugMode(enabled) {
|
|
14
15
|
debugMode = enabled;
|
|
15
16
|
}
|
|
16
17
|
function timestamp() {
|
|
17
18
|
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
18
19
|
}
|
|
19
|
-
var logger
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
var debugMode, logger;
|
|
21
|
+
var init_logger = __esm({
|
|
22
|
+
"src/utils/logger.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
debugMode = false;
|
|
25
|
+
logger = {
|
|
26
|
+
info(message) {
|
|
27
|
+
console.log(`${pc.blue("info")} ${message}`);
|
|
28
|
+
},
|
|
29
|
+
success(message) {
|
|
30
|
+
console.log(`
|
|
25
31
|
${pc.green("done")} ${message}`);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
},
|
|
33
|
+
warn(message) {
|
|
34
|
+
console.warn(`${pc.yellow("warn")} ${message}`);
|
|
35
|
+
},
|
|
36
|
+
error(message) {
|
|
37
|
+
console.error(`${pc.red("error")} ${message}`);
|
|
38
|
+
},
|
|
39
|
+
debug(message) {
|
|
40
|
+
if (debugMode) {
|
|
41
|
+
console.log(`${pc.gray(`[${timestamp()}] debug`)} ${message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
37
45
|
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// src/core/analyzer.ts
|
|
41
|
-
import { createRequire } from "module";
|
|
42
|
-
import sharp from "sharp";
|
|
46
|
+
});
|
|
43
47
|
|
|
44
48
|
// src/constants.ts
|
|
45
49
|
import { tmpdir } from "os";
|
|
46
50
|
import { join } from "path";
|
|
47
|
-
var APP_NAME = "scene-sieve";
|
|
48
|
-
var DEFAULT_COUNT = 20;
|
|
49
|
-
var DEFAULT_THRESHOLD = 0.5;
|
|
50
|
-
var DEFAULT_FPS = 5;
|
|
51
|
-
var DEFAULT_SCALE = 720;
|
|
52
|
-
var DEFAULT_QUALITY = 80;
|
|
53
|
-
var NORMALIZATION_PERCENTILE = 0.9;
|
|
54
|
-
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
55
|
-
var TEMP_BASE_DIR = tmpdir();
|
|
56
|
-
var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
57
|
-
".mp4",
|
|
58
|
-
".mov",
|
|
59
|
-
".avi",
|
|
60
|
-
".mkv",
|
|
61
|
-
".webm"
|
|
62
|
-
];
|
|
63
|
-
var SUPPORTED_GIF_EXTENSIONS = [".gif"];
|
|
64
|
-
var FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
65
|
-
var OPENCV_BATCH_SIZE = 10;
|
|
66
|
-
var MIN_IFRAME_COUNT = 3;
|
|
67
|
-
var DBSCAN_ALPHA = 0.03;
|
|
68
|
-
var DBSCAN_MIN_PTS = 4;
|
|
69
|
-
var IOU_THRESHOLD = 0.9;
|
|
70
|
-
var DECAY_LAMBDA = 0.95;
|
|
71
|
-
var ANIMATION_FRAME_THRESHOLD = 5;
|
|
72
|
-
var MATCH_DISTANCE_THRESHOLD = 0.75;
|
|
73
|
-
var PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
74
|
-
var PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
75
|
-
var PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
76
|
-
var PIXELDIFF_SAMPLE_SPACING = 8;
|
|
77
51
|
function getTempWorkspaceDir(sessionId) {
|
|
78
52
|
return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
79
53
|
}
|
|
54
|
+
var APP_NAME, DEFAULT_COUNT, DEFAULT_THRESHOLD, DEFAULT_FPS, DEFAULT_SCALE, DEFAULT_QUALITY, DEFAULT_MAX_FRAMES, NORMALIZATION_PERCENTILE, WORKSPACE_PREFIX, TEMP_BASE_DIR, SUPPORTED_VIDEO_EXTENSIONS, SUPPORTED_GIF_EXTENSIONS, FRAME_OUTPUT_EXTENSION, OPENCV_BATCH_SIZE, DBSCAN_ALPHA, DBSCAN_MIN_PTS, IOU_THRESHOLD, DECAY_LAMBDA, ANIMATION_FRAME_THRESHOLD, MATCH_DISTANCE_THRESHOLD, PIXELDIFF_GAUSSIAN_KERNEL, PIXELDIFF_BINARY_THRESHOLD, PIXELDIFF_CONTOUR_MIN_AREA, PIXELDIFF_SAMPLE_SPACING;
|
|
55
|
+
var init_constants = __esm({
|
|
56
|
+
"src/constants.ts"() {
|
|
57
|
+
"use strict";
|
|
58
|
+
APP_NAME = "scene-sieve";
|
|
59
|
+
DEFAULT_COUNT = 20;
|
|
60
|
+
DEFAULT_THRESHOLD = 0.5;
|
|
61
|
+
DEFAULT_FPS = 5;
|
|
62
|
+
DEFAULT_SCALE = 720;
|
|
63
|
+
DEFAULT_QUALITY = 80;
|
|
64
|
+
DEFAULT_MAX_FRAMES = 300;
|
|
65
|
+
NORMALIZATION_PERCENTILE = 0.9;
|
|
66
|
+
WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
67
|
+
TEMP_BASE_DIR = tmpdir();
|
|
68
|
+
SUPPORTED_VIDEO_EXTENSIONS = [
|
|
69
|
+
".mp4",
|
|
70
|
+
".mov",
|
|
71
|
+
".avi",
|
|
72
|
+
".mkv",
|
|
73
|
+
".webm"
|
|
74
|
+
];
|
|
75
|
+
SUPPORTED_GIF_EXTENSIONS = [".gif"];
|
|
76
|
+
FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
77
|
+
OPENCV_BATCH_SIZE = 10;
|
|
78
|
+
DBSCAN_ALPHA = 0.03;
|
|
79
|
+
DBSCAN_MIN_PTS = 4;
|
|
80
|
+
IOU_THRESHOLD = 0.9;
|
|
81
|
+
DECAY_LAMBDA = 0.95;
|
|
82
|
+
ANIMATION_FRAME_THRESHOLD = 5;
|
|
83
|
+
MATCH_DISTANCE_THRESHOLD = 0.25;
|
|
84
|
+
PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
85
|
+
PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
86
|
+
PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
87
|
+
PIXELDIFF_SAMPLE_SPACING = 8;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
80
90
|
|
|
81
91
|
// src/core/dbscan.ts
|
|
82
|
-
var UNVISITED = -2;
|
|
83
|
-
var NOISE = -1;
|
|
84
92
|
function dbscan(points, imageWidth, imageHeight, alpha, minPts) {
|
|
85
93
|
if (points.length === 0) {
|
|
86
94
|
return { labels: [], boundingBoxes: [] };
|
|
@@ -155,11 +163,19 @@ function findNeighbors(points, idx, epsSquared) {
|
|
|
155
163
|
}
|
|
156
164
|
return neighbors;
|
|
157
165
|
}
|
|
166
|
+
var UNVISITED, NOISE;
|
|
167
|
+
var init_dbscan = __esm({
|
|
168
|
+
"src/core/dbscan.ts"() {
|
|
169
|
+
"use strict";
|
|
170
|
+
init_constants();
|
|
171
|
+
UNVISITED = -2;
|
|
172
|
+
NOISE = -1;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
158
175
|
|
|
159
176
|
// src/core/analyzer.ts
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
var cvReady = null;
|
|
177
|
+
import { createRequire } from "module";
|
|
178
|
+
import sharp from "sharp";
|
|
163
179
|
async function ensureOpenCV() {
|
|
164
180
|
if (!cvReady) {
|
|
165
181
|
cvReady = (async () => {
|
|
@@ -182,7 +198,7 @@ async function ensureOpenCV() {
|
|
|
182
198
|
async function preprocessFrame(framePath, scale) {
|
|
183
199
|
const { data, info } = await sharp(framePath).resize({ width: scale, withoutEnlargement: true }).grayscale().blur(1).raw().toBuffer({ resolveWithObject: true });
|
|
184
200
|
return {
|
|
185
|
-
data: new Uint8Array(data.buffer),
|
|
201
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
186
202
|
width: info.width,
|
|
187
203
|
height: info.height
|
|
188
204
|
};
|
|
@@ -201,78 +217,12 @@ function computeIoU(a, b) {
|
|
|
201
217
|
const union = aArea + bArea - intersection;
|
|
202
218
|
return union === 0 ? 0 : intersection / union;
|
|
203
219
|
}
|
|
204
|
-
var IoUTracker = class {
|
|
205
|
-
regions = [];
|
|
206
|
-
update(boxes, pairIndex) {
|
|
207
|
-
const animationIndices = /* @__PURE__ */ new Set();
|
|
208
|
-
const matched = /* @__PURE__ */ new Set();
|
|
209
|
-
for (let bi = 0; bi < boxes.length; bi++) {
|
|
210
|
-
const box = boxes[bi];
|
|
211
|
-
let bestIoU = 0;
|
|
212
|
-
let bestRegionIdx = -1;
|
|
213
|
-
for (let ri = 0; ri < this.regions.length; ri++) {
|
|
214
|
-
if (matched.has(ri)) continue;
|
|
215
|
-
const iou = computeIoU(box, this.regions[ri].box);
|
|
216
|
-
if (iou > bestIoU) {
|
|
217
|
-
bestIoU = iou;
|
|
218
|
-
bestRegionIdx = ri;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (bestIoU > IOU_THRESHOLD && bestRegionIdx !== -1) {
|
|
222
|
-
const region = this.regions[bestRegionIdx];
|
|
223
|
-
const gap = pairIndex - region.lastSeen;
|
|
224
|
-
region.box = box;
|
|
225
|
-
region.consecutiveCount++;
|
|
226
|
-
region.lastSeen = pairIndex;
|
|
227
|
-
region.weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
228
|
-
matched.add(bestRegionIdx);
|
|
229
|
-
if (region.consecutiveCount >= ANIMATION_FRAME_THRESHOLD) {
|
|
230
|
-
animationIndices.add(bi);
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
this.regions.push({
|
|
234
|
-
box,
|
|
235
|
-
consecutiveCount: 1,
|
|
236
|
-
lastSeen: pairIndex,
|
|
237
|
-
weight: 1
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
for (let ri = 0; ri < this.regions.length; ri++) {
|
|
242
|
-
if (!matched.has(ri)) {
|
|
243
|
-
const gap = pairIndex - this.regions[ri].lastSeen;
|
|
244
|
-
this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
this.regions = this.regions.filter((r) => r.weight > 0.01);
|
|
248
|
-
return animationIndices;
|
|
249
|
-
}
|
|
250
|
-
getAnimationWeight(boxIndex, boxes) {
|
|
251
|
-
if (boxIndex >= boxes.length) return 0;
|
|
252
|
-
const box = boxes[boxIndex];
|
|
253
|
-
let maxWeight = 0;
|
|
254
|
-
for (const region of this.regions) {
|
|
255
|
-
if (region.consecutiveCount >= ANIMATION_FRAME_THRESHOLD) {
|
|
256
|
-
const iou = computeIoU(box, region.box);
|
|
257
|
-
if (iou > IOU_THRESHOLD) {
|
|
258
|
-
maxWeight = Math.max(maxWeight, region.weight);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return maxWeight;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
220
|
async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const mat2 = cvLib.matFromImageData({
|
|
272
|
-
data: frame2.data,
|
|
273
|
-
width: frame2.width,
|
|
274
|
-
height: frame2.height
|
|
275
|
-
});
|
|
221
|
+
const cv = cvLib;
|
|
222
|
+
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
223
|
+
mat1.data.set(frame1.data);
|
|
224
|
+
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
225
|
+
mat2.data.set(frame2.data);
|
|
276
226
|
const kp1 = new cvLib.KeyPointVector();
|
|
277
227
|
const kp2 = new cvLib.KeyPointVector();
|
|
278
228
|
const desc1 = new cvLib.Mat();
|
|
@@ -503,13 +453,79 @@ async function analyzeFrames(ctx) {
|
|
|
503
453
|
logger.debug(`Computed ${edges.length} score edges`);
|
|
504
454
|
return edges;
|
|
505
455
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
456
|
+
var OPENCV_INIT_TIMEOUT_MS, require2, cvReady, IoUTracker;
|
|
457
|
+
var init_analyzer = __esm({
|
|
458
|
+
"src/core/analyzer.ts"() {
|
|
459
|
+
"use strict";
|
|
460
|
+
init_constants();
|
|
461
|
+
init_logger();
|
|
462
|
+
init_dbscan();
|
|
463
|
+
OPENCV_INIT_TIMEOUT_MS = 3e4;
|
|
464
|
+
require2 = createRequire(import.meta.url);
|
|
465
|
+
cvReady = null;
|
|
466
|
+
IoUTracker = class {
|
|
467
|
+
regions = [];
|
|
468
|
+
update(boxes, pairIndex) {
|
|
469
|
+
const animationIndices = /* @__PURE__ */ new Set();
|
|
470
|
+
const matched = /* @__PURE__ */ new Set();
|
|
471
|
+
for (let bi = 0; bi < boxes.length; bi++) {
|
|
472
|
+
const box = boxes[bi];
|
|
473
|
+
let bestIoU = 0;
|
|
474
|
+
let bestRegionIdx = -1;
|
|
475
|
+
for (let ri = 0; ri < this.regions.length; ri++) {
|
|
476
|
+
if (matched.has(ri)) continue;
|
|
477
|
+
const iou = computeIoU(box, this.regions[ri].box);
|
|
478
|
+
if (iou > bestIoU) {
|
|
479
|
+
bestIoU = iou;
|
|
480
|
+
bestRegionIdx = ri;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (bestIoU > IOU_THRESHOLD && bestRegionIdx !== -1) {
|
|
484
|
+
const region = this.regions[bestRegionIdx];
|
|
485
|
+
const gap = pairIndex - region.lastSeen;
|
|
486
|
+
region.box = box;
|
|
487
|
+
region.consecutiveCount++;
|
|
488
|
+
region.lastSeen = pairIndex;
|
|
489
|
+
region.weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
490
|
+
matched.add(bestRegionIdx);
|
|
491
|
+
if (region.consecutiveCount >= ANIMATION_FRAME_THRESHOLD) {
|
|
492
|
+
animationIndices.add(bi);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
this.regions.push({
|
|
496
|
+
box,
|
|
497
|
+
consecutiveCount: 1,
|
|
498
|
+
lastSeen: pairIndex,
|
|
499
|
+
weight: 1
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (let ri = 0; ri < this.regions.length; ri++) {
|
|
504
|
+
if (!matched.has(ri)) {
|
|
505
|
+
const gap = pairIndex - this.regions[ri].lastSeen;
|
|
506
|
+
this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
this.regions = this.regions.filter((r) => r.weight > 0.01);
|
|
510
|
+
return animationIndices;
|
|
511
|
+
}
|
|
512
|
+
getAnimationWeight(boxIndex, boxes) {
|
|
513
|
+
if (boxIndex >= boxes.length) return 0;
|
|
514
|
+
const box = boxes[boxIndex];
|
|
515
|
+
let maxWeight = 0;
|
|
516
|
+
for (const region of this.regions) {
|
|
517
|
+
if (region.consecutiveCount >= ANIMATION_FRAME_THRESHOLD) {
|
|
518
|
+
const iou = computeIoU(box, region.box);
|
|
519
|
+
if (iou > IOU_THRESHOLD) {
|
|
520
|
+
maxWeight = Math.max(maxWeight, region.weight);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return maxWeight;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
});
|
|
513
529
|
|
|
514
530
|
// src/utils/paths.ts
|
|
515
531
|
import { mkdir, stat } from "fs/promises";
|
|
@@ -544,11 +560,21 @@ function deriveOutputPath(inputPath) {
|
|
|
544
560
|
function isSupportedFile(filePath, extensions) {
|
|
545
561
|
return extensions.includes(extname(filePath).toLowerCase());
|
|
546
562
|
}
|
|
563
|
+
var init_paths = __esm({
|
|
564
|
+
"src/utils/paths.ts"() {
|
|
565
|
+
"use strict";
|
|
566
|
+
}
|
|
567
|
+
});
|
|
547
568
|
|
|
548
569
|
// src/core/extractor.ts
|
|
570
|
+
import { readdir } from "fs/promises";
|
|
571
|
+
import { join as join2 } from "path";
|
|
572
|
+
import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
|
|
573
|
+
import { execa } from "execa";
|
|
574
|
+
import ffmpegPath from "ffmpeg-static";
|
|
549
575
|
async function extractFrames(ctx) {
|
|
550
576
|
const framesDir = join2(ctx.workspacePath, "frames");
|
|
551
|
-
const { inputPath, fps, scale } = ctx.options;
|
|
577
|
+
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
552
578
|
if (!inputPath) {
|
|
553
579
|
throw new Error("inputPath is required for frame extraction");
|
|
554
580
|
}
|
|
@@ -565,39 +591,21 @@ async function extractFrames(ctx) {
|
|
|
565
591
|
}
|
|
566
592
|
logger.debug(`Extracting frames from: ${inputPath}`);
|
|
567
593
|
await ensureDir(framesDir);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
`Insufficient I-frames (${frames.length}), falling back to FPS mode`
|
|
578
|
-
);
|
|
579
|
-
frames = await extractByFps(inputPath, framesDir, fps, scale);
|
|
580
|
-
}
|
|
594
|
+
let effectiveFps = fps;
|
|
595
|
+
const duration = await getVideoDuration(inputPath).catch(() => 0);
|
|
596
|
+
if (duration > 0) {
|
|
597
|
+
const fpsCap = maxFrames / duration;
|
|
598
|
+
effectiveFps = Math.min(fps, fpsCap);
|
|
599
|
+
effectiveFps = Math.max(0.5, effectiveFps);
|
|
600
|
+
logger.debug(
|
|
601
|
+
`Duration: ${duration.toFixed(1)}s, FPS: ${fps} \u2192 effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`
|
|
602
|
+
);
|
|
581
603
|
}
|
|
604
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
|
|
582
605
|
ctx.emitProgress(100);
|
|
583
606
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
584
607
|
return frames;
|
|
585
608
|
}
|
|
586
|
-
async function extractIFrames(inputPath, outputDir, scale) {
|
|
587
|
-
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
588
|
-
await execa(ffmpegPath, [
|
|
589
|
-
"-i",
|
|
590
|
-
inputPath,
|
|
591
|
-
"-vf",
|
|
592
|
-
`select='eq(pict_type,I)',scale=-1:${scale}`,
|
|
593
|
-
"-vsync",
|
|
594
|
-
"vfr",
|
|
595
|
-
"-q:v",
|
|
596
|
-
"2",
|
|
597
|
-
outputPattern
|
|
598
|
-
]);
|
|
599
|
-
return buildFrameList(outputDir, inputPath);
|
|
600
|
-
}
|
|
601
609
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
602
610
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
603
611
|
await execa(ffmpegPath, [
|
|
@@ -643,12 +651,17 @@ async function buildFrameList(framesDir, inputPath) {
|
|
|
643
651
|
extractPath: join2(framesDir, file)
|
|
644
652
|
}));
|
|
645
653
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
654
|
+
var init_extractor = __esm({
|
|
655
|
+
"src/core/extractor.ts"() {
|
|
656
|
+
"use strict";
|
|
657
|
+
init_constants();
|
|
658
|
+
init_logger();
|
|
659
|
+
init_paths();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
649
662
|
|
|
650
663
|
// src/core/workspace.ts
|
|
651
|
-
import { rename, rm, writeFile } from "fs/promises";
|
|
664
|
+
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
652
665
|
import { join as join3 } from "path";
|
|
653
666
|
import sharp2 from "sharp";
|
|
654
667
|
async function createWorkspace(sessionId) {
|
|
@@ -670,6 +683,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
670
683
|
outputFiles.push(join3(outputPath, destName));
|
|
671
684
|
}
|
|
672
685
|
await ensureDir(join3(outputPath, ".."));
|
|
686
|
+
await rm(outputPath, { recursive: true, force: true });
|
|
673
687
|
await rename(stagingDir, outputPath);
|
|
674
688
|
return outputFiles;
|
|
675
689
|
}
|
|
@@ -680,6 +694,21 @@ async function cleanupWorkspace(workspacePath) {
|
|
|
680
694
|
} catch {
|
|
681
695
|
}
|
|
682
696
|
}
|
|
697
|
+
async function cleanupStaleWorkspaces() {
|
|
698
|
+
const entries = await readdir2(TEMP_BASE_DIR);
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
if (!entry.startsWith(WORKSPACE_PREFIX)) continue;
|
|
702
|
+
const fullPath = join3(TEMP_BASE_DIR, entry);
|
|
703
|
+
try {
|
|
704
|
+
const info = await stat2(fullPath);
|
|
705
|
+
if (info.isDirectory() && now - info.mtimeMs > STALE_THRESHOLD_MS) {
|
|
706
|
+
await rm(fullPath, { recursive: true, force: true });
|
|
707
|
+
}
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
683
712
|
async function writeInputBuffer(buffer, workspacePath) {
|
|
684
713
|
const inputDir = join3(workspacePath, "input");
|
|
685
714
|
await ensureDir(inputDir);
|
|
@@ -706,8 +735,18 @@ async function readFramesAsBuffers(frameNodes, quality) {
|
|
|
706
735
|
)
|
|
707
736
|
);
|
|
708
737
|
}
|
|
738
|
+
var STALE_THRESHOLD_MS;
|
|
739
|
+
var init_workspace = __esm({
|
|
740
|
+
"src/core/workspace.ts"() {
|
|
741
|
+
"use strict";
|
|
742
|
+
init_constants();
|
|
743
|
+
init_paths();
|
|
744
|
+
STALE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
709
747
|
|
|
710
748
|
// src/core/input-resolver.ts
|
|
749
|
+
import { join as join4 } from "path";
|
|
711
750
|
function resolveOptions(options) {
|
|
712
751
|
const mode = options.mode;
|
|
713
752
|
const inputPath = mode === "file" ? resolveAbsolute(options.inputPath) : void 0;
|
|
@@ -727,6 +766,7 @@ function resolveOptions(options) {
|
|
|
727
766
|
pruneMode,
|
|
728
767
|
outputPath,
|
|
729
768
|
fps: options.fps ?? DEFAULT_FPS,
|
|
769
|
+
maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
730
770
|
scale: options.scale ?? DEFAULT_SCALE,
|
|
731
771
|
quality: options.quality ?? DEFAULT_QUALITY,
|
|
732
772
|
debug: options.debug ?? false
|
|
@@ -752,50 +792,64 @@ async function resolveInput(options, workspacePath) {
|
|
|
752
792
|
}
|
|
753
793
|
throw new Error(`Unsupported input mode: ${options.mode}`);
|
|
754
794
|
}
|
|
795
|
+
var init_input_resolver = __esm({
|
|
796
|
+
"src/core/input-resolver.ts"() {
|
|
797
|
+
"use strict";
|
|
798
|
+
init_constants();
|
|
799
|
+
init_paths();
|
|
800
|
+
init_workspace();
|
|
801
|
+
}
|
|
802
|
+
});
|
|
755
803
|
|
|
756
804
|
// src/utils/min-heap.ts
|
|
757
|
-
var MinHeap
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
805
|
+
var MinHeap;
|
|
806
|
+
var init_min_heap = __esm({
|
|
807
|
+
"src/utils/min-heap.ts"() {
|
|
808
|
+
"use strict";
|
|
809
|
+
MinHeap = class {
|
|
810
|
+
h = [];
|
|
811
|
+
get size() {
|
|
812
|
+
return this.h.length;
|
|
813
|
+
}
|
|
814
|
+
push(entry) {
|
|
815
|
+
this.h.push(entry);
|
|
816
|
+
this.siftUp(this.h.length - 1);
|
|
817
|
+
}
|
|
818
|
+
pop() {
|
|
819
|
+
const n = this.h.length;
|
|
820
|
+
if (n === 0) return void 0;
|
|
821
|
+
const top = this.h[0];
|
|
822
|
+
const last = this.h.pop();
|
|
823
|
+
if (n > 1) {
|
|
824
|
+
this.h[0] = last;
|
|
825
|
+
this.siftDown(0);
|
|
826
|
+
}
|
|
827
|
+
return top;
|
|
828
|
+
}
|
|
829
|
+
siftUp(i) {
|
|
830
|
+
while (i > 0) {
|
|
831
|
+
const p = i - 1 >> 1;
|
|
832
|
+
if (this.h[p].score <= this.h[i].score) break;
|
|
833
|
+
[this.h[p], this.h[i]] = [this.h[i], this.h[p]];
|
|
834
|
+
i = p;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
siftDown(i) {
|
|
838
|
+
const n = this.h.length;
|
|
839
|
+
for (; ; ) {
|
|
840
|
+
let m = i;
|
|
841
|
+
const l = 2 * i + 1;
|
|
842
|
+
const r = 2 * i + 2;
|
|
843
|
+
if (l < n && this.h[l].score < this.h[m].score) m = l;
|
|
844
|
+
if (r < n && this.h[r].score < this.h[m].score) m = r;
|
|
845
|
+
if (m === i) break;
|
|
846
|
+
[this.h[m], this.h[i]] = [this.h[i], this.h[m]];
|
|
847
|
+
i = m;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
};
|
|
797
851
|
}
|
|
798
|
-
};
|
|
852
|
+
});
|
|
799
853
|
|
|
800
854
|
// src/core/pruner.ts
|
|
801
855
|
function pruneTo(graph, frames, targetCount) {
|
|
@@ -957,8 +1011,20 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
957
1011
|
}
|
|
958
1012
|
return pruneTo(syntheticEdges, survivingFrames, maxCount);
|
|
959
1013
|
}
|
|
1014
|
+
var init_pruner = __esm({
|
|
1015
|
+
"src/core/pruner.ts"() {
|
|
1016
|
+
"use strict";
|
|
1017
|
+
init_constants();
|
|
1018
|
+
init_min_heap();
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
960
1021
|
|
|
961
1022
|
// src/core/orchestrator.ts
|
|
1023
|
+
var orchestrator_exports = {};
|
|
1024
|
+
__export(orchestrator_exports, {
|
|
1025
|
+
runPipeline: () => runPipeline
|
|
1026
|
+
});
|
|
1027
|
+
import { randomUUID } from "crypto";
|
|
962
1028
|
async function runPipeline(options) {
|
|
963
1029
|
const startTime = Date.now();
|
|
964
1030
|
const sessionId = randomUUID();
|
|
@@ -1047,6 +1113,286 @@ async function runPipeline(options) {
|
|
|
1047
1113
|
}
|
|
1048
1114
|
}
|
|
1049
1115
|
}
|
|
1116
|
+
var init_orchestrator = __esm({
|
|
1117
|
+
"src/core/orchestrator.ts"() {
|
|
1118
|
+
"use strict";
|
|
1119
|
+
init_logger();
|
|
1120
|
+
init_analyzer();
|
|
1121
|
+
init_extractor();
|
|
1122
|
+
init_input_resolver();
|
|
1123
|
+
init_pruner();
|
|
1124
|
+
init_workspace();
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
// src/cli.ts
|
|
1129
|
+
import { createRequire as createRequire2 } from "module";
|
|
1130
|
+
import { Command } from "commander";
|
|
1131
|
+
import { render } from "ink";
|
|
1132
|
+
import React2 from "react";
|
|
1133
|
+
|
|
1134
|
+
// src/commands/Sieve.tsx
|
|
1135
|
+
import { Box as Box2, Text as Text3, useApp } from "ink";
|
|
1136
|
+
import { useEffect, useState } from "react";
|
|
1137
|
+
|
|
1138
|
+
// src/components/PhaseStep.tsx
|
|
1139
|
+
import { Box, Text as Text2 } from "ink";
|
|
1140
|
+
import Spinner from "ink-spinner";
|
|
1141
|
+
|
|
1142
|
+
// src/components/ProgressBar.tsx
|
|
1143
|
+
import { Text } from "ink";
|
|
1144
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1145
|
+
var ProgressBar = ({
|
|
1146
|
+
percent,
|
|
1147
|
+
width = 30
|
|
1148
|
+
}) => {
|
|
1149
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
1150
|
+
const filled = Math.round(width * (clamped / 100));
|
|
1151
|
+
const empty = width - filled;
|
|
1152
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
1153
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "\u2588".repeat(filled) }),
|
|
1154
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2591".repeat(empty) }),
|
|
1155
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1156
|
+
" ",
|
|
1157
|
+
clamped,
|
|
1158
|
+
"%"
|
|
1159
|
+
] })
|
|
1160
|
+
] });
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/components/PhaseStep.tsx
|
|
1164
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1165
|
+
var PhaseStep = ({ phase }) => {
|
|
1166
|
+
const icon = (() => {
|
|
1167
|
+
switch (phase.status) {
|
|
1168
|
+
case "done":
|
|
1169
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u2713" });
|
|
1170
|
+
case "running":
|
|
1171
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) });
|
|
1172
|
+
case "failed":
|
|
1173
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717" });
|
|
1174
|
+
default:
|
|
1175
|
+
return /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u25CB" });
|
|
1176
|
+
}
|
|
1177
|
+
})();
|
|
1178
|
+
const duration = phase.status === "done" && phase.durationMs !== void 0 ? `Done (${Math.round(phase.durationMs / 1e3)}s)` : "";
|
|
1179
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
1180
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1181
|
+
" ",
|
|
1182
|
+
icon,
|
|
1183
|
+
" ",
|
|
1184
|
+
phase.label,
|
|
1185
|
+
duration ? /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
|
|
1186
|
+
" ",
|
|
1187
|
+
duration
|
|
1188
|
+
] }) : null
|
|
1189
|
+
] }),
|
|
1190
|
+
phase.status === "running" && phase.hasProgress && phase.percent > 0 && /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1191
|
+
" ",
|
|
1192
|
+
/* @__PURE__ */ jsx2(ProgressBar, { percent: phase.percent })
|
|
1193
|
+
] })
|
|
1194
|
+
] });
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// src/core/run-in-worker.ts
|
|
1198
|
+
import { dirname, join as join5 } from "path";
|
|
1199
|
+
import { fileURLToPath } from "url";
|
|
1200
|
+
import { Worker } from "worker_threads";
|
|
1201
|
+
async function runPipelineInWorker(options, onProgress) {
|
|
1202
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
1203
|
+
if (!currentFile.endsWith(".mjs")) {
|
|
1204
|
+
const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_orchestrator(), orchestrator_exports));
|
|
1205
|
+
return runPipeline2({ ...options, onProgress });
|
|
1206
|
+
}
|
|
1207
|
+
const workerPath = join5(dirname(currentFile), "pipeline-worker.mjs");
|
|
1208
|
+
return new Promise((resolve2, reject) => {
|
|
1209
|
+
const worker = new Worker(workerPath, { workerData: options });
|
|
1210
|
+
worker.on(
|
|
1211
|
+
"message",
|
|
1212
|
+
(msg) => {
|
|
1213
|
+
if (msg.type === "progress" && msg.phase && msg.percent !== void 0) {
|
|
1214
|
+
onProgress(msg.phase, msg.percent);
|
|
1215
|
+
} else if (msg.type === "result") {
|
|
1216
|
+
resolve2(msg.result);
|
|
1217
|
+
worker.terminate();
|
|
1218
|
+
} else if (msg.type === "error") {
|
|
1219
|
+
reject(new Error(msg.message));
|
|
1220
|
+
worker.terminate();
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
1224
|
+
worker.on("error", reject);
|
|
1225
|
+
worker.on("exit", (code) => {
|
|
1226
|
+
if (code !== 0 && code !== 1) {
|
|
1227
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/commands/Sieve.tsx
|
|
1234
|
+
init_workspace();
|
|
1235
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1236
|
+
var PHASE_DEFS = [
|
|
1237
|
+
{ key: "INIT", label: "Initializing workspace", hasProgress: false },
|
|
1238
|
+
{ key: "EXTRACTING", label: "Extracting frames", hasProgress: false },
|
|
1239
|
+
{ key: "ANALYZING", label: "Analyzing frame similarity", hasProgress: true },
|
|
1240
|
+
{ key: "PRUNING", label: "Pruning similar frames", hasProgress: false },
|
|
1241
|
+
{ key: "FINALIZING", label: "Finalizing output", hasProgress: false }
|
|
1242
|
+
];
|
|
1243
|
+
function createInitialPhases() {
|
|
1244
|
+
return PHASE_DEFS.map((def) => ({
|
|
1245
|
+
label: def.label,
|
|
1246
|
+
status: "pending",
|
|
1247
|
+
hasProgress: def.hasProgress,
|
|
1248
|
+
percent: 0
|
|
1249
|
+
}));
|
|
1250
|
+
}
|
|
1251
|
+
function phaseKeyToIndex(phase) {
|
|
1252
|
+
return PHASE_DEFS.findIndex((d) => d.key === phase);
|
|
1253
|
+
}
|
|
1254
|
+
var SieveView = (props) => {
|
|
1255
|
+
const { exit } = useApp();
|
|
1256
|
+
const [phases, setPhases] = useState(createInitialPhases);
|
|
1257
|
+
const [result, setResult] = useState(null);
|
|
1258
|
+
const [error, setError] = useState(null);
|
|
1259
|
+
useEffect(() => {
|
|
1260
|
+
const phaseStartTimes = PHASE_DEFS.map(() => 0);
|
|
1261
|
+
let currentPhaseKey = "";
|
|
1262
|
+
(async () => {
|
|
1263
|
+
try {
|
|
1264
|
+
await cleanupStaleWorkspaces().catch(() => {
|
|
1265
|
+
});
|
|
1266
|
+
phaseStartTimes[0] = Date.now();
|
|
1267
|
+
setPhases((prev) => {
|
|
1268
|
+
const next = [...prev];
|
|
1269
|
+
next[0] = { ...next[0], status: "running" };
|
|
1270
|
+
return next;
|
|
1271
|
+
});
|
|
1272
|
+
const res = await runPipelineInWorker(
|
|
1273
|
+
{
|
|
1274
|
+
mode: "file",
|
|
1275
|
+
inputPath: props.input,
|
|
1276
|
+
...props.threshold !== void 0 ? { threshold: props.threshold } : {},
|
|
1277
|
+
...props.count !== void 0 ? { count: props.count } : {},
|
|
1278
|
+
outputPath: props.output,
|
|
1279
|
+
fps: props.fps,
|
|
1280
|
+
maxFrames: props.maxFrames,
|
|
1281
|
+
scale: props.scale,
|
|
1282
|
+
quality: props.quality,
|
|
1283
|
+
debug: props.debug
|
|
1284
|
+
},
|
|
1285
|
+
(phase, percent) => {
|
|
1286
|
+
const phaseIdx = phaseKeyToIndex(phase);
|
|
1287
|
+
if (phaseIdx < 0) return;
|
|
1288
|
+
if (phase !== currentPhaseKey) {
|
|
1289
|
+
const now2 = Date.now();
|
|
1290
|
+
currentPhaseKey = phase;
|
|
1291
|
+
phaseStartTimes[phaseIdx] = now2;
|
|
1292
|
+
setPhases((prev) => {
|
|
1293
|
+
const next = [...prev];
|
|
1294
|
+
for (let i = 0; i < next.length; i++) {
|
|
1295
|
+
if (i < phaseIdx) {
|
|
1296
|
+
if (next[i].status !== "done") {
|
|
1297
|
+
next[i] = {
|
|
1298
|
+
...next[i],
|
|
1299
|
+
status: "done",
|
|
1300
|
+
percent: 100,
|
|
1301
|
+
durationMs: phaseStartTimes[i] ? now2 - phaseStartTimes[i] : 0
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
} else if (i === phaseIdx) {
|
|
1305
|
+
next[i] = { ...next[i], status: "running", percent: 0 };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return next;
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
setPhases((prev) => {
|
|
1312
|
+
const next = [...prev];
|
|
1313
|
+
if (next[phaseIdx].status === "running") {
|
|
1314
|
+
next[phaseIdx] = {
|
|
1315
|
+
...next[phaseIdx],
|
|
1316
|
+
percent: Math.round(percent)
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
return next;
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
const now = Date.now();
|
|
1324
|
+
setPhases(
|
|
1325
|
+
(prev) => prev.map((p, i) => {
|
|
1326
|
+
if (p.status !== "done") {
|
|
1327
|
+
return {
|
|
1328
|
+
...p,
|
|
1329
|
+
status: "done",
|
|
1330
|
+
percent: 100,
|
|
1331
|
+
durationMs: phaseStartTimes[i] ? now - phaseStartTimes[i] : 0
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
return p;
|
|
1335
|
+
})
|
|
1336
|
+
);
|
|
1337
|
+
setResult(res);
|
|
1338
|
+
setTimeout(() => exit(), 100);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
const now = Date.now();
|
|
1341
|
+
setPhases((prev) => {
|
|
1342
|
+
const next = [...prev];
|
|
1343
|
+
for (let i = 0; i < next.length; i++) {
|
|
1344
|
+
if (next[i].status === "running") {
|
|
1345
|
+
next[i] = {
|
|
1346
|
+
...next[i],
|
|
1347
|
+
status: "failed",
|
|
1348
|
+
durationMs: phaseStartTimes[i] ? now - phaseStartTimes[i] : 0
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return next;
|
|
1353
|
+
});
|
|
1354
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1355
|
+
setTimeout(() => exit(), 100);
|
|
1356
|
+
}
|
|
1357
|
+
})();
|
|
1358
|
+
}, []);
|
|
1359
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
|
|
1360
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
|
|
1361
|
+
"\u25B8 scene-sieve",
|
|
1362
|
+
" \u2014 ",
|
|
1363
|
+
props.input.split("/").pop()
|
|
1364
|
+
] }),
|
|
1365
|
+
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
1366
|
+
phases.map((phase, i) => /* @__PURE__ */ jsx3(PhaseStep, { phase }, i)),
|
|
1367
|
+
error && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
|
|
1368
|
+
"\u2717 Failed \u2014 ",
|
|
1369
|
+
error
|
|
1370
|
+
] }) }),
|
|
1371
|
+
result && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
1372
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
|
|
1373
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "green", bold: true, children: [
|
|
1374
|
+
"\u2713 Done",
|
|
1375
|
+
" \u2014 ",
|
|
1376
|
+
result.originalFramesCount,
|
|
1377
|
+
" frames \u2192 ",
|
|
1378
|
+
result.prunedFramesCount,
|
|
1379
|
+
" scenes (",
|
|
1380
|
+
(result.executionTimeMs / 1e3).toFixed(1),
|
|
1381
|
+
"s)"
|
|
1382
|
+
] }),
|
|
1383
|
+
props.debug && result.outputFiles.length > 0 && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
1384
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
1385
|
+
"Output: ",
|
|
1386
|
+
result.outputFiles[0]?.replace(/\/[^/]+$/, "/")
|
|
1387
|
+
] }),
|
|
1388
|
+
result.outputFiles.map((f, i) => /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
|
|
1389
|
+
" - ",
|
|
1390
|
+
f
|
|
1391
|
+
] }, i))
|
|
1392
|
+
] })
|
|
1393
|
+
] })
|
|
1394
|
+
] });
|
|
1395
|
+
};
|
|
1050
1396
|
|
|
1051
1397
|
// src/cli.ts
|
|
1052
1398
|
var require3 = createRequire2(import.meta.url);
|
|
@@ -1055,50 +1401,25 @@ var program = new Command();
|
|
|
1055
1401
|
program.name("scene-sieve").description("Extract key frames from video and GIF files").version(version).argument("<input>", "Input video or GIF file path").option("-n, --count <number>", "Max number of frames to keep (default: 20)").option(
|
|
1056
1402
|
"-t, --threshold <number>",
|
|
1057
1403
|
"Normalized threshold 0~1 (default: 0.5; keeps frames above ratio of max change)"
|
|
1058
|
-
).option("-o, --output <path>", "Output directory path").option("--fps <number>", "
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
barCompleteChar: "\u2588",
|
|
1067
|
-
barIncompleteChar: "\u2591",
|
|
1068
|
-
hideCursor: true
|
|
1069
|
-
});
|
|
1070
|
-
bar.start(100, 0, { phase: "EXTRACTING" });
|
|
1071
|
-
const result = await runPipeline({
|
|
1072
|
-
mode: "file",
|
|
1073
|
-
inputPath: input,
|
|
1404
|
+
).option("-o, --output <path>", "Output directory path").option("--fps <number>", "Max FPS for frame extraction", "5").option(
|
|
1405
|
+
"--max-frames <number>",
|
|
1406
|
+
"Max frames to extract (auto-reduces FPS for long videos)",
|
|
1407
|
+
"300"
|
|
1408
|
+
).option("-s, --scale <number>", "Scale size for vision analysis", "720").option("-q, --quality <number>", "JPEG output quality 1-100", "80").option("--debug", "Enable debug mode (preserve temp workspace)").action(async (input, opts) => {
|
|
1409
|
+
const { waitUntilExit } = render(
|
|
1410
|
+
React2.createElement(SieveView, {
|
|
1411
|
+
input,
|
|
1074
1412
|
...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
|
|
1075
1413
|
...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
|
|
1076
|
-
|
|
1414
|
+
output: opts.output,
|
|
1077
1415
|
fps: parseInt(opts.fps, 10),
|
|
1416
|
+
maxFrames: parseInt(opts.maxFrames, 10),
|
|
1078
1417
|
scale: parseInt(opts.scale, 10),
|
|
1079
1418
|
quality: parseInt(opts.quality, 10),
|
|
1080
|
-
debug: opts.debug ?? false
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
});
|
|
1085
|
-
bar.stop();
|
|
1086
|
-
console.log(
|
|
1087
|
-
`
|
|
1088
|
-
Done! ${result.originalFramesCount} frames -> ${result.prunedFramesCount} scenes (${result.executionTimeMs}ms)`
|
|
1089
|
-
);
|
|
1090
|
-
if (opts.debug) {
|
|
1091
|
-
console.log(
|
|
1092
|
-
`Output: ${result.outputFiles[0]?.replace(/\/[^/]+$/, "/")}`
|
|
1093
|
-
);
|
|
1094
|
-
result.outputFiles.forEach((f) => console.log(` - ${f}`));
|
|
1095
|
-
}
|
|
1096
|
-
} catch (error) {
|
|
1097
|
-
spinner.fail(
|
|
1098
|
-
`Failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1099
|
-
);
|
|
1100
|
-
process.exit(1);
|
|
1101
|
-
}
|
|
1419
|
+
debug: opts.debug ?? false
|
|
1420
|
+
})
|
|
1421
|
+
);
|
|
1422
|
+
await waitUntilExit();
|
|
1102
1423
|
});
|
|
1103
1424
|
program.parseAsync(process.argv).catch((error) => {
|
|
1104
1425
|
console.error("Fatal error:", error.message);
|