@lumy-pack/scene-sieve 0.0.4 → 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;
@@ -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,7 +98,6 @@ 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;
@@ -577,7 +577,7 @@ function isSupportedFile(filePath, extensions) {
577
577
  // src/core/extractor.ts
578
578
  async function extractFrames(ctx) {
579
579
  const framesDir = (0, import_node_path3.join)(ctx.workspacePath, "frames");
580
- const { inputPath, fps, scale } = ctx.options;
580
+ const { inputPath, fps, maxFrames, scale } = ctx.options;
581
581
  if (!inputPath) {
582
582
  throw new Error("inputPath is required for frame extraction");
583
583
  }
@@ -594,39 +594,21 @@ async function extractFrames(ctx) {
594
594
  }
595
595
  logger.debug(`Extracting frames from: ${inputPath}`);
596
596
  await ensureDir(framesDir);
597
- const isGif = isSupportedFile(inputPath, SUPPORTED_GIF_EXTENSIONS);
598
- let frames;
599
- if (isGif) {
600
- logger.debug("GIF detected \u2014 using FPS extraction");
601
- frames = await extractByFps(inputPath, framesDir, fps, scale);
602
- } else {
603
- frames = await extractIFrames(inputPath, framesDir, scale);
604
- if (frames.length < MIN_IFRAME_COUNT) {
605
- logger.debug(
606
- `Insufficient I-frames (${frames.length}), falling back to FPS mode`
607
- );
608
- frames = await extractByFps(inputPath, framesDir, fps, scale);
609
- }
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
+ );
610
606
  }
607
+ const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
611
608
  ctx.emitProgress(100);
612
609
  logger.debug(`Extracted ${frames.length} frames`);
613
610
  return frames;
614
611
  }
615
- async function extractIFrames(inputPath, outputDir, scale) {
616
- const outputPattern = (0, import_node_path3.join)(outputDir, "frame_%06d.jpg");
617
- await (0, import_execa.execa)(import_ffmpeg_static.default, [
618
- "-i",
619
- inputPath,
620
- "-vf",
621
- `select='eq(pict_type,I)',scale=-1:${scale}`,
622
- "-vsync",
623
- "vfr",
624
- "-q:v",
625
- "2",
626
- outputPattern
627
- ]);
628
- return buildFrameList(outputDir, inputPath);
629
- }
630
612
  async function extractByFps(inputPath, outputDir, fps, scale) {
631
613
  const outputPattern = (0, import_node_path3.join)(outputDir, "frame_%06d.jpg");
632
614
  await (0, import_execa.execa)(import_ffmpeg_static.default, [
@@ -710,6 +692,7 @@ async function cleanupWorkspace(workspacePath) {
710
692
  } catch {
711
693
  }
712
694
  }
695
+ var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
713
696
  async function writeInputBuffer(buffer, workspacePath) {
714
697
  const inputDir = (0, import_node_path4.join)(workspacePath, "input");
715
698
  await ensureDir(inputDir);
@@ -757,6 +740,7 @@ function resolveOptions(options) {
757
740
  pruneMode,
758
741
  outputPath,
759
742
  fps: options.fps ?? DEFAULT_FPS,
743
+ maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
760
744
  scale: options.scale ?? DEFAULT_SCALE,
761
745
  quality: options.quality ?? DEFAULT_QUALITY,
762
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,7 +58,6 @@ 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;
@@ -537,7 +537,7 @@ function isSupportedFile(filePath, extensions) {
537
537
  // src/core/extractor.ts
538
538
  async function extractFrames(ctx) {
539
539
  const framesDir = join2(ctx.workspacePath, "frames");
540
- const { inputPath, fps, scale } = ctx.options;
540
+ const { inputPath, fps, maxFrames, scale } = ctx.options;
541
541
  if (!inputPath) {
542
542
  throw new Error("inputPath is required for frame extraction");
543
543
  }
@@ -554,39 +554,21 @@ async function extractFrames(ctx) {
554
554
  }
555
555
  logger.debug(`Extracting frames from: ${inputPath}`);
556
556
  await ensureDir(framesDir);
557
- const isGif = isSupportedFile(inputPath, SUPPORTED_GIF_EXTENSIONS);
558
- let frames;
559
- if (isGif) {
560
- logger.debug("GIF detected \u2014 using FPS extraction");
561
- frames = await extractByFps(inputPath, framesDir, fps, scale);
562
- } else {
563
- frames = await extractIFrames(inputPath, framesDir, scale);
564
- if (frames.length < MIN_IFRAME_COUNT) {
565
- logger.debug(
566
- `Insufficient I-frames (${frames.length}), falling back to FPS mode`
567
- );
568
- frames = await extractByFps(inputPath, framesDir, fps, scale);
569
- }
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
+ );
570
566
  }
567
+ const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
571
568
  ctx.emitProgress(100);
572
569
  logger.debug(`Extracted ${frames.length} frames`);
573
570
  return frames;
574
571
  }
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
572
  async function extractByFps(inputPath, outputDir, fps, scale) {
591
573
  const outputPattern = join2(outputDir, "frame_%06d.jpg");
592
574
  await execa(ffmpegPath, [
@@ -637,7 +619,7 @@ async function buildFrameList(framesDir, inputPath) {
637
619
  import { join as join4 } from "path";
638
620
 
639
621
  // src/core/workspace.ts
640
- import { rename, rm, writeFile } from "fs/promises";
622
+ import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
641
623
  import { join as join3 } from "path";
642
624
  import sharp2 from "sharp";
643
625
  async function createWorkspace(sessionId) {
@@ -670,6 +652,7 @@ async function cleanupWorkspace(workspacePath) {
670
652
  } catch {
671
653
  }
672
654
  }
655
+ var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
673
656
  async function writeInputBuffer(buffer, workspacePath) {
674
657
  const inputDir = join3(workspacePath, "input");
675
658
  await ensureDir(inputDir);
@@ -717,6 +700,7 @@ function resolveOptions(options) {
717
700
  pruneMode,
718
701
  outputPath,
719
702
  fps: options.fps ?? DEFAULT_FPS,
703
+ maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
720
704
  scale: options.scale ?? DEFAULT_SCALE,
721
705
  quality: options.quality ?? DEFAULT_QUALITY,
722
706
  debug: options.debug ?? false