@lumy-pack/scene-sieve 0.0.4 → 0.0.6
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 +693 -255
- package/dist/commands/Sieve.d.ts +15 -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/analyzer.d.ts +9 -2
- 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 +115 -44
- package/dist/index.mjs +116 -45
- package/dist/pipeline-worker.mjs +1136 -0
- package/dist/types/index.d.ts +28 -0
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -44,6 +44,7 @@ var DEFAULT_THRESHOLD = 0.5;
|
|
|
44
44
|
var DEFAULT_FPS = 5;
|
|
45
45
|
var DEFAULT_SCALE = 720;
|
|
46
46
|
var DEFAULT_QUALITY = 80;
|
|
47
|
+
var DEFAULT_MAX_FRAMES = 300;
|
|
47
48
|
var NORMALIZATION_PERCENTILE = 0.9;
|
|
48
49
|
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
49
50
|
var TEMP_BASE_DIR = tmpdir();
|
|
@@ -56,8 +57,8 @@ var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
|
56
57
|
];
|
|
57
58
|
var SUPPORTED_GIF_EXTENSIONS = [".gif"];
|
|
58
59
|
var FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
60
|
+
var FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
|
|
59
61
|
var OPENCV_BATCH_SIZE = 10;
|
|
60
|
-
var MIN_IFRAME_COUNT = 3;
|
|
61
62
|
var DBSCAN_ALPHA = 0.03;
|
|
62
63
|
var DBSCAN_MIN_PTS = 4;
|
|
63
64
|
var IOU_THRESHOLD = 0.9;
|
|
@@ -196,7 +197,13 @@ function computeIoU(a, b) {
|
|
|
196
197
|
return union === 0 ? 0 : intersection / union;
|
|
197
198
|
}
|
|
198
199
|
var IoUTracker = class {
|
|
200
|
+
constructor(fps = DEFAULT_FPS, iouThreshold = IOU_THRESHOLD, animationThreshold = ANIMATION_FRAME_THRESHOLD) {
|
|
201
|
+
this.fps = fps;
|
|
202
|
+
this.iouThreshold = iouThreshold;
|
|
203
|
+
this.animationThreshold = animationThreshold;
|
|
204
|
+
}
|
|
199
205
|
regions = [];
|
|
206
|
+
extractedAnimations = [];
|
|
200
207
|
update(boxes, pairIndex) {
|
|
201
208
|
const animationIndices = /* @__PURE__ */ new Set();
|
|
202
209
|
const matched = /* @__PURE__ */ new Set();
|
|
@@ -212,7 +219,7 @@ var IoUTracker = class {
|
|
|
212
219
|
bestRegionIdx = ri;
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
|
-
if (bestIoU >
|
|
222
|
+
if (bestIoU > this.iouThreshold && bestRegionIdx !== -1) {
|
|
216
223
|
const region = this.regions[bestRegionIdx];
|
|
217
224
|
const gap = pairIndex - region.lastSeen;
|
|
218
225
|
region.box = box;
|
|
@@ -220,13 +227,14 @@ var IoUTracker = class {
|
|
|
220
227
|
region.lastSeen = pairIndex;
|
|
221
228
|
region.weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
222
229
|
matched.add(bestRegionIdx);
|
|
223
|
-
if (region.consecutiveCount >=
|
|
230
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
224
231
|
animationIndices.add(bi);
|
|
225
232
|
}
|
|
226
233
|
} else {
|
|
227
234
|
this.regions.push({
|
|
228
235
|
box,
|
|
229
236
|
consecutiveCount: 1,
|
|
237
|
+
firstSeen: pairIndex,
|
|
230
238
|
lastSeen: pairIndex,
|
|
231
239
|
weight: 1
|
|
232
240
|
});
|
|
@@ -238,17 +246,45 @@ var IoUTracker = class {
|
|
|
238
246
|
this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
239
247
|
}
|
|
240
248
|
}
|
|
241
|
-
|
|
249
|
+
for (let i = 0; i < this.regions.length; i++) {
|
|
250
|
+
const region = this.regions[i];
|
|
251
|
+
if (region.weight <= 0.01 && !matched.has(i)) {
|
|
252
|
+
this.collectAnimation(region);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.regions = this.regions.filter(
|
|
256
|
+
(r, i) => r.weight > 0.01 || matched.has(i)
|
|
257
|
+
);
|
|
242
258
|
return animationIndices;
|
|
243
259
|
}
|
|
260
|
+
collectAnimation(region) {
|
|
261
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
262
|
+
const durationMs = region.consecutiveCount / this.fps * 1e3;
|
|
263
|
+
this.extractedAnimations.push({
|
|
264
|
+
type: "loading_spinner",
|
|
265
|
+
// 기본값으로 loading_spinner 사용
|
|
266
|
+
boundingBox: region.box,
|
|
267
|
+
startFrameId: region.firstSeen,
|
|
268
|
+
endFrameId: region.lastSeen,
|
|
269
|
+
durationMs
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
flushAndGetAnimations() {
|
|
274
|
+
for (const region of this.regions) {
|
|
275
|
+
this.collectAnimation(region);
|
|
276
|
+
}
|
|
277
|
+
this.regions = [];
|
|
278
|
+
return this.extractedAnimations;
|
|
279
|
+
}
|
|
244
280
|
getAnimationWeight(boxIndex, boxes) {
|
|
245
281
|
if (boxIndex >= boxes.length) return 0;
|
|
246
282
|
const box = boxes[boxIndex];
|
|
247
283
|
let maxWeight = 0;
|
|
248
284
|
for (const region of this.regions) {
|
|
249
|
-
if (region.consecutiveCount >=
|
|
285
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
250
286
|
const iou = computeIoU(box, region.box);
|
|
251
|
-
if (iou >
|
|
287
|
+
if (iou > this.iouThreshold) {
|
|
252
288
|
maxWeight = Math.max(maxWeight, region.weight);
|
|
253
289
|
}
|
|
254
290
|
}
|
|
@@ -470,13 +506,17 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
470
506
|
}
|
|
471
507
|
async function analyzeFrames(ctx) {
|
|
472
508
|
const { frames } = ctx;
|
|
473
|
-
if (frames.length < 2) return [];
|
|
509
|
+
if (frames.length < 2) return { edges: [], animations: [] };
|
|
474
510
|
logger.debug(
|
|
475
511
|
`Analyzing ${frames.length} frames in batches of ${OPENCV_BATCH_SIZE}`
|
|
476
512
|
);
|
|
477
513
|
const cvLib = await ensureOpenCV();
|
|
478
514
|
const edges = [];
|
|
479
|
-
const tracker = new IoUTracker(
|
|
515
|
+
const tracker = new IoUTracker(
|
|
516
|
+
ctx.options.fps,
|
|
517
|
+
ctx.options.iouThreshold,
|
|
518
|
+
ctx.options.animationThreshold
|
|
519
|
+
);
|
|
480
520
|
const scale = ctx.options.scale;
|
|
481
521
|
for (let i = 0; i < frames.length - 1; i += OPENCV_BATCH_SIZE) {
|
|
482
522
|
const batchEnd = Math.min(i + OPENCV_BATCH_SIZE + 1, frames.length);
|
|
@@ -489,8 +529,11 @@ async function analyzeFrames(ctx) {
|
|
|
489
529
|
);
|
|
490
530
|
ctx.emitProgress(progress);
|
|
491
531
|
}
|
|
492
|
-
|
|
493
|
-
|
|
532
|
+
const animations = tracker.flushAndGetAnimations();
|
|
533
|
+
logger.debug(
|
|
534
|
+
`Computed ${edges.length} score edges and ${animations.length} animations`
|
|
535
|
+
);
|
|
536
|
+
return { edges, animations };
|
|
494
537
|
}
|
|
495
538
|
|
|
496
539
|
// src/core/extractor.ts
|
|
@@ -537,7 +580,7 @@ function isSupportedFile(filePath, extensions) {
|
|
|
537
580
|
// src/core/extractor.ts
|
|
538
581
|
async function extractFrames(ctx) {
|
|
539
582
|
const framesDir = join2(ctx.workspacePath, "frames");
|
|
540
|
-
const { inputPath, fps, scale } = ctx.options;
|
|
583
|
+
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
541
584
|
if (!inputPath) {
|
|
542
585
|
throw new Error("inputPath is required for frame extraction");
|
|
543
586
|
}
|
|
@@ -554,41 +597,23 @@ async function extractFrames(ctx) {
|
|
|
554
597
|
}
|
|
555
598
|
logger.debug(`Extracting frames from: ${inputPath}`);
|
|
556
599
|
await ensureDir(framesDir);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
`Insufficient I-frames (${frames.length}), falling back to FPS mode`
|
|
567
|
-
);
|
|
568
|
-
frames = await extractByFps(inputPath, framesDir, fps, scale);
|
|
569
|
-
}
|
|
600
|
+
let effectiveFps = fps;
|
|
601
|
+
const duration = await getVideoDuration(inputPath).catch(() => 0);
|
|
602
|
+
if (duration > 0) {
|
|
603
|
+
const fpsCap = maxFrames / duration;
|
|
604
|
+
effectiveFps = Math.min(fps, fpsCap);
|
|
605
|
+
effectiveFps = Math.max(0.5, effectiveFps);
|
|
606
|
+
logger.debug(
|
|
607
|
+
`Duration: ${duration.toFixed(1)}s, FPS: ${fps} \u2192 effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`
|
|
608
|
+
);
|
|
570
609
|
}
|
|
610
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
|
|
571
611
|
ctx.emitProgress(100);
|
|
572
612
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
573
613
|
return frames;
|
|
574
614
|
}
|
|
575
|
-
async function extractIFrames(inputPath, outputDir, scale) {
|
|
576
|
-
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
577
|
-
await execa(ffmpegPath, [
|
|
578
|
-
"-i",
|
|
579
|
-
inputPath,
|
|
580
|
-
"-vf",
|
|
581
|
-
`select='eq(pict_type,I)',scale=-1:${scale}`,
|
|
582
|
-
"-vsync",
|
|
583
|
-
"vfr",
|
|
584
|
-
"-q:v",
|
|
585
|
-
"2",
|
|
586
|
-
outputPattern
|
|
587
|
-
]);
|
|
588
|
-
return buildFrameList(outputDir, inputPath);
|
|
589
|
-
}
|
|
590
615
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
591
|
-
const outputPattern = join2(outputDir,
|
|
616
|
+
const outputPattern = join2(outputDir, FRAME_FILENAME_PATTERN);
|
|
592
617
|
await execa(ffmpegPath, [
|
|
593
618
|
"-i",
|
|
594
619
|
inputPath,
|
|
@@ -637,7 +662,7 @@ async function buildFrameList(framesDir, inputPath) {
|
|
|
637
662
|
import { join as join4 } from "path";
|
|
638
663
|
|
|
639
664
|
// src/core/workspace.ts
|
|
640
|
-
import { rename, rm, writeFile } from "fs/promises";
|
|
665
|
+
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
641
666
|
import { join as join3 } from "path";
|
|
642
667
|
import sharp2 from "sharp";
|
|
643
668
|
async function createWorkspace(sessionId) {
|
|
@@ -651,13 +676,44 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
651
676
|
const outputPath = ctx.options.outputPath;
|
|
652
677
|
const quality = ctx.options.quality;
|
|
653
678
|
const outputFiles = [];
|
|
679
|
+
const framesMetadata = [];
|
|
680
|
+
const totalFramesCount = ctx.frames.length;
|
|
681
|
+
const padding = Math.max(4, String(totalFramesCount).length);
|
|
654
682
|
for (let i = 0; i < selectedFrames.length; i++) {
|
|
655
683
|
const frame = selectedFrames[i];
|
|
656
|
-
const
|
|
657
|
-
const destPath = join3(stagingDir,
|
|
684
|
+
const fileName = `frame_${String(frame.id + 1).padStart(padding, "0")}.jpg`;
|
|
685
|
+
const destPath = join3(stagingDir, fileName);
|
|
658
686
|
await sharp2(frame.extractPath).jpeg({ quality, mozjpeg: true }).toFile(destPath);
|
|
659
|
-
outputFiles.push(join3(outputPath,
|
|
687
|
+
outputFiles.push(join3(outputPath, fileName));
|
|
688
|
+
framesMetadata.push({
|
|
689
|
+
step: i + 1,
|
|
690
|
+
fileName,
|
|
691
|
+
frameId: frame.id + 1,
|
|
692
|
+
timestampMs: Math.round(frame.timestamp * 1e3)
|
|
693
|
+
});
|
|
660
694
|
}
|
|
695
|
+
const metadata = {
|
|
696
|
+
video: {
|
|
697
|
+
originalDurationMs: Math.round(
|
|
698
|
+
(ctx.frames.length > 0 ? ctx.frames[ctx.frames.length - 1].timestamp : 0) * 1e3
|
|
699
|
+
),
|
|
700
|
+
fps: ctx.options.fps,
|
|
701
|
+
resolution: {
|
|
702
|
+
width: ctx.options.scale,
|
|
703
|
+
height: Math.round(ctx.options.scale * 9 / 16)
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
frames: framesMetadata,
|
|
707
|
+
animations: (ctx.animations || []).map((anim) => ({
|
|
708
|
+
...anim,
|
|
709
|
+
startFrameId: anim.startFrameId + 1,
|
|
710
|
+
endFrameId: anim.endFrameId + 1,
|
|
711
|
+
durationMs: Math.round(anim.durationMs)
|
|
712
|
+
}))
|
|
713
|
+
};
|
|
714
|
+
const metadataPath = join3(stagingDir, ".metadata.json");
|
|
715
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
716
|
+
outputFiles.push(join3(outputPath, ".metadata.json"));
|
|
661
717
|
await ensureDir(join3(outputPath, ".."));
|
|
662
718
|
await rm(outputPath, { recursive: true, force: true });
|
|
663
719
|
await rename(stagingDir, outputPath);
|
|
@@ -670,6 +726,7 @@ async function cleanupWorkspace(workspacePath) {
|
|
|
670
726
|
} catch {
|
|
671
727
|
}
|
|
672
728
|
}
|
|
729
|
+
var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
673
730
|
async function writeInputBuffer(buffer, workspacePath) {
|
|
674
731
|
const inputDir = join3(workspacePath, "input");
|
|
675
732
|
await ensureDir(inputDir);
|
|
@@ -717,8 +774,11 @@ function resolveOptions(options) {
|
|
|
717
774
|
pruneMode,
|
|
718
775
|
outputPath,
|
|
719
776
|
fps: options.fps ?? DEFAULT_FPS,
|
|
777
|
+
maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
720
778
|
scale: options.scale ?? DEFAULT_SCALE,
|
|
721
779
|
quality: options.quality ?? DEFAULT_QUALITY,
|
|
780
|
+
iouThreshold: options.iouThreshold ?? IOU_THRESHOLD,
|
|
781
|
+
animationThreshold: options.animationThreshold ?? ANIMATION_FRAME_THRESHOLD,
|
|
722
782
|
debug: options.debug ?? false
|
|
723
783
|
};
|
|
724
784
|
}
|
|
@@ -989,7 +1049,9 @@ async function runPipeline(options) {
|
|
|
989
1049
|
}
|
|
990
1050
|
ctx.emitProgress(100);
|
|
991
1051
|
ctx.status = "ANALYZING";
|
|
992
|
-
|
|
1052
|
+
const { edges, animations } = await analyzeFrames(ctx);
|
|
1053
|
+
ctx.graph = edges;
|
|
1054
|
+
ctx.animations = animations;
|
|
993
1055
|
ctx.status = "PRUNING";
|
|
994
1056
|
const survivingIds = pruneByThresholdWithCap(
|
|
995
1057
|
ctx.graph,
|
|
@@ -1022,6 +1084,15 @@ async function runPipeline(options) {
|
|
|
1022
1084
|
prunedFramesCount: prunedFrames.length,
|
|
1023
1085
|
outputFiles,
|
|
1024
1086
|
outputBuffers,
|
|
1087
|
+
animations: ctx.animations,
|
|
1088
|
+
video: {
|
|
1089
|
+
originalDurationMs: ctx.frames.length / ctx.options.fps * 1e3,
|
|
1090
|
+
fps: ctx.options.fps,
|
|
1091
|
+
resolution: {
|
|
1092
|
+
width: ctx.options.scale,
|
|
1093
|
+
height: Math.round(ctx.options.scale * 9 / 16)
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1025
1096
|
executionTimeMs: Date.now() - startTime
|
|
1026
1097
|
};
|
|
1027
1098
|
} catch (error) {
|