@lumy-pack/scene-sieve 0.0.1 → 0.0.3
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 +230 -72
- package/dist/constants.d.ts +7 -1
- package/dist/core/analyzer.d.ts +26 -0
- package/dist/core/index.d.ts +2 -2
- package/dist/core/input-resolver.d.ts +1 -1
- package/dist/core/pruner.d.ts +27 -8
- package/dist/index.cjs +220 -69
- package/dist/index.mjs +220 -69
- package/dist/types/index.d.ts +2 -2
- package/dist/utils/paths.d.ts +10 -0
- package/package.json +2 -3
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,8 @@ var logger = {
|
|
|
15
15
|
console.log(`${pc.blue("info")} ${message}`);
|
|
16
16
|
},
|
|
17
17
|
success(message) {
|
|
18
|
-
console.log(
|
|
18
|
+
console.log(`
|
|
19
|
+
${pc.green("done")} ${message}`);
|
|
19
20
|
},
|
|
20
21
|
warn(message) {
|
|
21
22
|
console.warn(`${pc.yellow("warn")} ${message}`);
|
|
@@ -38,10 +39,12 @@ import sharp from "sharp";
|
|
|
38
39
|
import { tmpdir } from "os";
|
|
39
40
|
import { join } from "path";
|
|
40
41
|
var APP_NAME = "scene-sieve";
|
|
41
|
-
var DEFAULT_COUNT =
|
|
42
|
+
var DEFAULT_COUNT = 20;
|
|
43
|
+
var DEFAULT_THRESHOLD = 0.5;
|
|
42
44
|
var DEFAULT_FPS = 5;
|
|
43
45
|
var DEFAULT_SCALE = 720;
|
|
44
46
|
var DEFAULT_QUALITY = 80;
|
|
47
|
+
var NORMALIZATION_PERCENTILE = 0.9;
|
|
45
48
|
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
46
49
|
var TEMP_BASE_DIR = tmpdir();
|
|
47
50
|
var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
@@ -61,6 +64,10 @@ var IOU_THRESHOLD = 0.9;
|
|
|
61
64
|
var DECAY_LAMBDA = 0.95;
|
|
62
65
|
var ANIMATION_FRAME_THRESHOLD = 5;
|
|
63
66
|
var MATCH_DISTANCE_THRESHOLD = 0.75;
|
|
67
|
+
var PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
68
|
+
var PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
69
|
+
var PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
70
|
+
var PIXELDIFF_SAMPLE_SPACING = 8;
|
|
64
71
|
function getTempWorkspaceDir(sessionId) {
|
|
65
72
|
return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
66
73
|
}
|
|
@@ -320,6 +327,60 @@ async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
|
320
327
|
if (matches) matches.delete();
|
|
321
328
|
}
|
|
322
329
|
}
|
|
330
|
+
function computePixelDiff(cvLib, frame1, frame2) {
|
|
331
|
+
const cv = cvLib;
|
|
332
|
+
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
333
|
+
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
334
|
+
const diff = new cv.Mat();
|
|
335
|
+
const blurred = new cv.Mat();
|
|
336
|
+
const binary = new cv.Mat();
|
|
337
|
+
const contours = new cv.MatVector();
|
|
338
|
+
const hierarchy = new cv.Mat();
|
|
339
|
+
try {
|
|
340
|
+
mat1.data.set(frame1.data);
|
|
341
|
+
mat2.data.set(frame2.data);
|
|
342
|
+
cv.absdiff(mat1, mat2, diff);
|
|
343
|
+
const ksize = new cv.Size(
|
|
344
|
+
PIXELDIFF_GAUSSIAN_KERNEL,
|
|
345
|
+
PIXELDIFF_GAUSSIAN_KERNEL
|
|
346
|
+
);
|
|
347
|
+
cv.GaussianBlur(diff, blurred, ksize, 0);
|
|
348
|
+
cv.threshold(
|
|
349
|
+
blurred,
|
|
350
|
+
binary,
|
|
351
|
+
PIXELDIFF_BINARY_THRESHOLD,
|
|
352
|
+
255,
|
|
353
|
+
cv.THRESH_BINARY
|
|
354
|
+
);
|
|
355
|
+
cv.findContours(
|
|
356
|
+
binary,
|
|
357
|
+
contours,
|
|
358
|
+
hierarchy,
|
|
359
|
+
cv.RETR_EXTERNAL,
|
|
360
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
361
|
+
);
|
|
362
|
+
const points = [];
|
|
363
|
+
for (let c = 0; c < contours.size(); c++) {
|
|
364
|
+
const contour = contours.get(c);
|
|
365
|
+
const rect = cv.boundingRect(contour);
|
|
366
|
+
if (rect.width * rect.height < PIXELDIFF_CONTOUR_MIN_AREA) continue;
|
|
367
|
+
for (let y = rect.y; y < rect.y + rect.height; y += PIXELDIFF_SAMPLE_SPACING) {
|
|
368
|
+
for (let x = rect.x; x < rect.x + rect.width; x += PIXELDIFF_SAMPLE_SPACING) {
|
|
369
|
+
points.push({ x, y });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return points;
|
|
374
|
+
} finally {
|
|
375
|
+
mat1.delete();
|
|
376
|
+
mat2.delete();
|
|
377
|
+
diff.delete();
|
|
378
|
+
blurred.delete();
|
|
379
|
+
binary.delete();
|
|
380
|
+
contours.delete();
|
|
381
|
+
hierarchy.delete();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
323
384
|
function computeInformationGain(clusters, clusterPoints, imageArea, animationIndices, animationWeights) {
|
|
324
385
|
if (clusters.length === 0) return 0;
|
|
325
386
|
let gain = 0;
|
|
@@ -354,8 +415,28 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
354
415
|
preprocessed[i],
|
|
355
416
|
preprocessed[i + 1]
|
|
356
417
|
);
|
|
357
|
-
|
|
358
|
-
|
|
418
|
+
let dbscanResult = dbscan(sNew, imageWidth, imageHeight);
|
|
419
|
+
let clusters = dbscanResult.boundingBoxes;
|
|
420
|
+
if (clusters.length === 0) {
|
|
421
|
+
const pixelDiffPoints = computePixelDiff(
|
|
422
|
+
cvLib,
|
|
423
|
+
preprocessed[i],
|
|
424
|
+
preprocessed[i + 1]
|
|
425
|
+
);
|
|
426
|
+
if (pixelDiffPoints.length > 0) {
|
|
427
|
+
logger.debug(
|
|
428
|
+
`Edge ${frames[i].id}->${frames[i + 1].id}: pixel-diff fallback (${pixelDiffPoints.length} points)`
|
|
429
|
+
);
|
|
430
|
+
dbscanResult = dbscan(
|
|
431
|
+
pixelDiffPoints,
|
|
432
|
+
imageWidth,
|
|
433
|
+
imageHeight,
|
|
434
|
+
void 0,
|
|
435
|
+
2
|
|
436
|
+
);
|
|
437
|
+
clusters = dbscanResult.boundingBoxes;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
359
440
|
const clusterPointCounts = new Array(clusters.length).fill(0);
|
|
360
441
|
for (const label of dbscanResult.labels) {
|
|
361
442
|
if (label >= 0) {
|
|
@@ -373,7 +454,9 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
373
454
|
animationIndices,
|
|
374
455
|
animationWeights
|
|
375
456
|
);
|
|
376
|
-
logger.debug(
|
|
457
|
+
logger.debug(
|
|
458
|
+
`Edge ${frames[i].id}->${frames[i + 1].id} G(t)=${score.toFixed(6)}`
|
|
459
|
+
);
|
|
377
460
|
edges.push({
|
|
378
461
|
sourceId: frames[i].id,
|
|
379
462
|
targetId: frames[i + 1].id,
|
|
@@ -418,12 +501,13 @@ async function analyzeFrames(ctx) {
|
|
|
418
501
|
// src/core/extractor.ts
|
|
419
502
|
import { readdir } from "fs/promises";
|
|
420
503
|
import { join as join2 } from "path";
|
|
421
|
-
import ffmpeg from "fluent-ffmpeg";
|
|
422
|
-
import ffmpegStatic from "ffmpeg-static";
|
|
423
504
|
import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
|
|
505
|
+
import { execa } from "execa";
|
|
506
|
+
import ffmpegPath from "ffmpeg-static";
|
|
424
507
|
|
|
425
508
|
// src/utils/paths.ts
|
|
426
509
|
import { mkdir, stat } from "fs/promises";
|
|
510
|
+
import { homedir } from "os";
|
|
427
511
|
import { basename, extname, resolve } from "path";
|
|
428
512
|
async function ensureDir(dirPath) {
|
|
429
513
|
await mkdir(dirPath, { recursive: true });
|
|
@@ -436,6 +520,16 @@ async function fileExists(filePath) {
|
|
|
436
520
|
return false;
|
|
437
521
|
}
|
|
438
522
|
}
|
|
523
|
+
function expandTilde(p) {
|
|
524
|
+
if (p === "~") return homedir();
|
|
525
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) {
|
|
526
|
+
return resolve(homedir(), p.slice(2));
|
|
527
|
+
}
|
|
528
|
+
return p;
|
|
529
|
+
}
|
|
530
|
+
function resolveAbsolute(p) {
|
|
531
|
+
return resolve(expandTilde(p));
|
|
532
|
+
}
|
|
439
533
|
function deriveOutputPath(inputPath) {
|
|
440
534
|
const dir = resolve(inputPath, "..");
|
|
441
535
|
const name = basename(inputPath, extname(inputPath));
|
|
@@ -446,8 +540,6 @@ function isSupportedFile(filePath, extensions) {
|
|
|
446
540
|
}
|
|
447
541
|
|
|
448
542
|
// src/core/extractor.ts
|
|
449
|
-
if (ffmpegStatic) ffmpeg.setFfmpegPath(ffmpegStatic);
|
|
450
|
-
ffmpeg.setFfprobePath(ffprobePath);
|
|
451
543
|
async function extractFrames(ctx) {
|
|
452
544
|
const framesDir = join2(ctx.workspacePath, "frames");
|
|
453
545
|
const { inputPath, fps, scale } = ctx.options;
|
|
@@ -458,7 +550,10 @@ async function extractFrames(ctx) {
|
|
|
458
550
|
if (!exists) {
|
|
459
551
|
throw new Error(`Input file not found: ${inputPath}`);
|
|
460
552
|
}
|
|
461
|
-
const allExtensions = [
|
|
553
|
+
const allExtensions = [
|
|
554
|
+
...SUPPORTED_VIDEO_EXTENSIONS,
|
|
555
|
+
...SUPPORTED_GIF_EXTENSIONS
|
|
556
|
+
];
|
|
462
557
|
if (!isSupportedFile(inputPath, allExtensions)) {
|
|
463
558
|
throw new Error(`Unsupported file format: ${inputPath}`);
|
|
464
559
|
}
|
|
@@ -484,35 +579,43 @@ async function extractFrames(ctx) {
|
|
|
484
579
|
}
|
|
485
580
|
async function extractIFrames(inputPath, outputDir, scale) {
|
|
486
581
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
487
|
-
await
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
582
|
+
await execa(ffmpegPath, [
|
|
583
|
+
"-i",
|
|
584
|
+
inputPath,
|
|
585
|
+
"-vf",
|
|
586
|
+
`select='eq(pict_type,I)',scale=-1:${scale}`,
|
|
587
|
+
"-vsync",
|
|
588
|
+
"vfr",
|
|
589
|
+
"-q:v",
|
|
590
|
+
"2",
|
|
591
|
+
outputPattern
|
|
592
|
+
]);
|
|
494
593
|
return buildFrameList(outputDir, inputPath);
|
|
495
594
|
}
|
|
496
595
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
497
596
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
498
|
-
await
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
597
|
+
await execa(ffmpegPath, [
|
|
598
|
+
"-i",
|
|
599
|
+
inputPath,
|
|
600
|
+
"-vf",
|
|
601
|
+
`fps=${fps},scale=-1:${scale}`,
|
|
602
|
+
"-q:v",
|
|
603
|
+
"2",
|
|
604
|
+
outputPattern
|
|
605
|
+
]);
|
|
504
606
|
return buildFrameList(outputDir, inputPath);
|
|
505
607
|
}
|
|
506
608
|
async function getVideoDuration(inputPath) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
609
|
+
const { stdout } = await execa(ffprobePath, [
|
|
610
|
+
"-v",
|
|
611
|
+
"quiet",
|
|
612
|
+
"-print_format",
|
|
613
|
+
"json",
|
|
614
|
+
"-show_format",
|
|
615
|
+
inputPath
|
|
616
|
+
]);
|
|
617
|
+
const metadata = JSON.parse(stdout);
|
|
618
|
+
return parseFloat(metadata.format?.duration ?? "0");
|
|
516
619
|
}
|
|
517
620
|
async function buildFrameList(framesDir, inputPath) {
|
|
518
621
|
const files = await readdir(framesDir);
|
|
@@ -524,7 +627,9 @@ async function buildFrameList(framesDir, inputPath) {
|
|
|
524
627
|
try {
|
|
525
628
|
duration = await getVideoDuration(inputPath);
|
|
526
629
|
} catch {
|
|
527
|
-
logger.debug(
|
|
630
|
+
logger.debug(
|
|
631
|
+
"Could not determine video duration; using frame index for timestamps"
|
|
632
|
+
);
|
|
528
633
|
}
|
|
529
634
|
return jpgFiles.map((file, index) => ({
|
|
530
635
|
id: index,
|
|
@@ -599,17 +704,15 @@ async function readFramesAsBuffers(frameNodes, quality) {
|
|
|
599
704
|
// src/core/input-resolver.ts
|
|
600
705
|
function resolveOptions(options) {
|
|
601
706
|
const mode = options.mode;
|
|
602
|
-
const inputPath = mode === "file" ? options.inputPath : void 0;
|
|
707
|
+
const inputPath = mode === "file" ? resolveAbsolute(options.inputPath) : void 0;
|
|
603
708
|
const outputPath = options.outputPath ?? (inputPath ? deriveOutputPath(inputPath) : join4(process.cwd(), "scene-sieve-output"));
|
|
604
|
-
const threshold = options.threshold;
|
|
605
|
-
if (threshold
|
|
709
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
710
|
+
if (threshold <= 0 || threshold > 1) {
|
|
606
711
|
throw new Error(
|
|
607
712
|
`threshold must be in range (0, 1], received: ${threshold}`
|
|
608
713
|
);
|
|
609
714
|
}
|
|
610
|
-
const
|
|
611
|
-
const hasExplicitCount = options.count !== void 0;
|
|
612
|
-
const pruneMode = hasThreshold && hasExplicitCount ? "threshold-with-cap" : hasThreshold ? "threshold" : "count";
|
|
715
|
+
const pruneMode = "threshold-with-cap";
|
|
613
716
|
return {
|
|
614
717
|
mode,
|
|
615
718
|
inputPath,
|
|
@@ -625,10 +728,16 @@ function resolveOptions(options) {
|
|
|
625
728
|
}
|
|
626
729
|
async function resolveInput(options, workspacePath) {
|
|
627
730
|
if (options.mode === "file") {
|
|
628
|
-
return {
|
|
731
|
+
return {
|
|
732
|
+
frames: [],
|
|
733
|
+
resolvedInputPath: resolveAbsolute(options.inputPath)
|
|
734
|
+
};
|
|
629
735
|
}
|
|
630
736
|
if (options.mode === "buffer") {
|
|
631
|
-
const resolvedInputPath = await writeInputBuffer(
|
|
737
|
+
const resolvedInputPath = await writeInputBuffer(
|
|
738
|
+
options.inputBuffer,
|
|
739
|
+
workspacePath
|
|
740
|
+
);
|
|
632
741
|
return { frames: [], resolvedInputPath };
|
|
633
742
|
}
|
|
634
743
|
if (options.mode === "frames") {
|
|
@@ -697,7 +806,11 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
697
806
|
const heap = new MinHeap();
|
|
698
807
|
for (const edge of graph) {
|
|
699
808
|
edgeScore.set(`${edge.sourceId}:${edge.targetId}`, edge.score);
|
|
700
|
-
heap.push({
|
|
809
|
+
heap.push({
|
|
810
|
+
score: edge.score,
|
|
811
|
+
srcId: edge.sourceId,
|
|
812
|
+
tgtId: edge.targetId
|
|
813
|
+
});
|
|
701
814
|
}
|
|
702
815
|
const surviving = new Set(frames.map((f) => f.id));
|
|
703
816
|
const firstId = frames[0].id;
|
|
@@ -733,9 +846,55 @@ function normalizeScores(graph) {
|
|
|
733
846
|
const safeScores = graph.map(
|
|
734
847
|
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
735
848
|
);
|
|
736
|
-
const
|
|
737
|
-
if (
|
|
738
|
-
|
|
849
|
+
const sorted = [...safeScores].filter((s) => s > 0).sort((a, b) => a - b);
|
|
850
|
+
if (sorted.length === 0) return safeScores;
|
|
851
|
+
const pIdx = Math.min(
|
|
852
|
+
Math.floor(sorted.length * NORMALIZATION_PERCENTILE),
|
|
853
|
+
sorted.length - 1
|
|
854
|
+
);
|
|
855
|
+
const refScore = sorted[pIdx];
|
|
856
|
+
return safeScores.map((s) => Math.min(s / refScore, 1));
|
|
857
|
+
}
|
|
858
|
+
function suppressConsecutiveRuns(graph, passingIndices, normalizedScores) {
|
|
859
|
+
const result = /* @__PURE__ */ new Set();
|
|
860
|
+
let runStart = 0;
|
|
861
|
+
while (runStart < passingIndices.length) {
|
|
862
|
+
let runEnd = runStart;
|
|
863
|
+
while (runEnd + 1 < passingIndices.length && passingIndices[runEnd + 1] === passingIndices[runEnd] + 1) {
|
|
864
|
+
runEnd++;
|
|
865
|
+
}
|
|
866
|
+
const runLen = runEnd - runStart + 1;
|
|
867
|
+
if (runLen === 1) {
|
|
868
|
+
result.add(graph[passingIndices[runStart]].targetId);
|
|
869
|
+
} else {
|
|
870
|
+
const peaks = [];
|
|
871
|
+
for (let j = runStart; j <= runEnd; j++) {
|
|
872
|
+
const idx = passingIndices[j];
|
|
873
|
+
const score = normalizedScores[idx];
|
|
874
|
+
const prevScore = j > runStart ? normalizedScores[passingIndices[j - 1]] : -Infinity;
|
|
875
|
+
const nextScore = j < runEnd ? normalizedScores[passingIndices[j + 1]] : -Infinity;
|
|
876
|
+
if (score > prevScore && score > nextScore) {
|
|
877
|
+
peaks.push(idx);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (peaks.length > 0) {
|
|
881
|
+
for (const peakIdx of peaks) {
|
|
882
|
+
result.add(graph[peakIdx].targetId);
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
let peakIdx = passingIndices[runStart];
|
|
886
|
+
for (let j = runStart + 1; j <= runEnd; j++) {
|
|
887
|
+
const idx = passingIndices[j];
|
|
888
|
+
if (normalizedScores[idx] > normalizedScores[peakIdx]) {
|
|
889
|
+
peakIdx = idx;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
result.add(graph[peakIdx].targetId);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
runStart = runEnd + 1;
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
739
898
|
}
|
|
740
899
|
function pruneByThreshold(graph, frames, threshold) {
|
|
741
900
|
if (frames.length === 0) return /* @__PURE__ */ new Set();
|
|
@@ -743,11 +902,16 @@ function pruneByThreshold(graph, frames, threshold) {
|
|
|
743
902
|
surviving.add(frames[0].id);
|
|
744
903
|
surviving.add(frames[frames.length - 1].id);
|
|
745
904
|
const normalized = normalizeScores(graph);
|
|
905
|
+
const passingIndices = [];
|
|
746
906
|
for (let i = 0; i < graph.length; i++) {
|
|
747
907
|
if (normalized[i] >= threshold) {
|
|
748
|
-
|
|
908
|
+
passingIndices.push(i);
|
|
749
909
|
}
|
|
750
910
|
}
|
|
911
|
+
const nmsTargets = suppressConsecutiveRuns(graph, passingIndices, normalized);
|
|
912
|
+
for (const id of nmsTargets) {
|
|
913
|
+
surviving.add(id);
|
|
914
|
+
}
|
|
751
915
|
return surviving;
|
|
752
916
|
}
|
|
753
917
|
function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
@@ -831,35 +995,22 @@ async function runPipeline(options) {
|
|
|
831
995
|
ctx.status = "ANALYZING";
|
|
832
996
|
ctx.graph = await analyzeFrames(ctx);
|
|
833
997
|
ctx.status = "PRUNING";
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
resolvedOptions.threshold,
|
|
841
|
-
resolvedOptions.count
|
|
842
|
-
);
|
|
843
|
-
break;
|
|
844
|
-
case "threshold":
|
|
845
|
-
survivingIds = pruneByThreshold(
|
|
846
|
-
ctx.graph,
|
|
847
|
-
ctx.frames,
|
|
848
|
-
resolvedOptions.threshold
|
|
849
|
-
);
|
|
850
|
-
break;
|
|
851
|
-
case "count":
|
|
852
|
-
default:
|
|
853
|
-
survivingIds = pruneTo(ctx.graph, ctx.frames, resolvedOptions.count);
|
|
854
|
-
break;
|
|
855
|
-
}
|
|
998
|
+
const survivingIds = pruneByThresholdWithCap(
|
|
999
|
+
ctx.graph,
|
|
1000
|
+
ctx.frames,
|
|
1001
|
+
resolvedOptions.threshold,
|
|
1002
|
+
resolvedOptions.count
|
|
1003
|
+
);
|
|
856
1004
|
const prunedFrames = ctx.frames.filter((f) => survivingIds.has(f.id));
|
|
857
1005
|
ctx.emitProgress(100);
|
|
858
1006
|
ctx.status = "FINALIZING";
|
|
859
1007
|
let outputFiles = [];
|
|
860
1008
|
let outputBuffers;
|
|
861
1009
|
if (resolvedOptions.mode === "buffer" || resolvedOptions.mode === "frames") {
|
|
862
|
-
outputBuffers = await readFramesAsBuffers(
|
|
1010
|
+
outputBuffers = await readFramesAsBuffers(
|
|
1011
|
+
prunedFrames,
|
|
1012
|
+
resolvedOptions.quality
|
|
1013
|
+
);
|
|
863
1014
|
ctx.emitProgress(100);
|
|
864
1015
|
} else {
|
|
865
1016
|
outputFiles = await finalizeOutput(ctx, prunedFrames);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface ResolvedOptions {
|
|
|
24
24
|
mode: 'file' | 'buffer' | 'frames';
|
|
25
25
|
inputPath?: string;
|
|
26
26
|
count: number;
|
|
27
|
-
threshold
|
|
28
|
-
pruneMode: '
|
|
27
|
+
threshold: number;
|
|
28
|
+
pruneMode: 'threshold-with-cap';
|
|
29
29
|
outputPath: string;
|
|
30
30
|
fps: number;
|
|
31
31
|
scale: number;
|
package/dist/utils/paths.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
2
|
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
3
|
+
/**
|
|
4
|
+
* Expand leading ~ to homedir. Node's path.resolve() does not expand ~,
|
|
5
|
+
* so paths like ~/Desktop/foo depend on process.cwd() and can produce
|
|
6
|
+
* different results when run from different directories.
|
|
7
|
+
*/
|
|
8
|
+
export declare function expandTilde(p: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve path to absolute. Expands ~ to homedir first so that the result
|
|
11
|
+
* does not depend on process.cwd().
|
|
12
|
+
*/
|
|
3
13
|
export declare function resolveAbsolute(p: string): string;
|
|
4
14
|
/**
|
|
5
15
|
* Derive default output directory name from input file path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumy-pack/scene-sieve",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "CLI tool for extracting key frames from video and GIF files",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -58,15 +58,14 @@
|
|
|
58
58
|
"@techstark/opencv-js": "4.12.0-release.1",
|
|
59
59
|
"cli-progress": "^3.12.0",
|
|
60
60
|
"commander": "^12.1.0",
|
|
61
|
+
"execa": "^9.5.0",
|
|
61
62
|
"ffmpeg-static": "^5.2.0",
|
|
62
|
-
"fluent-ffmpeg": "^2.1.3",
|
|
63
63
|
"ora": "^8.0.0",
|
|
64
64
|
"picocolors": "^1.1.1",
|
|
65
65
|
"sharp": "^0.33.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/cli-progress": "^3.11.6",
|
|
69
|
-
"@types/fluent-ffmpeg": "^2.1.27",
|
|
70
69
|
"@types/node": "^20.11.0",
|
|
71
70
|
"@vitest/coverage-v8": "^3.2.4"
|
|
72
71
|
}
|