@lumy-pack/scene-sieve 0.0.4 → 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 +571 -246
- 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 +1 -0
- 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 +14 -30
- package/dist/index.mjs +15 -31
- 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.25;
|
|
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 () => {
|
|
@@ -201,67 +217,6 @@ 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
221
|
const cv = cvLib;
|
|
267
222
|
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
@@ -498,13 +453,79 @@ async function analyzeFrames(ctx) {
|
|
|
498
453
|
logger.debug(`Computed ${edges.length} score edges`);
|
|
499
454
|
return edges;
|
|
500
455
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
});
|
|
508
529
|
|
|
509
530
|
// src/utils/paths.ts
|
|
510
531
|
import { mkdir, stat } from "fs/promises";
|
|
@@ -539,11 +560,21 @@ function deriveOutputPath(inputPath) {
|
|
|
539
560
|
function isSupportedFile(filePath, extensions) {
|
|
540
561
|
return extensions.includes(extname(filePath).toLowerCase());
|
|
541
562
|
}
|
|
563
|
+
var init_paths = __esm({
|
|
564
|
+
"src/utils/paths.ts"() {
|
|
565
|
+
"use strict";
|
|
566
|
+
}
|
|
567
|
+
});
|
|
542
568
|
|
|
543
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";
|
|
544
575
|
async function extractFrames(ctx) {
|
|
545
576
|
const framesDir = join2(ctx.workspacePath, "frames");
|
|
546
|
-
const { inputPath, fps, scale } = ctx.options;
|
|
577
|
+
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
547
578
|
if (!inputPath) {
|
|
548
579
|
throw new Error("inputPath is required for frame extraction");
|
|
549
580
|
}
|
|
@@ -560,39 +591,21 @@ async function extractFrames(ctx) {
|
|
|
560
591
|
}
|
|
561
592
|
logger.debug(`Extracting frames from: ${inputPath}`);
|
|
562
593
|
await ensureDir(framesDir);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
`Insufficient I-frames (${frames.length}), falling back to FPS mode`
|
|
573
|
-
);
|
|
574
|
-
frames = await extractByFps(inputPath, framesDir, fps, scale);
|
|
575
|
-
}
|
|
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
|
+
);
|
|
576
603
|
}
|
|
604
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
|
|
577
605
|
ctx.emitProgress(100);
|
|
578
606
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
579
607
|
return frames;
|
|
580
608
|
}
|
|
581
|
-
async function extractIFrames(inputPath, outputDir, scale) {
|
|
582
|
-
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
583
|
-
await execa(ffmpegPath, [
|
|
584
|
-
"-i",
|
|
585
|
-
inputPath,
|
|
586
|
-
"-vf",
|
|
587
|
-
`select='eq(pict_type,I)',scale=-1:${scale}`,
|
|
588
|
-
"-vsync",
|
|
589
|
-
"vfr",
|
|
590
|
-
"-q:v",
|
|
591
|
-
"2",
|
|
592
|
-
outputPattern
|
|
593
|
-
]);
|
|
594
|
-
return buildFrameList(outputDir, inputPath);
|
|
595
|
-
}
|
|
596
609
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
597
610
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
598
611
|
await execa(ffmpegPath, [
|
|
@@ -638,12 +651,17 @@ async function buildFrameList(framesDir, inputPath) {
|
|
|
638
651
|
extractPath: join2(framesDir, file)
|
|
639
652
|
}));
|
|
640
653
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
654
|
+
var init_extractor = __esm({
|
|
655
|
+
"src/core/extractor.ts"() {
|
|
656
|
+
"use strict";
|
|
657
|
+
init_constants();
|
|
658
|
+
init_logger();
|
|
659
|
+
init_paths();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
644
662
|
|
|
645
663
|
// src/core/workspace.ts
|
|
646
|
-
import { rename, rm, writeFile } from "fs/promises";
|
|
664
|
+
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
647
665
|
import { join as join3 } from "path";
|
|
648
666
|
import sharp2 from "sharp";
|
|
649
667
|
async function createWorkspace(sessionId) {
|
|
@@ -676,6 +694,21 @@ async function cleanupWorkspace(workspacePath) {
|
|
|
676
694
|
} catch {
|
|
677
695
|
}
|
|
678
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
|
+
}
|
|
679
712
|
async function writeInputBuffer(buffer, workspacePath) {
|
|
680
713
|
const inputDir = join3(workspacePath, "input");
|
|
681
714
|
await ensureDir(inputDir);
|
|
@@ -702,8 +735,18 @@ async function readFramesAsBuffers(frameNodes, quality) {
|
|
|
702
735
|
)
|
|
703
736
|
);
|
|
704
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
|
+
});
|
|
705
747
|
|
|
706
748
|
// src/core/input-resolver.ts
|
|
749
|
+
import { join as join4 } from "path";
|
|
707
750
|
function resolveOptions(options) {
|
|
708
751
|
const mode = options.mode;
|
|
709
752
|
const inputPath = mode === "file" ? resolveAbsolute(options.inputPath) : void 0;
|
|
@@ -723,6 +766,7 @@ function resolveOptions(options) {
|
|
|
723
766
|
pruneMode,
|
|
724
767
|
outputPath,
|
|
725
768
|
fps: options.fps ?? DEFAULT_FPS,
|
|
769
|
+
maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
726
770
|
scale: options.scale ?? DEFAULT_SCALE,
|
|
727
771
|
quality: options.quality ?? DEFAULT_QUALITY,
|
|
728
772
|
debug: options.debug ?? false
|
|
@@ -748,50 +792,64 @@ async function resolveInput(options, workspacePath) {
|
|
|
748
792
|
}
|
|
749
793
|
throw new Error(`Unsupported input mode: ${options.mode}`);
|
|
750
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
|
+
});
|
|
751
803
|
|
|
752
804
|
// src/utils/min-heap.ts
|
|
753
|
-
var MinHeap
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
793
851
|
}
|
|
794
|
-
};
|
|
852
|
+
});
|
|
795
853
|
|
|
796
854
|
// src/core/pruner.ts
|
|
797
855
|
function pruneTo(graph, frames, targetCount) {
|
|
@@ -953,8 +1011,20 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
953
1011
|
}
|
|
954
1012
|
return pruneTo(syntheticEdges, survivingFrames, maxCount);
|
|
955
1013
|
}
|
|
1014
|
+
var init_pruner = __esm({
|
|
1015
|
+
"src/core/pruner.ts"() {
|
|
1016
|
+
"use strict";
|
|
1017
|
+
init_constants();
|
|
1018
|
+
init_min_heap();
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
956
1021
|
|
|
957
1022
|
// src/core/orchestrator.ts
|
|
1023
|
+
var orchestrator_exports = {};
|
|
1024
|
+
__export(orchestrator_exports, {
|
|
1025
|
+
runPipeline: () => runPipeline
|
|
1026
|
+
});
|
|
1027
|
+
import { randomUUID } from "crypto";
|
|
958
1028
|
async function runPipeline(options) {
|
|
959
1029
|
const startTime = Date.now();
|
|
960
1030
|
const sessionId = randomUUID();
|
|
@@ -1043,6 +1113,286 @@ async function runPipeline(options) {
|
|
|
1043
1113
|
}
|
|
1044
1114
|
}
|
|
1045
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
|
+
};
|
|
1046
1396
|
|
|
1047
1397
|
// src/cli.ts
|
|
1048
1398
|
var require3 = createRequire2(import.meta.url);
|
|
@@ -1051,50 +1401,25 @@ var program = new Command();
|
|
|
1051
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(
|
|
1052
1402
|
"-t, --threshold <number>",
|
|
1053
1403
|
"Normalized threshold 0~1 (default: 0.5; keeps frames above ratio of max change)"
|
|
1054
|
-
).option("-o, --output <path>", "Output directory path").option("--fps <number>", "
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
barCompleteChar: "\u2588",
|
|
1063
|
-
barIncompleteChar: "\u2591",
|
|
1064
|
-
hideCursor: true
|
|
1065
|
-
});
|
|
1066
|
-
bar.start(100, 0, { phase: "EXTRACTING" });
|
|
1067
|
-
const result = await runPipeline({
|
|
1068
|
-
mode: "file",
|
|
1069
|
-
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,
|
|
1070
1412
|
...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
|
|
1071
1413
|
...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
|
|
1072
|
-
|
|
1414
|
+
output: opts.output,
|
|
1073
1415
|
fps: parseInt(opts.fps, 10),
|
|
1416
|
+
maxFrames: parseInt(opts.maxFrames, 10),
|
|
1074
1417
|
scale: parseInt(opts.scale, 10),
|
|
1075
1418
|
quality: parseInt(opts.quality, 10),
|
|
1076
|
-
debug: opts.debug ?? false
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
});
|
|
1081
|
-
bar.stop();
|
|
1082
|
-
console.log(
|
|
1083
|
-
`
|
|
1084
|
-
Done! ${result.originalFramesCount} frames -> ${result.prunedFramesCount} scenes (${result.executionTimeMs}ms)`
|
|
1085
|
-
);
|
|
1086
|
-
if (opts.debug) {
|
|
1087
|
-
console.log(
|
|
1088
|
-
`Output: ${result.outputFiles[0]?.replace(/\/[^/]+$/, "/")}`
|
|
1089
|
-
);
|
|
1090
|
-
result.outputFiles.forEach((f) => console.log(` - ${f}`));
|
|
1091
|
-
}
|
|
1092
|
-
} catch (error) {
|
|
1093
|
-
spinner.fail(
|
|
1094
|
-
`Failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1095
|
-
);
|
|
1096
|
-
process.exit(1);
|
|
1097
|
-
}
|
|
1419
|
+
debug: opts.debug ?? false
|
|
1420
|
+
})
|
|
1421
|
+
);
|
|
1422
|
+
await waitUntilExit();
|
|
1098
1423
|
});
|
|
1099
1424
|
program.parseAsync(process.argv).catch((error) => {
|
|
1100
1425
|
console.error("Fatal error:", error.message);
|