@lumy-pack/scene-sieve 0.0.9 → 0.0.10

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/index.mjs CHANGED
@@ -1152,10 +1152,7 @@ function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
1152
1152
  }
1153
1153
  return segments;
1154
1154
  }
1155
- function mergeSegmentFrames(segmentResults) {
1156
- if (segmentResults.length === 0) {
1157
- return { frames: [], edges: [], animations: [] };
1158
- }
1155
+ function collectAllFrames(segmentResults) {
1159
1156
  const allFrames = [];
1160
1157
  for (const result of segmentResults) {
1161
1158
  for (const frame of result.frames) {
@@ -1170,19 +1167,24 @@ function mergeSegmentFrames(segmentResults) {
1170
1167
  });
1171
1168
  }
1172
1169
  }
1173
- allFrames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1174
- const effectiveFps = segmentResults[0].segment.effectiveFps;
1170
+ return allFrames;
1171
+ }
1172
+ function deduplicateFrames(frames, effectiveFps) {
1173
+ frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1175
1174
  const dupThreshold = 1 / (effectiveFps * 2);
1176
- const uniqueFrames = [];
1177
- for (const entry of allFrames) {
1178
- if (uniqueFrames.length > 0) {
1179
- const last = uniqueFrames[uniqueFrames.length - 1];
1175
+ const unique = [];
1176
+ for (const entry of frames) {
1177
+ if (unique.length > 0) {
1178
+ const last = unique[unique.length - 1];
1180
1179
  if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
1181
1180
  continue;
1182
1181
  }
1183
1182
  }
1184
- uniqueFrames.push(entry);
1183
+ unique.push(entry);
1185
1184
  }
1185
+ return unique;
1186
+ }
1187
+ function remapFrameIds(uniqueFrames) {
1186
1188
  const globalIdMap = /* @__PURE__ */ new Map();
1187
1189
  const frames = uniqueFrames.map((entry, globalId) => {
1188
1190
  globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
@@ -1192,54 +1194,52 @@ function mergeSegmentFrames(segmentResults) {
1192
1194
  extractPath: entry.frame.extractPath
1193
1195
  };
1194
1196
  });
1197
+ return { frames, globalIdMap };
1198
+ }
1199
+ function remapEdges(segmentResults, globalIdMap) {
1195
1200
  const edges = [];
1196
1201
  const edgeMap = /* @__PURE__ */ new Map();
1197
1202
  for (const result of segmentResults) {
1198
1203
  for (const edge of result.edges) {
1199
- const newSourceId = globalIdMap.get(
1200
- `${result.segment.index}:${edge.sourceId}`
1201
- );
1202
- const newTargetId = globalIdMap.get(
1203
- `${result.segment.index}:${edge.targetId}`
1204
- );
1204
+ const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
1205
+ const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
1205
1206
  if (newSourceId === void 0 || newTargetId === void 0) continue;
1206
1207
  const edgeKey = `${newSourceId}-${newTargetId}`;
1207
1208
  const existingIdx = edgeMap.get(edgeKey);
1208
1209
  if (existingIdx !== void 0) {
1209
1210
  if (edges[existingIdx].score < edge.score) {
1210
- edges[existingIdx] = {
1211
- sourceId: newSourceId,
1212
- targetId: newTargetId,
1213
- score: edge.score
1214
- };
1211
+ edges[existingIdx] = { sourceId: newSourceId, targetId: newTargetId, score: edge.score };
1215
1212
  }
1216
1213
  } else {
1217
1214
  edgeMap.set(edgeKey, edges.length);
1218
- edges.push({
1219
- sourceId: newSourceId,
1220
- targetId: newTargetId,
1221
- score: edge.score
1222
- });
1215
+ edges.push({ sourceId: newSourceId, targetId: newTargetId, score: edge.score });
1223
1216
  }
1224
1217
  }
1225
1218
  }
1219
+ return edges;
1220
+ }
1221
+ function remapAnimations(segmentResults, globalIdMap) {
1226
1222
  const animations = [];
1227
1223
  for (const result of segmentResults) {
1228
1224
  for (const anim of result.animations) {
1229
- const newStartId = globalIdMap.get(
1230
- `${result.segment.index}:${anim.startFrameId}`
1231
- );
1232
- const newEndId = globalIdMap.get(
1233
- `${result.segment.index}:${anim.endFrameId}`
1234
- );
1225
+ const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
1226
+ const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
1235
1227
  if (newStartId === void 0 || newEndId === void 0) continue;
1236
- animations.push({
1237
- ...anim,
1238
- startFrameId: newStartId,
1239
- endFrameId: newEndId
1240
- });
1228
+ animations.push({ ...anim, startFrameId: newStartId, endFrameId: newEndId });
1241
1229
  }
1242
1230
  }
1231
+ return animations;
1232
+ }
1233
+ function mergeSegmentFrames(segmentResults) {
1234
+ if (segmentResults.length === 0) {
1235
+ return { frames: [], edges: [], animations: [] };
1236
+ }
1237
+ const effectiveFps = segmentResults[0].segment.effectiveFps;
1238
+ const allFrames = collectAllFrames(segmentResults);
1239
+ const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
1240
+ const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
1241
+ const edges = remapEdges(segmentResults, globalIdMap);
1242
+ const animations = remapAnimations(segmentResults, globalIdMap);
1243
1243
  return { frames, edges, animations };
1244
1244
  }
1245
1245
  function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
@@ -1155,10 +1155,7 @@ function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
1155
1155
  }
1156
1156
  return segments;
1157
1157
  }
1158
- function mergeSegmentFrames(segmentResults) {
1159
- if (segmentResults.length === 0) {
1160
- return { frames: [], edges: [], animations: [] };
1161
- }
1158
+ function collectAllFrames(segmentResults) {
1162
1159
  const allFrames = [];
1163
1160
  for (const result of segmentResults) {
1164
1161
  for (const frame of result.frames) {
@@ -1173,19 +1170,24 @@ function mergeSegmentFrames(segmentResults) {
1173
1170
  });
1174
1171
  }
1175
1172
  }
1176
- allFrames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1177
- const effectiveFps = segmentResults[0].segment.effectiveFps;
1173
+ return allFrames;
1174
+ }
1175
+ function deduplicateFrames(frames, effectiveFps) {
1176
+ frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1178
1177
  const dupThreshold = 1 / (effectiveFps * 2);
1179
- const uniqueFrames = [];
1180
- for (const entry of allFrames) {
1181
- if (uniqueFrames.length > 0) {
1182
- const last = uniqueFrames[uniqueFrames.length - 1];
1178
+ const unique = [];
1179
+ for (const entry of frames) {
1180
+ if (unique.length > 0) {
1181
+ const last = unique[unique.length - 1];
1183
1182
  if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
1184
1183
  continue;
1185
1184
  }
1186
1185
  }
1187
- uniqueFrames.push(entry);
1186
+ unique.push(entry);
1188
1187
  }
1188
+ return unique;
1189
+ }
1190
+ function remapFrameIds(uniqueFrames) {
1189
1191
  const globalIdMap = /* @__PURE__ */ new Map();
1190
1192
  const frames = uniqueFrames.map((entry, globalId) => {
1191
1193
  globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
@@ -1195,54 +1197,52 @@ function mergeSegmentFrames(segmentResults) {
1195
1197
  extractPath: entry.frame.extractPath
1196
1198
  };
1197
1199
  });
1200
+ return { frames, globalIdMap };
1201
+ }
1202
+ function remapEdges(segmentResults, globalIdMap) {
1198
1203
  const edges = [];
1199
1204
  const edgeMap = /* @__PURE__ */ new Map();
1200
1205
  for (const result of segmentResults) {
1201
1206
  for (const edge of result.edges) {
1202
- const newSourceId = globalIdMap.get(
1203
- `${result.segment.index}:${edge.sourceId}`
1204
- );
1205
- const newTargetId = globalIdMap.get(
1206
- `${result.segment.index}:${edge.targetId}`
1207
- );
1207
+ const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
1208
+ const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
1208
1209
  if (newSourceId === void 0 || newTargetId === void 0) continue;
1209
1210
  const edgeKey = `${newSourceId}-${newTargetId}`;
1210
1211
  const existingIdx = edgeMap.get(edgeKey);
1211
1212
  if (existingIdx !== void 0) {
1212
1213
  if (edges[existingIdx].score < edge.score) {
1213
- edges[existingIdx] = {
1214
- sourceId: newSourceId,
1215
- targetId: newTargetId,
1216
- score: edge.score
1217
- };
1214
+ edges[existingIdx] = { sourceId: newSourceId, targetId: newTargetId, score: edge.score };
1218
1215
  }
1219
1216
  } else {
1220
1217
  edgeMap.set(edgeKey, edges.length);
1221
- edges.push({
1222
- sourceId: newSourceId,
1223
- targetId: newTargetId,
1224
- score: edge.score
1225
- });
1218
+ edges.push({ sourceId: newSourceId, targetId: newTargetId, score: edge.score });
1226
1219
  }
1227
1220
  }
1228
1221
  }
1222
+ return edges;
1223
+ }
1224
+ function remapAnimations(segmentResults, globalIdMap) {
1229
1225
  const animations = [];
1230
1226
  for (const result of segmentResults) {
1231
1227
  for (const anim of result.animations) {
1232
- const newStartId = globalIdMap.get(
1233
- `${result.segment.index}:${anim.startFrameId}`
1234
- );
1235
- const newEndId = globalIdMap.get(
1236
- `${result.segment.index}:${anim.endFrameId}`
1237
- );
1228
+ const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
1229
+ const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
1238
1230
  if (newStartId === void 0 || newEndId === void 0) continue;
1239
- animations.push({
1240
- ...anim,
1241
- startFrameId: newStartId,
1242
- endFrameId: newEndId
1243
- });
1231
+ animations.push({ ...anim, startFrameId: newStartId, endFrameId: newEndId });
1244
1232
  }
1245
1233
  }
1234
+ return animations;
1235
+ }
1236
+ function mergeSegmentFrames(segmentResults) {
1237
+ if (segmentResults.length === 0) {
1238
+ return { frames: [], edges: [], animations: [] };
1239
+ }
1240
+ const effectiveFps = segmentResults[0].segment.effectiveFps;
1241
+ const allFrames = collectAllFrames(segmentResults);
1242
+ const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
1243
+ const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
1244
+ const edges = remapEdges(segmentResults, globalIdMap);
1245
+ const animations = remapAnimations(segmentResults, globalIdMap);
1246
1246
  return { frames, edges, animations };
1247
1247
  }
1248
1248
  function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumy-pack/scene-sieve",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "CLI tool for extracting key frames from video and GIF files",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,7 +33,7 @@
33
33
  "main": "dist/index.cjs",
34
34
  "module": "dist/index.mjs",
35
35
  "types": "dist/index.d.ts",
36
- "bin": "./dist/cli.mjs",
36
+ "bin": "dist/cli.mjs",
37
37
  "files": [
38
38
  "dist",
39
39
  "!dist/tsconfig.tsbuildinfo"
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@ffprobe-installer/ffprobe": "^1.4.1",
58
+ "@lumy-pack/shared": "0.0.1",
58
59
  "@techstark/opencv-js": "4.12.0-release.1",
59
60
  "commander": "^12.1.0",
60
61
  "execa": "^9.5.0",
package/dist/cli.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,17 +0,0 @@
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
- iouThreshold?: number;
12
- animationThreshold?: number;
13
- maxSegmentDuration?: number;
14
- concurrency?: number;
15
- debug: boolean;
16
- }
17
- export declare const SieveView: React.FC<SieveViewProps>;
@@ -1,14 +0,0 @@
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 {};
@@ -1,7 +0,0 @@
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 {};
@@ -1,32 +0,0 @@
1
- export declare const APP_NAME = "scene-sieve";
2
- export declare const DEFAULT_COUNT = 20;
3
- export declare const DEFAULT_THRESHOLD = 0.5;
4
- export declare const DEFAULT_FPS = 5;
5
- export declare const DEFAULT_SCALE = 720;
6
- export declare const DEFAULT_QUALITY = 80;
7
- export declare const DEFAULT_MAX_FRAMES = 300;
8
- export declare const NORMALIZATION_MIN_PERCENTILE = 0.1;
9
- export declare const NORMALIZATION_MAX_PERCENTILE = 0.9;
10
- export declare const NORMALIZATION_LOGISTIC_K = 3;
11
- export declare const NORMALIZATION_ALPHA = 0.4;
12
- export declare const NORMALIZATION_MAD_COEFFICIENT = 1.4826;
13
- export declare const NORMALIZATION_MIN_SAMPLE_SIZE = 10;
14
- export declare const WORKSPACE_PREFIX = "scene-sieve-";
15
- export declare const TEMP_BASE_DIR: string;
16
- export declare const FRAME_OUTPUT_EXTENSION = ".jpg";
17
- export declare const FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
18
- export declare const OPENCV_BATCH_SIZE = 10;
19
- export declare const MIN_IFRAME_COUNT = 3;
20
- export declare const DBSCAN_ALPHA = 0.03;
21
- export declare const DBSCAN_MIN_PTS = 4;
22
- export declare const IOU_THRESHOLD = 0.9;
23
- export declare const DECAY_LAMBDA = 0.95;
24
- export declare const ANIMATION_FRAME_THRESHOLD = 5;
25
- export declare const MATCH_DISTANCE_THRESHOLD = 0.25;
26
- export declare const PIXELDIFF_GAUSSIAN_KERNEL = 3;
27
- export declare const PIXELDIFF_BINARY_THRESHOLD = 30;
28
- export declare const PIXELDIFF_CONTOUR_MIN_AREA = 100;
29
- export declare const PIXELDIFF_SAMPLE_SPACING = 8;
30
- export declare const DEFAULT_MAX_SEGMENT_DURATION = 300;
31
- export declare const DEFAULT_SEGMENT_CONCURRENCY = 2;
32
- export declare function getTempWorkspaceDir(sessionId: string): string;
@@ -1,62 +0,0 @@
1
- import type { AnalysisResult, AnimationMetadata, BoundingBox, ProcessContext } from '../types/index.js';
2
- import type { Point2D } from './dbscan.js';
3
- type CvLib = typeof import('@techstark/opencv-js');
4
- export declare function preprocessFrame(framePath: string, scale: number): Promise<{
5
- data: Uint8Array;
6
- width: number;
7
- height: number;
8
- }>;
9
- export declare function computeIoU(a: BoundingBox, b: BoundingBox): number;
10
- export declare class IoUTracker {
11
- private fps;
12
- private iouThreshold;
13
- private animationThreshold;
14
- private regions;
15
- private extractedAnimations;
16
- constructor(fps?: number, iouThreshold?: number, animationThreshold?: number);
17
- update(boxes: BoundingBox[], pairIndex: number): Set<number>;
18
- private collectAnimation;
19
- flushAndGetAnimations(): AnimationMetadata[];
20
- getAnimationWeight(boxIndex: number, boxes: BoundingBox[]): number;
21
- }
22
- export interface AKAZEResult {
23
- sNew: Point2D[];
24
- sLoss: Point2D[];
25
- }
26
- /**
27
- * Pixel-level difference fallback for AKAZE blind spots.
28
- *
29
- * When AKAZE produces sparse results (typical for UI screen recordings
30
- * where form fields, dropdowns, or overlays change), this function
31
- * detects changed regions via cv.absdiff and generates synthetic
32
- * Point2D[] that feed into the existing DBSCAN → IoU → G(t) pipeline.
33
- *
34
- * Algorithm:
35
- * 1. absdiff(frame1, frame2) → grayscale difference
36
- * 2. GaussianBlur → reduce JPEG compression noise
37
- * 3. threshold → binary mask of significant changes
38
- * 4. findContours → bounding rects of changed regions
39
- * 5. Grid sampling within each bounding rect → Point2D[]
40
- */
41
- export declare function computePixelDiff(cvLib: CvLib, frame1: {
42
- data: Uint8Array;
43
- width: number;
44
- height: number;
45
- }, frame2: {
46
- data: Uint8Array;
47
- width: number;
48
- height: number;
49
- }): Point2D[];
50
- export declare function computeInformationGain(clusters: BoundingBox[], clusterPoints: number[], imageArea: number, animationIndices: Set<number>, animationWeights: number[]): number;
51
- /**
52
- * Analyze adjacent frame pairs to compute information gain scores (G(t)).
53
- * Processes frames in batches for memory efficiency.
54
- *
55
- * Pipeline:
56
- * 1. AKAZE Feature Set Difference
57
- * 2. DBSCAN Spatial Clustering
58
- * 3. Spatio-temporal IoU Tracking
59
- * 4. G(t) Information Gain Scoring
60
- */
61
- export declare function analyzeFrames(ctx: ProcessContext): Promise<AnalysisResult>;
62
- export {};
@@ -1,10 +0,0 @@
1
- import type { DBSCANResult } from '../types/index.js';
2
- export interface Point2D {
3
- x: number;
4
- y: number;
5
- }
6
- /**
7
- * DBSCAN clustering with resolution-independent eps.
8
- * eps = alpha * sqrt(width^2 + height^2)
9
- */
10
- export declare function dbscan(points: Point2D[], imageWidth: number, imageHeight: number, alpha?: number, minPts?: number): DBSCANResult;
@@ -1,30 +0,0 @@
1
- import type { FrameNode, ProcessContext } from '../types/index.js';
2
- export interface FFprobeMetadata {
3
- format?: {
4
- format_name?: string;
5
- duration?: string;
6
- };
7
- streams?: Array<{
8
- codec_type?: string;
9
- }>;
10
- }
11
- /**
12
- * Extract frames from video/GIF using FFmpeg.
13
- * Always uses FPS-based extraction. For long videos, FPS is automatically
14
- * reduced to stay within maxFrames budget.
15
- */
16
- export declare function extractFrames(ctx: ProcessContext): Promise<FrameNode[]>;
17
- export declare function getVideoMetadata(inputPath: string): Promise<FFprobeMetadata>;
18
- /**
19
- * Extract frames from a specific time range of a video using FFmpeg.
20
- * Uses input seeking (-ss before -i) for fast seek + -t for duration.
21
- *
22
- * @param inputPath - Path to the video file
23
- * @param outputDir - Directory to write extracted frames
24
- * @param fps - Frames per second for extraction
25
- * @param scale - Height scale for vision analysis
26
- * @param startTime - Start time in seconds
27
- * @param duration - Duration in seconds to extract
28
- * @returns Array of FrameNode with segment-local timestamps (starting from 0)
29
- */
30
- export declare function extractFramesForRange(inputPath: string, outputDir: string, fps: number, scale: number, startTime: number, duration: number): Promise<FrameNode[]>;
@@ -1,9 +0,0 @@
1
- export { runPipeline } from './orchestrator.js';
2
- export { analyzeFrames, computeIoU, computeInformationGain, } from './analyzer.js';
3
- export { extractFrames } from './extractor.js';
4
- export { pruneTo, pruneByThreshold, pruneByThresholdWithCap, suppressConsecutiveRuns, } from './pruner.js';
5
- export { dbscan } from './dbscan.js';
6
- export type { Point2D } from './dbscan.js';
7
- export { resolveInput, resolveOptions } from './input-resolver.js';
8
- export { shouldSegment, computeSegmentPlan, processSegment, mergeSegmentFrames, runSegmentedPipeline, } from './segmenter.js';
9
- export { createWorkspace, createSegmentWorkspace, cleanupWorkspace, finalizeOutput, readFramesAsBuffers, writeInputBuffer, writeInputFrames, } from './workspace.js';
@@ -1,13 +0,0 @@
1
- import type { FrameNode, ResolvedOptions, SieveOptions } from '../types/index.js';
2
- export declare function resolveOptions(options: SieveOptions): ResolvedOptions;
3
- /**
4
- * Resolve the input source to a list of FrameNode[].
5
- *
6
- * - 'file' mode: validate file exists and delegate to extractor (caller's responsibility)
7
- * - 'buffer' mode: write buffer as temp video file, return path via FrameNode trick (empty list)
8
- * - 'frames' mode: write frame buffers as JPGs, return FrameNode[]
9
- */
10
- export declare function resolveInput(options: SieveOptions, workspacePath: string): Promise<{
11
- frames: FrameNode[];
12
- resolvedInputPath?: string;
13
- }>;
@@ -1,2 +0,0 @@
1
- import type { SieveOptions, SieveResult } from '../types/index.js';
2
- export declare function runPipeline(options: SieveOptions): Promise<SieveResult>;
@@ -1 +0,0 @@
1
- export {};
@@ -1,61 +0,0 @@
1
- import type { FrameNode, ScoreEdge } from '../types/index.js';
2
- /**
3
- * Edge-aware greedy merge with re-linking — O(N log N).
4
- *
5
- * 1. Build a doubly-linked list of frames
6
- * 2. Insert all edges into a min-heap
7
- * 3. Pop the lowest-score edge (most similar pair)
8
- * 4. Remove the later frame (tgtId), re-link neighbors
9
- * 5. Push synthetic edge with score = max(left, right)
10
- * 6. Repeat until surviving count === targetCount
11
- * 7. First and last frames are never removed (boundary preservation)
12
- *
13
- * Stale heap entries (involving removed frames) are lazily skipped on pop.
14
- */
15
- export declare function pruneTo(graph: ScoreEdge[], frames: FrameNode[], targetCount: number): Set<number>;
16
- /**
17
- * Non-Maximum Suppression (NMS) for consecutive edge runs.
18
- *
19
- * Consecutive edges share overlapping frames (edge i: frame i->i+1,
20
- * edge i+1: frame i+1->i+2), so consecutive passing edges indicate
21
- * the same visual transition region. This function groups consecutive
22
- * passing edge indices into "runs" and keeps all distinct peaks per run.
23
- *
24
- * Multi-peak detection: within each run, strict local maxima (score higher
25
- * than both neighbors) are identified. Each local maximum represents a
26
- * distinct visual transition. If no strict local maxima exist (plateau or
27
- * monotonic sequence), the global peak of the run is selected as fallback.
28
- *
29
- * Single-element runs are unaffected (isolated transitions preserved).
30
- *
31
- * @param graph - full ScoreEdge array (for targetId lookup)
32
- * @param passingIndices - edge indices that passed threshold filtering (sorted ascending)
33
- * @param normalizedScores - normalized score array (same length as graph)
34
- * @returns Set of targetIds to add to surviving set (one or more per run)
35
- */
36
- export declare function suppressConsecutiveRuns(graph: ScoreEdge[], passingIndices: number[], normalizedScores: number[]): Set<number>;
37
- /**
38
- * Threshold-based pruning with NMS -- O(N).
39
- *
40
- * 1. Scores are normalized to [0, 1] via percentile normalization.
41
- * 2. Edges with normalized score >= threshold are collected.
42
- * 3. Non-Maximum Suppression groups consecutive passing edges and keeps
43
- * only the peak per run, preventing near-duplicate frame selection
44
- * from a single visual transition.
45
- *
46
- * First and last frames are always preserved (boundary protection).
47
- */
48
- export declare function pruneByThreshold(graph: ScoreEdge[], frames: FrameNode[], threshold: number): Set<number>;
49
- /**
50
- * Combined threshold + count pruning -- 2-stage pipeline.
51
- *
52
- * Stage 1: pruneByThreshold -- keep all frames with normalized score >= threshold
53
- * Stage 2: if result exceeds maxCount, rebuild subgraph with synthetic edges
54
- * (min-score over each gap) and apply pruneTo on the surviving subset
55
- *
56
- * Edge reconstruction: for consecutive survivors A, B with removed frames
57
- * [x1, x2, ...] between them, the synthetic edge score is:
58
- * min(score(A->x1), score(x1->x2), ..., score(xN->B))
59
- * This preserves the "weakest link" semantics.
60
- */
61
- export declare function pruneByThresholdWithCap(graph: ScoreEdge[], frames: FrameNode[], threshold: number, maxCount: number): Set<number>;
@@ -1,9 +0,0 @@
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>;
@@ -1,38 +0,0 @@
1
- import type { AnimationMetadata, FrameNode, ResolvedOptions, ScoreEdge, SegmentPlan, SegmentResult, SieveOptions, SieveResult } from '../types/index.js';
2
- /**
3
- * Determine whether segmentation should be used.
4
- * Returns false for frames mode and GIF files.
5
- * Actual duration check happens inside runSegmentedPipeline after metadata fetch.
6
- */
7
- export declare function shouldSegment(resolvedOptions: ResolvedOptions, originalOptions: SieveOptions): boolean;
8
- /**
9
- * Compute segment boundaries with overlap, frame allocation, and effectiveFps.
10
- * Pure function — no I/O.
11
- *
12
- * - effectiveFps is uniform across all segments
13
- * - Overlap: 1 frame at each internal boundary
14
- * - allocatedFrames total <= maxFrames (last segment adjusted if needed)
15
- */
16
- export declare function computeSegmentPlan(totalDuration: number, maxSegmentDuration: number, maxFrames: number, fps: number): SegmentPlan[];
17
- /**
18
- * Merge multiple segment results into a single unified frame/edge/animation set.
19
- * - Timestamps adjusted using extractStartTime (Section 18 note 1)
20
- * - Overlap frames deduplicated by threshold 1/(effectiveFps*2) (Section 18 note 5)
21
- * - Global IDs reassigned after dedup
22
- * - Duplicate edges keep higher score
23
- */
24
- export declare function mergeSegmentFrames(segmentResults: SegmentResult[]): {
25
- frames: FrameNode[];
26
- edges: ScoreEdge[];
27
- animations: AnimationMetadata[];
28
- };
29
- /**
30
- * Extract frames for a single segment and analyze them.
31
- * Each segment uses an isolated workspace directory.
32
- */
33
- export declare function processSegment(inputPath: string, segment: SegmentPlan, workspacePath: string, resolvedOptions: ResolvedOptions, onProgress: (percent: number) => void): Promise<SegmentResult>;
34
- /**
35
- * Full segmented pipeline: metadata → plan → parallel extract+analyze → merge → prune → finalize.
36
- * Called from runPipeline when shouldSegment() returns true.
37
- */
38
- export declare function runSegmentedPipeline(options: SieveOptions, resolvedOptions: ResolvedOptions): Promise<SieveResult>;
@@ -1,25 +0,0 @@
1
- import type { FrameNode, ProcessContext } from '../types/index.js';
2
- export declare function createWorkspace(sessionId: string): Promise<string>;
3
- export declare function finalizeOutput(ctx: ProcessContext, selectedFrames: FrameNode[]): Promise<string[]>;
4
- export declare function createSegmentWorkspace(parentWorkspacePath: string, segmentIndex: number): Promise<string>;
5
- export declare function cleanupWorkspace(workspacePath: string): Promise<void>;
6
- /**
7
- * Remove stale workspace directories left by previous interrupted runs.
8
- * Only deletes directories older than 1 hour to avoid removing active workspaces.
9
- */
10
- export declare function cleanupStaleWorkspaces(): Promise<void>;
11
- /**
12
- * Write a video buffer to a temp file in the workspace and return the path.
13
- * Used by 'buffer' input mode.
14
- */
15
- export declare function writeInputBuffer(buffer: Buffer, workspacePath: string): Promise<string>;
16
- /**
17
- * Write an array of frame Buffers as JPG files and return FrameNode[].
18
- * Used by 'frames' input mode.
19
- */
20
- export declare function writeInputFrames(frames: Buffer[], workspacePath: string): Promise<FrameNode[]>;
21
- /**
22
- * Read selected FrameNode files as Buffers with JPEG compression.
23
- * Used to return output buffers in 'buffer' and 'frames' modes.
24
- */
25
- export declare function readFramesAsBuffers(frameNodes: FrameNode[], quality: number): Promise<Buffer[]>;
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export type { BoundingBox, DBSCANResult, FrameNode, ProcessContext, ProgressPhase, ResolvedOptions, ScoreEdge, SieveInput, SieveOptions, SieveOptionsBase, SieveResult, } from './types/index.js';
2
- export { runPipeline as extractScenes } from './core/orchestrator.js';