@lumy-pack/scene-sieve 0.0.5 → 0.0.7
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/README.md +64 -19
- package/dist/cli.mjs +164 -55
- package/dist/commands/Sieve.d.ts +2 -0
- package/dist/constants.d.ts +0 -2
- package/dist/core/analyzer.d.ts +9 -2
- package/dist/index.cjs +132 -49
- package/dist/index.mjs +132 -49
- package/dist/pipeline-worker.mjs +132 -49
- package/dist/types/index.d.ts +26 -0
- package/dist/utils/paths.d.ts +0 -1
- package/package.json +1 -1
package/dist/pipeline-worker.mjs
CHANGED
|
@@ -51,15 +51,8 @@ var DEFAULT_MAX_FRAMES = 300;
|
|
|
51
51
|
var NORMALIZATION_PERCENTILE = 0.9;
|
|
52
52
|
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
53
53
|
var TEMP_BASE_DIR = tmpdir();
|
|
54
|
-
var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
55
|
-
".mp4",
|
|
56
|
-
".mov",
|
|
57
|
-
".avi",
|
|
58
|
-
".mkv",
|
|
59
|
-
".webm"
|
|
60
|
-
];
|
|
61
|
-
var SUPPORTED_GIF_EXTENSIONS = [".gif"];
|
|
62
54
|
var FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
55
|
+
var FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
|
|
63
56
|
var OPENCV_BATCH_SIZE = 10;
|
|
64
57
|
var DBSCAN_ALPHA = 0.03;
|
|
65
58
|
var DBSCAN_MIN_PTS = 4;
|
|
@@ -199,7 +192,13 @@ function computeIoU(a, b) {
|
|
|
199
192
|
return union === 0 ? 0 : intersection / union;
|
|
200
193
|
}
|
|
201
194
|
var IoUTracker = class {
|
|
195
|
+
constructor(fps = DEFAULT_FPS, iouThreshold = IOU_THRESHOLD, animationThreshold = ANIMATION_FRAME_THRESHOLD) {
|
|
196
|
+
this.fps = fps;
|
|
197
|
+
this.iouThreshold = iouThreshold;
|
|
198
|
+
this.animationThreshold = animationThreshold;
|
|
199
|
+
}
|
|
202
200
|
regions = [];
|
|
201
|
+
extractedAnimations = [];
|
|
203
202
|
update(boxes, pairIndex) {
|
|
204
203
|
const animationIndices = /* @__PURE__ */ new Set();
|
|
205
204
|
const matched = /* @__PURE__ */ new Set();
|
|
@@ -215,7 +214,7 @@ var IoUTracker = class {
|
|
|
215
214
|
bestRegionIdx = ri;
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
|
-
if (bestIoU >
|
|
217
|
+
if (bestIoU > this.iouThreshold && bestRegionIdx !== -1) {
|
|
219
218
|
const region = this.regions[bestRegionIdx];
|
|
220
219
|
const gap = pairIndex - region.lastSeen;
|
|
221
220
|
region.box = box;
|
|
@@ -223,13 +222,14 @@ var IoUTracker = class {
|
|
|
223
222
|
region.lastSeen = pairIndex;
|
|
224
223
|
region.weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
225
224
|
matched.add(bestRegionIdx);
|
|
226
|
-
if (region.consecutiveCount >=
|
|
225
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
227
226
|
animationIndices.add(bi);
|
|
228
227
|
}
|
|
229
228
|
} else {
|
|
230
229
|
this.regions.push({
|
|
231
230
|
box,
|
|
232
231
|
consecutiveCount: 1,
|
|
232
|
+
firstSeen: pairIndex,
|
|
233
233
|
lastSeen: pairIndex,
|
|
234
234
|
weight: 1
|
|
235
235
|
});
|
|
@@ -241,17 +241,45 @@ var IoUTracker = class {
|
|
|
241
241
|
this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
|
|
244
|
+
for (let i = 0; i < this.regions.length; i++) {
|
|
245
|
+
const region = this.regions[i];
|
|
246
|
+
if (region.weight <= 0.01 && !matched.has(i)) {
|
|
247
|
+
this.collectAnimation(region);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
this.regions = this.regions.filter(
|
|
251
|
+
(r, i) => r.weight > 0.01 || matched.has(i)
|
|
252
|
+
);
|
|
245
253
|
return animationIndices;
|
|
246
254
|
}
|
|
255
|
+
collectAnimation(region) {
|
|
256
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
257
|
+
const durationMs = region.consecutiveCount / this.fps * 1e3;
|
|
258
|
+
this.extractedAnimations.push({
|
|
259
|
+
type: "loading_spinner",
|
|
260
|
+
// 기본값으로 loading_spinner 사용
|
|
261
|
+
boundingBox: region.box,
|
|
262
|
+
startFrameId: region.firstSeen,
|
|
263
|
+
endFrameId: region.lastSeen,
|
|
264
|
+
durationMs
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
flushAndGetAnimations() {
|
|
269
|
+
for (const region of this.regions) {
|
|
270
|
+
this.collectAnimation(region);
|
|
271
|
+
}
|
|
272
|
+
this.regions = [];
|
|
273
|
+
return this.extractedAnimations;
|
|
274
|
+
}
|
|
247
275
|
getAnimationWeight(boxIndex, boxes) {
|
|
248
276
|
if (boxIndex >= boxes.length) return 0;
|
|
249
277
|
const box = boxes[boxIndex];
|
|
250
278
|
let maxWeight = 0;
|
|
251
279
|
for (const region of this.regions) {
|
|
252
|
-
if (region.consecutiveCount >=
|
|
280
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
253
281
|
const iou = computeIoU(box, region.box);
|
|
254
|
-
if (iou >
|
|
282
|
+
if (iou > this.iouThreshold) {
|
|
255
283
|
maxWeight = Math.max(maxWeight, region.weight);
|
|
256
284
|
}
|
|
257
285
|
}
|
|
@@ -473,13 +501,17 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
473
501
|
}
|
|
474
502
|
async function analyzeFrames(ctx) {
|
|
475
503
|
const { frames } = ctx;
|
|
476
|
-
if (frames.length < 2) return [];
|
|
504
|
+
if (frames.length < 2) return { edges: [], animations: [] };
|
|
477
505
|
logger.debug(
|
|
478
506
|
`Analyzing ${frames.length} frames in batches of ${OPENCV_BATCH_SIZE}`
|
|
479
507
|
);
|
|
480
508
|
const cvLib = await ensureOpenCV();
|
|
481
509
|
const edges = [];
|
|
482
|
-
const tracker = new IoUTracker(
|
|
510
|
+
const tracker = new IoUTracker(
|
|
511
|
+
ctx.options.fps,
|
|
512
|
+
ctx.options.iouThreshold,
|
|
513
|
+
ctx.options.animationThreshold
|
|
514
|
+
);
|
|
483
515
|
const scale = ctx.options.scale;
|
|
484
516
|
for (let i = 0; i < frames.length - 1; i += OPENCV_BATCH_SIZE) {
|
|
485
517
|
const batchEnd = Math.min(i + OPENCV_BATCH_SIZE + 1, frames.length);
|
|
@@ -492,8 +524,11 @@ async function analyzeFrames(ctx) {
|
|
|
492
524
|
);
|
|
493
525
|
ctx.emitProgress(progress);
|
|
494
526
|
}
|
|
495
|
-
|
|
496
|
-
|
|
527
|
+
const animations = tracker.flushAndGetAnimations();
|
|
528
|
+
logger.debug(
|
|
529
|
+
`Computed ${edges.length} score edges and ${animations.length} animations`
|
|
530
|
+
);
|
|
531
|
+
return { edges, animations };
|
|
497
532
|
}
|
|
498
533
|
|
|
499
534
|
// src/core/extractor.ts
|
|
@@ -533,9 +568,6 @@ function deriveOutputPath(inputPath) {
|
|
|
533
568
|
const name = basename(inputPath, extname(inputPath));
|
|
534
569
|
return resolve(dir, `${name}_scenes`);
|
|
535
570
|
}
|
|
536
|
-
function isSupportedFile(filePath, extensions) {
|
|
537
|
-
return extensions.includes(extname(filePath).toLowerCase());
|
|
538
|
-
}
|
|
539
571
|
|
|
540
572
|
// src/core/extractor.ts
|
|
541
573
|
async function extractFrames(ctx) {
|
|
@@ -548,32 +580,47 @@ async function extractFrames(ctx) {
|
|
|
548
580
|
if (!exists) {
|
|
549
581
|
throw new Error(`Input file not found: ${inputPath}`);
|
|
550
582
|
}
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (!
|
|
556
|
-
throw new Error(`
|
|
583
|
+
const metadata = await getVideoMetadata(inputPath).catch((err) => {
|
|
584
|
+
logger.debug(`ffprobe failed: ${err.message}`);
|
|
585
|
+
return null;
|
|
586
|
+
});
|
|
587
|
+
if (!metadata || !metadata.format) {
|
|
588
|
+
throw new Error(`Could not read file metadata: ${inputPath}`);
|
|
589
|
+
}
|
|
590
|
+
const formatName = metadata.format.format_name ?? "";
|
|
591
|
+
const duration = parseFloat(metadata.format.duration ?? "0");
|
|
592
|
+
const hasVideoStream = metadata.streams?.some((s) => s.codec_type === "video") ?? false;
|
|
593
|
+
if (!hasVideoStream) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`No video stream found in file: ${inputPath} (detected format: ${formatName})`
|
|
596
|
+
);
|
|
557
597
|
}
|
|
558
|
-
logger.debug(
|
|
598
|
+
logger.debug(
|
|
599
|
+
`Detected format: ${formatName} (Duration: ${duration.toFixed(1)}s), path: ${inputPath}`
|
|
600
|
+
);
|
|
559
601
|
await ensureDir(framesDir);
|
|
560
602
|
let effectiveFps = fps;
|
|
561
|
-
const duration = await getVideoDuration(inputPath).catch(() => 0);
|
|
562
603
|
if (duration > 0) {
|
|
563
604
|
const fpsCap = maxFrames / duration;
|
|
564
605
|
effectiveFps = Math.min(fps, fpsCap);
|
|
565
606
|
effectiveFps = Math.max(0.5, effectiveFps);
|
|
566
607
|
logger.debug(
|
|
567
|
-
`
|
|
608
|
+
`FPS: ${fps} \u2192 effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`
|
|
568
609
|
);
|
|
569
610
|
}
|
|
570
|
-
const frames = await extractByFps(
|
|
611
|
+
const frames = await extractByFps(
|
|
612
|
+
inputPath,
|
|
613
|
+
framesDir,
|
|
614
|
+
effectiveFps,
|
|
615
|
+
scale,
|
|
616
|
+
duration
|
|
617
|
+
);
|
|
571
618
|
ctx.emitProgress(100);
|
|
572
619
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
573
620
|
return frames;
|
|
574
621
|
}
|
|
575
|
-
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
576
|
-
const outputPattern = join2(outputDir,
|
|
622
|
+
async function extractByFps(inputPath, outputDir, fps, scale, duration) {
|
|
623
|
+
const outputPattern = join2(outputDir, FRAME_FILENAME_PATTERN);
|
|
577
624
|
await execa(ffmpegPath, [
|
|
578
625
|
"-i",
|
|
579
626
|
inputPath,
|
|
@@ -583,34 +630,26 @@ async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
|
583
630
|
"2",
|
|
584
631
|
outputPattern
|
|
585
632
|
]);
|
|
586
|
-
return buildFrameList(outputDir,
|
|
633
|
+
return buildFrameList(outputDir, duration);
|
|
587
634
|
}
|
|
588
|
-
async function
|
|
635
|
+
async function getVideoMetadata(inputPath) {
|
|
589
636
|
const { stdout } = await execa(ffprobePath, [
|
|
590
637
|
"-v",
|
|
591
638
|
"quiet",
|
|
592
639
|
"-print_format",
|
|
593
640
|
"json",
|
|
594
641
|
"-show_format",
|
|
642
|
+
"-show_streams",
|
|
595
643
|
inputPath
|
|
596
644
|
]);
|
|
597
|
-
|
|
598
|
-
return parseFloat(metadata.format?.duration ?? "0");
|
|
645
|
+
return JSON.parse(stdout);
|
|
599
646
|
}
|
|
600
|
-
async function buildFrameList(framesDir,
|
|
647
|
+
async function buildFrameList(framesDir, duration) {
|
|
601
648
|
const files = await readdir(framesDir);
|
|
602
649
|
const jpgFiles = files.filter((f) => f.endsWith(".jpg")).sort();
|
|
603
650
|
if (jpgFiles.length === 0) {
|
|
604
651
|
return [];
|
|
605
652
|
}
|
|
606
|
-
let duration = 0;
|
|
607
|
-
try {
|
|
608
|
-
duration = await getVideoDuration(inputPath);
|
|
609
|
-
} catch {
|
|
610
|
-
logger.debug(
|
|
611
|
-
"Could not determine video duration; using frame index for timestamps"
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
653
|
return jpgFiles.map((file, index) => ({
|
|
615
654
|
id: index,
|
|
616
655
|
timestamp: duration > 0 && jpgFiles.length > 1 ? duration * index / (jpgFiles.length - 1) : index,
|
|
@@ -636,13 +675,44 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
636
675
|
const outputPath = ctx.options.outputPath;
|
|
637
676
|
const quality = ctx.options.quality;
|
|
638
677
|
const outputFiles = [];
|
|
678
|
+
const framesMetadata = [];
|
|
679
|
+
const totalFramesCount = ctx.frames.length;
|
|
680
|
+
const padding = Math.max(4, String(totalFramesCount).length);
|
|
639
681
|
for (let i = 0; i < selectedFrames.length; i++) {
|
|
640
682
|
const frame = selectedFrames[i];
|
|
641
|
-
const
|
|
642
|
-
const destPath = join3(stagingDir,
|
|
683
|
+
const fileName = `frame_${String(frame.id + 1).padStart(padding, "0")}.jpg`;
|
|
684
|
+
const destPath = join3(stagingDir, fileName);
|
|
643
685
|
await sharp2(frame.extractPath).jpeg({ quality, mozjpeg: true }).toFile(destPath);
|
|
644
|
-
outputFiles.push(join3(outputPath,
|
|
686
|
+
outputFiles.push(join3(outputPath, fileName));
|
|
687
|
+
framesMetadata.push({
|
|
688
|
+
step: i + 1,
|
|
689
|
+
fileName,
|
|
690
|
+
frameId: frame.id + 1,
|
|
691
|
+
timestampMs: Math.round(frame.timestamp * 1e3)
|
|
692
|
+
});
|
|
645
693
|
}
|
|
694
|
+
const metadata = {
|
|
695
|
+
video: {
|
|
696
|
+
originalDurationMs: Math.round(
|
|
697
|
+
(ctx.frames.length > 0 ? ctx.frames[ctx.frames.length - 1].timestamp : 0) * 1e3
|
|
698
|
+
),
|
|
699
|
+
fps: ctx.options.fps,
|
|
700
|
+
resolution: {
|
|
701
|
+
width: ctx.options.scale,
|
|
702
|
+
height: Math.round(ctx.options.scale * 9 / 16)
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
frames: framesMetadata,
|
|
706
|
+
animations: (ctx.animations || []).map((anim) => ({
|
|
707
|
+
...anim,
|
|
708
|
+
startFrameId: anim.startFrameId + 1,
|
|
709
|
+
endFrameId: anim.endFrameId + 1,
|
|
710
|
+
durationMs: Math.round(anim.durationMs)
|
|
711
|
+
}))
|
|
712
|
+
};
|
|
713
|
+
const metadataPath = join3(stagingDir, ".metadata.json");
|
|
714
|
+
await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
715
|
+
outputFiles.push(join3(outputPath, ".metadata.json"));
|
|
646
716
|
await ensureDir(join3(outputPath, ".."));
|
|
647
717
|
await rm(outputPath, { recursive: true, force: true });
|
|
648
718
|
await rename(stagingDir, outputPath);
|
|
@@ -706,6 +776,8 @@ function resolveOptions(options2) {
|
|
|
706
776
|
maxFrames: options2.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
707
777
|
scale: options2.scale ?? DEFAULT_SCALE,
|
|
708
778
|
quality: options2.quality ?? DEFAULT_QUALITY,
|
|
779
|
+
iouThreshold: options2.iouThreshold ?? IOU_THRESHOLD,
|
|
780
|
+
animationThreshold: options2.animationThreshold ?? ANIMATION_FRAME_THRESHOLD,
|
|
709
781
|
debug: options2.debug ?? false
|
|
710
782
|
};
|
|
711
783
|
}
|
|
@@ -976,7 +1048,9 @@ async function runPipeline(options2) {
|
|
|
976
1048
|
}
|
|
977
1049
|
ctx.emitProgress(100);
|
|
978
1050
|
ctx.status = "ANALYZING";
|
|
979
|
-
|
|
1051
|
+
const { edges, animations } = await analyzeFrames(ctx);
|
|
1052
|
+
ctx.graph = edges;
|
|
1053
|
+
ctx.animations = animations;
|
|
980
1054
|
ctx.status = "PRUNING";
|
|
981
1055
|
const survivingIds = pruneByThresholdWithCap(
|
|
982
1056
|
ctx.graph,
|
|
@@ -1009,6 +1083,15 @@ async function runPipeline(options2) {
|
|
|
1009
1083
|
prunedFramesCount: prunedFrames.length,
|
|
1010
1084
|
outputFiles,
|
|
1011
1085
|
outputBuffers,
|
|
1086
|
+
animations: ctx.animations,
|
|
1087
|
+
video: {
|
|
1088
|
+
originalDurationMs: ctx.frames.length / ctx.options.fps * 1e3,
|
|
1089
|
+
fps: ctx.options.fps,
|
|
1090
|
+
resolution: {
|
|
1091
|
+
width: ctx.options.scale,
|
|
1092
|
+
height: Math.round(ctx.options.scale * 9 / 16)
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1012
1095
|
executionTimeMs: Date.now() - startTime
|
|
1013
1096
|
};
|
|
1014
1097
|
} catch (error) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface SieveOptionsBase {
|
|
|
17
17
|
maxFrames?: number;
|
|
18
18
|
scale?: number;
|
|
19
19
|
quality?: number;
|
|
20
|
+
iouThreshold?: number;
|
|
21
|
+
animationThreshold?: number;
|
|
20
22
|
debug?: boolean;
|
|
21
23
|
onProgress?: (phase: ProgressPhase, percent: number) => void;
|
|
22
24
|
}
|
|
@@ -32,6 +34,8 @@ export interface ResolvedOptions {
|
|
|
32
34
|
maxFrames: number;
|
|
33
35
|
scale: number;
|
|
34
36
|
quality: number;
|
|
37
|
+
iouThreshold: number;
|
|
38
|
+
animationThreshold: number;
|
|
35
39
|
debug: boolean;
|
|
36
40
|
}
|
|
37
41
|
export interface SieveResult {
|
|
@@ -40,8 +44,25 @@ export interface SieveResult {
|
|
|
40
44
|
prunedFramesCount: number;
|
|
41
45
|
outputFiles: string[];
|
|
42
46
|
outputBuffers?: Buffer[];
|
|
47
|
+
animations?: AnimationMetadata[];
|
|
48
|
+
video?: VideoMetadata;
|
|
43
49
|
executionTimeMs: number;
|
|
44
50
|
}
|
|
51
|
+
export interface AnimationMetadata {
|
|
52
|
+
type: string;
|
|
53
|
+
boundingBox: BoundingBox;
|
|
54
|
+
startFrameId: number;
|
|
55
|
+
endFrameId: number;
|
|
56
|
+
durationMs: number;
|
|
57
|
+
}
|
|
58
|
+
export interface VideoMetadata {
|
|
59
|
+
originalDurationMs: number;
|
|
60
|
+
fps: number;
|
|
61
|
+
resolution: {
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
45
66
|
export interface FrameNode {
|
|
46
67
|
id: number;
|
|
47
68
|
timestamp: number;
|
|
@@ -75,7 +96,12 @@ export interface ProcessContext {
|
|
|
75
96
|
workspacePath: string;
|
|
76
97
|
frames: FrameNode[];
|
|
77
98
|
graph: ScoreEdge[];
|
|
99
|
+
animations?: AnimationMetadata[];
|
|
78
100
|
status: 'INIT' | ProgressPhase | 'SUCCESS' | 'FAILED';
|
|
79
101
|
emitProgress: (percent: number) => void;
|
|
80
102
|
error?: Error;
|
|
81
103
|
}
|
|
104
|
+
export interface AnalysisResult {
|
|
105
|
+
edges: ScoreEdge[];
|
|
106
|
+
animations: AnimationMetadata[];
|
|
107
|
+
}
|
package/dist/utils/paths.d.ts
CHANGED
|
@@ -16,4 +16,3 @@ export declare function resolveAbsolute(p: string): string;
|
|
|
16
16
|
* e.g., /path/to/video.mp4 -> /path/to/video_scenes
|
|
17
17
|
*/
|
|
18
18
|
export declare function deriveOutputPath(inputPath: string): string;
|
|
19
|
-
export declare function isSupportedFile(filePath: string, extensions: string[]): boolean;
|