@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.
@@ -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 {};
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface ProgressBarProps {
3
+ percent: number;
4
+ width?: number;
5
+ }
6
+ export declare const ProgressBar: React.FC<ProgressBarProps>;
7
+ export {};
@@ -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.75;
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;
@@ -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
- * Attempts I-Frame extraction first; falls back to fixed FPS if insufficient.
5
- * GIF inputs always use FPS fallback.
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>;
@@ -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.75;
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 mat1 = cvLib.matFromImageData({
301
- data: frame1.data,
302
- width: frame1.width,
303
- height: frame1.height
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
- const isGif = isSupportedFile(inputPath, SUPPORTED_GIF_EXTENSIONS);
603
- let frames;
604
- if (isGif) {
605
- logger.debug("GIF detected \u2014 using FPS extraction");
606
- frames = await extractByFps(inputPath, framesDir, fps, scale);
607
- } else {
608
- frames = await extractIFrames(inputPath, framesDir, scale);
609
- if (frames.length < MIN_IFRAME_COUNT) {
610
- logger.debug(
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.75;
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 mat1 = cvLib.matFromImageData({
261
- data: frame1.data,
262
- width: frame1.width,
263
- height: frame1.height
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
- const isGif = isSupportedFile(inputPath, SUPPORTED_GIF_EXTENSIONS);
563
- let frames;
564
- if (isGif) {
565
- logger.debug("GIF detected \u2014 using FPS extraction");
566
- frames = await extractByFps(inputPath, framesDir, fps, scale);
567
- } else {
568
- frames = await extractIFrames(inputPath, framesDir, scale);
569
- if (frames.length < MIN_IFRAME_COUNT) {
570
- logger.debug(
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