@lumy-pack/scene-sieve 0.1.0 → 0.2.0
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 +47 -24
- package/dist/{errors.d.ts → cli/errors/classify-error.d.ts} +5 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/{utils → cli/options}/parse-options.d.ts +6 -1
- package/dist/cli.mjs +755 -319
- package/dist/constants/pipeline-defaults.d.ts +13 -0
- package/dist/core/{analyzer.d.ts → analyzer/analyzer.d.ts} +11 -6
- package/dist/core/{dbscan.d.ts → analyzer/clustering/dbscan.d.ts} +1 -1
- package/dist/core/analyzer/constants/vision-tuning.d.ts +12 -0
- package/dist/core/analyzer/features/feature-diff.d.ts +14 -0
- package/dist/core/analyzer/features/frame-features.d.ts +32 -0
- package/dist/core/analyzer/index.d.ts +3 -0
- package/dist/core/constants/workspace-layout.d.ts +9 -0
- package/dist/core/{extractor.d.ts → extractor/extractor.d.ts} +6 -4
- package/dist/core/extractor/index.d.ts +1 -0
- package/dist/core/index.d.ts +8 -9
- package/dist/core/input-resolver/index.d.ts +1 -0
- package/dist/core/{input-resolver.d.ts → input-resolver/input-resolver.d.ts} +11 -1
- package/dist/core/input-resolver/validation/validate-options.d.ts +8 -0
- package/dist/core/orchestrator/index.d.ts +3 -0
- package/dist/core/{orchestrator.d.ts → orchestrator/orchestrator.d.ts} +1 -1
- package/dist/core/{run-in-worker.d.ts → orchestrator/worker/run-in-worker.d.ts} +5 -1
- package/dist/core/pruner/index.d.ts +1 -0
- package/dist/core/{pruner.d.ts → pruner/pruner.d.ts} +2 -2
- package/dist/{utils/math.d.ts → core/pruner/scoring/normalize-scores.d.ts} +4 -0
- package/dist/core/segmenter/index.d.ts +1 -0
- package/dist/core/{segmenter.d.ts → segmenter/segmenter.d.ts} +11 -11
- package/dist/core/utils/metadata/build-video-metadata.d.ts +14 -0
- package/dist/core/workspace/index.d.ts +1 -0
- package/dist/core/{workspace.d.ts → workspace/workspace.d.ts} +1 -1
- package/dist/index.cjs +910 -434
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +913 -434
- package/dist/pipeline-worker.mjs +635 -278
- package/dist/types/index.d.ts +25 -0
- package/package.json +1 -1
- package/dist/constants.d.ts +0 -32
- /package/dist/{commands → cli/commands}/Sieve.d.ts +0 -0
- /package/dist/{utils → cli/commands}/command-registry.d.ts +0 -0
- /package/dist/{components → cli/components}/PhaseStep.d.ts +0 -0
- /package/dist/{components → cli/components}/ProgressBar.d.ts +0 -0
- /package/dist/core/{pipeline-worker.d.ts → orchestrator/worker/pipeline-worker.d.ts} +0 -0
- /package/dist/{utils → core/pruner/heap}/min-heap.d.ts +0 -0
- /package/dist/{utils → core/segmenter/scheduling}/concurrency.d.ts +0 -0
- /package/dist/{utils → core/utils/filesystem}/paths.d.ts +0 -0
- /package/dist/{utils → logging}/logger.d.ts +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -6,16 +6,16 @@ import { Box, Text, render, useApp } from "ink";
|
|
|
6
6
|
import React, { useEffect, useState } from "react";
|
|
7
7
|
import Spinner from "ink-spinner";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { homedir, tmpdir } from "node:os";
|
|
10
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
11
9
|
import { randomUUID } from "node:crypto";
|
|
12
10
|
import { filter, map } from "@winglet/common-utils";
|
|
13
11
|
import pc from "picocolors";
|
|
14
12
|
import sharp from "sharp";
|
|
15
13
|
import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
16
15
|
import { path } from "@ffprobe-installer/ffprobe";
|
|
17
16
|
import { execa } from "execa";
|
|
18
17
|
import ffmpegPath from "ffmpeg-static";
|
|
18
|
+
import { homedir, tmpdir } from "node:os";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { Worker } from "node:worker_threads";
|
|
21
21
|
|
|
@@ -84,7 +84,7 @@ function respondError(command, code, message, startTime, version, details) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
//#endregion
|
|
87
|
-
//#region src/components/ProgressBar.tsx
|
|
87
|
+
//#region src/cli/components/ProgressBar.tsx
|
|
88
88
|
const ProgressBar = ({ percent, width = 30 }) => {
|
|
89
89
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
90
90
|
const filled = Math.round(width * (clamped / 100));
|
|
@@ -107,7 +107,7 @@ const ProgressBar = ({ percent, width = 30 }) => {
|
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
//#endregion
|
|
110
|
-
//#region src/components/PhaseStep.tsx
|
|
110
|
+
//#region src/cli/components/PhaseStep.tsx
|
|
111
111
|
const PhaseStep = ({ phase }) => {
|
|
112
112
|
const icon = (() => {
|
|
113
113
|
switch (phase.status) {
|
|
@@ -146,28 +146,15 @@ const PhaseStep = ({ phase }) => {
|
|
|
146
146
|
};
|
|
147
147
|
|
|
148
148
|
//#endregion
|
|
149
|
-
//#region src/constants.ts
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
var APP_NAME, DEFAULT_THRESHOLD, NORMALIZATION_ALPHA, NORMALIZATION_MAD_COEFFICIENT, WORKSPACE_PREFIX, TEMP_BASE_DIR, FRAME_OUTPUT_EXTENSION, FRAME_FILENAME_PATTERN, DBSCAN_ALPHA, IOU_THRESHOLD, DECAY_LAMBDA, MATCH_DISTANCE_THRESHOLD;
|
|
154
|
-
var init_constants = __esmMin((() => {
|
|
155
|
-
APP_NAME = "scene-sieve";
|
|
149
|
+
//#region src/constants/pipeline-defaults.ts
|
|
150
|
+
var DEFAULT_THRESHOLD, IOU_THRESHOLD;
|
|
151
|
+
var init_pipeline_defaults = __esmMin((() => {
|
|
156
152
|
DEFAULT_THRESHOLD = .5;
|
|
157
|
-
NORMALIZATION_ALPHA = .4;
|
|
158
|
-
NORMALIZATION_MAD_COEFFICIENT = 1.4826;
|
|
159
|
-
WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
160
|
-
TEMP_BASE_DIR = tmpdir();
|
|
161
|
-
FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
162
|
-
FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
|
|
163
|
-
DBSCAN_ALPHA = .03;
|
|
164
153
|
IOU_THRESHOLD = .9;
|
|
165
|
-
DECAY_LAMBDA = .95;
|
|
166
|
-
MATCH_DISTANCE_THRESHOLD = .25;
|
|
167
154
|
}));
|
|
168
155
|
|
|
169
156
|
//#endregion
|
|
170
|
-
//#region src/
|
|
157
|
+
//#region src/logging/logger.ts
|
|
171
158
|
function setDebugMode(enabled) {
|
|
172
159
|
debugMode = enabled;
|
|
173
160
|
}
|
|
@@ -204,7 +191,16 @@ var init_logger = __esmMin((() => {
|
|
|
204
191
|
}));
|
|
205
192
|
|
|
206
193
|
//#endregion
|
|
207
|
-
//#region src/core/
|
|
194
|
+
//#region src/core/analyzer/constants/vision-tuning.ts
|
|
195
|
+
var DBSCAN_ALPHA, DECAY_LAMBDA, MATCH_DISTANCE_THRESHOLD;
|
|
196
|
+
var init_vision_tuning = __esmMin((() => {
|
|
197
|
+
DBSCAN_ALPHA = .03;
|
|
198
|
+
DECAY_LAMBDA = .95;
|
|
199
|
+
MATCH_DISTANCE_THRESHOLD = .25;
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/core/analyzer/clustering/dbscan.ts
|
|
208
204
|
/**
|
|
209
205
|
* DBSCAN clustering with resolution-independent eps.
|
|
210
206
|
* eps = alpha * sqrt(width^2 + height^2)
|
|
@@ -282,13 +278,116 @@ function findNeighbors(points, idx, epsSquared) {
|
|
|
282
278
|
}
|
|
283
279
|
var UNVISITED, NOISE;
|
|
284
280
|
var init_dbscan = __esmMin((() => {
|
|
285
|
-
|
|
281
|
+
init_vision_tuning();
|
|
286
282
|
UNVISITED = -2;
|
|
287
283
|
NOISE = -1;
|
|
288
284
|
}));
|
|
289
285
|
|
|
290
286
|
//#endregion
|
|
291
|
-
//#region src/core/analyzer.ts
|
|
287
|
+
//#region src/core/analyzer/features/feature-diff.ts
|
|
288
|
+
/**
|
|
289
|
+
* Match prev to next with Hamming k=2, crossCheck=false and strict ratio 0.25.
|
|
290
|
+
* @param cvLib - Initialized OpenCV runtime.
|
|
291
|
+
* @param prev - Previous frame's live features, owned by the caller.
|
|
292
|
+
* @param next - Next frame's live features, owned by the caller.
|
|
293
|
+
* @returns Unmatched next-frame coordinates without changing input ownership.
|
|
294
|
+
* @throws Propagates matching errors after releasing temporary native handles.
|
|
295
|
+
*/
|
|
296
|
+
function computeNewPoints(cvLib, prev, next) {
|
|
297
|
+
let matcher = null;
|
|
298
|
+
let matches = null;
|
|
299
|
+
try {
|
|
300
|
+
const matchedIndices = /* @__PURE__ */ new Set();
|
|
301
|
+
if (prev.descriptors.rows > 0 && next.descriptors.rows > 0) try {
|
|
302
|
+
matcher = new cvLib.BFMatcher(cvLib.NORM_HAMMING, false);
|
|
303
|
+
matches = new cvLib.DMatchVectorVector();
|
|
304
|
+
matcher.knnMatch(prev.descriptors, next.descriptors, matches, 2);
|
|
305
|
+
for (let i = 0; i < matches.size(); i++) {
|
|
306
|
+
const pair = matches.get(i);
|
|
307
|
+
try {
|
|
308
|
+
if (pair.size() < 2) continue;
|
|
309
|
+
const best = pair.get(0);
|
|
310
|
+
const second = pair.get(1);
|
|
311
|
+
if (best.distance < .25 * second.distance) matchedIndices.add(best.trainIdx);
|
|
312
|
+
} finally {
|
|
313
|
+
pair.delete();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} finally {
|
|
317
|
+
matcher?.delete();
|
|
318
|
+
}
|
|
319
|
+
const points = [];
|
|
320
|
+
for (let i = 0; i < next.keypoints.size(); i++) if (!matchedIndices.has(i)) {
|
|
321
|
+
const { x, y } = next.keypoints.get(i).pt;
|
|
322
|
+
points.push({
|
|
323
|
+
x,
|
|
324
|
+
y
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return points;
|
|
328
|
+
} finally {
|
|
329
|
+
matches?.delete();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
var init_feature_diff = __esmMin((() => {
|
|
333
|
+
init_vision_tuning();
|
|
334
|
+
}));
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/core/analyzer/features/frame-features.ts
|
|
338
|
+
/**
|
|
339
|
+
* Detect one frame's features without retaining its image or mask.
|
|
340
|
+
* @param cvLib - Initialized OpenCV runtime.
|
|
341
|
+
* @param akaze - Detector owned and released by the caller.
|
|
342
|
+
* @param frame - Grayscale bytes with matching width and height.
|
|
343
|
+
* @returns Feature handles that the caller must delete.
|
|
344
|
+
* @throws Propagates native errors after releasing partial allocations.
|
|
345
|
+
*/
|
|
346
|
+
function computeFrameFeatures(cvLib, akaze, frame) {
|
|
347
|
+
let image = null;
|
|
348
|
+
let mask = null;
|
|
349
|
+
let keypoints = null;
|
|
350
|
+
let descriptors = null;
|
|
351
|
+
try {
|
|
352
|
+
image = new cvLib.Mat(frame.height, frame.width, cvLib.CV_8UC1);
|
|
353
|
+
image.data.set(frame.data);
|
|
354
|
+
mask = new cvLib.Mat();
|
|
355
|
+
keypoints = new cvLib.KeyPointVector();
|
|
356
|
+
descriptors = new cvLib.Mat();
|
|
357
|
+
akaze.detectAndCompute(image, mask, keypoints, descriptors);
|
|
358
|
+
const ownedKeypoints = keypoints;
|
|
359
|
+
const ownedDescriptors = descriptors;
|
|
360
|
+
let deleted = false;
|
|
361
|
+
const features = {
|
|
362
|
+
width: frame.width,
|
|
363
|
+
height: frame.height,
|
|
364
|
+
keypoints: ownedKeypoints,
|
|
365
|
+
descriptors: ownedDescriptors,
|
|
366
|
+
/** Release the transferred handles exactly once. */
|
|
367
|
+
delete() {
|
|
368
|
+
if (deleted) return;
|
|
369
|
+
deleted = true;
|
|
370
|
+
try {
|
|
371
|
+
ownedKeypoints.delete();
|
|
372
|
+
} finally {
|
|
373
|
+
ownedDescriptors.delete();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
keypoints = null;
|
|
378
|
+
descriptors = null;
|
|
379
|
+
return features;
|
|
380
|
+
} finally {
|
|
381
|
+
image?.delete();
|
|
382
|
+
mask?.delete();
|
|
383
|
+
keypoints?.delete();
|
|
384
|
+
descriptors?.delete();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
var init_frame_features = __esmMin((() => {}));
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/core/analyzer/analyzer.ts
|
|
292
391
|
async function ensureOpenCV() {
|
|
293
392
|
if (!cvReady) cvReady = (async () => {
|
|
294
393
|
const cvObj = require("@techstark/opencv-js");
|
|
@@ -327,77 +426,6 @@ function computeIoU(a, b) {
|
|
|
327
426
|
const union = a.width * a.height + b.width * b.height - intersection;
|
|
328
427
|
return union === 0 ? 0 : intersection / union;
|
|
329
428
|
}
|
|
330
|
-
async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
331
|
-
const cv = cvLib;
|
|
332
|
-
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
333
|
-
mat1.data.set(frame1.data);
|
|
334
|
-
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
335
|
-
mat2.data.set(frame2.data);
|
|
336
|
-
const kp1 = new cvLib.KeyPointVector();
|
|
337
|
-
const kp2 = new cvLib.KeyPointVector();
|
|
338
|
-
const desc1 = new cvLib.Mat();
|
|
339
|
-
const desc2 = new cvLib.Mat();
|
|
340
|
-
const mask1 = new cvLib.Mat();
|
|
341
|
-
const mask2 = new cvLib.Mat();
|
|
342
|
-
const akaze = new cvLib.AKAZE();
|
|
343
|
-
let matches = null;
|
|
344
|
-
try {
|
|
345
|
-
akaze.detectAndCompute(mat1, mask1, kp1, desc1);
|
|
346
|
-
akaze.detectAndCompute(mat2, mask2, kp2, desc2);
|
|
347
|
-
const matchedKp1Indices = /* @__PURE__ */ new Set();
|
|
348
|
-
const matchedKp2Indices = /* @__PURE__ */ new Set();
|
|
349
|
-
if (desc1.rows > 0 && desc2.rows > 0) {
|
|
350
|
-
const matcher = new cvLib.BFMatcher(cvLib.NORM_HAMMING, false);
|
|
351
|
-
try {
|
|
352
|
-
matches = new cvLib.DMatchVectorVector();
|
|
353
|
-
matcher.knnMatch(desc1, desc2, matches, 2);
|
|
354
|
-
for (let i = 0; i < matches.size(); i++) {
|
|
355
|
-
const pair = matches.get(i);
|
|
356
|
-
if (pair.size() < 2) continue;
|
|
357
|
-
const m0 = pair.get(0);
|
|
358
|
-
const m1 = pair.get(1);
|
|
359
|
-
if (m0.distance < .25 * m1.distance) {
|
|
360
|
-
matchedKp1Indices.add(m0.queryIdx);
|
|
361
|
-
matchedKp2Indices.add(m0.trainIdx);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
} finally {
|
|
365
|
-
matcher.delete();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const sNew = [];
|
|
369
|
-
for (let i = 0; i < kp2.size(); i++) if (!matchedKp2Indices.has(i)) {
|
|
370
|
-
const pt = kp2.get(i).pt;
|
|
371
|
-
sNew.push({
|
|
372
|
-
x: pt.x,
|
|
373
|
-
y: pt.y
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
const sLoss = [];
|
|
377
|
-
for (let i = 0; i < kp1.size(); i++) if (!matchedKp1Indices.has(i)) {
|
|
378
|
-
const pt = kp1.get(i).pt;
|
|
379
|
-
sLoss.push({
|
|
380
|
-
x: pt.x,
|
|
381
|
-
y: pt.y
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
return {
|
|
385
|
-
sNew,
|
|
386
|
-
sLoss
|
|
387
|
-
};
|
|
388
|
-
} finally {
|
|
389
|
-
mat1.delete();
|
|
390
|
-
mat2.delete();
|
|
391
|
-
kp1.delete();
|
|
392
|
-
kp2.delete();
|
|
393
|
-
desc1.delete();
|
|
394
|
-
desc2.delete();
|
|
395
|
-
mask1.delete();
|
|
396
|
-
mask2.delete();
|
|
397
|
-
akaze.delete();
|
|
398
|
-
if (matches) matches.delete();
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
429
|
/**
|
|
402
430
|
* Pixel-level difference fallback for AKAZE blind spots.
|
|
403
431
|
*
|
|
@@ -412,17 +440,30 @@ async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
|
412
440
|
* 3. threshold → binary mask of significant changes
|
|
413
441
|
* 4. findContours → bounding rects of changed regions
|
|
414
442
|
* 5. Grid sampling within each bounding rect → Point2D[]
|
|
443
|
+
*
|
|
444
|
+
* @param cvLib - Initialized OpenCV runtime shared by the analyzer.
|
|
445
|
+
* @param frame1 - Previous grayscale frame, with the same dimensions as frame2.
|
|
446
|
+
* @param frame2 - Next grayscale frame, with the same dimensions as frame1.
|
|
447
|
+
* @returns Grid-sampled points from changed regions.
|
|
448
|
+
* @throws Propagates allocation or OpenCV errors after releasing acquired handles.
|
|
415
449
|
*/
|
|
416
450
|
function computePixelDiff(cvLib, frame1, frame2) {
|
|
417
451
|
const cv = cvLib;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
452
|
+
let mat1 = null;
|
|
453
|
+
let mat2 = null;
|
|
454
|
+
let diff = null;
|
|
455
|
+
let blurred = null;
|
|
456
|
+
let binary = null;
|
|
457
|
+
let contours = null;
|
|
458
|
+
let hierarchy = null;
|
|
425
459
|
try {
|
|
460
|
+
mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
461
|
+
mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
462
|
+
diff = new cv.Mat();
|
|
463
|
+
blurred = new cv.Mat();
|
|
464
|
+
binary = new cv.Mat();
|
|
465
|
+
contours = new cv.MatVector();
|
|
466
|
+
hierarchy = new cv.Mat();
|
|
426
467
|
mat1.data.set(frame1.data);
|
|
427
468
|
mat2.data.set(frame2.data);
|
|
428
469
|
cv.absdiff(mat1, mat2, diff);
|
|
@@ -433,22 +474,26 @@ function computePixelDiff(cvLib, frame1, frame2) {
|
|
|
433
474
|
const points = [];
|
|
434
475
|
for (let c = 0; c < contours.size(); c++) {
|
|
435
476
|
const contour = contours.get(c);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
x
|
|
440
|
-
|
|
441
|
-
|
|
477
|
+
try {
|
|
478
|
+
const rect = cv.boundingRect(contour);
|
|
479
|
+
if (rect.width * rect.height < 100) continue;
|
|
480
|
+
for (let y = rect.y; y < rect.y + rect.height; y += 8) for (let x = rect.x; x < rect.x + rect.width; x += 8) points.push({
|
|
481
|
+
x,
|
|
482
|
+
y
|
|
483
|
+
});
|
|
484
|
+
} finally {
|
|
485
|
+
contour.delete();
|
|
486
|
+
}
|
|
442
487
|
}
|
|
443
488
|
return points;
|
|
444
489
|
} finally {
|
|
445
|
-
mat1
|
|
446
|
-
mat2
|
|
447
|
-
diff
|
|
448
|
-
blurred
|
|
449
|
-
binary
|
|
450
|
-
contours
|
|
451
|
-
hierarchy
|
|
490
|
+
mat1?.delete();
|
|
491
|
+
mat2?.delete();
|
|
492
|
+
diff?.delete();
|
|
493
|
+
blurred?.delete();
|
|
494
|
+
binary?.delete();
|
|
495
|
+
contours?.delete();
|
|
496
|
+
hierarchy?.delete();
|
|
452
497
|
}
|
|
453
498
|
}
|
|
454
499
|
function computeInformationGain(clusters, clusterPoints, imageArea, animationIndices, animationWeights) {
|
|
@@ -467,47 +512,85 @@ function computeInformationGain(clusters, clusterPoints, imageArea, animationInd
|
|
|
467
512
|
}
|
|
468
513
|
return gain;
|
|
469
514
|
}
|
|
470
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Analyze one batch, retaining its boundary frame for the following batch.
|
|
517
|
+
* @param cvLib - Initialized OpenCV runtime.
|
|
518
|
+
* @param akaze - Detector owned by analyzeFrames.
|
|
519
|
+
* @param frames - Boundary frame followed by new adjacent frames.
|
|
520
|
+
* @param carry - Boundary bytes and live features transferred from the previous batch.
|
|
521
|
+
* @param scale - Maximum preprocessing width.
|
|
522
|
+
* @param tracker - Stateful animation tracker shared across batches.
|
|
523
|
+
* @param pairOffset - Global position of this batch's first pair.
|
|
524
|
+
* @returns Scores, pair failure count, and ownership of the final frame's features.
|
|
525
|
+
*/
|
|
526
|
+
async function analyzeBatch(cvLib, akaze, frames, carry, scale, tracker, pairOffset) {
|
|
471
527
|
const edges = [];
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
528
|
+
let failures = 0;
|
|
529
|
+
let prev = carry?.features ?? null;
|
|
530
|
+
let next = null;
|
|
531
|
+
try {
|
|
532
|
+
const preprocessed = await Promise.all(map(frames, (f, index) => index === 0 && carry ? carry.preprocessed : preprocessFrame(f.extractPath, scale)));
|
|
533
|
+
const imageWidth = preprocessed[0]?.width ?? scale;
|
|
534
|
+
const imageHeight = preprocessed[0]?.height ?? Math.round(scale * 9 / 16);
|
|
535
|
+
const imageArea = imageWidth * imageHeight;
|
|
536
|
+
for (let i = 0; i < frames.length - 1; i++) {
|
|
537
|
+
const pairIndex = pairOffset + i;
|
|
538
|
+
try {
|
|
539
|
+
prev ??= computeFrameFeatures(cvLib, akaze, preprocessed[i]);
|
|
540
|
+
next = computeFrameFeatures(cvLib, akaze, preprocessed[i + 1]);
|
|
541
|
+
let dbscanResult = dbscan(computeNewPoints(cvLib, prev, next), imageWidth, imageHeight);
|
|
542
|
+
let clusters = dbscanResult.boundingBoxes;
|
|
543
|
+
if (clusters.length === 0) {
|
|
544
|
+
const pixelDiffPoints = computePixelDiff(cvLib, preprocessed[i], preprocessed[i + 1]);
|
|
545
|
+
if (pixelDiffPoints.length > 0) {
|
|
546
|
+
logger.debug(`Edge ${frames[i].id}->${frames[i + 1].id}: pixel-diff fallback (${pixelDiffPoints.length} points)`);
|
|
547
|
+
dbscanResult = dbscan(pixelDiffPoints, imageWidth, imageHeight, void 0, 2);
|
|
548
|
+
clusters = dbscanResult.boundingBoxes;
|
|
549
|
+
}
|
|
488
550
|
}
|
|
551
|
+
const clusterPointCounts = new Array(clusters.length).fill(0);
|
|
552
|
+
for (const label of dbscanResult.labels) if (label >= 0) clusterPointCounts[label]++;
|
|
553
|
+
const animationIndices = tracker.update(clusters, pairIndex);
|
|
554
|
+
const animationWeights = map(clusters, (_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0);
|
|
555
|
+
const score = computeInformationGain(clusters, clusterPointCounts, imageArea, animationIndices, animationWeights);
|
|
556
|
+
logger.debug(`Edge ${frames[i].id}->${frames[i + 1].id} G(t)=${score.toFixed(6)}`);
|
|
557
|
+
edges.push({
|
|
558
|
+
sourceId: frames[i].id,
|
|
559
|
+
targetId: frames[i + 1].id,
|
|
560
|
+
score
|
|
561
|
+
});
|
|
562
|
+
} catch (err) {
|
|
563
|
+
logger.warn(`Frame pair analysis failed: ${String(err)}`);
|
|
564
|
+
failures++;
|
|
565
|
+
edges.push({
|
|
566
|
+
sourceId: frames[i].id,
|
|
567
|
+
targetId: frames[i + 1].id,
|
|
568
|
+
score: 0
|
|
569
|
+
});
|
|
570
|
+
} finally {
|
|
571
|
+
prev?.delete();
|
|
572
|
+
prev = next;
|
|
573
|
+
next = null;
|
|
489
574
|
}
|
|
490
|
-
const clusterPointCounts = new Array(clusters.length).fill(0);
|
|
491
|
-
for (const label of dbscanResult.labels) if (label >= 0) clusterPointCounts[label]++;
|
|
492
|
-
const animationIndices = tracker.update(clusters, pairIndex);
|
|
493
|
-
const animationWeights = map(clusters, (_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0);
|
|
494
|
-
const score = computeInformationGain(clusters, clusterPointCounts, imageArea, animationIndices, animationWeights);
|
|
495
|
-
logger.debug(`Edge ${frames[i].id}->${frames[i + 1].id} G(t)=${score.toFixed(6)}`);
|
|
496
|
-
edges.push({
|
|
497
|
-
sourceId: frames[i].id,
|
|
498
|
-
targetId: frames[i + 1].id,
|
|
499
|
-
score
|
|
500
|
-
});
|
|
501
|
-
} catch (err) {
|
|
502
|
-
logger.debug(`Frame pair analysis failed: ${String(err)}`);
|
|
503
|
-
edges.push({
|
|
504
|
-
sourceId: frames[i].id,
|
|
505
|
-
targetId: frames[i + 1].id,
|
|
506
|
-
score: 0
|
|
507
|
-
});
|
|
508
575
|
}
|
|
576
|
+
const result = {
|
|
577
|
+
edges,
|
|
578
|
+
failures,
|
|
579
|
+
analysisResolution: {
|
|
580
|
+
width: imageWidth,
|
|
581
|
+
height: imageHeight
|
|
582
|
+
},
|
|
583
|
+
carry: prev ? {
|
|
584
|
+
preprocessed: preprocessed[preprocessed.length - 1],
|
|
585
|
+
features: prev
|
|
586
|
+
} : null
|
|
587
|
+
};
|
|
588
|
+
prev = null;
|
|
589
|
+
return result;
|
|
590
|
+
} finally {
|
|
591
|
+
prev?.delete();
|
|
592
|
+
next?.delete();
|
|
509
593
|
}
|
|
510
|
-
return edges;
|
|
511
594
|
}
|
|
512
595
|
/**
|
|
513
596
|
* Analyze adjacent frame pairs to compute information gain scores (G(t)).
|
|
@@ -518,37 +601,66 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
518
601
|
* 2. DBSCAN Spatial Clustering
|
|
519
602
|
* 3. Spatio-temporal IoU Tracking
|
|
520
603
|
* 4. G(t) Information Gain Scoring
|
|
604
|
+
* @param ctx - Frames, analysis options, and the progress callback for this run.
|
|
605
|
+
* @returns Adjacent scores and tracked animations in analysis coordinates.
|
|
606
|
+
* @throws Propagates runtime errors and rejects total failure of two or more pairs after cleanup.
|
|
521
607
|
*/
|
|
522
608
|
async function analyzeFrames(ctx) {
|
|
523
609
|
const { frames } = ctx;
|
|
524
610
|
if (frames.length < 2) return {
|
|
525
611
|
edges: [],
|
|
526
|
-
animations: []
|
|
612
|
+
animations: [],
|
|
613
|
+
analysisResolution: {
|
|
614
|
+
width: 0,
|
|
615
|
+
height: 0
|
|
616
|
+
}
|
|
527
617
|
};
|
|
528
618
|
logger.debug(`Analyzing ${frames.length} frames in batches of ${10}`);
|
|
529
619
|
const cvLib = await ensureOpenCV();
|
|
530
620
|
const edges = [];
|
|
531
|
-
const tracker = new IoUTracker(ctx.options.fps, ctx.options.iouThreshold, ctx.options.animationThreshold);
|
|
621
|
+
const tracker = new IoUTracker(ctx.effectiveFps ?? ctx.options.fps, ctx.options.iouThreshold, ctx.options.animationThreshold);
|
|
532
622
|
const scale = ctx.options.scale;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
623
|
+
let akaze = null;
|
|
624
|
+
let carry = null;
|
|
625
|
+
let analysisResolution = {
|
|
626
|
+
width: 0,
|
|
627
|
+
height: 0
|
|
628
|
+
};
|
|
629
|
+
let failures = 0;
|
|
630
|
+
try {
|
|
631
|
+
akaze = new cvLib.AKAZE();
|
|
632
|
+
for (let i = 0; i < frames.length - 1; i += 10) {
|
|
633
|
+
const batch = [frames[i], ...frames.slice(i + 1, i + 1 + 10)];
|
|
634
|
+
const result = await analyzeBatch(cvLib, akaze, batch, carry, scale, tracker, i);
|
|
635
|
+
carry = result.carry;
|
|
636
|
+
failures += result.failures;
|
|
637
|
+
if (i === 0) analysisResolution = result.analysisResolution;
|
|
638
|
+
edges.push(...result.edges);
|
|
639
|
+
const progress = Math.min(100, (i + 10) / (frames.length - 1) * 100);
|
|
640
|
+
ctx.emitProgress(progress);
|
|
641
|
+
}
|
|
642
|
+
} finally {
|
|
643
|
+
carry?.features.delete();
|
|
644
|
+
akaze?.delete();
|
|
539
645
|
}
|
|
646
|
+
const pairs = frames.length - 1;
|
|
647
|
+
if (pairs >= 2 && failures === pairs) throw new Error(`All ${pairs} frame pairs failed analysis`);
|
|
540
648
|
const animations = tracker.flushAndGetAnimations();
|
|
541
649
|
logger.debug(`Computed ${edges.length} score edges and ${animations.length} animations`);
|
|
542
650
|
return {
|
|
543
651
|
edges,
|
|
544
|
-
animations
|
|
652
|
+
animations,
|
|
653
|
+
analysisResolution
|
|
545
654
|
};
|
|
546
655
|
}
|
|
547
656
|
var OPENCV_INIT_TIMEOUT_MS, require, cvReady, IoUTracker;
|
|
548
|
-
var init_analyzer = __esmMin((() => {
|
|
549
|
-
|
|
657
|
+
var init_analyzer$1 = __esmMin((() => {
|
|
658
|
+
init_pipeline_defaults();
|
|
659
|
+
init_vision_tuning();
|
|
550
660
|
init_logger();
|
|
551
661
|
init_dbscan();
|
|
662
|
+
init_feature_diff();
|
|
663
|
+
init_frame_features();
|
|
552
664
|
OPENCV_INIT_TIMEOUT_MS = 3e4;
|
|
553
665
|
require = createRequire(import.meta.url);
|
|
554
666
|
cvReady = null;
|
|
@@ -636,7 +748,75 @@ var init_analyzer = __esmMin((() => {
|
|
|
636
748
|
}));
|
|
637
749
|
|
|
638
750
|
//#endregion
|
|
639
|
-
//#region src/
|
|
751
|
+
//#region src/core/analyzer/index.ts
|
|
752
|
+
var init_analyzer = __esmMin((() => {
|
|
753
|
+
init_analyzer$1();
|
|
754
|
+
init_dbscan();
|
|
755
|
+
}));
|
|
756
|
+
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/core/utils/metadata/build-video-metadata.ts
|
|
759
|
+
/**
|
|
760
|
+
* Read output dimensions and build consistent video and animation metadata.
|
|
761
|
+
* JPEG finalization does not resize, so the source dimensions match the output.
|
|
762
|
+
* @param ctx Pipeline state with source duration, effective FPS and analysis-space animations.
|
|
763
|
+
* @param selected Selected frames in output order; the first candidate is the fallback.
|
|
764
|
+
* @param analysisResolution Analysis dimensions; absent or zero dimensions imply no scaling.
|
|
765
|
+
* @returns Video metadata and new output-space animations, retaining zero-based frame IDs.
|
|
766
|
+
* @throws If sharp cannot read the selected or fallback image. Empty input performs no image I/O.
|
|
767
|
+
*/
|
|
768
|
+
async function buildVideoMetadata(ctx, selected, analysisResolution) {
|
|
769
|
+
const firstFrame = selected[0] ?? ctx.frames[0];
|
|
770
|
+
const dimensions = firstFrame ? await sharp(firstFrame.extractPath).metadata() : void 0;
|
|
771
|
+
const width = dimensions?.width ?? 0;
|
|
772
|
+
const height = dimensions?.height ?? 0;
|
|
773
|
+
const sx = analysisResolution?.width ? width / analysisResolution.width : 1;
|
|
774
|
+
const sy = analysisResolution?.height ? height / analysisResolution.height : 1;
|
|
775
|
+
const lastTimestamp = ctx.frames[ctx.frames.length - 1]?.timestamp ?? 0;
|
|
776
|
+
const duration = ctx.options.mode === "frames" ? lastTimestamp : ctx.sourceDurationSec ?? lastTimestamp;
|
|
777
|
+
return {
|
|
778
|
+
video: {
|
|
779
|
+
originalDurationMs: Math.round(duration * 1e3),
|
|
780
|
+
fps: ctx.options.mode === "frames" ? 1 : ctx.effectiveFps ?? ctx.options.fps,
|
|
781
|
+
resolution: {
|
|
782
|
+
width,
|
|
783
|
+
height
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
animations: (ctx.animations ?? []).map((animation) => {
|
|
787
|
+
const box = animation.boundingBox;
|
|
788
|
+
const x = Math.max(0, Math.min(width, Math.round(box.x * sx)));
|
|
789
|
+
const y = Math.max(0, Math.min(height, Math.round(box.y * sy)));
|
|
790
|
+
return {
|
|
791
|
+
...animation,
|
|
792
|
+
boundingBox: {
|
|
793
|
+
x,
|
|
794
|
+
y,
|
|
795
|
+
width: Math.max(0, Math.min(width - x, Math.round(box.width * sx))),
|
|
796
|
+
height: Math.max(0, Math.min(height - y, Math.round(box.height * sy)))
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
})
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
var init_build_video_metadata = __esmMin((() => {}));
|
|
803
|
+
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region src/core/constants/workspace-layout.ts
|
|
806
|
+
function getTempWorkspaceDir(sessionId) {
|
|
807
|
+
return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
808
|
+
}
|
|
809
|
+
var APP_NAME, WORKSPACE_PREFIX, TEMP_BASE_DIR, FRAME_OUTPUT_EXTENSION, FRAME_FILENAME_PATTERN;
|
|
810
|
+
var init_workspace_layout = __esmMin((() => {
|
|
811
|
+
APP_NAME = "scene-sieve";
|
|
812
|
+
WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
813
|
+
TEMP_BASE_DIR = tmpdir();
|
|
814
|
+
FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
815
|
+
FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
|
|
816
|
+
}));
|
|
817
|
+
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/core/utils/filesystem/paths.ts
|
|
640
820
|
async function ensureDir(dirPath) {
|
|
641
821
|
await mkdir(dirPath, { recursive: true });
|
|
642
822
|
}
|
|
@@ -675,13 +855,18 @@ function deriveOutputPath(inputPath) {
|
|
|
675
855
|
var init_paths = __esmMin((() => {}));
|
|
676
856
|
|
|
677
857
|
//#endregion
|
|
678
|
-
//#region src/core/extractor.ts
|
|
858
|
+
//#region src/core/extractor/extractor.ts
|
|
679
859
|
/**
|
|
680
860
|
* Extract frames from video/GIF using FFmpeg.
|
|
681
|
-
*
|
|
682
|
-
*
|
|
861
|
+
* @param ctx Pipeline context; records effectiveFps and sourceDurationSec for video input.
|
|
862
|
+
* @returns Extracted candidates, or the unchanged input array in frames mode.
|
|
863
|
+
* @throws When the input is missing, metadata has no video stream, or FFmpeg fails.
|
|
683
864
|
*/
|
|
684
865
|
async function extractFrames(ctx) {
|
|
866
|
+
if (ctx.options.mode === "frames") {
|
|
867
|
+
ctx.effectiveFps = 1;
|
|
868
|
+
return ctx.frames;
|
|
869
|
+
}
|
|
685
870
|
const framesDir = join(ctx.workspacePath, "frames");
|
|
686
871
|
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
687
872
|
if (!inputPath) throw new Error("inputPath is required for frame extraction");
|
|
@@ -696,19 +881,30 @@ async function extractFrames(ctx) {
|
|
|
696
881
|
if (!(metadata.streams?.some((s) => s.codec_type === "video") ?? false)) throw new Error(`No video stream found in file: ${inputPath} (detected format: ${formatName})`);
|
|
697
882
|
logger.debug(`Detected format: ${formatName} (Duration: ${duration.toFixed(1)}s), path: ${inputPath}`);
|
|
698
883
|
await ensureDir(framesDir);
|
|
884
|
+
const frameLimit = Math.max(2, maxFrames);
|
|
699
885
|
let effectiveFps = fps;
|
|
700
886
|
if (duration > 0) {
|
|
701
|
-
const fpsCap =
|
|
887
|
+
const fpsCap = frameLimit / duration;
|
|
702
888
|
effectiveFps = Math.min(fps, fpsCap);
|
|
703
|
-
effectiveFps = Math.max(.5, effectiveFps);
|
|
704
889
|
logger.debug(`FPS: ${fps} → effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`);
|
|
705
890
|
}
|
|
706
|
-
|
|
891
|
+
ctx.effectiveFps = effectiveFps;
|
|
892
|
+
ctx.sourceDurationSec = duration;
|
|
893
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale, frameLimit);
|
|
707
894
|
ctx.emitProgress(100);
|
|
708
895
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
709
896
|
return frames;
|
|
710
897
|
}
|
|
711
|
-
|
|
898
|
+
/**
|
|
899
|
+
* Write scaled JPEG candidates through the bundled FFmpeg runtime.
|
|
900
|
+
* @param inputPath Readable video input.
|
|
901
|
+
* @param outputDir Existing frame directory.
|
|
902
|
+
* @param fps Positive effective sampling frequency.
|
|
903
|
+
* @param scale Output image height.
|
|
904
|
+
* @param frameLimit Maximum number of output frames.
|
|
905
|
+
* @returns Candidates with local output-grid timestamps; rejects on extraction failure.
|
|
906
|
+
*/
|
|
907
|
+
async function extractByFps(inputPath, outputDir, fps, scale, frameLimit) {
|
|
712
908
|
const outputPattern = join(outputDir, FRAME_FILENAME_PATTERN);
|
|
713
909
|
await execa(ffmpegPath, [
|
|
714
910
|
"-i",
|
|
@@ -717,9 +913,11 @@ async function extractByFps(inputPath, outputDir, fps, scale, duration) {
|
|
|
717
913
|
`fps=${fps},scale=-1:${scale}`,
|
|
718
914
|
"-q:v",
|
|
719
915
|
"2",
|
|
916
|
+
"-frames:v",
|
|
917
|
+
String(frameLimit),
|
|
720
918
|
outputPattern
|
|
721
919
|
]);
|
|
722
|
-
return buildFrameList(outputDir,
|
|
920
|
+
return buildFrameList(outputDir, fps);
|
|
723
921
|
}
|
|
724
922
|
async function getVideoMetadata(inputPath) {
|
|
725
923
|
const { stdout } = await execa(path, [
|
|
@@ -733,12 +931,18 @@ async function getVideoMetadata(inputPath) {
|
|
|
733
931
|
]);
|
|
734
932
|
return JSON.parse(stdout);
|
|
735
933
|
}
|
|
736
|
-
|
|
934
|
+
/**
|
|
935
|
+
* Read sorted JPEG paths and attach local output-grid times.
|
|
936
|
+
* @param framesDir Extracted frame directory; filesystem errors propagate.
|
|
937
|
+
* @param effectiveFps Positive frequency used by the fps filter.
|
|
938
|
+
* @returns Zero-based candidates without a segment seek offset.
|
|
939
|
+
*/
|
|
940
|
+
async function buildFrameList(framesDir, effectiveFps) {
|
|
737
941
|
const jpgFiles = filter(await readdir(framesDir), (f) => f.endsWith(".jpg")).sort();
|
|
738
942
|
if (jpgFiles.length === 0) return [];
|
|
739
943
|
return map(jpgFiles, (file, index) => ({
|
|
740
944
|
id: index,
|
|
741
|
-
timestamp:
|
|
945
|
+
timestamp: index / effectiveFps,
|
|
742
946
|
extractPath: join(framesDir, file)
|
|
743
947
|
}));
|
|
744
948
|
}
|
|
@@ -752,9 +956,10 @@ async function buildFrameList(framesDir, duration) {
|
|
|
752
956
|
* @param scale - Height scale for vision analysis
|
|
753
957
|
* @param startTime - Start time in seconds
|
|
754
958
|
* @param duration - Duration in seconds to extract
|
|
959
|
+
* @param frameLimit - Positive output limit; defaults to the range's grid capacity
|
|
755
960
|
* @returns Array of FrameNode with segment-local timestamps (starting from 0)
|
|
756
961
|
*/
|
|
757
|
-
async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime, duration) {
|
|
962
|
+
async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime, duration, frameLimit = Math.ceil(duration * fps)) {
|
|
758
963
|
const outputPattern = join(outputDir, FRAME_FILENAME_PATTERN);
|
|
759
964
|
await execa(ffmpegPath, [
|
|
760
965
|
"-ss",
|
|
@@ -767,18 +972,124 @@ async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime
|
|
|
767
972
|
`fps=${fps},scale=-1:${scale}`,
|
|
768
973
|
"-q:v",
|
|
769
974
|
"2",
|
|
975
|
+
"-frames:v",
|
|
976
|
+
String(frameLimit),
|
|
770
977
|
outputPattern
|
|
771
978
|
]);
|
|
772
|
-
return buildFrameList(outputDir,
|
|
979
|
+
return buildFrameList(outputDir, fps);
|
|
773
980
|
}
|
|
774
|
-
var init_extractor = __esmMin((() => {
|
|
775
|
-
|
|
981
|
+
var init_extractor$1 = __esmMin((() => {
|
|
982
|
+
init_workspace_layout();
|
|
776
983
|
init_logger();
|
|
777
984
|
init_paths();
|
|
778
985
|
}));
|
|
779
986
|
|
|
780
987
|
//#endregion
|
|
781
|
-
//#region src/core/
|
|
988
|
+
//#region src/core/extractor/index.ts
|
|
989
|
+
var init_extractor = __esmMin((() => {
|
|
990
|
+
init_extractor$1();
|
|
991
|
+
}));
|
|
992
|
+
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/core/input-resolver/validation/validate-options.ts
|
|
995
|
+
/**
|
|
996
|
+
* Reject invalid numeric options before defaults or pipeline effects are applied.
|
|
997
|
+
* @param options - Supplied options; omitted numeric fields use pipeline defaults.
|
|
998
|
+
* @returns Nothing when all supplied numeric fields satisfy their contracts.
|
|
999
|
+
* @throws An input error naming the invalid option and its received value.
|
|
1000
|
+
*/
|
|
1001
|
+
function validateOptions(options) {
|
|
1002
|
+
for (const [name, min, max, integer, exclusiveMin, requirement] of [
|
|
1003
|
+
[
|
|
1004
|
+
"count",
|
|
1005
|
+
1,
|
|
1006
|
+
Infinity,
|
|
1007
|
+
true,
|
|
1008
|
+
false,
|
|
1009
|
+
"an integer >= 1"
|
|
1010
|
+
],
|
|
1011
|
+
[
|
|
1012
|
+
"threshold",
|
|
1013
|
+
0,
|
|
1014
|
+
1,
|
|
1015
|
+
false,
|
|
1016
|
+
true,
|
|
1017
|
+
"in range (0, 1] and finite"
|
|
1018
|
+
],
|
|
1019
|
+
[
|
|
1020
|
+
"fps",
|
|
1021
|
+
0,
|
|
1022
|
+
Infinity,
|
|
1023
|
+
false,
|
|
1024
|
+
true,
|
|
1025
|
+
"finite and > 0"
|
|
1026
|
+
],
|
|
1027
|
+
[
|
|
1028
|
+
"maxFrames",
|
|
1029
|
+
2,
|
|
1030
|
+
Infinity,
|
|
1031
|
+
true,
|
|
1032
|
+
false,
|
|
1033
|
+
"an integer >= 2"
|
|
1034
|
+
],
|
|
1035
|
+
[
|
|
1036
|
+
"scale",
|
|
1037
|
+
16,
|
|
1038
|
+
Infinity,
|
|
1039
|
+
true,
|
|
1040
|
+
false,
|
|
1041
|
+
"an integer >= 16"
|
|
1042
|
+
],
|
|
1043
|
+
[
|
|
1044
|
+
"quality",
|
|
1045
|
+
1,
|
|
1046
|
+
100,
|
|
1047
|
+
true,
|
|
1048
|
+
false,
|
|
1049
|
+
"an integer in range [1, 100]"
|
|
1050
|
+
],
|
|
1051
|
+
[
|
|
1052
|
+
"iouThreshold",
|
|
1053
|
+
0,
|
|
1054
|
+
1,
|
|
1055
|
+
false,
|
|
1056
|
+
false,
|
|
1057
|
+
"finite and in range [0, 1]"
|
|
1058
|
+
],
|
|
1059
|
+
[
|
|
1060
|
+
"animationThreshold",
|
|
1061
|
+
1,
|
|
1062
|
+
Infinity,
|
|
1063
|
+
true,
|
|
1064
|
+
false,
|
|
1065
|
+
"an integer >= 1"
|
|
1066
|
+
],
|
|
1067
|
+
[
|
|
1068
|
+
"maxSegmentDuration",
|
|
1069
|
+
0,
|
|
1070
|
+
Infinity,
|
|
1071
|
+
false,
|
|
1072
|
+
true,
|
|
1073
|
+
"finite and > 0"
|
|
1074
|
+
],
|
|
1075
|
+
[
|
|
1076
|
+
"concurrency",
|
|
1077
|
+
1,
|
|
1078
|
+
Infinity,
|
|
1079
|
+
true,
|
|
1080
|
+
false,
|
|
1081
|
+
"an integer >= 1"
|
|
1082
|
+
]
|
|
1083
|
+
]) {
|
|
1084
|
+
const value = options[name];
|
|
1085
|
+
if (value === void 0) continue;
|
|
1086
|
+
if (!Number.isFinite(value) || integer && !Number.isInteger(value) || (exclusiveMin ? value <= min : value < min) || value > max) throw new Error(`${name} must be ${requirement}, received: ${value}`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
var init_validate_options = __esmMin((() => {}));
|
|
1090
|
+
|
|
1091
|
+
//#endregion
|
|
1092
|
+
//#region src/core/workspace/workspace.ts
|
|
782
1093
|
async function createWorkspace(sessionId) {
|
|
783
1094
|
const workspacePath = getTempWorkspaceDir(sessionId);
|
|
784
1095
|
await ensureDir(join(workspacePath, "frames"));
|
|
@@ -809,17 +1120,11 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
809
1120
|
timestampMs: Math.round(frame.timestamp * 1e3)
|
|
810
1121
|
});
|
|
811
1122
|
}
|
|
1123
|
+
const { video, animations } = await buildVideoMetadata(ctx, selectedFrames, ctx.analysisResolution);
|
|
812
1124
|
const metadata = {
|
|
813
|
-
video
|
|
814
|
-
originalDurationMs: Math.round((ctx.frames.length > 0 ? ctx.frames[ctx.frames.length - 1].timestamp : 0) * 1e3),
|
|
815
|
-
fps: ctx.options.fps,
|
|
816
|
-
resolution: {
|
|
817
|
-
width: ctx.options.scale,
|
|
818
|
-
height: Math.round(ctx.options.scale * 9 / 16)
|
|
819
|
-
}
|
|
820
|
-
},
|
|
1125
|
+
video,
|
|
821
1126
|
frames: framesMetadata,
|
|
822
|
-
animations: map(
|
|
1127
|
+
animations: map(animations, (anim) => ({
|
|
823
1128
|
...anim,
|
|
824
1129
|
startFrameId: anim.startFrameId + 1,
|
|
825
1130
|
endFrameId: anim.endFrameId + 1,
|
|
@@ -910,20 +1215,33 @@ async function readFramesAsBuffers(frameNodes, quality) {
|
|
|
910
1215
|
}).toBuffer()));
|
|
911
1216
|
}
|
|
912
1217
|
var STALE_THRESHOLD_MS;
|
|
913
|
-
var init_workspace = __esmMin((() => {
|
|
914
|
-
|
|
1218
|
+
var init_workspace$1 = __esmMin((() => {
|
|
1219
|
+
init_workspace_layout();
|
|
915
1220
|
init_paths();
|
|
1221
|
+
init_build_video_metadata();
|
|
916
1222
|
STALE_THRESHOLD_MS = 3600 * 1e3;
|
|
917
1223
|
}));
|
|
918
1224
|
|
|
919
1225
|
//#endregion
|
|
920
|
-
//#region src/core/
|
|
1226
|
+
//#region src/core/workspace/index.ts
|
|
1227
|
+
var init_workspace = __esmMin((() => {
|
|
1228
|
+
init_workspace$1();
|
|
1229
|
+
}));
|
|
1230
|
+
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/core/input-resolver/input-resolver.ts
|
|
1233
|
+
/**
|
|
1234
|
+
* Validate supplied options and resolve defaults and paths for the pipeline.
|
|
1235
|
+
* @param options - Mode-specific input and optional numeric settings.
|
|
1236
|
+
* @returns Complete pipeline settings with absolute file input paths.
|
|
1237
|
+
* @throws An input error if a supplied numeric setting is invalid.
|
|
1238
|
+
*/
|
|
921
1239
|
function resolveOptions(options) {
|
|
1240
|
+
validateOptions(options);
|
|
922
1241
|
const mode = options.mode;
|
|
923
1242
|
const inputPath = mode === "file" ? resolveAbsolute(options.inputPath) : void 0;
|
|
924
1243
|
const outputPath = options.outputPath ?? (inputPath ? deriveOutputPath(inputPath) : join(process.cwd(), "scene-sieve-output"));
|
|
925
1244
|
const threshold = options.threshold ?? .5;
|
|
926
|
-
if (threshold <= 0 || threshold > 1) throw new Error(`threshold must be in range (0, 1], received: ${threshold}`);
|
|
927
1245
|
return {
|
|
928
1246
|
mode,
|
|
929
1247
|
inputPath,
|
|
@@ -948,6 +1266,10 @@ function resolveOptions(options) {
|
|
|
948
1266
|
* - 'file' mode: validate file exists and delegate to extractor (caller's responsibility)
|
|
949
1267
|
* - 'buffer' mode: write buffer as temp video file, return path via FrameNode trick (empty list)
|
|
950
1268
|
* - 'frames' mode: write frame buffers as JPGs, return FrameNode[]
|
|
1269
|
+
* @param options - Input source; encoded frames must have matching dimensions.
|
|
1270
|
+
* @param workspacePath - Workspace receiving temporary input files.
|
|
1271
|
+
* @returns Frame nodes or a resolved video path for extraction.
|
|
1272
|
+
* @throws Propagates metadata or write errors and rejects mismatched frame sizes.
|
|
951
1273
|
*/
|
|
952
1274
|
async function resolveInput(options, workspacePath) {
|
|
953
1275
|
if (options.mode === "file") return {
|
|
@@ -958,17 +1280,51 @@ async function resolveInput(options, workspacePath) {
|
|
|
958
1280
|
frames: [],
|
|
959
1281
|
resolvedInputPath: await writeInputBuffer(options.inputBuffer, workspacePath)
|
|
960
1282
|
};
|
|
961
|
-
if (options.mode === "frames")
|
|
1283
|
+
if (options.mode === "frames") {
|
|
1284
|
+
let dimensions;
|
|
1285
|
+
for (const buffer of options.inputFrames) {
|
|
1286
|
+
const { width, height } = await sharp(buffer).metadata();
|
|
1287
|
+
if (dimensions && (width !== dimensions.width || height !== dimensions.height)) throw new Error(`inputFrames must be the same size (${dimensions.width}x${dimensions.height}), received: ${width}x${height}`);
|
|
1288
|
+
dimensions = {
|
|
1289
|
+
width,
|
|
1290
|
+
height
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
return { frames: await writeInputFrames(options.inputFrames, workspacePath) };
|
|
1294
|
+
}
|
|
962
1295
|
throw new Error(`Unsupported input mode: ${options.mode}`);
|
|
963
1296
|
}
|
|
964
|
-
var init_input_resolver = __esmMin((() => {
|
|
965
|
-
|
|
1297
|
+
var init_input_resolver$1 = __esmMin((() => {
|
|
1298
|
+
init_pipeline_defaults();
|
|
966
1299
|
init_paths();
|
|
1300
|
+
init_validate_options();
|
|
967
1301
|
init_workspace();
|
|
968
1302
|
}));
|
|
969
1303
|
|
|
970
1304
|
//#endregion
|
|
971
|
-
//#region src/
|
|
1305
|
+
//#region src/core/input-resolver/index.ts
|
|
1306
|
+
var init_input_resolver = __esmMin((() => {
|
|
1307
|
+
init_input_resolver$1();
|
|
1308
|
+
}));
|
|
1309
|
+
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/core/pruner/scoring/normalize-scores.ts
|
|
1312
|
+
/**
|
|
1313
|
+
* Find the first position whose score is at least the requested value.
|
|
1314
|
+
* @param sorted - Finite positive scores sorted in ascending order.
|
|
1315
|
+
* @param value - A finite positive score present in sorted.
|
|
1316
|
+
* @returns The first matching rank, including the first position of any tie.
|
|
1317
|
+
*/
|
|
1318
|
+
function lowerBound(sorted, value) {
|
|
1319
|
+
let low = 0;
|
|
1320
|
+
let high = sorted.length;
|
|
1321
|
+
while (low < high) {
|
|
1322
|
+
const mid = Math.floor((low + high) / 2);
|
|
1323
|
+
if (sorted[mid] < value) low = mid + 1;
|
|
1324
|
+
else high = mid;
|
|
1325
|
+
}
|
|
1326
|
+
return low;
|
|
1327
|
+
}
|
|
972
1328
|
/**
|
|
973
1329
|
* Normalize raw scores to [0, 1] range via Robust Hybrid Normalization.
|
|
974
1330
|
*
|
|
@@ -1013,16 +1369,19 @@ function normalizeScores(items) {
|
|
|
1013
1369
|
});
|
|
1014
1370
|
const cdf = map(safeScores, (s) => {
|
|
1015
1371
|
if (s <= 0) return 0;
|
|
1016
|
-
return sorted
|
|
1372
|
+
return lowerBound(sorted, s) / sorted.length;
|
|
1017
1373
|
});
|
|
1018
1374
|
return map(logisticZ, (z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA);
|
|
1019
1375
|
}
|
|
1020
|
-
var
|
|
1021
|
-
|
|
1376
|
+
var NORMALIZATION_ALPHA, NORMALIZATION_MAD_COEFFICIENT, NORMALIZATION_MIN_SAMPLE_SIZE;
|
|
1377
|
+
var init_normalize_scores = __esmMin((() => {
|
|
1378
|
+
NORMALIZATION_ALPHA = .4;
|
|
1379
|
+
NORMALIZATION_MAD_COEFFICIENT = 1.4826;
|
|
1380
|
+
NORMALIZATION_MIN_SAMPLE_SIZE = 10;
|
|
1022
1381
|
}));
|
|
1023
1382
|
|
|
1024
1383
|
//#endregion
|
|
1025
|
-
//#region src/
|
|
1384
|
+
//#region src/core/pruner/heap/min-heap.ts
|
|
1026
1385
|
var MinHeap;
|
|
1027
1386
|
var init_min_heap = __esmMin((() => {
|
|
1028
1387
|
MinHeap = class {
|
|
@@ -1070,7 +1429,7 @@ var init_min_heap = __esmMin((() => {
|
|
|
1070
1429
|
}));
|
|
1071
1430
|
|
|
1072
1431
|
//#endregion
|
|
1073
|
-
//#region src/core/pruner.ts
|
|
1432
|
+
//#region src/core/pruner/pruner.ts
|
|
1074
1433
|
/**
|
|
1075
1434
|
* Edge-aware greedy merge with re-linking — O(N log N).
|
|
1076
1435
|
*
|
|
@@ -1184,7 +1543,7 @@ function suppressConsecutiveRuns(graph, passingIndices, normalizedScores) {
|
|
|
1184
1543
|
return result;
|
|
1185
1544
|
}
|
|
1186
1545
|
/**
|
|
1187
|
-
* Threshold-based pruning with NMS -- O(N).
|
|
1546
|
+
* Threshold-based pruning with NMS -- including normalization, O(N log N).
|
|
1188
1547
|
*
|
|
1189
1548
|
* 1. Scores are normalized to [0, 1] via percentile normalization.
|
|
1190
1549
|
* 2. Edges with normalized score >= threshold are collected.
|
|
@@ -1247,13 +1606,19 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1247
1606
|
}
|
|
1248
1607
|
return pruneTo(syntheticEdges, survivingFrames, maxCount);
|
|
1249
1608
|
}
|
|
1250
|
-
var init_pruner = __esmMin((() => {
|
|
1251
|
-
|
|
1609
|
+
var init_pruner$1 = __esmMin((() => {
|
|
1610
|
+
init_normalize_scores();
|
|
1252
1611
|
init_min_heap();
|
|
1253
1612
|
}));
|
|
1254
1613
|
|
|
1255
1614
|
//#endregion
|
|
1256
|
-
//#region src/
|
|
1615
|
+
//#region src/core/pruner/index.ts
|
|
1616
|
+
var init_pruner = __esmMin((() => {
|
|
1617
|
+
init_pruner$1();
|
|
1618
|
+
}));
|
|
1619
|
+
|
|
1620
|
+
//#endregion
|
|
1621
|
+
//#region src/core/segmenter/scheduling/concurrency.ts
|
|
1257
1622
|
/**
|
|
1258
1623
|
* Creates a concurrency limiter that runs at most `limit` tasks in parallel.
|
|
1259
1624
|
* Lightweight replacement for p-limit to avoid external dependency.
|
|
@@ -1276,7 +1641,7 @@ function concurrencyLimit(limit) {
|
|
|
1276
1641
|
var init_concurrency = __esmMin((() => {}));
|
|
1277
1642
|
|
|
1278
1643
|
//#endregion
|
|
1279
|
-
//#region src/core/segmenter.ts
|
|
1644
|
+
//#region src/core/segmenter/segmenter.ts
|
|
1280
1645
|
/**
|
|
1281
1646
|
* Determine whether segmentation should be used.
|
|
1282
1647
|
* Returns false for frames mode and GIF files.
|
|
@@ -1290,53 +1655,62 @@ function shouldSegment(resolvedOptions, originalOptions) {
|
|
|
1290
1655
|
return true;
|
|
1291
1656
|
}
|
|
1292
1657
|
/**
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1658
|
+
* Partition the global extraction grid into nonempty logical segments.
|
|
1659
|
+
* @param totalDuration Positive source duration in seconds.
|
|
1660
|
+
* @param maxSegmentDuration Positive logical segment width in seconds.
|
|
1661
|
+
* @param maxFrames Candidate budget, defensively raised to at least two.
|
|
1662
|
+
* @param fps Positive requested sampling frequency.
|
|
1663
|
+
* @returns Contiguous plan indices with grid-aligned seeks and overlap-inclusive limits.
|
|
1299
1664
|
*/
|
|
1300
1665
|
function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
|
|
1301
|
-
const
|
|
1666
|
+
const frameLimit = Math.max(2, maxFrames);
|
|
1667
|
+
const effectiveFps = Math.min(fps, frameLimit / totalDuration);
|
|
1302
1668
|
if (totalDuration <= maxSegmentDuration) return [{
|
|
1303
1669
|
index: 0,
|
|
1304
1670
|
startTime: 0,
|
|
1305
1671
|
endTime: totalDuration,
|
|
1306
1672
|
duration: totalDuration,
|
|
1307
|
-
allocatedFrames:
|
|
1673
|
+
allocatedFrames: frameLimit,
|
|
1308
1674
|
effectiveFps,
|
|
1309
1675
|
overlapBefore: 0,
|
|
1310
1676
|
overlapAfter: 0,
|
|
1311
1677
|
extractStartTime: 0,
|
|
1312
1678
|
extractDuration: totalDuration
|
|
1313
1679
|
}];
|
|
1314
|
-
const segmentCount = Math.ceil(totalDuration / maxSegmentDuration);
|
|
1315
|
-
const overlapTime = 1 / effectiveFps;
|
|
1316
1680
|
const segments = [];
|
|
1317
|
-
for (let
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1681
|
+
for (let slot = 0; slot < frameLimit; slot++) {
|
|
1682
|
+
const timestamp = slot / effectiveFps;
|
|
1683
|
+
if (timestamp >= totalDuration) break;
|
|
1684
|
+
const startTime = Math.floor(timestamp / maxSegmentDuration) * maxSegmentDuration;
|
|
1685
|
+
const previous = segments[segments.length - 1];
|
|
1686
|
+
if (previous?.startTime === startTime) {
|
|
1687
|
+
previous.allocatedFrames++;
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
const endTime = Math.min(startTime + maxSegmentDuration, totalDuration);
|
|
1325
1691
|
segments.push({
|
|
1326
|
-
index:
|
|
1692
|
+
index: segments.length,
|
|
1327
1693
|
startTime,
|
|
1328
1694
|
endTime,
|
|
1329
|
-
duration,
|
|
1330
|
-
allocatedFrames:
|
|
1695
|
+
duration: endTime - startTime,
|
|
1696
|
+
allocatedFrames: 1,
|
|
1331
1697
|
effectiveFps,
|
|
1332
|
-
overlapBefore,
|
|
1333
|
-
overlapAfter,
|
|
1334
|
-
extractStartTime,
|
|
1335
|
-
extractDuration
|
|
1698
|
+
overlapBefore: 0,
|
|
1699
|
+
overlapAfter: 0,
|
|
1700
|
+
extractStartTime: timestamp,
|
|
1701
|
+
extractDuration: 0
|
|
1336
1702
|
});
|
|
1337
1703
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1704
|
+
let firstSlot = 0;
|
|
1705
|
+
for (const segment of segments) {
|
|
1706
|
+
const nextSlot = firstSlot + segment.allocatedFrames;
|
|
1707
|
+
segment.overlapBefore = segment.index > 0 ? 1 : 0;
|
|
1708
|
+
segment.overlapAfter = segment.index < segments.length - 1 ? 1 : 0;
|
|
1709
|
+
segment.extractStartTime = (firstSlot - segment.overlapBefore) / effectiveFps;
|
|
1710
|
+
segment.extractDuration = (segment.overlapAfter ? Math.min(totalDuration, (nextSlot + 1) / effectiveFps) : totalDuration) - segment.extractStartTime;
|
|
1711
|
+
segment.allocatedFrames += segment.overlapBefore + segment.overlapAfter;
|
|
1712
|
+
firstSlot = nextSlot;
|
|
1713
|
+
}
|
|
1340
1714
|
return segments;
|
|
1341
1715
|
}
|
|
1342
1716
|
/**
|
|
@@ -1355,44 +1729,58 @@ function collectAllFrames(segmentResults) {
|
|
|
1355
1729
|
return allFrames;
|
|
1356
1730
|
}
|
|
1357
1731
|
/**
|
|
1358
|
-
* Sort frames
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1732
|
+
* Sort frames in place and alias overlap duplicates to the first survivor.
|
|
1733
|
+
* @param frames Collected entries whose segment index and local ID identify a frame.
|
|
1734
|
+
* @param effectiveFps Positive sampling frequency; half a frame interval is the threshold.
|
|
1735
|
+
* @returns Timestamp-ordered survivors and duplicate keys pointing directly to survivor keys.
|
|
1361
1736
|
*/
|
|
1362
1737
|
function deduplicateFrames(frames, effectiveFps) {
|
|
1363
1738
|
frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
|
|
1364
1739
|
const dupThreshold = 1 / (effectiveFps * 2);
|
|
1365
1740
|
const unique = [];
|
|
1741
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1366
1742
|
for (const entry of frames) {
|
|
1367
1743
|
if (unique.length > 0) {
|
|
1368
1744
|
const last = unique[unique.length - 1];
|
|
1369
|
-
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold)
|
|
1745
|
+
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
|
|
1746
|
+
aliases.set(`${entry.segmentIndex}:${entry.localId}`, `${last.segmentIndex}:${last.localId}`);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1370
1749
|
}
|
|
1371
1750
|
unique.push(entry);
|
|
1372
1751
|
}
|
|
1373
|
-
return
|
|
1752
|
+
return {
|
|
1753
|
+
unique,
|
|
1754
|
+
aliases
|
|
1755
|
+
};
|
|
1374
1756
|
}
|
|
1375
1757
|
/**
|
|
1376
|
-
* Assign sequential global IDs
|
|
1377
|
-
*
|
|
1758
|
+
* Assign sequential global IDs and retain duplicate local IDs as aliases.
|
|
1759
|
+
* @param uniqueFrames Timestamp-ordered survivors with distinct segment/local keys.
|
|
1760
|
+
* @param aliases Duplicate keys pointing directly to keys in uniqueFrames.
|
|
1761
|
+
* @returns Remapped frames and a global ID lookup covering survivors and duplicates.
|
|
1378
1762
|
*/
|
|
1379
|
-
function remapFrameIds(uniqueFrames) {
|
|
1763
|
+
function remapFrameIds(uniqueFrames, aliases) {
|
|
1380
1764
|
const globalIdMap = /* @__PURE__ */ new Map();
|
|
1765
|
+
const frames = uniqueFrames.map((entry, globalId) => {
|
|
1766
|
+
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
1767
|
+
return {
|
|
1768
|
+
id: globalId,
|
|
1769
|
+
timestamp: entry.frame.timestamp,
|
|
1770
|
+
extractPath: entry.frame.extractPath
|
|
1771
|
+
};
|
|
1772
|
+
});
|
|
1773
|
+
for (const [alias, survivor] of aliases) globalIdMap.set(alias, globalIdMap.get(survivor));
|
|
1381
1774
|
return {
|
|
1382
|
-
frames
|
|
1383
|
-
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
1384
|
-
return {
|
|
1385
|
-
id: globalId,
|
|
1386
|
-
timestamp: entry.frame.timestamp,
|
|
1387
|
-
extractPath: entry.frame.extractPath
|
|
1388
|
-
};
|
|
1389
|
-
}),
|
|
1775
|
+
frames,
|
|
1390
1776
|
globalIdMap
|
|
1391
1777
|
};
|
|
1392
1778
|
}
|
|
1393
1779
|
/**
|
|
1394
|
-
* Remap
|
|
1395
|
-
*
|
|
1780
|
+
* Remap edges, dropping missing endpoints and self loops while keeping the highest pair score.
|
|
1781
|
+
* @param segmentResults Segment-local edges in encounter order.
|
|
1782
|
+
* @param globalIdMap Survivor and duplicate local keys mapped to global IDs.
|
|
1783
|
+
* @returns One edge per surviving directed pair without changing its score.
|
|
1396
1784
|
*/
|
|
1397
1785
|
function remapEdges(segmentResults, globalIdMap) {
|
|
1398
1786
|
const edges = [];
|
|
@@ -1401,6 +1789,7 @@ function remapEdges(segmentResults, globalIdMap) {
|
|
|
1401
1789
|
const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
|
|
1402
1790
|
const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
|
|
1403
1791
|
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1792
|
+
if (newSourceId === newTargetId) continue;
|
|
1404
1793
|
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1405
1794
|
const existingIdx = edgeMap.get(edgeKey);
|
|
1406
1795
|
if (existingIdx !== void 0) {
|
|
@@ -1421,42 +1810,52 @@ function remapEdges(segmentResults, globalIdMap) {
|
|
|
1421
1810
|
return edges;
|
|
1422
1811
|
}
|
|
1423
1812
|
/**
|
|
1424
|
-
* Remap
|
|
1425
|
-
*
|
|
1813
|
+
* Remap animations, dropping missing or collapsed endpoints and retaining the first pair entry.
|
|
1814
|
+
* @param segmentResults Segment-local tracker entries in encounter order.
|
|
1815
|
+
* @param globalIdMap Survivor and duplicate local keys mapped to global IDs.
|
|
1816
|
+
* @returns One animation per directed pair with its original tracker duration and metadata.
|
|
1426
1817
|
*/
|
|
1427
1818
|
function remapAnimations(segmentResults, globalIdMap) {
|
|
1428
|
-
const animations =
|
|
1819
|
+
const animations = /* @__PURE__ */ new Map();
|
|
1429
1820
|
for (const result of segmentResults) for (const anim of result.animations) {
|
|
1430
1821
|
const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
|
|
1431
1822
|
const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
|
|
1432
1823
|
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1433
|
-
|
|
1824
|
+
if (newStartId === newEndId) continue;
|
|
1825
|
+
const animationKey = `${newStartId}-${newEndId}`;
|
|
1826
|
+
if (animations.has(animationKey)) continue;
|
|
1827
|
+
animations.set(animationKey, {
|
|
1434
1828
|
...anim,
|
|
1435
1829
|
startFrameId: newStartId,
|
|
1436
1830
|
endFrameId: newEndId
|
|
1437
1831
|
});
|
|
1438
1832
|
}
|
|
1439
|
-
return animations;
|
|
1833
|
+
return [...animations.values()];
|
|
1440
1834
|
}
|
|
1441
1835
|
/**
|
|
1442
1836
|
* Merge multiple segment results into a single unified frame/edge/animation set.
|
|
1443
|
-
*
|
|
1444
|
-
* -
|
|
1445
|
-
*
|
|
1446
|
-
* - Duplicate edges keep higher score
|
|
1837
|
+
* @param segmentResults Local frames, edges and tracker entries with distinct segment indices.
|
|
1838
|
+
* @returns Global timestamp-ordered frames, aliased edges and animations without self loops.
|
|
1839
|
+
* Duplicate edges keep the higher score; duplicate animations keep the first tracker entry.
|
|
1447
1840
|
*/
|
|
1448
1841
|
function mergeSegmentFrames(segmentResults) {
|
|
1449
1842
|
if (segmentResults.length === 0) return {
|
|
1450
1843
|
frames: [],
|
|
1451
1844
|
edges: [],
|
|
1452
|
-
animations: []
|
|
1845
|
+
animations: [],
|
|
1846
|
+
analysisResolution: {
|
|
1847
|
+
width: 0,
|
|
1848
|
+
height: 0
|
|
1849
|
+
}
|
|
1453
1850
|
};
|
|
1454
1851
|
const effectiveFps = segmentResults[0].segment.effectiveFps;
|
|
1455
|
-
const {
|
|
1852
|
+
const { unique, aliases } = deduplicateFrames(collectAllFrames(segmentResults), effectiveFps);
|
|
1853
|
+
const { frames, globalIdMap } = remapFrameIds(unique, aliases);
|
|
1456
1854
|
return {
|
|
1457
1855
|
frames,
|
|
1458
1856
|
edges: remapEdges(segmentResults, globalIdMap),
|
|
1459
|
-
animations: remapAnimations(segmentResults, globalIdMap)
|
|
1857
|
+
animations: remapAnimations(segmentResults, globalIdMap),
|
|
1858
|
+
analysisResolution: segmentResults[0].analysisResolution
|
|
1460
1859
|
};
|
|
1461
1860
|
}
|
|
1462
1861
|
function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
|
|
@@ -1467,6 +1866,7 @@ function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOpti
|
|
|
1467
1866
|
maxFrames: segment.allocatedFrames
|
|
1468
1867
|
},
|
|
1469
1868
|
workspacePath: segmentWorkspacePath,
|
|
1869
|
+
effectiveFps: segment.effectiveFps,
|
|
1470
1870
|
frames,
|
|
1471
1871
|
graph: [],
|
|
1472
1872
|
status: "ANALYZING",
|
|
@@ -1478,19 +1878,24 @@ function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOpti
|
|
|
1478
1878
|
* Each segment uses an isolated workspace directory.
|
|
1479
1879
|
*/
|
|
1480
1880
|
async function processSegment(inputPath, segment, workspacePath, resolvedOptions, onProgress) {
|
|
1481
|
-
const frames = await extractFramesForRange(inputPath, join(workspacePath, "frames"), segment.effectiveFps, resolvedOptions.scale, segment.extractStartTime, segment.extractDuration);
|
|
1881
|
+
const frames = await extractFramesForRange(inputPath, join(workspacePath, "frames"), segment.effectiveFps, resolvedOptions.scale, segment.extractStartTime, segment.extractDuration, segment.allocatedFrames);
|
|
1482
1882
|
if (frames.length < 2) return {
|
|
1483
1883
|
segment,
|
|
1484
1884
|
frames,
|
|
1485
1885
|
edges: [],
|
|
1486
|
-
animations: []
|
|
1886
|
+
animations: [],
|
|
1887
|
+
analysisResolution: {
|
|
1888
|
+
width: 0,
|
|
1889
|
+
height: 0
|
|
1890
|
+
}
|
|
1487
1891
|
};
|
|
1488
|
-
const { edges, animations } = await analyzeFrames(buildSegmentContext(segment, frames, workspacePath, resolvedOptions, onProgress));
|
|
1892
|
+
const { edges, animations, analysisResolution } = await analyzeFrames(buildSegmentContext(segment, frames, workspacePath, resolvedOptions, onProgress));
|
|
1489
1893
|
return {
|
|
1490
1894
|
segment,
|
|
1491
1895
|
frames,
|
|
1492
1896
|
edges,
|
|
1493
|
-
animations
|
|
1897
|
+
animations,
|
|
1898
|
+
analysisResolution
|
|
1494
1899
|
};
|
|
1495
1900
|
}
|
|
1496
1901
|
/**
|
|
@@ -1530,7 +1935,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1530
1935
|
});
|
|
1531
1936
|
})));
|
|
1532
1937
|
options.onProgress?.("ANALYZING", 100);
|
|
1533
|
-
const { frames, edges, animations } = mergeSegmentFrames(results);
|
|
1938
|
+
const { frames, edges, animations, analysisResolution } = mergeSegmentFrames(results);
|
|
1534
1939
|
logger.debug(`Merged: ${frames.length} frames, ${edges.length} edges, ${animations.length} animations`);
|
|
1535
1940
|
options.onProgress?.("PRUNING", 0);
|
|
1536
1941
|
const survivingIds = pruneByThresholdWithCap(edges, frames, resolvedOptions.threshold, resolvedOptions.count);
|
|
@@ -1539,6 +1944,9 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1539
1944
|
options.onProgress?.("FINALIZING", 0);
|
|
1540
1945
|
const ctx = {
|
|
1541
1946
|
options: resolvedOptions,
|
|
1947
|
+
effectiveFps: segments[0]?.effectiveFps,
|
|
1948
|
+
sourceDurationSec: totalDuration,
|
|
1949
|
+
analysisResolution,
|
|
1542
1950
|
workspacePath: mainWorkspace,
|
|
1543
1951
|
frames,
|
|
1544
1952
|
graph: edges,
|
|
@@ -1552,21 +1960,14 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1552
1960
|
else outputFiles = await finalizeOutput(ctx, prunedFrames);
|
|
1553
1961
|
options.onProgress?.("FINALIZING", 100);
|
|
1554
1962
|
logger.success(`Segmented pipeline: ${prunedFrames.length} scenes from ${frames.length} frames (${segments.length} segments)`);
|
|
1963
|
+
const outputMetadata = await buildVideoMetadata(ctx, prunedFrames, analysisResolution);
|
|
1555
1964
|
return {
|
|
1556
1965
|
success: true,
|
|
1557
1966
|
originalFramesCount: frames.length,
|
|
1558
1967
|
prunedFramesCount: prunedFrames.length,
|
|
1559
1968
|
outputFiles,
|
|
1560
1969
|
outputBuffers,
|
|
1561
|
-
|
|
1562
|
-
video: {
|
|
1563
|
-
originalDurationMs: totalDuration * 1e3,
|
|
1564
|
-
fps: resolvedOptions.fps,
|
|
1565
|
-
resolution: {
|
|
1566
|
-
width: resolvedOptions.scale,
|
|
1567
|
-
height: Math.round(resolvedOptions.scale * 9 / 16)
|
|
1568
|
-
}
|
|
1569
|
-
},
|
|
1970
|
+
...outputMetadata,
|
|
1570
1971
|
executionTimeMs: Date.now() - pipelineStart
|
|
1571
1972
|
};
|
|
1572
1973
|
} catch (error) {
|
|
@@ -1578,10 +1979,11 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1578
1979
|
else logger.debug(`Debug mode: workspace preserved at ${mainWorkspace}`);
|
|
1579
1980
|
}
|
|
1580
1981
|
}
|
|
1581
|
-
var init_segmenter = __esmMin((() => {
|
|
1982
|
+
var init_segmenter$1 = __esmMin((() => {
|
|
1582
1983
|
init_concurrency();
|
|
1583
1984
|
init_logger();
|
|
1584
1985
|
init_analyzer();
|
|
1986
|
+
init_build_video_metadata();
|
|
1585
1987
|
init_extractor();
|
|
1586
1988
|
init_input_resolver();
|
|
1587
1989
|
init_pruner();
|
|
@@ -1589,10 +1991,16 @@ var init_segmenter = __esmMin((() => {
|
|
|
1589
1991
|
}));
|
|
1590
1992
|
|
|
1591
1993
|
//#endregion
|
|
1592
|
-
//#region src/core/
|
|
1994
|
+
//#region src/core/segmenter/index.ts
|
|
1995
|
+
var init_segmenter = __esmMin((() => {
|
|
1996
|
+
init_segmenter$1();
|
|
1997
|
+
}));
|
|
1998
|
+
|
|
1999
|
+
//#endregion
|
|
2000
|
+
//#region src/core/orchestrator/orchestrator.ts
|
|
1593
2001
|
var orchestrator_exports = /* @__PURE__ */ __exportAll({ runPipeline: () => runPipeline });
|
|
1594
2002
|
async function runPipeline(options) {
|
|
1595
|
-
|
|
2003
|
+
setDebugMode(options.debug ?? false);
|
|
1596
2004
|
const resolvedOptions = resolveOptions(options);
|
|
1597
2005
|
if (shouldSegment(resolvedOptions, options)) return runSegmentedPipeline(options, resolvedOptions);
|
|
1598
2006
|
const startTime = Date.now();
|
|
@@ -1612,19 +2020,27 @@ async function runPipeline(options) {
|
|
|
1612
2020
|
logger.debug(`Workspace created: ${ctx.workspacePath}`);
|
|
1613
2021
|
ctx.status = "EXTRACTING";
|
|
1614
2022
|
const { frames: resolvedFrames, resolvedInputPath } = await resolveInput(options, ctx.workspacePath);
|
|
1615
|
-
if (resolvedOptions.mode === "frames")
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2023
|
+
if (resolvedOptions.mode === "frames") {
|
|
2024
|
+
ctx.frames = resolvedFrames;
|
|
2025
|
+
ctx.effectiveFps = 1;
|
|
2026
|
+
} else {
|
|
2027
|
+
const extractCtx = {
|
|
2028
|
+
...ctx,
|
|
2029
|
+
options: {
|
|
2030
|
+
...resolvedOptions,
|
|
2031
|
+
inputPath: resolvedInputPath
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
ctx.frames = await extractFrames(extractCtx);
|
|
2035
|
+
ctx.effectiveFps = extractCtx.effectiveFps;
|
|
2036
|
+
ctx.sourceDurationSec = extractCtx.sourceDurationSec;
|
|
2037
|
+
}
|
|
1623
2038
|
ctx.emitProgress(100);
|
|
1624
2039
|
ctx.status = "ANALYZING";
|
|
1625
|
-
const { edges, animations } = await analyzeFrames(ctx);
|
|
2040
|
+
const { edges, animations, analysisResolution } = await analyzeFrames(ctx);
|
|
1626
2041
|
ctx.graph = edges;
|
|
1627
2042
|
ctx.animations = animations;
|
|
2043
|
+
ctx.analysisResolution = analysisResolution;
|
|
1628
2044
|
ctx.status = "PRUNING";
|
|
1629
2045
|
const survivingIds = pruneByThresholdWithCap(ctx.graph, ctx.frames, resolvedOptions.threshold, resolvedOptions.count);
|
|
1630
2046
|
const prunedFrames = filter(ctx.frames, (f) => survivingIds.has(f.id));
|
|
@@ -1641,21 +2057,14 @@ async function runPipeline(options) {
|
|
|
1641
2057
|
}
|
|
1642
2058
|
ctx.status = "SUCCESS";
|
|
1643
2059
|
logger.success(`Extracted ${prunedFrames.length} scenes from ${ctx.frames.length} frames`);
|
|
2060
|
+
const metadata = await buildVideoMetadata(ctx, prunedFrames, analysisResolution);
|
|
1644
2061
|
return {
|
|
1645
2062
|
success: true,
|
|
1646
2063
|
originalFramesCount: ctx.frames.length,
|
|
1647
2064
|
prunedFramesCount: prunedFrames.length,
|
|
1648
2065
|
outputFiles,
|
|
1649
2066
|
outputBuffers,
|
|
1650
|
-
|
|
1651
|
-
video: {
|
|
1652
|
-
originalDurationMs: ctx.frames.length / ctx.options.fps * 1e3,
|
|
1653
|
-
fps: ctx.options.fps,
|
|
1654
|
-
resolution: {
|
|
1655
|
-
width: ctx.options.scale,
|
|
1656
|
-
height: Math.round(ctx.options.scale * 9 / 16)
|
|
1657
|
-
}
|
|
1658
|
-
},
|
|
2067
|
+
...metadata,
|
|
1659
2068
|
executionTimeMs: Date.now() - startTime
|
|
1660
2069
|
};
|
|
1661
2070
|
} catch (error) {
|
|
@@ -1671,6 +2080,7 @@ async function runPipeline(options) {
|
|
|
1671
2080
|
var init_orchestrator = __esmMin((() => {
|
|
1672
2081
|
init_logger();
|
|
1673
2082
|
init_analyzer();
|
|
2083
|
+
init_build_video_metadata();
|
|
1674
2084
|
init_extractor();
|
|
1675
2085
|
init_input_resolver();
|
|
1676
2086
|
init_pruner();
|
|
@@ -1679,13 +2089,16 @@ var init_orchestrator = __esmMin((() => {
|
|
|
1679
2089
|
}));
|
|
1680
2090
|
|
|
1681
2091
|
//#endregion
|
|
1682
|
-
//#region src/core/run-in-worker.ts
|
|
1683
|
-
init_orchestrator();
|
|
2092
|
+
//#region src/core/orchestrator/worker/run-in-worker.ts
|
|
1684
2093
|
/**
|
|
1685
2094
|
* Run the pipeline, choosing the best execution strategy:
|
|
1686
2095
|
*
|
|
1687
2096
|
* - Production (bundled .mjs): Worker thread — spinner never freezes
|
|
1688
2097
|
* - Dev mode (tsx .ts): Main thread — simpler, spinner may stutter during CPU work
|
|
2098
|
+
* @param options - Serializable input and pipeline settings for this run.
|
|
2099
|
+
* @param onProgress - Receives worker progress updates.
|
|
2100
|
+
* @returns The pipeline result; settlement is unchanged by later exit events.
|
|
2101
|
+
* @throws Rejects worker errors or any worker exit before a result is received.
|
|
1689
2102
|
*/
|
|
1690
2103
|
async function runPipelineInWorker(options, onProgress) {
|
|
1691
2104
|
const currentFile = fileURLToPath(import.meta.url);
|
|
@@ -1711,13 +2124,13 @@ async function runPipelineInWorker(options, onProgress) {
|
|
|
1711
2124
|
});
|
|
1712
2125
|
worker.on("error", reject);
|
|
1713
2126
|
worker.on("exit", (code) => {
|
|
1714
|
-
|
|
2127
|
+
reject(/* @__PURE__ */ new Error(`Worker exited with code ${code} without a result`));
|
|
1715
2128
|
});
|
|
1716
2129
|
});
|
|
1717
2130
|
}
|
|
1718
2131
|
|
|
1719
2132
|
//#endregion
|
|
1720
|
-
//#region src/errors.ts
|
|
2133
|
+
//#region src/cli/errors/classify-error.ts
|
|
1721
2134
|
const SieveErrorCode = {
|
|
1722
2135
|
INVALID_INPUT: "INVALID_INPUT",
|
|
1723
2136
|
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
@@ -1726,16 +2139,22 @@ const SieveErrorCode = {
|
|
|
1726
2139
|
WORKER_ERROR: "WORKER_ERROR",
|
|
1727
2140
|
UNKNOWN: "UNKNOWN"
|
|
1728
2141
|
};
|
|
2142
|
+
/**
|
|
2143
|
+
* Classify pipeline errors for structured CLI responses.
|
|
2144
|
+
* @param error - Failure with a diagnostic message and optional filesystem code.
|
|
2145
|
+
* @returns The existing error code matching the failure.
|
|
2146
|
+
*/
|
|
1729
2147
|
function classifyError(error) {
|
|
1730
2148
|
const msg = error.message.toLowerCase();
|
|
1731
|
-
if (
|
|
2149
|
+
if (msg.includes("must be")) return SieveErrorCode.INVALID_INPUT;
|
|
2150
|
+
else if (error.code === "ENOENT" || msg.includes("not found")) return SieveErrorCode.FILE_NOT_FOUND;
|
|
1732
2151
|
else if (msg.includes("no video stream") || msg.includes("invalid format")) return SieveErrorCode.INVALID_FORMAT;
|
|
1733
2152
|
else if (msg.includes("worker")) return SieveErrorCode.WORKER_ERROR;
|
|
1734
2153
|
else return SieveErrorCode.PIPELINE_ERROR;
|
|
1735
2154
|
}
|
|
1736
2155
|
|
|
1737
2156
|
//#endregion
|
|
1738
|
-
//#region src/
|
|
2157
|
+
//#region src/cli/commands/command-registry.ts
|
|
1739
2158
|
const SIEVE_COMMAND = {
|
|
1740
2159
|
name: "scene-sieve",
|
|
1741
2160
|
description: "Extract key frames from video and GIF files",
|
|
@@ -1831,28 +2250,44 @@ const SIEVE_COMMAND = {
|
|
|
1831
2250
|
};
|
|
1832
2251
|
|
|
1833
2252
|
//#endregion
|
|
1834
|
-
//#region src/
|
|
2253
|
+
//#region src/cli/options/parse-options.ts
|
|
2254
|
+
/**
|
|
2255
|
+
* Parse one complete decimal string without truncating fractional values.
|
|
2256
|
+
* @param value - Decimal CLI argument, optionally using an exponent.
|
|
2257
|
+
* @param integer - Whether the result must be an integer.
|
|
2258
|
+
* @returns The finite parsed number, or NaN for invalid input.
|
|
2259
|
+
*/
|
|
2260
|
+
function parseNumberStrict(value, integer = false) {
|
|
2261
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value)) return NaN;
|
|
2262
|
+
const number = Number(value);
|
|
2263
|
+
return Number.isFinite(number) && (!integer || Number.isInteger(number)) ? number : NaN;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Convert CLI strings to pipeline values for subsequent range validation.
|
|
2267
|
+
* @param opts - Commander options with raw numeric strings.
|
|
2268
|
+
* @returns Typed settings, preserving invalid numeric input as NaN.
|
|
2269
|
+
*/
|
|
1835
2270
|
function parsePipelineOptions(opts) {
|
|
1836
2271
|
return {
|
|
1837
|
-
...opts.threshold !== void 0 ? { threshold:
|
|
1838
|
-
...opts.count !== void 0 ? { count:
|
|
2272
|
+
...opts.threshold !== void 0 ? { threshold: parseNumberStrict(opts.threshold) } : {},
|
|
2273
|
+
...opts.count !== void 0 ? { count: parseNumberStrict(opts.count, true) } : {},
|
|
1839
2274
|
outputPath: opts.output,
|
|
1840
|
-
fps:
|
|
1841
|
-
maxFrames:
|
|
1842
|
-
scale:
|
|
1843
|
-
quality:
|
|
1844
|
-
iouThreshold: opts.iouThreshold !== void 0 ?
|
|
1845
|
-
animationThreshold: opts.animThreshold !== void 0 ?
|
|
1846
|
-
maxSegmentDuration: opts.maxSegmentDuration !== void 0 ?
|
|
1847
|
-
concurrency: opts.concurrency !== void 0 ?
|
|
2275
|
+
fps: parseNumberStrict(opts.fps),
|
|
2276
|
+
maxFrames: parseNumberStrict(opts.maxFrames, true),
|
|
2277
|
+
scale: parseNumberStrict(opts.scale, true),
|
|
2278
|
+
quality: parseNumberStrict(opts.quality, true),
|
|
2279
|
+
iouThreshold: opts.iouThreshold !== void 0 ? parseNumberStrict(opts.iouThreshold) : void 0,
|
|
2280
|
+
animationThreshold: opts.animThreshold !== void 0 ? parseNumberStrict(opts.animThreshold, true) : void 0,
|
|
2281
|
+
maxSegmentDuration: opts.maxSegmentDuration !== void 0 ? parseNumberStrict(opts.maxSegmentDuration) : void 0,
|
|
2282
|
+
concurrency: opts.concurrency !== void 0 ? parseNumberStrict(opts.concurrency, true) : void 0,
|
|
1848
2283
|
debug: opts.debug ?? false
|
|
1849
2284
|
};
|
|
1850
2285
|
}
|
|
1851
2286
|
|
|
1852
2287
|
//#endregion
|
|
1853
|
-
//#region src/commands/Sieve.tsx
|
|
1854
|
-
|
|
1855
|
-
init_workspace();
|
|
2288
|
+
//#region src/cli/commands/Sieve.tsx
|
|
2289
|
+
init_pipeline_defaults();
|
|
2290
|
+
init_orchestrator(), init_workspace$1();
|
|
1856
2291
|
init_logger();
|
|
1857
2292
|
const PHASE_DEFS = [
|
|
1858
2293
|
{
|
|
@@ -2029,8 +2464,9 @@ const SieveView = (props) => {
|
|
|
2029
2464
|
};
|
|
2030
2465
|
return next;
|
|
2031
2466
|
});
|
|
2032
|
-
|
|
2033
|
-
|
|
2467
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
2468
|
+
setError(failure.message);
|
|
2469
|
+
setTimeout(() => exit(failure), 100);
|
|
2034
2470
|
}
|
|
2035
2471
|
})();
|
|
2036
2472
|
}, []);
|