@lumy-pack/scene-sieve 0.0.1 → 0.0.2
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 +212 -69
- 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 +202 -66
- package/dist/index.mjs +202 -66
- package/dist/types/index.d.ts +2 -2
- package/package.json +2 -3
package/dist/index.mjs
CHANGED
|
@@ -38,10 +38,12 @@ import sharp from "sharp";
|
|
|
38
38
|
import { tmpdir } from "os";
|
|
39
39
|
import { join } from "path";
|
|
40
40
|
var APP_NAME = "scene-sieve";
|
|
41
|
-
var DEFAULT_COUNT =
|
|
41
|
+
var DEFAULT_COUNT = 20;
|
|
42
|
+
var DEFAULT_THRESHOLD = 0.5;
|
|
42
43
|
var DEFAULT_FPS = 5;
|
|
43
44
|
var DEFAULT_SCALE = 720;
|
|
44
45
|
var DEFAULT_QUALITY = 80;
|
|
46
|
+
var NORMALIZATION_PERCENTILE = 0.9;
|
|
45
47
|
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
46
48
|
var TEMP_BASE_DIR = tmpdir();
|
|
47
49
|
var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
@@ -61,6 +63,10 @@ var IOU_THRESHOLD = 0.9;
|
|
|
61
63
|
var DECAY_LAMBDA = 0.95;
|
|
62
64
|
var ANIMATION_FRAME_THRESHOLD = 5;
|
|
63
65
|
var MATCH_DISTANCE_THRESHOLD = 0.75;
|
|
66
|
+
var PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
67
|
+
var PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
68
|
+
var PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
69
|
+
var PIXELDIFF_SAMPLE_SPACING = 8;
|
|
64
70
|
function getTempWorkspaceDir(sessionId) {
|
|
65
71
|
return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
66
72
|
}
|
|
@@ -320,6 +326,60 @@ async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
|
320
326
|
if (matches) matches.delete();
|
|
321
327
|
}
|
|
322
328
|
}
|
|
329
|
+
function computePixelDiff(cvLib, frame1, frame2) {
|
|
330
|
+
const cv = cvLib;
|
|
331
|
+
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
332
|
+
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
333
|
+
const diff = new cv.Mat();
|
|
334
|
+
const blurred = new cv.Mat();
|
|
335
|
+
const binary = new cv.Mat();
|
|
336
|
+
const contours = new cv.MatVector();
|
|
337
|
+
const hierarchy = new cv.Mat();
|
|
338
|
+
try {
|
|
339
|
+
mat1.data.set(frame1.data);
|
|
340
|
+
mat2.data.set(frame2.data);
|
|
341
|
+
cv.absdiff(mat1, mat2, diff);
|
|
342
|
+
const ksize = new cv.Size(
|
|
343
|
+
PIXELDIFF_GAUSSIAN_KERNEL,
|
|
344
|
+
PIXELDIFF_GAUSSIAN_KERNEL
|
|
345
|
+
);
|
|
346
|
+
cv.GaussianBlur(diff, blurred, ksize, 0);
|
|
347
|
+
cv.threshold(
|
|
348
|
+
blurred,
|
|
349
|
+
binary,
|
|
350
|
+
PIXELDIFF_BINARY_THRESHOLD,
|
|
351
|
+
255,
|
|
352
|
+
cv.THRESH_BINARY
|
|
353
|
+
);
|
|
354
|
+
cv.findContours(
|
|
355
|
+
binary,
|
|
356
|
+
contours,
|
|
357
|
+
hierarchy,
|
|
358
|
+
cv.RETR_EXTERNAL,
|
|
359
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
360
|
+
);
|
|
361
|
+
const points = [];
|
|
362
|
+
for (let c = 0; c < contours.size(); c++) {
|
|
363
|
+
const contour = contours.get(c);
|
|
364
|
+
const rect = cv.boundingRect(contour);
|
|
365
|
+
if (rect.width * rect.height < PIXELDIFF_CONTOUR_MIN_AREA) continue;
|
|
366
|
+
for (let y = rect.y; y < rect.y + rect.height; y += PIXELDIFF_SAMPLE_SPACING) {
|
|
367
|
+
for (let x = rect.x; x < rect.x + rect.width; x += PIXELDIFF_SAMPLE_SPACING) {
|
|
368
|
+
points.push({ x, y });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return points;
|
|
373
|
+
} finally {
|
|
374
|
+
mat1.delete();
|
|
375
|
+
mat2.delete();
|
|
376
|
+
diff.delete();
|
|
377
|
+
blurred.delete();
|
|
378
|
+
binary.delete();
|
|
379
|
+
contours.delete();
|
|
380
|
+
hierarchy.delete();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
323
383
|
function computeInformationGain(clusters, clusterPoints, imageArea, animationIndices, animationWeights) {
|
|
324
384
|
if (clusters.length === 0) return 0;
|
|
325
385
|
let gain = 0;
|
|
@@ -354,8 +414,28 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
354
414
|
preprocessed[i],
|
|
355
415
|
preprocessed[i + 1]
|
|
356
416
|
);
|
|
357
|
-
|
|
358
|
-
|
|
417
|
+
let dbscanResult = dbscan(sNew, imageWidth, imageHeight);
|
|
418
|
+
let clusters = dbscanResult.boundingBoxes;
|
|
419
|
+
if (clusters.length === 0) {
|
|
420
|
+
const pixelDiffPoints = computePixelDiff(
|
|
421
|
+
cvLib,
|
|
422
|
+
preprocessed[i],
|
|
423
|
+
preprocessed[i + 1]
|
|
424
|
+
);
|
|
425
|
+
if (pixelDiffPoints.length > 0) {
|
|
426
|
+
logger.debug(
|
|
427
|
+
`Edge ${frames[i].id}->${frames[i + 1].id}: pixel-diff fallback (${pixelDiffPoints.length} points)`
|
|
428
|
+
);
|
|
429
|
+
dbscanResult = dbscan(
|
|
430
|
+
pixelDiffPoints,
|
|
431
|
+
imageWidth,
|
|
432
|
+
imageHeight,
|
|
433
|
+
void 0,
|
|
434
|
+
2
|
|
435
|
+
);
|
|
436
|
+
clusters = dbscanResult.boundingBoxes;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
359
439
|
const clusterPointCounts = new Array(clusters.length).fill(0);
|
|
360
440
|
for (const label of dbscanResult.labels) {
|
|
361
441
|
if (label >= 0) {
|
|
@@ -373,7 +453,9 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
373
453
|
animationIndices,
|
|
374
454
|
animationWeights
|
|
375
455
|
);
|
|
376
|
-
logger.debug(
|
|
456
|
+
logger.debug(
|
|
457
|
+
`Edge ${frames[i].id}->${frames[i + 1].id} G(t)=${score.toFixed(6)}`
|
|
458
|
+
);
|
|
377
459
|
edges.push({
|
|
378
460
|
sourceId: frames[i].id,
|
|
379
461
|
targetId: frames[i + 1].id,
|
|
@@ -418,9 +500,9 @@ async function analyzeFrames(ctx) {
|
|
|
418
500
|
// src/core/extractor.ts
|
|
419
501
|
import { readdir } from "fs/promises";
|
|
420
502
|
import { join as join2 } from "path";
|
|
421
|
-
import ffmpeg from "fluent-ffmpeg";
|
|
422
|
-
import ffmpegStatic from "ffmpeg-static";
|
|
423
503
|
import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
|
|
504
|
+
import { execa } from "execa";
|
|
505
|
+
import ffmpegPath from "ffmpeg-static";
|
|
424
506
|
|
|
425
507
|
// src/utils/paths.ts
|
|
426
508
|
import { mkdir, stat } from "fs/promises";
|
|
@@ -446,8 +528,6 @@ function isSupportedFile(filePath, extensions) {
|
|
|
446
528
|
}
|
|
447
529
|
|
|
448
530
|
// src/core/extractor.ts
|
|
449
|
-
if (ffmpegStatic) ffmpeg.setFfmpegPath(ffmpegStatic);
|
|
450
|
-
ffmpeg.setFfprobePath(ffprobePath);
|
|
451
531
|
async function extractFrames(ctx) {
|
|
452
532
|
const framesDir = join2(ctx.workspacePath, "frames");
|
|
453
533
|
const { inputPath, fps, scale } = ctx.options;
|
|
@@ -458,7 +538,10 @@ async function extractFrames(ctx) {
|
|
|
458
538
|
if (!exists) {
|
|
459
539
|
throw new Error(`Input file not found: ${inputPath}`);
|
|
460
540
|
}
|
|
461
|
-
const allExtensions = [
|
|
541
|
+
const allExtensions = [
|
|
542
|
+
...SUPPORTED_VIDEO_EXTENSIONS,
|
|
543
|
+
...SUPPORTED_GIF_EXTENSIONS
|
|
544
|
+
];
|
|
462
545
|
if (!isSupportedFile(inputPath, allExtensions)) {
|
|
463
546
|
throw new Error(`Unsupported file format: ${inputPath}`);
|
|
464
547
|
}
|
|
@@ -484,35 +567,43 @@ async function extractFrames(ctx) {
|
|
|
484
567
|
}
|
|
485
568
|
async function extractIFrames(inputPath, outputDir, scale) {
|
|
486
569
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
487
|
-
await
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
570
|
+
await execa(ffmpegPath, [
|
|
571
|
+
"-i",
|
|
572
|
+
inputPath,
|
|
573
|
+
"-vf",
|
|
574
|
+
`select='eq(pict_type,I)',scale=-1:${scale}`,
|
|
575
|
+
"-vsync",
|
|
576
|
+
"vfr",
|
|
577
|
+
"-q:v",
|
|
578
|
+
"2",
|
|
579
|
+
outputPattern
|
|
580
|
+
]);
|
|
494
581
|
return buildFrameList(outputDir, inputPath);
|
|
495
582
|
}
|
|
496
583
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
497
584
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
498
|
-
await
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
585
|
+
await execa(ffmpegPath, [
|
|
586
|
+
"-i",
|
|
587
|
+
inputPath,
|
|
588
|
+
"-vf",
|
|
589
|
+
`fps=${fps},scale=-1:${scale}`,
|
|
590
|
+
"-q:v",
|
|
591
|
+
"2",
|
|
592
|
+
outputPattern
|
|
593
|
+
]);
|
|
504
594
|
return buildFrameList(outputDir, inputPath);
|
|
505
595
|
}
|
|
506
596
|
async function getVideoDuration(inputPath) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
597
|
+
const { stdout } = await execa(ffprobePath, [
|
|
598
|
+
"-v",
|
|
599
|
+
"quiet",
|
|
600
|
+
"-print_format",
|
|
601
|
+
"json",
|
|
602
|
+
"-show_format",
|
|
603
|
+
inputPath
|
|
604
|
+
]);
|
|
605
|
+
const metadata = JSON.parse(stdout);
|
|
606
|
+
return parseFloat(metadata.format?.duration ?? "0");
|
|
516
607
|
}
|
|
517
608
|
async function buildFrameList(framesDir, inputPath) {
|
|
518
609
|
const files = await readdir(framesDir);
|
|
@@ -524,7 +615,9 @@ async function buildFrameList(framesDir, inputPath) {
|
|
|
524
615
|
try {
|
|
525
616
|
duration = await getVideoDuration(inputPath);
|
|
526
617
|
} catch {
|
|
527
|
-
logger.debug(
|
|
618
|
+
logger.debug(
|
|
619
|
+
"Could not determine video duration; using frame index for timestamps"
|
|
620
|
+
);
|
|
528
621
|
}
|
|
529
622
|
return jpgFiles.map((file, index) => ({
|
|
530
623
|
id: index,
|
|
@@ -601,15 +694,13 @@ function resolveOptions(options) {
|
|
|
601
694
|
const mode = options.mode;
|
|
602
695
|
const inputPath = mode === "file" ? options.inputPath : void 0;
|
|
603
696
|
const outputPath = options.outputPath ?? (inputPath ? deriveOutputPath(inputPath) : join4(process.cwd(), "scene-sieve-output"));
|
|
604
|
-
const threshold = options.threshold;
|
|
605
|
-
if (threshold
|
|
697
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
698
|
+
if (threshold <= 0 || threshold > 1) {
|
|
606
699
|
throw new Error(
|
|
607
700
|
`threshold must be in range (0, 1], received: ${threshold}`
|
|
608
701
|
);
|
|
609
702
|
}
|
|
610
|
-
const
|
|
611
|
-
const hasExplicitCount = options.count !== void 0;
|
|
612
|
-
const pruneMode = hasThreshold && hasExplicitCount ? "threshold-with-cap" : hasThreshold ? "threshold" : "count";
|
|
703
|
+
const pruneMode = "threshold-with-cap";
|
|
613
704
|
return {
|
|
614
705
|
mode,
|
|
615
706
|
inputPath,
|
|
@@ -628,7 +719,10 @@ async function resolveInput(options, workspacePath) {
|
|
|
628
719
|
return { frames: [], resolvedInputPath: options.inputPath };
|
|
629
720
|
}
|
|
630
721
|
if (options.mode === "buffer") {
|
|
631
|
-
const resolvedInputPath = await writeInputBuffer(
|
|
722
|
+
const resolvedInputPath = await writeInputBuffer(
|
|
723
|
+
options.inputBuffer,
|
|
724
|
+
workspacePath
|
|
725
|
+
);
|
|
632
726
|
return { frames: [], resolvedInputPath };
|
|
633
727
|
}
|
|
634
728
|
if (options.mode === "frames") {
|
|
@@ -697,7 +791,11 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
697
791
|
const heap = new MinHeap();
|
|
698
792
|
for (const edge of graph) {
|
|
699
793
|
edgeScore.set(`${edge.sourceId}:${edge.targetId}`, edge.score);
|
|
700
|
-
heap.push({
|
|
794
|
+
heap.push({
|
|
795
|
+
score: edge.score,
|
|
796
|
+
srcId: edge.sourceId,
|
|
797
|
+
tgtId: edge.targetId
|
|
798
|
+
});
|
|
701
799
|
}
|
|
702
800
|
const surviving = new Set(frames.map((f) => f.id));
|
|
703
801
|
const firstId = frames[0].id;
|
|
@@ -733,9 +831,55 @@ function normalizeScores(graph) {
|
|
|
733
831
|
const safeScores = graph.map(
|
|
734
832
|
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
735
833
|
);
|
|
736
|
-
const
|
|
737
|
-
if (
|
|
738
|
-
|
|
834
|
+
const sorted = [...safeScores].filter((s) => s > 0).sort((a, b) => a - b);
|
|
835
|
+
if (sorted.length === 0) return safeScores;
|
|
836
|
+
const pIdx = Math.min(
|
|
837
|
+
Math.floor(sorted.length * NORMALIZATION_PERCENTILE),
|
|
838
|
+
sorted.length - 1
|
|
839
|
+
);
|
|
840
|
+
const refScore = sorted[pIdx];
|
|
841
|
+
return safeScores.map((s) => Math.min(s / refScore, 1));
|
|
842
|
+
}
|
|
843
|
+
function suppressConsecutiveRuns(graph, passingIndices, normalizedScores) {
|
|
844
|
+
const result = /* @__PURE__ */ new Set();
|
|
845
|
+
let runStart = 0;
|
|
846
|
+
while (runStart < passingIndices.length) {
|
|
847
|
+
let runEnd = runStart;
|
|
848
|
+
while (runEnd + 1 < passingIndices.length && passingIndices[runEnd + 1] === passingIndices[runEnd] + 1) {
|
|
849
|
+
runEnd++;
|
|
850
|
+
}
|
|
851
|
+
const runLen = runEnd - runStart + 1;
|
|
852
|
+
if (runLen === 1) {
|
|
853
|
+
result.add(graph[passingIndices[runStart]].targetId);
|
|
854
|
+
} else {
|
|
855
|
+
const peaks = [];
|
|
856
|
+
for (let j = runStart; j <= runEnd; j++) {
|
|
857
|
+
const idx = passingIndices[j];
|
|
858
|
+
const score = normalizedScores[idx];
|
|
859
|
+
const prevScore = j > runStart ? normalizedScores[passingIndices[j - 1]] : -Infinity;
|
|
860
|
+
const nextScore = j < runEnd ? normalizedScores[passingIndices[j + 1]] : -Infinity;
|
|
861
|
+
if (score > prevScore && score > nextScore) {
|
|
862
|
+
peaks.push(idx);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (peaks.length > 0) {
|
|
866
|
+
for (const peakIdx of peaks) {
|
|
867
|
+
result.add(graph[peakIdx].targetId);
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
let peakIdx = passingIndices[runStart];
|
|
871
|
+
for (let j = runStart + 1; j <= runEnd; j++) {
|
|
872
|
+
const idx = passingIndices[j];
|
|
873
|
+
if (normalizedScores[idx] > normalizedScores[peakIdx]) {
|
|
874
|
+
peakIdx = idx;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
result.add(graph[peakIdx].targetId);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
runStart = runEnd + 1;
|
|
881
|
+
}
|
|
882
|
+
return result;
|
|
739
883
|
}
|
|
740
884
|
function pruneByThreshold(graph, frames, threshold) {
|
|
741
885
|
if (frames.length === 0) return /* @__PURE__ */ new Set();
|
|
@@ -743,11 +887,16 @@ function pruneByThreshold(graph, frames, threshold) {
|
|
|
743
887
|
surviving.add(frames[0].id);
|
|
744
888
|
surviving.add(frames[frames.length - 1].id);
|
|
745
889
|
const normalized = normalizeScores(graph);
|
|
890
|
+
const passingIndices = [];
|
|
746
891
|
for (let i = 0; i < graph.length; i++) {
|
|
747
892
|
if (normalized[i] >= threshold) {
|
|
748
|
-
|
|
893
|
+
passingIndices.push(i);
|
|
749
894
|
}
|
|
750
895
|
}
|
|
896
|
+
const nmsTargets = suppressConsecutiveRuns(graph, passingIndices, normalized);
|
|
897
|
+
for (const id of nmsTargets) {
|
|
898
|
+
surviving.add(id);
|
|
899
|
+
}
|
|
751
900
|
return surviving;
|
|
752
901
|
}
|
|
753
902
|
function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
@@ -831,35 +980,22 @@ async function runPipeline(options) {
|
|
|
831
980
|
ctx.status = "ANALYZING";
|
|
832
981
|
ctx.graph = await analyzeFrames(ctx);
|
|
833
982
|
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
|
-
}
|
|
983
|
+
const survivingIds = pruneByThresholdWithCap(
|
|
984
|
+
ctx.graph,
|
|
985
|
+
ctx.frames,
|
|
986
|
+
resolvedOptions.threshold,
|
|
987
|
+
resolvedOptions.count
|
|
988
|
+
);
|
|
856
989
|
const prunedFrames = ctx.frames.filter((f) => survivingIds.has(f.id));
|
|
857
990
|
ctx.emitProgress(100);
|
|
858
991
|
ctx.status = "FINALIZING";
|
|
859
992
|
let outputFiles = [];
|
|
860
993
|
let outputBuffers;
|
|
861
994
|
if (resolvedOptions.mode === "buffer" || resolvedOptions.mode === "frames") {
|
|
862
|
-
outputBuffers = await readFramesAsBuffers(
|
|
995
|
+
outputBuffers = await readFramesAsBuffers(
|
|
996
|
+
prunedFrames,
|
|
997
|
+
resolvedOptions.quality
|
|
998
|
+
);
|
|
863
999
|
ctx.emitProgress(100);
|
|
864
1000
|
} else {
|
|
865
1001
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumy-pack/scene-sieve",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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
|
}
|