@lumy-pack/scene-sieve 0.0.3 → 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 +578 -257
- 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 +2 -1
- 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 +22 -42
- package/dist/index.mjs +23 -43
- package/dist/pipeline-worker.mjs +1049 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +5 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface SieveViewProps {
|
|
3
|
+
input: string;
|
|
4
|
+
count?: number;
|
|
5
|
+
threshold?: number;
|
|
6
|
+
output?: string;
|
|
7
|
+
fps: number;
|
|
8
|
+
maxFrames: number;
|
|
9
|
+
scale: number;
|
|
10
|
+
quality: number;
|
|
11
|
+
debug: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const SieveView: React.FC<SieveViewProps>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type PhaseStatus = 'pending' | 'running' | 'done' | 'failed';
|
|
3
|
+
export interface PhaseState {
|
|
4
|
+
label: string;
|
|
5
|
+
status: PhaseStatus;
|
|
6
|
+
hasProgress: boolean;
|
|
7
|
+
percent: number;
|
|
8
|
+
durationMs?: number;
|
|
9
|
+
}
|
|
10
|
+
interface PhaseStepProps {
|
|
11
|
+
phase: PhaseState;
|
|
12
|
+
}
|
|
13
|
+
export declare const PhaseStep: React.FC<PhaseStepProps>;
|
|
14
|
+
export {};
|
package/dist/constants.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const DEFAULT_THRESHOLD = 0.5;
|
|
|
4
4
|
export declare const DEFAULT_FPS = 5;
|
|
5
5
|
export declare const DEFAULT_SCALE = 720;
|
|
6
6
|
export declare const DEFAULT_QUALITY = 80;
|
|
7
|
+
export declare const DEFAULT_MAX_FRAMES = 300;
|
|
7
8
|
export declare const NORMALIZATION_PERCENTILE = 0.9;
|
|
8
9
|
export declare const WORKSPACE_PREFIX = "scene-sieve-";
|
|
9
10
|
export declare const TEMP_BASE_DIR: string;
|
|
@@ -18,7 +19,7 @@ export declare const DBSCAN_MIN_PTS = 4;
|
|
|
18
19
|
export declare const IOU_THRESHOLD = 0.9;
|
|
19
20
|
export declare const DECAY_LAMBDA = 0.95;
|
|
20
21
|
export declare const ANIMATION_FRAME_THRESHOLD = 5;
|
|
21
|
-
export declare const MATCH_DISTANCE_THRESHOLD = 0.
|
|
22
|
+
export declare const MATCH_DISTANCE_THRESHOLD = 0.25;
|
|
22
23
|
export declare const PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
23
24
|
export declare const PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
24
25
|
export declare const PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
package/dist/core/extractor.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FrameNode, ProcessContext } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Extract frames from video/GIF using FFmpeg.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Always uses FPS-based extraction. For long videos, FPS is automatically
|
|
5
|
+
* reduced to stay within maxFrames budget.
|
|
6
6
|
*/
|
|
7
7
|
export declare function extractFrames(ctx: ProcessContext): Promise<FrameNode[]>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProgressPhase, SieveInput, SieveOptionsBase, SieveResult } from '../types/index.js';
|
|
2
|
+
export type SieveWorkerOptions = Omit<SieveOptionsBase, 'onProgress'> & SieveInput;
|
|
3
|
+
/**
|
|
4
|
+
* Run the pipeline, choosing the best execution strategy:
|
|
5
|
+
*
|
|
6
|
+
* - Production (bundled .mjs): Worker thread — spinner never freezes
|
|
7
|
+
* - Dev mode (tsx .ts): Main thread — simpler, spinner may stutter during CPU work
|
|
8
|
+
*/
|
|
9
|
+
export declare function runPipelineInWorker(options: SieveWorkerOptions, onProgress: (phase: ProgressPhase, percent: number) => void): Promise<SieveResult>;
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { FrameNode, ProcessContext } from '../types/index.js';
|
|
|
2
2
|
export declare function createWorkspace(sessionId: string): Promise<string>;
|
|
3
3
|
export declare function finalizeOutput(ctx: ProcessContext, selectedFrames: FrameNode[]): Promise<string[]>;
|
|
4
4
|
export declare function cleanupWorkspace(workspacePath: string): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Remove stale workspace directories left by previous interrupted runs.
|
|
7
|
+
* Only deletes directories older than 1 hour to avoid removing active workspaces.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cleanupStaleWorkspaces(): Promise<void>;
|
|
5
10
|
/**
|
|
6
11
|
* Write a video buffer to a temp file in the workspace and return the path.
|
|
7
12
|
* Used by 'buffer' input mode.
|
package/dist/index.cjs
CHANGED
|
@@ -84,6 +84,7 @@ var DEFAULT_THRESHOLD = 0.5;
|
|
|
84
84
|
var DEFAULT_FPS = 5;
|
|
85
85
|
var DEFAULT_SCALE = 720;
|
|
86
86
|
var DEFAULT_QUALITY = 80;
|
|
87
|
+
var DEFAULT_MAX_FRAMES = 300;
|
|
87
88
|
var NORMALIZATION_PERCENTILE = 0.9;
|
|
88
89
|
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
89
90
|
var TEMP_BASE_DIR = (0, import_node_os.tmpdir)();
|
|
@@ -97,13 +98,12 @@ var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
|
97
98
|
var SUPPORTED_GIF_EXTENSIONS = [".gif"];
|
|
98
99
|
var FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
99
100
|
var OPENCV_BATCH_SIZE = 10;
|
|
100
|
-
var MIN_IFRAME_COUNT = 3;
|
|
101
101
|
var DBSCAN_ALPHA = 0.03;
|
|
102
102
|
var DBSCAN_MIN_PTS = 4;
|
|
103
103
|
var IOU_THRESHOLD = 0.9;
|
|
104
104
|
var DECAY_LAMBDA = 0.95;
|
|
105
105
|
var ANIMATION_FRAME_THRESHOLD = 5;
|
|
106
|
-
var MATCH_DISTANCE_THRESHOLD = 0.
|
|
106
|
+
var MATCH_DISTANCE_THRESHOLD = 0.25;
|
|
107
107
|
var PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
108
108
|
var PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
109
109
|
var PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
@@ -216,7 +216,7 @@ async function ensureOpenCV() {
|
|
|
216
216
|
async function preprocessFrame(framePath, scale) {
|
|
217
217
|
const { data, info } = await (0, import_sharp.default)(framePath).resize({ width: scale, withoutEnlargement: true }).grayscale().blur(1).raw().toBuffer({ resolveWithObject: true });
|
|
218
218
|
return {
|
|
219
|
-
data: new Uint8Array(data.buffer),
|
|
219
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
220
220
|
width: info.width,
|
|
221
221
|
height: info.height
|
|
222
222
|
};
|
|
@@ -297,16 +297,11 @@ var IoUTracker = class {
|
|
|
297
297
|
}
|
|
298
298
|
};
|
|
299
299
|
async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const mat2 = cvLib.matFromImageData({
|
|
306
|
-
data: frame2.data,
|
|
307
|
-
width: frame2.width,
|
|
308
|
-
height: frame2.height
|
|
309
|
-
});
|
|
300
|
+
const cv = cvLib;
|
|
301
|
+
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
302
|
+
mat1.data.set(frame1.data);
|
|
303
|
+
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
304
|
+
mat2.data.set(frame2.data);
|
|
310
305
|
const kp1 = new cvLib.KeyPointVector();
|
|
311
306
|
const kp2 = new cvLib.KeyPointVector();
|
|
312
307
|
const desc1 = new cvLib.Mat();
|
|
@@ -582,7 +577,7 @@ function isSupportedFile(filePath, extensions) {
|
|
|
582
577
|
// src/core/extractor.ts
|
|
583
578
|
async function extractFrames(ctx) {
|
|
584
579
|
const framesDir = (0, import_node_path3.join)(ctx.workspacePath, "frames");
|
|
585
|
-
const { inputPath, fps, scale } = ctx.options;
|
|
580
|
+
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
586
581
|
if (!inputPath) {
|
|
587
582
|
throw new Error("inputPath is required for frame extraction");
|
|
588
583
|
}
|
|
@@ -599,39 +594,21 @@ async function extractFrames(ctx) {
|
|
|
599
594
|
}
|
|
600
595
|
logger.debug(`Extracting frames from: ${inputPath}`);
|
|
601
596
|
await ensureDir(framesDir);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
`Insufficient I-frames (${frames.length}), falling back to FPS mode`
|
|
612
|
-
);
|
|
613
|
-
frames = await extractByFps(inputPath, framesDir, fps, scale);
|
|
614
|
-
}
|
|
597
|
+
let effectiveFps = fps;
|
|
598
|
+
const duration = await getVideoDuration(inputPath).catch(() => 0);
|
|
599
|
+
if (duration > 0) {
|
|
600
|
+
const fpsCap = maxFrames / duration;
|
|
601
|
+
effectiveFps = Math.min(fps, fpsCap);
|
|
602
|
+
effectiveFps = Math.max(0.5, effectiveFps);
|
|
603
|
+
logger.debug(
|
|
604
|
+
`Duration: ${duration.toFixed(1)}s, FPS: ${fps} \u2192 effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`
|
|
605
|
+
);
|
|
615
606
|
}
|
|
607
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
|
|
616
608
|
ctx.emitProgress(100);
|
|
617
609
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
618
610
|
return frames;
|
|
619
611
|
}
|
|
620
|
-
async function extractIFrames(inputPath, outputDir, scale) {
|
|
621
|
-
const outputPattern = (0, import_node_path3.join)(outputDir, "frame_%06d.jpg");
|
|
622
|
-
await (0, import_execa.execa)(import_ffmpeg_static.default, [
|
|
623
|
-
"-i",
|
|
624
|
-
inputPath,
|
|
625
|
-
"-vf",
|
|
626
|
-
`select='eq(pict_type,I)',scale=-1:${scale}`,
|
|
627
|
-
"-vsync",
|
|
628
|
-
"vfr",
|
|
629
|
-
"-q:v",
|
|
630
|
-
"2",
|
|
631
|
-
outputPattern
|
|
632
|
-
]);
|
|
633
|
-
return buildFrameList(outputDir, inputPath);
|
|
634
|
-
}
|
|
635
612
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
636
613
|
const outputPattern = (0, import_node_path3.join)(outputDir, "frame_%06d.jpg");
|
|
637
614
|
await (0, import_execa.execa)(import_ffmpeg_static.default, [
|
|
@@ -704,6 +681,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
704
681
|
outputFiles.push((0, import_node_path4.join)(outputPath, destName));
|
|
705
682
|
}
|
|
706
683
|
await ensureDir((0, import_node_path4.join)(outputPath, ".."));
|
|
684
|
+
await (0, import_promises3.rm)(outputPath, { recursive: true, force: true });
|
|
707
685
|
await (0, import_promises3.rename)(stagingDir, outputPath);
|
|
708
686
|
return outputFiles;
|
|
709
687
|
}
|
|
@@ -714,6 +692,7 @@ async function cleanupWorkspace(workspacePath) {
|
|
|
714
692
|
} catch {
|
|
715
693
|
}
|
|
716
694
|
}
|
|
695
|
+
var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
717
696
|
async function writeInputBuffer(buffer, workspacePath) {
|
|
718
697
|
const inputDir = (0, import_node_path4.join)(workspacePath, "input");
|
|
719
698
|
await ensureDir(inputDir);
|
|
@@ -761,6 +740,7 @@ function resolveOptions(options) {
|
|
|
761
740
|
pruneMode,
|
|
762
741
|
outputPath,
|
|
763
742
|
fps: options.fps ?? DEFAULT_FPS,
|
|
743
|
+
maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
764
744
|
scale: options.scale ?? DEFAULT_SCALE,
|
|
765
745
|
quality: options.quality ?? DEFAULT_QUALITY,
|
|
766
746
|
debug: options.debug ?? false
|
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();
|
|
@@ -57,13 +58,12 @@ var SUPPORTED_VIDEO_EXTENSIONS = [
|
|
|
57
58
|
var SUPPORTED_GIF_EXTENSIONS = [".gif"];
|
|
58
59
|
var FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
59
60
|
var OPENCV_BATCH_SIZE = 10;
|
|
60
|
-
var MIN_IFRAME_COUNT = 3;
|
|
61
61
|
var DBSCAN_ALPHA = 0.03;
|
|
62
62
|
var DBSCAN_MIN_PTS = 4;
|
|
63
63
|
var IOU_THRESHOLD = 0.9;
|
|
64
64
|
var DECAY_LAMBDA = 0.95;
|
|
65
65
|
var ANIMATION_FRAME_THRESHOLD = 5;
|
|
66
|
-
var MATCH_DISTANCE_THRESHOLD = 0.
|
|
66
|
+
var MATCH_DISTANCE_THRESHOLD = 0.25;
|
|
67
67
|
var PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
68
68
|
var PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
69
69
|
var PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
@@ -176,7 +176,7 @@ async function ensureOpenCV() {
|
|
|
176
176
|
async function preprocessFrame(framePath, scale) {
|
|
177
177
|
const { data, info } = await sharp(framePath).resize({ width: scale, withoutEnlargement: true }).grayscale().blur(1).raw().toBuffer({ resolveWithObject: true });
|
|
178
178
|
return {
|
|
179
|
-
data: new Uint8Array(data.buffer),
|
|
179
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
180
180
|
width: info.width,
|
|
181
181
|
height: info.height
|
|
182
182
|
};
|
|
@@ -257,16 +257,11 @@ var IoUTracker = class {
|
|
|
257
257
|
}
|
|
258
258
|
};
|
|
259
259
|
async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const mat2 = cvLib.matFromImageData({
|
|
266
|
-
data: frame2.data,
|
|
267
|
-
width: frame2.width,
|
|
268
|
-
height: frame2.height
|
|
269
|
-
});
|
|
260
|
+
const cv = cvLib;
|
|
261
|
+
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
262
|
+
mat1.data.set(frame1.data);
|
|
263
|
+
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
264
|
+
mat2.data.set(frame2.data);
|
|
270
265
|
const kp1 = new cvLib.KeyPointVector();
|
|
271
266
|
const kp2 = new cvLib.KeyPointVector();
|
|
272
267
|
const desc1 = new cvLib.Mat();
|
|
@@ -542,7 +537,7 @@ function isSupportedFile(filePath, extensions) {
|
|
|
542
537
|
// src/core/extractor.ts
|
|
543
538
|
async function extractFrames(ctx) {
|
|
544
539
|
const framesDir = join2(ctx.workspacePath, "frames");
|
|
545
|
-
const { inputPath, fps, scale } = ctx.options;
|
|
540
|
+
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
546
541
|
if (!inputPath) {
|
|
547
542
|
throw new Error("inputPath is required for frame extraction");
|
|
548
543
|
}
|
|
@@ -559,39 +554,21 @@ async function extractFrames(ctx) {
|
|
|
559
554
|
}
|
|
560
555
|
logger.debug(`Extracting frames from: ${inputPath}`);
|
|
561
556
|
await ensureDir(framesDir);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
`Insufficient I-frames (${frames.length}), falling back to FPS mode`
|
|
572
|
-
);
|
|
573
|
-
frames = await extractByFps(inputPath, framesDir, fps, scale);
|
|
574
|
-
}
|
|
557
|
+
let effectiveFps = fps;
|
|
558
|
+
const duration = await getVideoDuration(inputPath).catch(() => 0);
|
|
559
|
+
if (duration > 0) {
|
|
560
|
+
const fpsCap = maxFrames / duration;
|
|
561
|
+
effectiveFps = Math.min(fps, fpsCap);
|
|
562
|
+
effectiveFps = Math.max(0.5, effectiveFps);
|
|
563
|
+
logger.debug(
|
|
564
|
+
`Duration: ${duration.toFixed(1)}s, FPS: ${fps} \u2192 effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`
|
|
565
|
+
);
|
|
575
566
|
}
|
|
567
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
|
|
576
568
|
ctx.emitProgress(100);
|
|
577
569
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
578
570
|
return frames;
|
|
579
571
|
}
|
|
580
|
-
async function extractIFrames(inputPath, outputDir, scale) {
|
|
581
|
-
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
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
|
-
]);
|
|
593
|
-
return buildFrameList(outputDir, inputPath);
|
|
594
|
-
}
|
|
595
572
|
async function extractByFps(inputPath, outputDir, fps, scale) {
|
|
596
573
|
const outputPattern = join2(outputDir, "frame_%06d.jpg");
|
|
597
574
|
await execa(ffmpegPath, [
|
|
@@ -642,7 +619,7 @@ async function buildFrameList(framesDir, inputPath) {
|
|
|
642
619
|
import { join as join4 } from "path";
|
|
643
620
|
|
|
644
621
|
// src/core/workspace.ts
|
|
645
|
-
import { rename, rm, writeFile } from "fs/promises";
|
|
622
|
+
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
646
623
|
import { join as join3 } from "path";
|
|
647
624
|
import sharp2 from "sharp";
|
|
648
625
|
async function createWorkspace(sessionId) {
|
|
@@ -664,6 +641,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
664
641
|
outputFiles.push(join3(outputPath, destName));
|
|
665
642
|
}
|
|
666
643
|
await ensureDir(join3(outputPath, ".."));
|
|
644
|
+
await rm(outputPath, { recursive: true, force: true });
|
|
667
645
|
await rename(stagingDir, outputPath);
|
|
668
646
|
return outputFiles;
|
|
669
647
|
}
|
|
@@ -674,6 +652,7 @@ async function cleanupWorkspace(workspacePath) {
|
|
|
674
652
|
} catch {
|
|
675
653
|
}
|
|
676
654
|
}
|
|
655
|
+
var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
|
|
677
656
|
async function writeInputBuffer(buffer, workspacePath) {
|
|
678
657
|
const inputDir = join3(workspacePath, "input");
|
|
679
658
|
await ensureDir(inputDir);
|
|
@@ -721,6 +700,7 @@ function resolveOptions(options) {
|
|
|
721
700
|
pruneMode,
|
|
722
701
|
outputPath,
|
|
723
702
|
fps: options.fps ?? DEFAULT_FPS,
|
|
703
|
+
maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
|
|
724
704
|
scale: options.scale ?? DEFAULT_SCALE,
|
|
725
705
|
quality: options.quality ?? DEFAULT_QUALITY,
|
|
726
706
|
debug: options.debug ?? false
|