@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/index.cjs
CHANGED
|
@@ -6,6 +6,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esmMin = (fn, res, err) => () => {
|
|
10
|
+
if (err) throw err[0];
|
|
11
|
+
try {
|
|
12
|
+
return fn && (res = fn(fn = 0)), res;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
throw err = [e], e;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
9
17
|
var __copyProps = (to, from, except, desc) => {
|
|
10
18
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
19
|
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
@@ -33,66 +41,66 @@ picocolors = __toESM(picocolors, 1);
|
|
|
33
41
|
let node_module = require("node:module");
|
|
34
42
|
let sharp = require("sharp");
|
|
35
43
|
sharp = __toESM(sharp, 1);
|
|
36
|
-
let node_os = require("node:os");
|
|
37
|
-
let node_path = require("node:path");
|
|
38
44
|
let node_fs_promises = require("node:fs/promises");
|
|
45
|
+
let node_path = require("node:path");
|
|
39
46
|
let _ffprobe_installer_ffprobe = require("@ffprobe-installer/ffprobe");
|
|
40
47
|
let execa = require("execa");
|
|
41
48
|
let ffmpeg_static = require("ffmpeg-static");
|
|
42
49
|
ffmpeg_static = __toESM(ffmpeg_static, 1);
|
|
50
|
+
let node_os = require("node:os");
|
|
43
51
|
|
|
44
|
-
//#region src/
|
|
45
|
-
let debugMode = false;
|
|
46
|
-
let jsonMode = false;
|
|
52
|
+
//#region src/logging/logger.ts
|
|
47
53
|
function setDebugMode(enabled) {
|
|
48
54
|
debugMode = enabled;
|
|
49
55
|
}
|
|
50
56
|
function timestamp() {
|
|
51
57
|
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
52
58
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
var debugMode, jsonMode, logger;
|
|
60
|
+
var init_logger = __esmMin((() => {
|
|
61
|
+
debugMode = false;
|
|
62
|
+
jsonMode = false;
|
|
63
|
+
logger = {
|
|
64
|
+
info(message) {
|
|
65
|
+
if (jsonMode) process.stderr.write(`${picocolors.default.blue("info")} ${message}\n`);
|
|
66
|
+
else console.log(`${picocolors.default.blue("info")} ${message}`);
|
|
67
|
+
},
|
|
68
|
+
success(message) {
|
|
69
|
+
if (jsonMode) process.stderr.write(`${picocolors.default.green("done")} ${message}\n`);
|
|
70
|
+
else console.log(`\n${picocolors.default.green("done")} ${message}`);
|
|
71
|
+
},
|
|
72
|
+
warn(message) {
|
|
73
|
+
console.warn(`${picocolors.default.yellow("warn")} ${message}`);
|
|
74
|
+
},
|
|
75
|
+
error(message) {
|
|
76
|
+
console.error(`${picocolors.default.red("error")} ${message}`);
|
|
77
|
+
},
|
|
78
|
+
debug(message) {
|
|
79
|
+
if (debugMode) if (jsonMode) process.stderr.write(`${picocolors.default.gray(`[${timestamp()}] debug`)} ${message}\n`);
|
|
80
|
+
else console.log(`${picocolors.default.gray(`[${timestamp()}] debug`)} ${message}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}));
|
|
73
84
|
|
|
74
85
|
//#endregion
|
|
75
|
-
//#region src/constants.ts
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const TEMP_BASE_DIR = (0, node_os.tmpdir)();
|
|
82
|
-
const FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
83
|
-
const FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
|
|
84
|
-
const DBSCAN_ALPHA = .03;
|
|
85
|
-
const IOU_THRESHOLD = .9;
|
|
86
|
-
const DECAY_LAMBDA = .95;
|
|
87
|
-
const MATCH_DISTANCE_THRESHOLD = .25;
|
|
88
|
-
function getTempWorkspaceDir(sessionId) {
|
|
89
|
-
return (0, node_path.join)(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
90
|
-
}
|
|
86
|
+
//#region src/constants/pipeline-defaults.ts
|
|
87
|
+
var DEFAULT_THRESHOLD, IOU_THRESHOLD;
|
|
88
|
+
var init_pipeline_defaults = __esmMin((() => {
|
|
89
|
+
DEFAULT_THRESHOLD = .5;
|
|
90
|
+
IOU_THRESHOLD = .9;
|
|
91
|
+
}));
|
|
91
92
|
|
|
92
93
|
//#endregion
|
|
93
|
-
//#region src/core/
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
//#region src/core/analyzer/constants/vision-tuning.ts
|
|
95
|
+
var DBSCAN_ALPHA, DECAY_LAMBDA, MATCH_DISTANCE_THRESHOLD;
|
|
96
|
+
var init_vision_tuning = __esmMin((() => {
|
|
97
|
+
DBSCAN_ALPHA = .03;
|
|
98
|
+
DECAY_LAMBDA = .95;
|
|
99
|
+
MATCH_DISTANCE_THRESHOLD = .25;
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/core/analyzer/clustering/dbscan.ts
|
|
96
104
|
/**
|
|
97
105
|
* DBSCAN clustering with resolution-independent eps.
|
|
98
106
|
* eps = alpha * sqrt(width^2 + height^2)
|
|
@@ -168,12 +176,118 @@ function findNeighbors(points, idx, epsSquared) {
|
|
|
168
176
|
}
|
|
169
177
|
return neighbors;
|
|
170
178
|
}
|
|
179
|
+
var UNVISITED, NOISE;
|
|
180
|
+
var init_dbscan = __esmMin((() => {
|
|
181
|
+
init_vision_tuning();
|
|
182
|
+
UNVISITED = -2;
|
|
183
|
+
NOISE = -1;
|
|
184
|
+
}));
|
|
171
185
|
|
|
172
186
|
//#endregion
|
|
173
|
-
//#region src/core/analyzer.ts
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
//#region src/core/analyzer/features/feature-diff.ts
|
|
188
|
+
/**
|
|
189
|
+
* Match prev to next with Hamming k=2, crossCheck=false and strict ratio 0.25.
|
|
190
|
+
* @param cvLib - Initialized OpenCV runtime.
|
|
191
|
+
* @param prev - Previous frame's live features, owned by the caller.
|
|
192
|
+
* @param next - Next frame's live features, owned by the caller.
|
|
193
|
+
* @returns Unmatched next-frame coordinates without changing input ownership.
|
|
194
|
+
* @throws Propagates matching errors after releasing temporary native handles.
|
|
195
|
+
*/
|
|
196
|
+
function computeNewPoints(cvLib, prev, next) {
|
|
197
|
+
let matcher = null;
|
|
198
|
+
let matches = null;
|
|
199
|
+
try {
|
|
200
|
+
const matchedIndices = /* @__PURE__ */ new Set();
|
|
201
|
+
if (prev.descriptors.rows > 0 && next.descriptors.rows > 0) try {
|
|
202
|
+
matcher = new cvLib.BFMatcher(cvLib.NORM_HAMMING, false);
|
|
203
|
+
matches = new cvLib.DMatchVectorVector();
|
|
204
|
+
matcher.knnMatch(prev.descriptors, next.descriptors, matches, 2);
|
|
205
|
+
for (let i = 0; i < matches.size(); i++) {
|
|
206
|
+
const pair = matches.get(i);
|
|
207
|
+
try {
|
|
208
|
+
if (pair.size() < 2) continue;
|
|
209
|
+
const best = pair.get(0);
|
|
210
|
+
const second = pair.get(1);
|
|
211
|
+
if (best.distance < .25 * second.distance) matchedIndices.add(best.trainIdx);
|
|
212
|
+
} finally {
|
|
213
|
+
pair.delete();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
matcher?.delete();
|
|
218
|
+
}
|
|
219
|
+
const points = [];
|
|
220
|
+
for (let i = 0; i < next.keypoints.size(); i++) if (!matchedIndices.has(i)) {
|
|
221
|
+
const { x, y } = next.keypoints.get(i).pt;
|
|
222
|
+
points.push({
|
|
223
|
+
x,
|
|
224
|
+
y
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return points;
|
|
228
|
+
} finally {
|
|
229
|
+
matches?.delete();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var init_feature_diff = __esmMin((() => {
|
|
233
|
+
init_vision_tuning();
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/core/analyzer/features/frame-features.ts
|
|
238
|
+
/**
|
|
239
|
+
* Detect one frame's features without retaining its image or mask.
|
|
240
|
+
* @param cvLib - Initialized OpenCV runtime.
|
|
241
|
+
* @param akaze - Detector owned and released by the caller.
|
|
242
|
+
* @param frame - Grayscale bytes with matching width and height.
|
|
243
|
+
* @returns Feature handles that the caller must delete.
|
|
244
|
+
* @throws Propagates native errors after releasing partial allocations.
|
|
245
|
+
*/
|
|
246
|
+
function computeFrameFeatures(cvLib, akaze, frame) {
|
|
247
|
+
let image = null;
|
|
248
|
+
let mask = null;
|
|
249
|
+
let keypoints = null;
|
|
250
|
+
let descriptors = null;
|
|
251
|
+
try {
|
|
252
|
+
image = new cvLib.Mat(frame.height, frame.width, cvLib.CV_8UC1);
|
|
253
|
+
image.data.set(frame.data);
|
|
254
|
+
mask = new cvLib.Mat();
|
|
255
|
+
keypoints = new cvLib.KeyPointVector();
|
|
256
|
+
descriptors = new cvLib.Mat();
|
|
257
|
+
akaze.detectAndCompute(image, mask, keypoints, descriptors);
|
|
258
|
+
const ownedKeypoints = keypoints;
|
|
259
|
+
const ownedDescriptors = descriptors;
|
|
260
|
+
let deleted = false;
|
|
261
|
+
const features = {
|
|
262
|
+
width: frame.width,
|
|
263
|
+
height: frame.height,
|
|
264
|
+
keypoints: ownedKeypoints,
|
|
265
|
+
descriptors: ownedDescriptors,
|
|
266
|
+
/** Release the transferred handles exactly once. */
|
|
267
|
+
delete() {
|
|
268
|
+
if (deleted) return;
|
|
269
|
+
deleted = true;
|
|
270
|
+
try {
|
|
271
|
+
ownedKeypoints.delete();
|
|
272
|
+
} finally {
|
|
273
|
+
ownedDescriptors.delete();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
keypoints = null;
|
|
278
|
+
descriptors = null;
|
|
279
|
+
return features;
|
|
280
|
+
} finally {
|
|
281
|
+
image?.delete();
|
|
282
|
+
mask?.delete();
|
|
283
|
+
keypoints?.delete();
|
|
284
|
+
descriptors?.delete();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
var init_frame_features = __esmMin((() => {}));
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/core/analyzer/analyzer.ts
|
|
177
291
|
async function ensureOpenCV() {
|
|
178
292
|
if (!cvReady) cvReady = (async () => {
|
|
179
293
|
const cvObj = require$1("@techstark/opencv-js");
|
|
@@ -212,158 +326,6 @@ function computeIoU(a, b) {
|
|
|
212
326
|
const union = a.width * a.height + b.width * b.height - intersection;
|
|
213
327
|
return union === 0 ? 0 : intersection / union;
|
|
214
328
|
}
|
|
215
|
-
var IoUTracker = class {
|
|
216
|
-
fps;
|
|
217
|
-
iouThreshold;
|
|
218
|
-
animationThreshold;
|
|
219
|
-
regions = [];
|
|
220
|
-
extractedAnimations = [];
|
|
221
|
-
constructor(fps = 5, iouThreshold = IOU_THRESHOLD, animationThreshold = 5) {
|
|
222
|
-
this.fps = fps;
|
|
223
|
-
this.iouThreshold = iouThreshold;
|
|
224
|
-
this.animationThreshold = animationThreshold;
|
|
225
|
-
}
|
|
226
|
-
update(boxes, pairIndex) {
|
|
227
|
-
const animationIndices = /* @__PURE__ */ new Set();
|
|
228
|
-
const matched = /* @__PURE__ */ new Set();
|
|
229
|
-
for (let bi = 0; bi < boxes.length; bi++) {
|
|
230
|
-
const box = boxes[bi];
|
|
231
|
-
let bestIoU = 0;
|
|
232
|
-
let bestRegionIdx = -1;
|
|
233
|
-
for (let ri = 0; ri < this.regions.length; ri++) {
|
|
234
|
-
if (matched.has(ri)) continue;
|
|
235
|
-
const iou = computeIoU(box, this.regions[ri].box);
|
|
236
|
-
if (iou > bestIoU) {
|
|
237
|
-
bestIoU = iou;
|
|
238
|
-
bestRegionIdx = ri;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (bestIoU > this.iouThreshold && bestRegionIdx !== -1) {
|
|
242
|
-
const region = this.regions[bestRegionIdx];
|
|
243
|
-
const gap = pairIndex - region.lastSeen;
|
|
244
|
-
region.box = box;
|
|
245
|
-
region.consecutiveCount++;
|
|
246
|
-
region.lastSeen = pairIndex;
|
|
247
|
-
region.weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
248
|
-
matched.add(bestRegionIdx);
|
|
249
|
-
if (region.consecutiveCount >= this.animationThreshold) animationIndices.add(bi);
|
|
250
|
-
} else this.regions.push({
|
|
251
|
-
box,
|
|
252
|
-
consecutiveCount: 1,
|
|
253
|
-
firstSeen: pairIndex,
|
|
254
|
-
lastSeen: pairIndex,
|
|
255
|
-
weight: 1
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
for (let ri = 0; ri < this.regions.length; ri++) if (!matched.has(ri)) {
|
|
259
|
-
const gap = pairIndex - this.regions[ri].lastSeen;
|
|
260
|
-
this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
261
|
-
}
|
|
262
|
-
for (let i = 0; i < this.regions.length; i++) {
|
|
263
|
-
const region = this.regions[i];
|
|
264
|
-
if (region.weight <= .01 && !matched.has(i)) this.collectAnimation(region);
|
|
265
|
-
}
|
|
266
|
-
this.regions = (0, _winglet_common_utils.filter)(this.regions, (r, i) => r.weight > .01 || matched.has(i));
|
|
267
|
-
return animationIndices;
|
|
268
|
-
}
|
|
269
|
-
collectAnimation(region) {
|
|
270
|
-
if (region.consecutiveCount >= this.animationThreshold) {
|
|
271
|
-
const durationMs = region.consecutiveCount / this.fps * 1e3;
|
|
272
|
-
this.extractedAnimations.push({
|
|
273
|
-
type: "loading_spinner",
|
|
274
|
-
boundingBox: region.box,
|
|
275
|
-
startFrameId: region.firstSeen,
|
|
276
|
-
endFrameId: region.lastSeen,
|
|
277
|
-
durationMs
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
flushAndGetAnimations() {
|
|
282
|
-
for (const region of this.regions) this.collectAnimation(region);
|
|
283
|
-
this.regions = [];
|
|
284
|
-
return this.extractedAnimations;
|
|
285
|
-
}
|
|
286
|
-
getAnimationWeight(boxIndex, boxes) {
|
|
287
|
-
if (boxIndex >= boxes.length) return 0;
|
|
288
|
-
const box = boxes[boxIndex];
|
|
289
|
-
let maxWeight = 0;
|
|
290
|
-
for (const region of this.regions) if (region.consecutiveCount >= this.animationThreshold) {
|
|
291
|
-
if (computeIoU(box, region.box) > this.iouThreshold) maxWeight = Math.max(maxWeight, region.weight);
|
|
292
|
-
}
|
|
293
|
-
return maxWeight;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
297
|
-
const cv = cvLib;
|
|
298
|
-
const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
299
|
-
mat1.data.set(frame1.data);
|
|
300
|
-
const mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
301
|
-
mat2.data.set(frame2.data);
|
|
302
|
-
const kp1 = new cvLib.KeyPointVector();
|
|
303
|
-
const kp2 = new cvLib.KeyPointVector();
|
|
304
|
-
const desc1 = new cvLib.Mat();
|
|
305
|
-
const desc2 = new cvLib.Mat();
|
|
306
|
-
const mask1 = new cvLib.Mat();
|
|
307
|
-
const mask2 = new cvLib.Mat();
|
|
308
|
-
const akaze = new cvLib.AKAZE();
|
|
309
|
-
let matches = null;
|
|
310
|
-
try {
|
|
311
|
-
akaze.detectAndCompute(mat1, mask1, kp1, desc1);
|
|
312
|
-
akaze.detectAndCompute(mat2, mask2, kp2, desc2);
|
|
313
|
-
const matchedKp1Indices = /* @__PURE__ */ new Set();
|
|
314
|
-
const matchedKp2Indices = /* @__PURE__ */ new Set();
|
|
315
|
-
if (desc1.rows > 0 && desc2.rows > 0) {
|
|
316
|
-
const matcher = new cvLib.BFMatcher(cvLib.NORM_HAMMING, false);
|
|
317
|
-
try {
|
|
318
|
-
matches = new cvLib.DMatchVectorVector();
|
|
319
|
-
matcher.knnMatch(desc1, desc2, matches, 2);
|
|
320
|
-
for (let i = 0; i < matches.size(); i++) {
|
|
321
|
-
const pair = matches.get(i);
|
|
322
|
-
if (pair.size() < 2) continue;
|
|
323
|
-
const m0 = pair.get(0);
|
|
324
|
-
const m1 = pair.get(1);
|
|
325
|
-
if (m0.distance < .25 * m1.distance) {
|
|
326
|
-
matchedKp1Indices.add(m0.queryIdx);
|
|
327
|
-
matchedKp2Indices.add(m0.trainIdx);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
} finally {
|
|
331
|
-
matcher.delete();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const sNew = [];
|
|
335
|
-
for (let i = 0; i < kp2.size(); i++) if (!matchedKp2Indices.has(i)) {
|
|
336
|
-
const pt = kp2.get(i).pt;
|
|
337
|
-
sNew.push({
|
|
338
|
-
x: pt.x,
|
|
339
|
-
y: pt.y
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
const sLoss = [];
|
|
343
|
-
for (let i = 0; i < kp1.size(); i++) if (!matchedKp1Indices.has(i)) {
|
|
344
|
-
const pt = kp1.get(i).pt;
|
|
345
|
-
sLoss.push({
|
|
346
|
-
x: pt.x,
|
|
347
|
-
y: pt.y
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
return {
|
|
351
|
-
sNew,
|
|
352
|
-
sLoss
|
|
353
|
-
};
|
|
354
|
-
} finally {
|
|
355
|
-
mat1.delete();
|
|
356
|
-
mat2.delete();
|
|
357
|
-
kp1.delete();
|
|
358
|
-
kp2.delete();
|
|
359
|
-
desc1.delete();
|
|
360
|
-
desc2.delete();
|
|
361
|
-
mask1.delete();
|
|
362
|
-
mask2.delete();
|
|
363
|
-
akaze.delete();
|
|
364
|
-
if (matches) matches.delete();
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
329
|
/**
|
|
368
330
|
* Pixel-level difference fallback for AKAZE blind spots.
|
|
369
331
|
*
|
|
@@ -378,17 +340,30 @@ async function computeAKAZEDiff(cvLib, frame1, frame2) {
|
|
|
378
340
|
* 3. threshold → binary mask of significant changes
|
|
379
341
|
* 4. findContours → bounding rects of changed regions
|
|
380
342
|
* 5. Grid sampling within each bounding rect → Point2D[]
|
|
343
|
+
*
|
|
344
|
+
* @param cvLib - Initialized OpenCV runtime shared by the analyzer.
|
|
345
|
+
* @param frame1 - Previous grayscale frame, with the same dimensions as frame2.
|
|
346
|
+
* @param frame2 - Next grayscale frame, with the same dimensions as frame1.
|
|
347
|
+
* @returns Grid-sampled points from changed regions.
|
|
348
|
+
* @throws Propagates allocation or OpenCV errors after releasing acquired handles.
|
|
381
349
|
*/
|
|
382
350
|
function computePixelDiff(cvLib, frame1, frame2) {
|
|
383
351
|
const cv = cvLib;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
352
|
+
let mat1 = null;
|
|
353
|
+
let mat2 = null;
|
|
354
|
+
let diff = null;
|
|
355
|
+
let blurred = null;
|
|
356
|
+
let binary = null;
|
|
357
|
+
let contours = null;
|
|
358
|
+
let hierarchy = null;
|
|
391
359
|
try {
|
|
360
|
+
mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
|
|
361
|
+
mat2 = new cv.Mat(frame2.height, frame2.width, cv.CV_8UC1);
|
|
362
|
+
diff = new cv.Mat();
|
|
363
|
+
blurred = new cv.Mat();
|
|
364
|
+
binary = new cv.Mat();
|
|
365
|
+
contours = new cv.MatVector();
|
|
366
|
+
hierarchy = new cv.Mat();
|
|
392
367
|
mat1.data.set(frame1.data);
|
|
393
368
|
mat2.data.set(frame2.data);
|
|
394
369
|
cv.absdiff(mat1, mat2, diff);
|
|
@@ -399,22 +374,26 @@ function computePixelDiff(cvLib, frame1, frame2) {
|
|
|
399
374
|
const points = [];
|
|
400
375
|
for (let c = 0; c < contours.size(); c++) {
|
|
401
376
|
const contour = contours.get(c);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
x
|
|
406
|
-
|
|
407
|
-
|
|
377
|
+
try {
|
|
378
|
+
const rect = cv.boundingRect(contour);
|
|
379
|
+
if (rect.width * rect.height < 100) continue;
|
|
380
|
+
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({
|
|
381
|
+
x,
|
|
382
|
+
y
|
|
383
|
+
});
|
|
384
|
+
} finally {
|
|
385
|
+
contour.delete();
|
|
386
|
+
}
|
|
408
387
|
}
|
|
409
388
|
return points;
|
|
410
389
|
} finally {
|
|
411
|
-
mat1
|
|
412
|
-
mat2
|
|
413
|
-
diff
|
|
414
|
-
blurred
|
|
415
|
-
binary
|
|
416
|
-
contours
|
|
417
|
-
hierarchy
|
|
390
|
+
mat1?.delete();
|
|
391
|
+
mat2?.delete();
|
|
392
|
+
diff?.delete();
|
|
393
|
+
blurred?.delete();
|
|
394
|
+
binary?.delete();
|
|
395
|
+
contours?.delete();
|
|
396
|
+
hierarchy?.delete();
|
|
418
397
|
}
|
|
419
398
|
}
|
|
420
399
|
function computeInformationGain(clusters, clusterPoints, imageArea, animationIndices, animationWeights) {
|
|
@@ -433,47 +412,85 @@ function computeInformationGain(clusters, clusterPoints, imageArea, animationInd
|
|
|
433
412
|
}
|
|
434
413
|
return gain;
|
|
435
414
|
}
|
|
436
|
-
|
|
415
|
+
/**
|
|
416
|
+
* Analyze one batch, retaining its boundary frame for the following batch.
|
|
417
|
+
* @param cvLib - Initialized OpenCV runtime.
|
|
418
|
+
* @param akaze - Detector owned by analyzeFrames.
|
|
419
|
+
* @param frames - Boundary frame followed by new adjacent frames.
|
|
420
|
+
* @param carry - Boundary bytes and live features transferred from the previous batch.
|
|
421
|
+
* @param scale - Maximum preprocessing width.
|
|
422
|
+
* @param tracker - Stateful animation tracker shared across batches.
|
|
423
|
+
* @param pairOffset - Global position of this batch's first pair.
|
|
424
|
+
* @returns Scores, pair failure count, and ownership of the final frame's features.
|
|
425
|
+
*/
|
|
426
|
+
async function analyzeBatch(cvLib, akaze, frames, carry, scale, tracker, pairOffset) {
|
|
437
427
|
const edges = [];
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
428
|
+
let failures = 0;
|
|
429
|
+
let prev = carry?.features ?? null;
|
|
430
|
+
let next = null;
|
|
431
|
+
try {
|
|
432
|
+
const preprocessed = await Promise.all((0, _winglet_common_utils.map)(frames, (f, index) => index === 0 && carry ? carry.preprocessed : preprocessFrame(f.extractPath, scale)));
|
|
433
|
+
const imageWidth = preprocessed[0]?.width ?? scale;
|
|
434
|
+
const imageHeight = preprocessed[0]?.height ?? Math.round(scale * 9 / 16);
|
|
435
|
+
const imageArea = imageWidth * imageHeight;
|
|
436
|
+
for (let i = 0; i < frames.length - 1; i++) {
|
|
437
|
+
const pairIndex = pairOffset + i;
|
|
438
|
+
try {
|
|
439
|
+
prev ??= computeFrameFeatures(cvLib, akaze, preprocessed[i]);
|
|
440
|
+
next = computeFrameFeatures(cvLib, akaze, preprocessed[i + 1]);
|
|
441
|
+
let dbscanResult = dbscan(computeNewPoints(cvLib, prev, next), imageWidth, imageHeight);
|
|
442
|
+
let clusters = dbscanResult.boundingBoxes;
|
|
443
|
+
if (clusters.length === 0) {
|
|
444
|
+
const pixelDiffPoints = computePixelDiff(cvLib, preprocessed[i], preprocessed[i + 1]);
|
|
445
|
+
if (pixelDiffPoints.length > 0) {
|
|
446
|
+
logger.debug(`Edge ${frames[i].id}->${frames[i + 1].id}: pixel-diff fallback (${pixelDiffPoints.length} points)`);
|
|
447
|
+
dbscanResult = dbscan(pixelDiffPoints, imageWidth, imageHeight, void 0, 2);
|
|
448
|
+
clusters = dbscanResult.boundingBoxes;
|
|
449
|
+
}
|
|
454
450
|
}
|
|
451
|
+
const clusterPointCounts = new Array(clusters.length).fill(0);
|
|
452
|
+
for (const label of dbscanResult.labels) if (label >= 0) clusterPointCounts[label]++;
|
|
453
|
+
const animationIndices = tracker.update(clusters, pairIndex);
|
|
454
|
+
const animationWeights = (0, _winglet_common_utils.map)(clusters, (_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0);
|
|
455
|
+
const score = computeInformationGain(clusters, clusterPointCounts, imageArea, animationIndices, animationWeights);
|
|
456
|
+
logger.debug(`Edge ${frames[i].id}->${frames[i + 1].id} G(t)=${score.toFixed(6)}`);
|
|
457
|
+
edges.push({
|
|
458
|
+
sourceId: frames[i].id,
|
|
459
|
+
targetId: frames[i + 1].id,
|
|
460
|
+
score
|
|
461
|
+
});
|
|
462
|
+
} catch (err) {
|
|
463
|
+
logger.warn(`Frame pair analysis failed: ${String(err)}`);
|
|
464
|
+
failures++;
|
|
465
|
+
edges.push({
|
|
466
|
+
sourceId: frames[i].id,
|
|
467
|
+
targetId: frames[i + 1].id,
|
|
468
|
+
score: 0
|
|
469
|
+
});
|
|
470
|
+
} finally {
|
|
471
|
+
prev?.delete();
|
|
472
|
+
prev = next;
|
|
473
|
+
next = null;
|
|
455
474
|
}
|
|
456
|
-
const clusterPointCounts = new Array(clusters.length).fill(0);
|
|
457
|
-
for (const label of dbscanResult.labels) if (label >= 0) clusterPointCounts[label]++;
|
|
458
|
-
const animationIndices = tracker.update(clusters, pairIndex);
|
|
459
|
-
const animationWeights = (0, _winglet_common_utils.map)(clusters, (_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0);
|
|
460
|
-
const score = computeInformationGain(clusters, clusterPointCounts, imageArea, animationIndices, animationWeights);
|
|
461
|
-
logger.debug(`Edge ${frames[i].id}->${frames[i + 1].id} G(t)=${score.toFixed(6)}`);
|
|
462
|
-
edges.push({
|
|
463
|
-
sourceId: frames[i].id,
|
|
464
|
-
targetId: frames[i + 1].id,
|
|
465
|
-
score
|
|
466
|
-
});
|
|
467
|
-
} catch (err) {
|
|
468
|
-
logger.debug(`Frame pair analysis failed: ${String(err)}`);
|
|
469
|
-
edges.push({
|
|
470
|
-
sourceId: frames[i].id,
|
|
471
|
-
targetId: frames[i + 1].id,
|
|
472
|
-
score: 0
|
|
473
|
-
});
|
|
474
475
|
}
|
|
476
|
+
const result = {
|
|
477
|
+
edges,
|
|
478
|
+
failures,
|
|
479
|
+
analysisResolution: {
|
|
480
|
+
width: imageWidth,
|
|
481
|
+
height: imageHeight
|
|
482
|
+
},
|
|
483
|
+
carry: prev ? {
|
|
484
|
+
preprocessed: preprocessed[preprocessed.length - 1],
|
|
485
|
+
features: prev
|
|
486
|
+
} : null
|
|
487
|
+
};
|
|
488
|
+
prev = null;
|
|
489
|
+
return result;
|
|
490
|
+
} finally {
|
|
491
|
+
prev?.delete();
|
|
492
|
+
next?.delete();
|
|
475
493
|
}
|
|
476
|
-
return edges;
|
|
477
494
|
}
|
|
478
495
|
/**
|
|
479
496
|
* Analyze adjacent frame pairs to compute information gain scores (G(t)).
|
|
@@ -484,35 +501,222 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
484
501
|
* 2. DBSCAN Spatial Clustering
|
|
485
502
|
* 3. Spatio-temporal IoU Tracking
|
|
486
503
|
* 4. G(t) Information Gain Scoring
|
|
504
|
+
* @param ctx - Frames, analysis options, and the progress callback for this run.
|
|
505
|
+
* @returns Adjacent scores and tracked animations in analysis coordinates.
|
|
506
|
+
* @throws Propagates runtime errors and rejects total failure of two or more pairs after cleanup.
|
|
487
507
|
*/
|
|
488
508
|
async function analyzeFrames(ctx) {
|
|
489
509
|
const { frames } = ctx;
|
|
490
510
|
if (frames.length < 2) return {
|
|
491
511
|
edges: [],
|
|
492
|
-
animations: []
|
|
512
|
+
animations: [],
|
|
513
|
+
analysisResolution: {
|
|
514
|
+
width: 0,
|
|
515
|
+
height: 0
|
|
516
|
+
}
|
|
493
517
|
};
|
|
494
518
|
logger.debug(`Analyzing ${frames.length} frames in batches of ${10}`);
|
|
495
519
|
const cvLib = await ensureOpenCV();
|
|
496
520
|
const edges = [];
|
|
497
|
-
const tracker = new IoUTracker(ctx.options.fps, ctx.options.iouThreshold, ctx.options.animationThreshold);
|
|
521
|
+
const tracker = new IoUTracker(ctx.effectiveFps ?? ctx.options.fps, ctx.options.iouThreshold, ctx.options.animationThreshold);
|
|
498
522
|
const scale = ctx.options.scale;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
523
|
+
let akaze = null;
|
|
524
|
+
let carry = null;
|
|
525
|
+
let analysisResolution = {
|
|
526
|
+
width: 0,
|
|
527
|
+
height: 0
|
|
528
|
+
};
|
|
529
|
+
let failures = 0;
|
|
530
|
+
try {
|
|
531
|
+
akaze = new cvLib.AKAZE();
|
|
532
|
+
for (let i = 0; i < frames.length - 1; i += 10) {
|
|
533
|
+
const batch = [frames[i], ...frames.slice(i + 1, i + 1 + 10)];
|
|
534
|
+
const result = await analyzeBatch(cvLib, akaze, batch, carry, scale, tracker, i);
|
|
535
|
+
carry = result.carry;
|
|
536
|
+
failures += result.failures;
|
|
537
|
+
if (i === 0) analysisResolution = result.analysisResolution;
|
|
538
|
+
edges.push(...result.edges);
|
|
539
|
+
const progress = Math.min(100, (i + 10) / (frames.length - 1) * 100);
|
|
540
|
+
ctx.emitProgress(progress);
|
|
541
|
+
}
|
|
542
|
+
} finally {
|
|
543
|
+
carry?.features.delete();
|
|
544
|
+
akaze?.delete();
|
|
505
545
|
}
|
|
546
|
+
const pairs = frames.length - 1;
|
|
547
|
+
if (pairs >= 2 && failures === pairs) throw new Error(`All ${pairs} frame pairs failed analysis`);
|
|
506
548
|
const animations = tracker.flushAndGetAnimations();
|
|
507
549
|
logger.debug(`Computed ${edges.length} score edges and ${animations.length} animations`);
|
|
508
550
|
return {
|
|
509
551
|
edges,
|
|
510
|
-
animations
|
|
552
|
+
animations,
|
|
553
|
+
analysisResolution
|
|
511
554
|
};
|
|
512
555
|
}
|
|
556
|
+
var OPENCV_INIT_TIMEOUT_MS, require$1, cvReady, IoUTracker;
|
|
557
|
+
var init_analyzer$1 = __esmMin((() => {
|
|
558
|
+
init_pipeline_defaults();
|
|
559
|
+
init_vision_tuning();
|
|
560
|
+
init_logger();
|
|
561
|
+
init_dbscan();
|
|
562
|
+
init_feature_diff();
|
|
563
|
+
init_frame_features();
|
|
564
|
+
OPENCV_INIT_TIMEOUT_MS = 3e4;
|
|
565
|
+
require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
566
|
+
cvReady = null;
|
|
567
|
+
IoUTracker = class {
|
|
568
|
+
fps;
|
|
569
|
+
iouThreshold;
|
|
570
|
+
animationThreshold;
|
|
571
|
+
regions = [];
|
|
572
|
+
extractedAnimations = [];
|
|
573
|
+
constructor(fps = 5, iouThreshold = IOU_THRESHOLD, animationThreshold = 5) {
|
|
574
|
+
this.fps = fps;
|
|
575
|
+
this.iouThreshold = iouThreshold;
|
|
576
|
+
this.animationThreshold = animationThreshold;
|
|
577
|
+
}
|
|
578
|
+
update(boxes, pairIndex) {
|
|
579
|
+
const animationIndices = /* @__PURE__ */ new Set();
|
|
580
|
+
const matched = /* @__PURE__ */ new Set();
|
|
581
|
+
for (let bi = 0; bi < boxes.length; bi++) {
|
|
582
|
+
const box = boxes[bi];
|
|
583
|
+
let bestIoU = 0;
|
|
584
|
+
let bestRegionIdx = -1;
|
|
585
|
+
for (let ri = 0; ri < this.regions.length; ri++) {
|
|
586
|
+
if (matched.has(ri)) continue;
|
|
587
|
+
const iou = computeIoU(box, this.regions[ri].box);
|
|
588
|
+
if (iou > bestIoU) {
|
|
589
|
+
bestIoU = iou;
|
|
590
|
+
bestRegionIdx = ri;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (bestIoU > this.iouThreshold && bestRegionIdx !== -1) {
|
|
594
|
+
const region = this.regions[bestRegionIdx];
|
|
595
|
+
const gap = pairIndex - region.lastSeen;
|
|
596
|
+
region.box = box;
|
|
597
|
+
region.consecutiveCount++;
|
|
598
|
+
region.lastSeen = pairIndex;
|
|
599
|
+
region.weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
600
|
+
matched.add(bestRegionIdx);
|
|
601
|
+
if (region.consecutiveCount >= this.animationThreshold) animationIndices.add(bi);
|
|
602
|
+
} else this.regions.push({
|
|
603
|
+
box,
|
|
604
|
+
consecutiveCount: 1,
|
|
605
|
+
firstSeen: pairIndex,
|
|
606
|
+
lastSeen: pairIndex,
|
|
607
|
+
weight: 1
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
for (let ri = 0; ri < this.regions.length; ri++) if (!matched.has(ri)) {
|
|
611
|
+
const gap = pairIndex - this.regions[ri].lastSeen;
|
|
612
|
+
this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
|
|
613
|
+
}
|
|
614
|
+
for (let i = 0; i < this.regions.length; i++) {
|
|
615
|
+
const region = this.regions[i];
|
|
616
|
+
if (region.weight <= .01 && !matched.has(i)) this.collectAnimation(region);
|
|
617
|
+
}
|
|
618
|
+
this.regions = (0, _winglet_common_utils.filter)(this.regions, (r, i) => r.weight > .01 || matched.has(i));
|
|
619
|
+
return animationIndices;
|
|
620
|
+
}
|
|
621
|
+
collectAnimation(region) {
|
|
622
|
+
if (region.consecutiveCount >= this.animationThreshold) {
|
|
623
|
+
const durationMs = region.consecutiveCount / this.fps * 1e3;
|
|
624
|
+
this.extractedAnimations.push({
|
|
625
|
+
type: "loading_spinner",
|
|
626
|
+
boundingBox: region.box,
|
|
627
|
+
startFrameId: region.firstSeen,
|
|
628
|
+
endFrameId: region.lastSeen,
|
|
629
|
+
durationMs
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
flushAndGetAnimations() {
|
|
634
|
+
for (const region of this.regions) this.collectAnimation(region);
|
|
635
|
+
this.regions = [];
|
|
636
|
+
return this.extractedAnimations;
|
|
637
|
+
}
|
|
638
|
+
getAnimationWeight(boxIndex, boxes) {
|
|
639
|
+
if (boxIndex >= boxes.length) return 0;
|
|
640
|
+
const box = boxes[boxIndex];
|
|
641
|
+
let maxWeight = 0;
|
|
642
|
+
for (const region of this.regions) if (region.consecutiveCount >= this.animationThreshold) {
|
|
643
|
+
if (computeIoU(box, region.box) > this.iouThreshold) maxWeight = Math.max(maxWeight, region.weight);
|
|
644
|
+
}
|
|
645
|
+
return maxWeight;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}));
|
|
649
|
+
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/core/analyzer/index.ts
|
|
652
|
+
var init_analyzer = __esmMin((() => {
|
|
653
|
+
init_analyzer$1();
|
|
654
|
+
init_dbscan();
|
|
655
|
+
}));
|
|
513
656
|
|
|
514
657
|
//#endregion
|
|
515
|
-
//#region src/utils/
|
|
658
|
+
//#region src/core/utils/metadata/build-video-metadata.ts
|
|
659
|
+
/**
|
|
660
|
+
* Read output dimensions and build consistent video and animation metadata.
|
|
661
|
+
* JPEG finalization does not resize, so the source dimensions match the output.
|
|
662
|
+
* @param ctx Pipeline state with source duration, effective FPS and analysis-space animations.
|
|
663
|
+
* @param selected Selected frames in output order; the first candidate is the fallback.
|
|
664
|
+
* @param analysisResolution Analysis dimensions; absent or zero dimensions imply no scaling.
|
|
665
|
+
* @returns Video metadata and new output-space animations, retaining zero-based frame IDs.
|
|
666
|
+
* @throws If sharp cannot read the selected or fallback image. Empty input performs no image I/O.
|
|
667
|
+
*/
|
|
668
|
+
async function buildVideoMetadata(ctx, selected, analysisResolution) {
|
|
669
|
+
const firstFrame = selected[0] ?? ctx.frames[0];
|
|
670
|
+
const dimensions = firstFrame ? await (0, sharp.default)(firstFrame.extractPath).metadata() : void 0;
|
|
671
|
+
const width = dimensions?.width ?? 0;
|
|
672
|
+
const height = dimensions?.height ?? 0;
|
|
673
|
+
const sx = analysisResolution?.width ? width / analysisResolution.width : 1;
|
|
674
|
+
const sy = analysisResolution?.height ? height / analysisResolution.height : 1;
|
|
675
|
+
const lastTimestamp = ctx.frames[ctx.frames.length - 1]?.timestamp ?? 0;
|
|
676
|
+
const duration = ctx.options.mode === "frames" ? lastTimestamp : ctx.sourceDurationSec ?? lastTimestamp;
|
|
677
|
+
return {
|
|
678
|
+
video: {
|
|
679
|
+
originalDurationMs: Math.round(duration * 1e3),
|
|
680
|
+
fps: ctx.options.mode === "frames" ? 1 : ctx.effectiveFps ?? ctx.options.fps,
|
|
681
|
+
resolution: {
|
|
682
|
+
width,
|
|
683
|
+
height
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
animations: (ctx.animations ?? []).map((animation) => {
|
|
687
|
+
const box = animation.boundingBox;
|
|
688
|
+
const x = Math.max(0, Math.min(width, Math.round(box.x * sx)));
|
|
689
|
+
const y = Math.max(0, Math.min(height, Math.round(box.y * sy)));
|
|
690
|
+
return {
|
|
691
|
+
...animation,
|
|
692
|
+
boundingBox: {
|
|
693
|
+
x,
|
|
694
|
+
y,
|
|
695
|
+
width: Math.max(0, Math.min(width - x, Math.round(box.width * sx))),
|
|
696
|
+
height: Math.max(0, Math.min(height - y, Math.round(box.height * sy)))
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
})
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
var init_build_video_metadata = __esmMin((() => {}));
|
|
703
|
+
|
|
704
|
+
//#endregion
|
|
705
|
+
//#region src/core/constants/workspace-layout.ts
|
|
706
|
+
function getTempWorkspaceDir(sessionId) {
|
|
707
|
+
return (0, node_path.join)(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
708
|
+
}
|
|
709
|
+
var APP_NAME, WORKSPACE_PREFIX, TEMP_BASE_DIR, FRAME_OUTPUT_EXTENSION, FRAME_FILENAME_PATTERN;
|
|
710
|
+
var init_workspace_layout = __esmMin((() => {
|
|
711
|
+
APP_NAME = "scene-sieve";
|
|
712
|
+
WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
713
|
+
TEMP_BASE_DIR = (0, node_os.tmpdir)();
|
|
714
|
+
FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
715
|
+
FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
|
|
716
|
+
}));
|
|
717
|
+
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/core/utils/filesystem/paths.ts
|
|
516
720
|
async function ensureDir(dirPath) {
|
|
517
721
|
await (0, node_fs_promises.mkdir)(dirPath, { recursive: true });
|
|
518
722
|
}
|
|
@@ -548,15 +752,21 @@ function resolveAbsolute(p) {
|
|
|
548
752
|
function deriveOutputPath(inputPath) {
|
|
549
753
|
return (0, node_path.resolve)((0, node_path.resolve)(inputPath, ".."), `${(0, node_path.basename)(inputPath, (0, node_path.extname)(inputPath))}_scenes`);
|
|
550
754
|
}
|
|
755
|
+
var init_paths = __esmMin((() => {}));
|
|
551
756
|
|
|
552
757
|
//#endregion
|
|
553
|
-
//#region src/core/extractor.ts
|
|
758
|
+
//#region src/core/extractor/extractor.ts
|
|
554
759
|
/**
|
|
555
760
|
* Extract frames from video/GIF using FFmpeg.
|
|
556
|
-
*
|
|
557
|
-
*
|
|
761
|
+
* @param ctx Pipeline context; records effectiveFps and sourceDurationSec for video input.
|
|
762
|
+
* @returns Extracted candidates, or the unchanged input array in frames mode.
|
|
763
|
+
* @throws When the input is missing, metadata has no video stream, or FFmpeg fails.
|
|
558
764
|
*/
|
|
559
765
|
async function extractFrames(ctx) {
|
|
766
|
+
if (ctx.options.mode === "frames") {
|
|
767
|
+
ctx.effectiveFps = 1;
|
|
768
|
+
return ctx.frames;
|
|
769
|
+
}
|
|
560
770
|
const framesDir = (0, node_path.join)(ctx.workspacePath, "frames");
|
|
561
771
|
const { inputPath, fps, maxFrames, scale } = ctx.options;
|
|
562
772
|
if (!inputPath) throw new Error("inputPath is required for frame extraction");
|
|
@@ -571,19 +781,30 @@ async function extractFrames(ctx) {
|
|
|
571
781
|
if (!(metadata.streams?.some((s) => s.codec_type === "video") ?? false)) throw new Error(`No video stream found in file: ${inputPath} (detected format: ${formatName})`);
|
|
572
782
|
logger.debug(`Detected format: ${formatName} (Duration: ${duration.toFixed(1)}s), path: ${inputPath}`);
|
|
573
783
|
await ensureDir(framesDir);
|
|
784
|
+
const frameLimit = Math.max(2, maxFrames);
|
|
574
785
|
let effectiveFps = fps;
|
|
575
786
|
if (duration > 0) {
|
|
576
|
-
const fpsCap =
|
|
787
|
+
const fpsCap = frameLimit / duration;
|
|
577
788
|
effectiveFps = Math.min(fps, fpsCap);
|
|
578
|
-
effectiveFps = Math.max(.5, effectiveFps);
|
|
579
789
|
logger.debug(`FPS: ${fps} → effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`);
|
|
580
790
|
}
|
|
581
|
-
|
|
791
|
+
ctx.effectiveFps = effectiveFps;
|
|
792
|
+
ctx.sourceDurationSec = duration;
|
|
793
|
+
const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale, frameLimit);
|
|
582
794
|
ctx.emitProgress(100);
|
|
583
795
|
logger.debug(`Extracted ${frames.length} frames`);
|
|
584
796
|
return frames;
|
|
585
797
|
}
|
|
586
|
-
|
|
798
|
+
/**
|
|
799
|
+
* Write scaled JPEG candidates through the bundled FFmpeg runtime.
|
|
800
|
+
* @param inputPath Readable video input.
|
|
801
|
+
* @param outputDir Existing frame directory.
|
|
802
|
+
* @param fps Positive effective sampling frequency.
|
|
803
|
+
* @param scale Output image height.
|
|
804
|
+
* @param frameLimit Maximum number of output frames.
|
|
805
|
+
* @returns Candidates with local output-grid timestamps; rejects on extraction failure.
|
|
806
|
+
*/
|
|
807
|
+
async function extractByFps(inputPath, outputDir, fps, scale, frameLimit) {
|
|
587
808
|
const outputPattern = (0, node_path.join)(outputDir, FRAME_FILENAME_PATTERN);
|
|
588
809
|
await (0, execa.execa)(ffmpeg_static.default, [
|
|
589
810
|
"-i",
|
|
@@ -592,9 +813,11 @@ async function extractByFps(inputPath, outputDir, fps, scale, duration) {
|
|
|
592
813
|
`fps=${fps},scale=-1:${scale}`,
|
|
593
814
|
"-q:v",
|
|
594
815
|
"2",
|
|
816
|
+
"-frames:v",
|
|
817
|
+
String(frameLimit),
|
|
595
818
|
outputPattern
|
|
596
819
|
]);
|
|
597
|
-
return buildFrameList(outputDir,
|
|
820
|
+
return buildFrameList(outputDir, fps);
|
|
598
821
|
}
|
|
599
822
|
async function getVideoMetadata(inputPath) {
|
|
600
823
|
const { stdout } = await (0, execa.execa)(_ffprobe_installer_ffprobe.path, [
|
|
@@ -608,12 +831,18 @@ async function getVideoMetadata(inputPath) {
|
|
|
608
831
|
]);
|
|
609
832
|
return JSON.parse(stdout);
|
|
610
833
|
}
|
|
611
|
-
|
|
834
|
+
/**
|
|
835
|
+
* Read sorted JPEG paths and attach local output-grid times.
|
|
836
|
+
* @param framesDir Extracted frame directory; filesystem errors propagate.
|
|
837
|
+
* @param effectiveFps Positive frequency used by the fps filter.
|
|
838
|
+
* @returns Zero-based candidates without a segment seek offset.
|
|
839
|
+
*/
|
|
840
|
+
async function buildFrameList(framesDir, effectiveFps) {
|
|
612
841
|
const jpgFiles = (0, _winglet_common_utils.filter)(await (0, node_fs_promises.readdir)(framesDir), (f) => f.endsWith(".jpg")).sort();
|
|
613
842
|
if (jpgFiles.length === 0) return [];
|
|
614
843
|
return (0, _winglet_common_utils.map)(jpgFiles, (file, index) => ({
|
|
615
844
|
id: index,
|
|
616
|
-
timestamp:
|
|
845
|
+
timestamp: index / effectiveFps,
|
|
617
846
|
extractPath: (0, node_path.join)(framesDir, file)
|
|
618
847
|
}));
|
|
619
848
|
}
|
|
@@ -627,9 +856,10 @@ async function buildFrameList(framesDir, duration) {
|
|
|
627
856
|
* @param scale - Height scale for vision analysis
|
|
628
857
|
* @param startTime - Start time in seconds
|
|
629
858
|
* @param duration - Duration in seconds to extract
|
|
859
|
+
* @param frameLimit - Positive output limit; defaults to the range's grid capacity
|
|
630
860
|
* @returns Array of FrameNode with segment-local timestamps (starting from 0)
|
|
631
861
|
*/
|
|
632
|
-
async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime, duration) {
|
|
862
|
+
async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime, duration, frameLimit = Math.ceil(duration * fps)) {
|
|
633
863
|
const outputPattern = (0, node_path.join)(outputDir, FRAME_FILENAME_PATTERN);
|
|
634
864
|
await (0, execa.execa)(ffmpeg_static.default, [
|
|
635
865
|
"-ss",
|
|
@@ -642,13 +872,124 @@ async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime
|
|
|
642
872
|
`fps=${fps},scale=-1:${scale}`,
|
|
643
873
|
"-q:v",
|
|
644
874
|
"2",
|
|
875
|
+
"-frames:v",
|
|
876
|
+
String(frameLimit),
|
|
645
877
|
outputPattern
|
|
646
878
|
]);
|
|
647
|
-
return buildFrameList(outputDir,
|
|
879
|
+
return buildFrameList(outputDir, fps);
|
|
880
|
+
}
|
|
881
|
+
var init_extractor$1 = __esmMin((() => {
|
|
882
|
+
init_workspace_layout();
|
|
883
|
+
init_logger();
|
|
884
|
+
init_paths();
|
|
885
|
+
}));
|
|
886
|
+
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/core/extractor/index.ts
|
|
889
|
+
var init_extractor = __esmMin((() => {
|
|
890
|
+
init_extractor$1();
|
|
891
|
+
}));
|
|
892
|
+
|
|
893
|
+
//#endregion
|
|
894
|
+
//#region src/core/input-resolver/validation/validate-options.ts
|
|
895
|
+
/**
|
|
896
|
+
* Reject invalid numeric options before defaults or pipeline effects are applied.
|
|
897
|
+
* @param options - Supplied options; omitted numeric fields use pipeline defaults.
|
|
898
|
+
* @returns Nothing when all supplied numeric fields satisfy their contracts.
|
|
899
|
+
* @throws An input error naming the invalid option and its received value.
|
|
900
|
+
*/
|
|
901
|
+
function validateOptions(options) {
|
|
902
|
+
for (const [name, min, max, integer, exclusiveMin, requirement] of [
|
|
903
|
+
[
|
|
904
|
+
"count",
|
|
905
|
+
1,
|
|
906
|
+
Infinity,
|
|
907
|
+
true,
|
|
908
|
+
false,
|
|
909
|
+
"an integer >= 1"
|
|
910
|
+
],
|
|
911
|
+
[
|
|
912
|
+
"threshold",
|
|
913
|
+
0,
|
|
914
|
+
1,
|
|
915
|
+
false,
|
|
916
|
+
true,
|
|
917
|
+
"in range (0, 1] and finite"
|
|
918
|
+
],
|
|
919
|
+
[
|
|
920
|
+
"fps",
|
|
921
|
+
0,
|
|
922
|
+
Infinity,
|
|
923
|
+
false,
|
|
924
|
+
true,
|
|
925
|
+
"finite and > 0"
|
|
926
|
+
],
|
|
927
|
+
[
|
|
928
|
+
"maxFrames",
|
|
929
|
+
2,
|
|
930
|
+
Infinity,
|
|
931
|
+
true,
|
|
932
|
+
false,
|
|
933
|
+
"an integer >= 2"
|
|
934
|
+
],
|
|
935
|
+
[
|
|
936
|
+
"scale",
|
|
937
|
+
16,
|
|
938
|
+
Infinity,
|
|
939
|
+
true,
|
|
940
|
+
false,
|
|
941
|
+
"an integer >= 16"
|
|
942
|
+
],
|
|
943
|
+
[
|
|
944
|
+
"quality",
|
|
945
|
+
1,
|
|
946
|
+
100,
|
|
947
|
+
true,
|
|
948
|
+
false,
|
|
949
|
+
"an integer in range [1, 100]"
|
|
950
|
+
],
|
|
951
|
+
[
|
|
952
|
+
"iouThreshold",
|
|
953
|
+
0,
|
|
954
|
+
1,
|
|
955
|
+
false,
|
|
956
|
+
false,
|
|
957
|
+
"finite and in range [0, 1]"
|
|
958
|
+
],
|
|
959
|
+
[
|
|
960
|
+
"animationThreshold",
|
|
961
|
+
1,
|
|
962
|
+
Infinity,
|
|
963
|
+
true,
|
|
964
|
+
false,
|
|
965
|
+
"an integer >= 1"
|
|
966
|
+
],
|
|
967
|
+
[
|
|
968
|
+
"maxSegmentDuration",
|
|
969
|
+
0,
|
|
970
|
+
Infinity,
|
|
971
|
+
false,
|
|
972
|
+
true,
|
|
973
|
+
"finite and > 0"
|
|
974
|
+
],
|
|
975
|
+
[
|
|
976
|
+
"concurrency",
|
|
977
|
+
1,
|
|
978
|
+
Infinity,
|
|
979
|
+
true,
|
|
980
|
+
false,
|
|
981
|
+
"an integer >= 1"
|
|
982
|
+
]
|
|
983
|
+
]) {
|
|
984
|
+
const value = options[name];
|
|
985
|
+
if (value === void 0) continue;
|
|
986
|
+
if (!Number.isFinite(value) || integer && !Number.isInteger(value) || (exclusiveMin ? value <= min : value < min) || value > max) throw new Error(`${name} must be ${requirement}, received: ${value}`);
|
|
987
|
+
}
|
|
648
988
|
}
|
|
989
|
+
var init_validate_options = __esmMin((() => {}));
|
|
649
990
|
|
|
650
991
|
//#endregion
|
|
651
|
-
//#region src/core/workspace.ts
|
|
992
|
+
//#region src/core/workspace/workspace.ts
|
|
652
993
|
async function createWorkspace(sessionId) {
|
|
653
994
|
const workspacePath = getTempWorkspaceDir(sessionId);
|
|
654
995
|
await ensureDir((0, node_path.join)(workspacePath, "frames"));
|
|
@@ -679,17 +1020,11 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
679
1020
|
timestampMs: Math.round(frame.timestamp * 1e3)
|
|
680
1021
|
});
|
|
681
1022
|
}
|
|
1023
|
+
const { video, animations } = await buildVideoMetadata(ctx, selectedFrames, ctx.analysisResolution);
|
|
682
1024
|
const metadata = {
|
|
683
|
-
video
|
|
684
|
-
originalDurationMs: Math.round((ctx.frames.length > 0 ? ctx.frames[ctx.frames.length - 1].timestamp : 0) * 1e3),
|
|
685
|
-
fps: ctx.options.fps,
|
|
686
|
-
resolution: {
|
|
687
|
-
width: ctx.options.scale,
|
|
688
|
-
height: Math.round(ctx.options.scale * 9 / 16)
|
|
689
|
-
}
|
|
690
|
-
},
|
|
1025
|
+
video,
|
|
691
1026
|
frames: framesMetadata,
|
|
692
|
-
animations: (0, _winglet_common_utils.map)(
|
|
1027
|
+
animations: (0, _winglet_common_utils.map)(animations, (anim) => ({
|
|
693
1028
|
...anim,
|
|
694
1029
|
startFrameId: anim.startFrameId + 1,
|
|
695
1030
|
endFrameId: anim.endFrameId + 1,
|
|
@@ -760,15 +1095,32 @@ async function readFramesAsBuffers(frameNodes, quality) {
|
|
|
760
1095
|
mozjpeg: true
|
|
761
1096
|
}).toBuffer()));
|
|
762
1097
|
}
|
|
1098
|
+
var init_workspace$1 = __esmMin((() => {
|
|
1099
|
+
init_workspace_layout();
|
|
1100
|
+
init_paths();
|
|
1101
|
+
init_build_video_metadata();
|
|
1102
|
+
}));
|
|
763
1103
|
|
|
764
1104
|
//#endregion
|
|
765
|
-
//#region src/core/
|
|
1105
|
+
//#region src/core/workspace/index.ts
|
|
1106
|
+
var init_workspace = __esmMin((() => {
|
|
1107
|
+
init_workspace$1();
|
|
1108
|
+
}));
|
|
1109
|
+
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/core/input-resolver/input-resolver.ts
|
|
1112
|
+
/**
|
|
1113
|
+
* Validate supplied options and resolve defaults and paths for the pipeline.
|
|
1114
|
+
* @param options - Mode-specific input and optional numeric settings.
|
|
1115
|
+
* @returns Complete pipeline settings with absolute file input paths.
|
|
1116
|
+
* @throws An input error if a supplied numeric setting is invalid.
|
|
1117
|
+
*/
|
|
766
1118
|
function resolveOptions(options) {
|
|
1119
|
+
validateOptions(options);
|
|
767
1120
|
const mode = options.mode;
|
|
768
1121
|
const inputPath = mode === "file" ? resolveAbsolute(options.inputPath) : void 0;
|
|
769
1122
|
const outputPath = options.outputPath ?? (inputPath ? deriveOutputPath(inputPath) : (0, node_path.join)(process.cwd(), "scene-sieve-output"));
|
|
770
1123
|
const threshold = options.threshold ?? .5;
|
|
771
|
-
if (threshold <= 0 || threshold > 1) throw new Error(`threshold must be in range (0, 1], received: ${threshold}`);
|
|
772
1124
|
return {
|
|
773
1125
|
mode,
|
|
774
1126
|
inputPath,
|
|
@@ -793,6 +1145,10 @@ function resolveOptions(options) {
|
|
|
793
1145
|
* - 'file' mode: validate file exists and delegate to extractor (caller's responsibility)
|
|
794
1146
|
* - 'buffer' mode: write buffer as temp video file, return path via FrameNode trick (empty list)
|
|
795
1147
|
* - 'frames' mode: write frame buffers as JPGs, return FrameNode[]
|
|
1148
|
+
* @param options - Input source; encoded frames must have matching dimensions.
|
|
1149
|
+
* @param workspacePath - Workspace receiving temporary input files.
|
|
1150
|
+
* @returns Frame nodes or a resolved video path for extraction.
|
|
1151
|
+
* @throws Propagates metadata or write errors and rejects mismatched frame sizes.
|
|
796
1152
|
*/
|
|
797
1153
|
async function resolveInput(options, workspacePath) {
|
|
798
1154
|
if (options.mode === "file") return {
|
|
@@ -803,12 +1159,51 @@ async function resolveInput(options, workspacePath) {
|
|
|
803
1159
|
frames: [],
|
|
804
1160
|
resolvedInputPath: await writeInputBuffer(options.inputBuffer, workspacePath)
|
|
805
1161
|
};
|
|
806
|
-
if (options.mode === "frames")
|
|
1162
|
+
if (options.mode === "frames") {
|
|
1163
|
+
let dimensions;
|
|
1164
|
+
for (const buffer of options.inputFrames) {
|
|
1165
|
+
const { width, height } = await (0, sharp.default)(buffer).metadata();
|
|
1166
|
+
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}`);
|
|
1167
|
+
dimensions = {
|
|
1168
|
+
width,
|
|
1169
|
+
height
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
return { frames: await writeInputFrames(options.inputFrames, workspacePath) };
|
|
1173
|
+
}
|
|
807
1174
|
throw new Error(`Unsupported input mode: ${options.mode}`);
|
|
808
1175
|
}
|
|
1176
|
+
var init_input_resolver$1 = __esmMin((() => {
|
|
1177
|
+
init_pipeline_defaults();
|
|
1178
|
+
init_paths();
|
|
1179
|
+
init_validate_options();
|
|
1180
|
+
init_workspace();
|
|
1181
|
+
}));
|
|
809
1182
|
|
|
810
1183
|
//#endregion
|
|
811
|
-
//#region src/
|
|
1184
|
+
//#region src/core/input-resolver/index.ts
|
|
1185
|
+
var init_input_resolver = __esmMin((() => {
|
|
1186
|
+
init_input_resolver$1();
|
|
1187
|
+
}));
|
|
1188
|
+
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/core/pruner/scoring/normalize-scores.ts
|
|
1191
|
+
/**
|
|
1192
|
+
* Find the first position whose score is at least the requested value.
|
|
1193
|
+
* @param sorted - Finite positive scores sorted in ascending order.
|
|
1194
|
+
* @param value - A finite positive score present in sorted.
|
|
1195
|
+
* @returns The first matching rank, including the first position of any tie.
|
|
1196
|
+
*/
|
|
1197
|
+
function lowerBound(sorted, value) {
|
|
1198
|
+
let low = 0;
|
|
1199
|
+
let high = sorted.length;
|
|
1200
|
+
while (low < high) {
|
|
1201
|
+
const mid = Math.floor((low + high) / 2);
|
|
1202
|
+
if (sorted[mid] < value) low = mid + 1;
|
|
1203
|
+
else high = mid;
|
|
1204
|
+
}
|
|
1205
|
+
return low;
|
|
1206
|
+
}
|
|
812
1207
|
/**
|
|
813
1208
|
* Normalize raw scores to [0, 1] range via Robust Hybrid Normalization.
|
|
814
1209
|
*
|
|
@@ -853,64 +1248,67 @@ function normalizeScores(items) {
|
|
|
853
1248
|
});
|
|
854
1249
|
const cdf = (0, _winglet_common_utils.map)(safeScores, (s) => {
|
|
855
1250
|
if (s <= 0) return 0;
|
|
856
|
-
return sorted
|
|
1251
|
+
return lowerBound(sorted, s) / sorted.length;
|
|
857
1252
|
});
|
|
858
1253
|
return (0, _winglet_common_utils.map)(logisticZ, (z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA);
|
|
859
1254
|
}
|
|
1255
|
+
var NORMALIZATION_ALPHA, NORMALIZATION_MAD_COEFFICIENT, NORMALIZATION_MIN_SAMPLE_SIZE;
|
|
1256
|
+
var init_normalize_scores = __esmMin((() => {
|
|
1257
|
+
NORMALIZATION_ALPHA = .4;
|
|
1258
|
+
NORMALIZATION_MAD_COEFFICIENT = 1.4826;
|
|
1259
|
+
NORMALIZATION_MIN_SAMPLE_SIZE = 10;
|
|
1260
|
+
}));
|
|
860
1261
|
|
|
861
1262
|
//#endregion
|
|
862
|
-
//#region src/
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
var MinHeap = class {
|
|
870
|
-
h = [];
|
|
871
|
-
get size() {
|
|
872
|
-
return this.h.length;
|
|
873
|
-
}
|
|
874
|
-
push(entry) {
|
|
875
|
-
this.h.push(entry);
|
|
876
|
-
this.siftUp(this.h.length - 1);
|
|
877
|
-
}
|
|
878
|
-
pop() {
|
|
879
|
-
const n = this.h.length;
|
|
880
|
-
if (n === 0) return void 0;
|
|
881
|
-
const top = this.h[0];
|
|
882
|
-
const last = this.h.pop();
|
|
883
|
-
if (n > 1) {
|
|
884
|
-
this.h[0] = last;
|
|
885
|
-
this.siftDown(0);
|
|
1263
|
+
//#region src/core/pruner/heap/min-heap.ts
|
|
1264
|
+
var MinHeap;
|
|
1265
|
+
var init_min_heap = __esmMin((() => {
|
|
1266
|
+
MinHeap = class {
|
|
1267
|
+
h = [];
|
|
1268
|
+
get size() {
|
|
1269
|
+
return this.h.length;
|
|
886
1270
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
while (i > 0) {
|
|
891
|
-
const p = i - 1 >> 1;
|
|
892
|
-
if (this.h[p].score <= this.h[i].score) break;
|
|
893
|
-
[this.h[p], this.h[i]] = [this.h[i], this.h[p]];
|
|
894
|
-
i = p;
|
|
1271
|
+
push(entry) {
|
|
1272
|
+
this.h.push(entry);
|
|
1273
|
+
this.siftUp(this.h.length - 1);
|
|
895
1274
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
[this.h[m], this.h[i]] = [this.h[i], this.h[m]];
|
|
907
|
-
i = m;
|
|
1275
|
+
pop() {
|
|
1276
|
+
const n = this.h.length;
|
|
1277
|
+
if (n === 0) return void 0;
|
|
1278
|
+
const top = this.h[0];
|
|
1279
|
+
const last = this.h.pop();
|
|
1280
|
+
if (n > 1) {
|
|
1281
|
+
this.h[0] = last;
|
|
1282
|
+
this.siftDown(0);
|
|
1283
|
+
}
|
|
1284
|
+
return top;
|
|
908
1285
|
}
|
|
909
|
-
|
|
910
|
-
|
|
1286
|
+
siftUp(i) {
|
|
1287
|
+
while (i > 0) {
|
|
1288
|
+
const p = i - 1 >> 1;
|
|
1289
|
+
if (this.h[p].score <= this.h[i].score) break;
|
|
1290
|
+
[this.h[p], this.h[i]] = [this.h[i], this.h[p]];
|
|
1291
|
+
i = p;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
siftDown(i) {
|
|
1295
|
+
const n = this.h.length;
|
|
1296
|
+
for (;;) {
|
|
1297
|
+
let m = i;
|
|
1298
|
+
const l = 2 * i + 1;
|
|
1299
|
+
const r = 2 * i + 2;
|
|
1300
|
+
if (l < n && this.h[l].score < this.h[m].score) m = l;
|
|
1301
|
+
if (r < n && this.h[r].score < this.h[m].score) m = r;
|
|
1302
|
+
if (m === i) break;
|
|
1303
|
+
[this.h[m], this.h[i]] = [this.h[i], this.h[m]];
|
|
1304
|
+
i = m;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}));
|
|
911
1309
|
|
|
912
1310
|
//#endregion
|
|
913
|
-
//#region src/core/pruner.ts
|
|
1311
|
+
//#region src/core/pruner/pruner.ts
|
|
914
1312
|
/**
|
|
915
1313
|
* Edge-aware greedy merge with re-linking — O(N log N).
|
|
916
1314
|
*
|
|
@@ -1024,7 +1422,7 @@ function suppressConsecutiveRuns(graph, passingIndices, normalizedScores) {
|
|
|
1024
1422
|
return result;
|
|
1025
1423
|
}
|
|
1026
1424
|
/**
|
|
1027
|
-
* Threshold-based pruning with NMS -- O(N).
|
|
1425
|
+
* Threshold-based pruning with NMS -- including normalization, O(N log N).
|
|
1028
1426
|
*
|
|
1029
1427
|
* 1. Scores are normalized to [0, 1] via percentile normalization.
|
|
1030
1428
|
* 2. Edges with normalized score >= threshold are collected.
|
|
@@ -1087,9 +1485,19 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1087
1485
|
}
|
|
1088
1486
|
return pruneTo(syntheticEdges, survivingFrames, maxCount);
|
|
1089
1487
|
}
|
|
1488
|
+
var init_pruner$1 = __esmMin((() => {
|
|
1489
|
+
init_normalize_scores();
|
|
1490
|
+
init_min_heap();
|
|
1491
|
+
}));
|
|
1090
1492
|
|
|
1091
1493
|
//#endregion
|
|
1092
|
-
//#region src/
|
|
1494
|
+
//#region src/core/pruner/index.ts
|
|
1495
|
+
var init_pruner = __esmMin((() => {
|
|
1496
|
+
init_pruner$1();
|
|
1497
|
+
}));
|
|
1498
|
+
|
|
1499
|
+
//#endregion
|
|
1500
|
+
//#region src/core/segmenter/scheduling/concurrency.ts
|
|
1093
1501
|
/**
|
|
1094
1502
|
* Creates a concurrency limiter that runs at most `limit` tasks in parallel.
|
|
1095
1503
|
* Lightweight replacement for p-limit to avoid external dependency.
|
|
@@ -1109,9 +1517,10 @@ function concurrencyLimit(limit) {
|
|
|
1109
1517
|
}
|
|
1110
1518
|
};
|
|
1111
1519
|
}
|
|
1520
|
+
var init_concurrency = __esmMin((() => {}));
|
|
1112
1521
|
|
|
1113
1522
|
//#endregion
|
|
1114
|
-
//#region src/core/segmenter.ts
|
|
1523
|
+
//#region src/core/segmenter/segmenter.ts
|
|
1115
1524
|
/**
|
|
1116
1525
|
* Determine whether segmentation should be used.
|
|
1117
1526
|
* Returns false for frames mode and GIF files.
|
|
@@ -1125,53 +1534,62 @@ function shouldSegment(resolvedOptions, originalOptions) {
|
|
|
1125
1534
|
return true;
|
|
1126
1535
|
}
|
|
1127
1536
|
/**
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1537
|
+
* Partition the global extraction grid into nonempty logical segments.
|
|
1538
|
+
* @param totalDuration Positive source duration in seconds.
|
|
1539
|
+
* @param maxSegmentDuration Positive logical segment width in seconds.
|
|
1540
|
+
* @param maxFrames Candidate budget, defensively raised to at least two.
|
|
1541
|
+
* @param fps Positive requested sampling frequency.
|
|
1542
|
+
* @returns Contiguous plan indices with grid-aligned seeks and overlap-inclusive limits.
|
|
1134
1543
|
*/
|
|
1135
1544
|
function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
|
|
1136
|
-
const
|
|
1545
|
+
const frameLimit = Math.max(2, maxFrames);
|
|
1546
|
+
const effectiveFps = Math.min(fps, frameLimit / totalDuration);
|
|
1137
1547
|
if (totalDuration <= maxSegmentDuration) return [{
|
|
1138
1548
|
index: 0,
|
|
1139
1549
|
startTime: 0,
|
|
1140
1550
|
endTime: totalDuration,
|
|
1141
1551
|
duration: totalDuration,
|
|
1142
|
-
allocatedFrames:
|
|
1552
|
+
allocatedFrames: frameLimit,
|
|
1143
1553
|
effectiveFps,
|
|
1144
1554
|
overlapBefore: 0,
|
|
1145
1555
|
overlapAfter: 0,
|
|
1146
1556
|
extractStartTime: 0,
|
|
1147
1557
|
extractDuration: totalDuration
|
|
1148
1558
|
}];
|
|
1149
|
-
const segmentCount = Math.ceil(totalDuration / maxSegmentDuration);
|
|
1150
|
-
const overlapTime = 1 / effectiveFps;
|
|
1151
1559
|
const segments = [];
|
|
1152
|
-
for (let
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1560
|
+
for (let slot = 0; slot < frameLimit; slot++) {
|
|
1561
|
+
const timestamp = slot / effectiveFps;
|
|
1562
|
+
if (timestamp >= totalDuration) break;
|
|
1563
|
+
const startTime = Math.floor(timestamp / maxSegmentDuration) * maxSegmentDuration;
|
|
1564
|
+
const previous = segments[segments.length - 1];
|
|
1565
|
+
if (previous?.startTime === startTime) {
|
|
1566
|
+
previous.allocatedFrames++;
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const endTime = Math.min(startTime + maxSegmentDuration, totalDuration);
|
|
1160
1570
|
segments.push({
|
|
1161
|
-
index:
|
|
1571
|
+
index: segments.length,
|
|
1162
1572
|
startTime,
|
|
1163
1573
|
endTime,
|
|
1164
|
-
duration,
|
|
1165
|
-
allocatedFrames:
|
|
1574
|
+
duration: endTime - startTime,
|
|
1575
|
+
allocatedFrames: 1,
|
|
1166
1576
|
effectiveFps,
|
|
1167
|
-
overlapBefore,
|
|
1168
|
-
overlapAfter,
|
|
1169
|
-
extractStartTime,
|
|
1170
|
-
extractDuration
|
|
1577
|
+
overlapBefore: 0,
|
|
1578
|
+
overlapAfter: 0,
|
|
1579
|
+
extractStartTime: timestamp,
|
|
1580
|
+
extractDuration: 0
|
|
1171
1581
|
});
|
|
1172
1582
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1583
|
+
let firstSlot = 0;
|
|
1584
|
+
for (const segment of segments) {
|
|
1585
|
+
const nextSlot = firstSlot + segment.allocatedFrames;
|
|
1586
|
+
segment.overlapBefore = segment.index > 0 ? 1 : 0;
|
|
1587
|
+
segment.overlapAfter = segment.index < segments.length - 1 ? 1 : 0;
|
|
1588
|
+
segment.extractStartTime = (firstSlot - segment.overlapBefore) / effectiveFps;
|
|
1589
|
+
segment.extractDuration = (segment.overlapAfter ? Math.min(totalDuration, (nextSlot + 1) / effectiveFps) : totalDuration) - segment.extractStartTime;
|
|
1590
|
+
segment.allocatedFrames += segment.overlapBefore + segment.overlapAfter;
|
|
1591
|
+
firstSlot = nextSlot;
|
|
1592
|
+
}
|
|
1175
1593
|
return segments;
|
|
1176
1594
|
}
|
|
1177
1595
|
/**
|
|
@@ -1190,44 +1608,58 @@ function collectAllFrames(segmentResults) {
|
|
|
1190
1608
|
return allFrames;
|
|
1191
1609
|
}
|
|
1192
1610
|
/**
|
|
1193
|
-
* Sort frames
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1611
|
+
* Sort frames in place and alias overlap duplicates to the first survivor.
|
|
1612
|
+
* @param frames Collected entries whose segment index and local ID identify a frame.
|
|
1613
|
+
* @param effectiveFps Positive sampling frequency; half a frame interval is the threshold.
|
|
1614
|
+
* @returns Timestamp-ordered survivors and duplicate keys pointing directly to survivor keys.
|
|
1196
1615
|
*/
|
|
1197
1616
|
function deduplicateFrames(frames, effectiveFps) {
|
|
1198
1617
|
frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
|
|
1199
1618
|
const dupThreshold = 1 / (effectiveFps * 2);
|
|
1200
1619
|
const unique = [];
|
|
1620
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1201
1621
|
for (const entry of frames) {
|
|
1202
1622
|
if (unique.length > 0) {
|
|
1203
1623
|
const last = unique[unique.length - 1];
|
|
1204
|
-
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold)
|
|
1624
|
+
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
|
|
1625
|
+
aliases.set(`${entry.segmentIndex}:${entry.localId}`, `${last.segmentIndex}:${last.localId}`);
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1205
1628
|
}
|
|
1206
1629
|
unique.push(entry);
|
|
1207
1630
|
}
|
|
1208
|
-
return
|
|
1631
|
+
return {
|
|
1632
|
+
unique,
|
|
1633
|
+
aliases
|
|
1634
|
+
};
|
|
1209
1635
|
}
|
|
1210
1636
|
/**
|
|
1211
|
-
* Assign sequential global IDs
|
|
1212
|
-
*
|
|
1637
|
+
* Assign sequential global IDs and retain duplicate local IDs as aliases.
|
|
1638
|
+
* @param uniqueFrames Timestamp-ordered survivors with distinct segment/local keys.
|
|
1639
|
+
* @param aliases Duplicate keys pointing directly to keys in uniqueFrames.
|
|
1640
|
+
* @returns Remapped frames and a global ID lookup covering survivors and duplicates.
|
|
1213
1641
|
*/
|
|
1214
|
-
function remapFrameIds(uniqueFrames) {
|
|
1642
|
+
function remapFrameIds(uniqueFrames, aliases) {
|
|
1215
1643
|
const globalIdMap = /* @__PURE__ */ new Map();
|
|
1644
|
+
const frames = uniqueFrames.map((entry, globalId) => {
|
|
1645
|
+
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
1646
|
+
return {
|
|
1647
|
+
id: globalId,
|
|
1648
|
+
timestamp: entry.frame.timestamp,
|
|
1649
|
+
extractPath: entry.frame.extractPath
|
|
1650
|
+
};
|
|
1651
|
+
});
|
|
1652
|
+
for (const [alias, survivor] of aliases) globalIdMap.set(alias, globalIdMap.get(survivor));
|
|
1216
1653
|
return {
|
|
1217
|
-
frames
|
|
1218
|
-
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
1219
|
-
return {
|
|
1220
|
-
id: globalId,
|
|
1221
|
-
timestamp: entry.frame.timestamp,
|
|
1222
|
-
extractPath: entry.frame.extractPath
|
|
1223
|
-
};
|
|
1224
|
-
}),
|
|
1654
|
+
frames,
|
|
1225
1655
|
globalIdMap
|
|
1226
1656
|
};
|
|
1227
1657
|
}
|
|
1228
1658
|
/**
|
|
1229
|
-
* Remap
|
|
1230
|
-
*
|
|
1659
|
+
* Remap edges, dropping missing endpoints and self loops while keeping the highest pair score.
|
|
1660
|
+
* @param segmentResults Segment-local edges in encounter order.
|
|
1661
|
+
* @param globalIdMap Survivor and duplicate local keys mapped to global IDs.
|
|
1662
|
+
* @returns One edge per surviving directed pair without changing its score.
|
|
1231
1663
|
*/
|
|
1232
1664
|
function remapEdges(segmentResults, globalIdMap) {
|
|
1233
1665
|
const edges = [];
|
|
@@ -1236,6 +1668,7 @@ function remapEdges(segmentResults, globalIdMap) {
|
|
|
1236
1668
|
const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
|
|
1237
1669
|
const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
|
|
1238
1670
|
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1671
|
+
if (newSourceId === newTargetId) continue;
|
|
1239
1672
|
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1240
1673
|
const existingIdx = edgeMap.get(edgeKey);
|
|
1241
1674
|
if (existingIdx !== void 0) {
|
|
@@ -1256,42 +1689,52 @@ function remapEdges(segmentResults, globalIdMap) {
|
|
|
1256
1689
|
return edges;
|
|
1257
1690
|
}
|
|
1258
1691
|
/**
|
|
1259
|
-
* Remap
|
|
1260
|
-
*
|
|
1692
|
+
* Remap animations, dropping missing or collapsed endpoints and retaining the first pair entry.
|
|
1693
|
+
* @param segmentResults Segment-local tracker entries in encounter order.
|
|
1694
|
+
* @param globalIdMap Survivor and duplicate local keys mapped to global IDs.
|
|
1695
|
+
* @returns One animation per directed pair with its original tracker duration and metadata.
|
|
1261
1696
|
*/
|
|
1262
1697
|
function remapAnimations(segmentResults, globalIdMap) {
|
|
1263
|
-
const animations =
|
|
1698
|
+
const animations = /* @__PURE__ */ new Map();
|
|
1264
1699
|
for (const result of segmentResults) for (const anim of result.animations) {
|
|
1265
1700
|
const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
|
|
1266
1701
|
const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
|
|
1267
1702
|
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1268
|
-
|
|
1703
|
+
if (newStartId === newEndId) continue;
|
|
1704
|
+
const animationKey = `${newStartId}-${newEndId}`;
|
|
1705
|
+
if (animations.has(animationKey)) continue;
|
|
1706
|
+
animations.set(animationKey, {
|
|
1269
1707
|
...anim,
|
|
1270
1708
|
startFrameId: newStartId,
|
|
1271
1709
|
endFrameId: newEndId
|
|
1272
1710
|
});
|
|
1273
1711
|
}
|
|
1274
|
-
return animations;
|
|
1712
|
+
return [...animations.values()];
|
|
1275
1713
|
}
|
|
1276
1714
|
/**
|
|
1277
1715
|
* Merge multiple segment results into a single unified frame/edge/animation set.
|
|
1278
|
-
*
|
|
1279
|
-
* -
|
|
1280
|
-
*
|
|
1281
|
-
* - Duplicate edges keep higher score
|
|
1716
|
+
* @param segmentResults Local frames, edges and tracker entries with distinct segment indices.
|
|
1717
|
+
* @returns Global timestamp-ordered frames, aliased edges and animations without self loops.
|
|
1718
|
+
* Duplicate edges keep the higher score; duplicate animations keep the first tracker entry.
|
|
1282
1719
|
*/
|
|
1283
1720
|
function mergeSegmentFrames(segmentResults) {
|
|
1284
1721
|
if (segmentResults.length === 0) return {
|
|
1285
1722
|
frames: [],
|
|
1286
1723
|
edges: [],
|
|
1287
|
-
animations: []
|
|
1724
|
+
animations: [],
|
|
1725
|
+
analysisResolution: {
|
|
1726
|
+
width: 0,
|
|
1727
|
+
height: 0
|
|
1728
|
+
}
|
|
1288
1729
|
};
|
|
1289
1730
|
const effectiveFps = segmentResults[0].segment.effectiveFps;
|
|
1290
|
-
const {
|
|
1731
|
+
const { unique, aliases } = deduplicateFrames(collectAllFrames(segmentResults), effectiveFps);
|
|
1732
|
+
const { frames, globalIdMap } = remapFrameIds(unique, aliases);
|
|
1291
1733
|
return {
|
|
1292
1734
|
frames,
|
|
1293
1735
|
edges: remapEdges(segmentResults, globalIdMap),
|
|
1294
|
-
animations: remapAnimations(segmentResults, globalIdMap)
|
|
1736
|
+
animations: remapAnimations(segmentResults, globalIdMap),
|
|
1737
|
+
analysisResolution: segmentResults[0].analysisResolution
|
|
1295
1738
|
};
|
|
1296
1739
|
}
|
|
1297
1740
|
function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
|
|
@@ -1302,6 +1745,7 @@ function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOpti
|
|
|
1302
1745
|
maxFrames: segment.allocatedFrames
|
|
1303
1746
|
},
|
|
1304
1747
|
workspacePath: segmentWorkspacePath,
|
|
1748
|
+
effectiveFps: segment.effectiveFps,
|
|
1305
1749
|
frames,
|
|
1306
1750
|
graph: [],
|
|
1307
1751
|
status: "ANALYZING",
|
|
@@ -1313,19 +1757,24 @@ function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOpti
|
|
|
1313
1757
|
* Each segment uses an isolated workspace directory.
|
|
1314
1758
|
*/
|
|
1315
1759
|
async function processSegment(inputPath, segment, workspacePath, resolvedOptions, onProgress) {
|
|
1316
|
-
const frames = await extractFramesForRange(inputPath, (0, node_path.join)(workspacePath, "frames"), segment.effectiveFps, resolvedOptions.scale, segment.extractStartTime, segment.extractDuration);
|
|
1760
|
+
const frames = await extractFramesForRange(inputPath, (0, node_path.join)(workspacePath, "frames"), segment.effectiveFps, resolvedOptions.scale, segment.extractStartTime, segment.extractDuration, segment.allocatedFrames);
|
|
1317
1761
|
if (frames.length < 2) return {
|
|
1318
1762
|
segment,
|
|
1319
1763
|
frames,
|
|
1320
1764
|
edges: [],
|
|
1321
|
-
animations: []
|
|
1765
|
+
animations: [],
|
|
1766
|
+
analysisResolution: {
|
|
1767
|
+
width: 0,
|
|
1768
|
+
height: 0
|
|
1769
|
+
}
|
|
1322
1770
|
};
|
|
1323
|
-
const { edges, animations } = await analyzeFrames(buildSegmentContext(segment, frames, workspacePath, resolvedOptions, onProgress));
|
|
1771
|
+
const { edges, animations, analysisResolution } = await analyzeFrames(buildSegmentContext(segment, frames, workspacePath, resolvedOptions, onProgress));
|
|
1324
1772
|
return {
|
|
1325
1773
|
segment,
|
|
1326
1774
|
frames,
|
|
1327
1775
|
edges,
|
|
1328
|
-
animations
|
|
1776
|
+
animations,
|
|
1777
|
+
analysisResolution
|
|
1329
1778
|
};
|
|
1330
1779
|
}
|
|
1331
1780
|
/**
|
|
@@ -1365,7 +1814,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1365
1814
|
});
|
|
1366
1815
|
})));
|
|
1367
1816
|
options.onProgress?.("ANALYZING", 100);
|
|
1368
|
-
const { frames, edges, animations } = mergeSegmentFrames(results);
|
|
1817
|
+
const { frames, edges, animations, analysisResolution } = mergeSegmentFrames(results);
|
|
1369
1818
|
logger.debug(`Merged: ${frames.length} frames, ${edges.length} edges, ${animations.length} animations`);
|
|
1370
1819
|
options.onProgress?.("PRUNING", 0);
|
|
1371
1820
|
const survivingIds = pruneByThresholdWithCap(edges, frames, resolvedOptions.threshold, resolvedOptions.count);
|
|
@@ -1374,6 +1823,9 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1374
1823
|
options.onProgress?.("FINALIZING", 0);
|
|
1375
1824
|
const ctx = {
|
|
1376
1825
|
options: resolvedOptions,
|
|
1826
|
+
effectiveFps: segments[0]?.effectiveFps,
|
|
1827
|
+
sourceDurationSec: totalDuration,
|
|
1828
|
+
analysisResolution,
|
|
1377
1829
|
workspacePath: mainWorkspace,
|
|
1378
1830
|
frames,
|
|
1379
1831
|
graph: edges,
|
|
@@ -1387,21 +1839,14 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1387
1839
|
else outputFiles = await finalizeOutput(ctx, prunedFrames);
|
|
1388
1840
|
options.onProgress?.("FINALIZING", 100);
|
|
1389
1841
|
logger.success(`Segmented pipeline: ${prunedFrames.length} scenes from ${frames.length} frames (${segments.length} segments)`);
|
|
1842
|
+
const outputMetadata = await buildVideoMetadata(ctx, prunedFrames, analysisResolution);
|
|
1390
1843
|
return {
|
|
1391
1844
|
success: true,
|
|
1392
1845
|
originalFramesCount: frames.length,
|
|
1393
1846
|
prunedFramesCount: prunedFrames.length,
|
|
1394
1847
|
outputFiles,
|
|
1395
1848
|
outputBuffers,
|
|
1396
|
-
|
|
1397
|
-
video: {
|
|
1398
|
-
originalDurationMs: totalDuration * 1e3,
|
|
1399
|
-
fps: resolvedOptions.fps,
|
|
1400
|
-
resolution: {
|
|
1401
|
-
width: resolvedOptions.scale,
|
|
1402
|
-
height: Math.round(resolvedOptions.scale * 9 / 16)
|
|
1403
|
-
}
|
|
1404
|
-
},
|
|
1849
|
+
...outputMetadata,
|
|
1405
1850
|
executionTimeMs: Date.now() - pipelineStart
|
|
1406
1851
|
};
|
|
1407
1852
|
} catch (error) {
|
|
@@ -1413,11 +1858,27 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1413
1858
|
else logger.debug(`Debug mode: workspace preserved at ${mainWorkspace}`);
|
|
1414
1859
|
}
|
|
1415
1860
|
}
|
|
1861
|
+
var init_segmenter$1 = __esmMin((() => {
|
|
1862
|
+
init_concurrency();
|
|
1863
|
+
init_logger();
|
|
1864
|
+
init_analyzer();
|
|
1865
|
+
init_build_video_metadata();
|
|
1866
|
+
init_extractor();
|
|
1867
|
+
init_input_resolver();
|
|
1868
|
+
init_pruner();
|
|
1869
|
+
init_workspace();
|
|
1870
|
+
}));
|
|
1871
|
+
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/core/segmenter/index.ts
|
|
1874
|
+
var init_segmenter = __esmMin((() => {
|
|
1875
|
+
init_segmenter$1();
|
|
1876
|
+
}));
|
|
1416
1877
|
|
|
1417
1878
|
//#endregion
|
|
1418
|
-
//#region src/core/orchestrator.ts
|
|
1879
|
+
//#region src/core/orchestrator/orchestrator.ts
|
|
1419
1880
|
async function runPipeline(options) {
|
|
1420
|
-
|
|
1881
|
+
setDebugMode(options.debug ?? false);
|
|
1421
1882
|
const resolvedOptions = resolveOptions(options);
|
|
1422
1883
|
if (shouldSegment(resolvedOptions, options)) return runSegmentedPipeline(options, resolvedOptions);
|
|
1423
1884
|
const startTime = Date.now();
|
|
@@ -1437,19 +1898,27 @@ async function runPipeline(options) {
|
|
|
1437
1898
|
logger.debug(`Workspace created: ${ctx.workspacePath}`);
|
|
1438
1899
|
ctx.status = "EXTRACTING";
|
|
1439
1900
|
const { frames: resolvedFrames, resolvedInputPath } = await resolveInput(options, ctx.workspacePath);
|
|
1440
|
-
if (resolvedOptions.mode === "frames")
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1901
|
+
if (resolvedOptions.mode === "frames") {
|
|
1902
|
+
ctx.frames = resolvedFrames;
|
|
1903
|
+
ctx.effectiveFps = 1;
|
|
1904
|
+
} else {
|
|
1905
|
+
const extractCtx = {
|
|
1906
|
+
...ctx,
|
|
1907
|
+
options: {
|
|
1908
|
+
...resolvedOptions,
|
|
1909
|
+
inputPath: resolvedInputPath
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
ctx.frames = await extractFrames(extractCtx);
|
|
1913
|
+
ctx.effectiveFps = extractCtx.effectiveFps;
|
|
1914
|
+
ctx.sourceDurationSec = extractCtx.sourceDurationSec;
|
|
1915
|
+
}
|
|
1448
1916
|
ctx.emitProgress(100);
|
|
1449
1917
|
ctx.status = "ANALYZING";
|
|
1450
|
-
const { edges, animations } = await analyzeFrames(ctx);
|
|
1918
|
+
const { edges, animations, analysisResolution } = await analyzeFrames(ctx);
|
|
1451
1919
|
ctx.graph = edges;
|
|
1452
1920
|
ctx.animations = animations;
|
|
1921
|
+
ctx.analysisResolution = analysisResolution;
|
|
1453
1922
|
ctx.status = "PRUNING";
|
|
1454
1923
|
const survivingIds = pruneByThresholdWithCap(ctx.graph, ctx.frames, resolvedOptions.threshold, resolvedOptions.count);
|
|
1455
1924
|
const prunedFrames = (0, _winglet_common_utils.filter)(ctx.frames, (f) => survivingIds.has(f.id));
|
|
@@ -1466,21 +1935,14 @@ async function runPipeline(options) {
|
|
|
1466
1935
|
}
|
|
1467
1936
|
ctx.status = "SUCCESS";
|
|
1468
1937
|
logger.success(`Extracted ${prunedFrames.length} scenes from ${ctx.frames.length} frames`);
|
|
1938
|
+
const metadata = await buildVideoMetadata(ctx, prunedFrames, analysisResolution);
|
|
1469
1939
|
return {
|
|
1470
1940
|
success: true,
|
|
1471
1941
|
originalFramesCount: ctx.frames.length,
|
|
1472
1942
|
prunedFramesCount: prunedFrames.length,
|
|
1473
1943
|
outputFiles,
|
|
1474
1944
|
outputBuffers,
|
|
1475
|
-
|
|
1476
|
-
video: {
|
|
1477
|
-
originalDurationMs: ctx.frames.length / ctx.options.fps * 1e3,
|
|
1478
|
-
fps: ctx.options.fps,
|
|
1479
|
-
resolution: {
|
|
1480
|
-
width: ctx.options.scale,
|
|
1481
|
-
height: Math.round(ctx.options.scale * 9 / 16)
|
|
1482
|
-
}
|
|
1483
|
-
},
|
|
1945
|
+
...metadata,
|
|
1484
1946
|
executionTimeMs: Date.now() - startTime
|
|
1485
1947
|
};
|
|
1486
1948
|
} catch (error) {
|
|
@@ -1493,6 +1955,20 @@ async function runPipeline(options) {
|
|
|
1493
1955
|
else logger.debug(`Debug mode: workspace preserved at ${ctx.workspacePath}`);
|
|
1494
1956
|
}
|
|
1495
1957
|
}
|
|
1958
|
+
var init_orchestrator = __esmMin((() => {
|
|
1959
|
+
init_logger();
|
|
1960
|
+
init_analyzer();
|
|
1961
|
+
init_build_video_metadata();
|
|
1962
|
+
init_extractor();
|
|
1963
|
+
init_input_resolver();
|
|
1964
|
+
init_pruner();
|
|
1965
|
+
init_segmenter();
|
|
1966
|
+
init_workspace();
|
|
1967
|
+
}));
|
|
1968
|
+
|
|
1969
|
+
//#endregion
|
|
1970
|
+
//#region src/index.ts
|
|
1971
|
+
init_orchestrator();
|
|
1496
1972
|
|
|
1497
1973
|
//#endregion
|
|
1498
1974
|
exports.extractScenes = runPipeline;
|