@lumy-pack/scene-sieve 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/cli.mjs +708 -86
- package/dist/index.cjs +382 -4
- package/dist/index.mjs +382 -4
- package/dist/pipeline-worker.mjs +382 -4
- package/package.json +3 -2
- package/dist/cli.d.ts +0 -1
- package/dist/commands/Sieve.d.ts +0 -15
- package/dist/components/PhaseStep.d.ts +0 -14
- package/dist/components/ProgressBar.d.ts +0 -7
- package/dist/constants.d.ts +0 -30
- package/dist/core/analyzer.d.ts +0 -62
- package/dist/core/dbscan.d.ts +0 -10
- package/dist/core/extractor.d.ts +0 -7
- package/dist/core/index.d.ts +0 -8
- package/dist/core/input-resolver.d.ts +0 -13
- package/dist/core/orchestrator.d.ts +0 -2
- package/dist/core/pipeline-worker.d.ts +0 -1
- package/dist/core/pruner.d.ts +0 -61
- package/dist/core/run-in-worker.d.ts +0 -9
- package/dist/core/workspace.d.ts +0 -24
- package/dist/index.d.ts +0 -2
- package/dist/types/index.d.ts +0 -107
- package/dist/utils/logger.d.ts +0 -8
- package/dist/utils/math.d.ts +0 -27
- package/dist/utils/min-heap.d.ts +0 -16
- package/dist/utils/paths.d.ts +0 -18
package/dist/cli.mjs
CHANGED
|
@@ -9,49 +9,13 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/utils/logger.ts
|
|
13
|
-
import pc from "picocolors";
|
|
14
|
-
function setDebugMode(enabled) {
|
|
15
|
-
debugMode = enabled;
|
|
16
|
-
}
|
|
17
|
-
function timestamp() {
|
|
18
|
-
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
19
|
-
}
|
|
20
|
-
var debugMode, logger;
|
|
21
|
-
var init_logger = __esm({
|
|
22
|
-
"src/utils/logger.ts"() {
|
|
23
|
-
"use strict";
|
|
24
|
-
debugMode = false;
|
|
25
|
-
logger = {
|
|
26
|
-
info(message) {
|
|
27
|
-
console.log(`${pc.blue("info")} ${message}`);
|
|
28
|
-
},
|
|
29
|
-
success(message) {
|
|
30
|
-
console.log(`
|
|
31
|
-
${pc.green("done")} ${message}`);
|
|
32
|
-
},
|
|
33
|
-
warn(message) {
|
|
34
|
-
console.warn(`${pc.yellow("warn")} ${message}`);
|
|
35
|
-
},
|
|
36
|
-
error(message) {
|
|
37
|
-
console.error(`${pc.red("error")} ${message}`);
|
|
38
|
-
},
|
|
39
|
-
debug(message) {
|
|
40
|
-
if (debugMode) {
|
|
41
|
-
console.log(`${pc.gray(`[${timestamp()}] debug`)} ${message}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
12
|
// src/constants.ts
|
|
49
13
|
import { tmpdir } from "os";
|
|
50
14
|
import { join } from "path";
|
|
51
15
|
function getTempWorkspaceDir(sessionId) {
|
|
52
16
|
return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
53
17
|
}
|
|
54
|
-
var APP_NAME, DEFAULT_COUNT, DEFAULT_THRESHOLD, DEFAULT_FPS, DEFAULT_SCALE, DEFAULT_QUALITY, DEFAULT_MAX_FRAMES, NORMALIZATION_LOGISTIC_K, NORMALIZATION_ALPHA, NORMALIZATION_MAD_COEFFICIENT, NORMALIZATION_MIN_SAMPLE_SIZE, WORKSPACE_PREFIX, TEMP_BASE_DIR, FRAME_OUTPUT_EXTENSION, FRAME_FILENAME_PATTERN, OPENCV_BATCH_SIZE, DBSCAN_ALPHA, DBSCAN_MIN_PTS, IOU_THRESHOLD, DECAY_LAMBDA, ANIMATION_FRAME_THRESHOLD, MATCH_DISTANCE_THRESHOLD, PIXELDIFF_GAUSSIAN_KERNEL, PIXELDIFF_BINARY_THRESHOLD, PIXELDIFF_CONTOUR_MIN_AREA, PIXELDIFF_SAMPLE_SPACING;
|
|
18
|
+
var APP_NAME, DEFAULT_COUNT, DEFAULT_THRESHOLD, DEFAULT_FPS, DEFAULT_SCALE, DEFAULT_QUALITY, DEFAULT_MAX_FRAMES, NORMALIZATION_LOGISTIC_K, NORMALIZATION_ALPHA, NORMALIZATION_MAD_COEFFICIENT, NORMALIZATION_MIN_SAMPLE_SIZE, WORKSPACE_PREFIX, TEMP_BASE_DIR, FRAME_OUTPUT_EXTENSION, FRAME_FILENAME_PATTERN, OPENCV_BATCH_SIZE, DBSCAN_ALPHA, DBSCAN_MIN_PTS, IOU_THRESHOLD, DECAY_LAMBDA, ANIMATION_FRAME_THRESHOLD, MATCH_DISTANCE_THRESHOLD, PIXELDIFF_GAUSSIAN_KERNEL, PIXELDIFF_BINARY_THRESHOLD, PIXELDIFF_CONTOUR_MIN_AREA, PIXELDIFF_SAMPLE_SPACING, DEFAULT_MAX_SEGMENT_DURATION, DEFAULT_SEGMENT_CONCURRENCY;
|
|
55
19
|
var init_constants = __esm({
|
|
56
20
|
"src/constants.ts"() {
|
|
57
21
|
"use strict";
|
|
@@ -81,6 +45,44 @@ var init_constants = __esm({
|
|
|
81
45
|
PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
82
46
|
PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
83
47
|
PIXELDIFF_SAMPLE_SPACING = 8;
|
|
48
|
+
DEFAULT_MAX_SEGMENT_DURATION = 300;
|
|
49
|
+
DEFAULT_SEGMENT_CONCURRENCY = 2;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// src/utils/logger.ts
|
|
54
|
+
import pc from "picocolors";
|
|
55
|
+
function setDebugMode(enabled) {
|
|
56
|
+
debugMode = enabled;
|
|
57
|
+
}
|
|
58
|
+
function timestamp() {
|
|
59
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
60
|
+
}
|
|
61
|
+
var debugMode, logger;
|
|
62
|
+
var init_logger = __esm({
|
|
63
|
+
"src/utils/logger.ts"() {
|
|
64
|
+
"use strict";
|
|
65
|
+
debugMode = false;
|
|
66
|
+
logger = {
|
|
67
|
+
info(message) {
|
|
68
|
+
console.log(`${pc.blue("info")} ${message}`);
|
|
69
|
+
},
|
|
70
|
+
success(message) {
|
|
71
|
+
console.log(`
|
|
72
|
+
${pc.green("done")} ${message}`);
|
|
73
|
+
},
|
|
74
|
+
warn(message) {
|
|
75
|
+
console.warn(`${pc.yellow("warn")} ${message}`);
|
|
76
|
+
},
|
|
77
|
+
error(message) {
|
|
78
|
+
console.error(`${pc.red("error")} ${message}`);
|
|
79
|
+
},
|
|
80
|
+
debug(message) {
|
|
81
|
+
if (debugMode) {
|
|
82
|
+
console.log(`${pc.gray(`[${timestamp()}] debug`)} ${message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
84
86
|
}
|
|
85
87
|
});
|
|
86
88
|
|
|
@@ -693,6 +695,23 @@ async function buildFrameList(framesDir, duration) {
|
|
|
693
695
|
extractPath: join2(framesDir, file)
|
|
694
696
|
}));
|
|
695
697
|
}
|
|
698
|
+
async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime, duration) {
|
|
699
|
+
const outputPattern = join2(outputDir, FRAME_FILENAME_PATTERN);
|
|
700
|
+
await execa(ffmpegPath, [
|
|
701
|
+
"-ss",
|
|
702
|
+
String(startTime),
|
|
703
|
+
"-i",
|
|
704
|
+
inputPath,
|
|
705
|
+
"-t",
|
|
706
|
+
String(duration),
|
|
707
|
+
"-vf",
|
|
708
|
+
`fps=${fps},scale=-1:${scale}`,
|
|
709
|
+
"-q:v",
|
|
710
|
+
"2",
|
|
711
|
+
outputPattern
|
|
712
|
+
]);
|
|
713
|
+
return buildFrameList(outputDir, duration);
|
|
714
|
+
}
|
|
696
715
|
var init_extractor = __esm({
|
|
697
716
|
"src/core/extractor.ts"() {
|
|
698
717
|
"use strict";
|
|
@@ -760,6 +779,15 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
760
779
|
await rename(stagingDir, outputPath);
|
|
761
780
|
return outputFiles;
|
|
762
781
|
}
|
|
782
|
+
async function createSegmentWorkspace(parentWorkspacePath, segmentIndex) {
|
|
783
|
+
const segmentPath = join3(
|
|
784
|
+
parentWorkspacePath,
|
|
785
|
+
"segments",
|
|
786
|
+
String(segmentIndex)
|
|
787
|
+
);
|
|
788
|
+
await ensureDir(join3(segmentPath, "frames"));
|
|
789
|
+
return segmentPath;
|
|
790
|
+
}
|
|
763
791
|
async function cleanupWorkspace(workspacePath) {
|
|
764
792
|
if (!workspacePath) return;
|
|
765
793
|
try {
|
|
@@ -844,7 +872,9 @@ function resolveOptions(options) {
|
|
|
844
872
|
quality: options.quality ?? DEFAULT_QUALITY,
|
|
845
873
|
iouThreshold: options.iouThreshold ?? IOU_THRESHOLD,
|
|
846
874
|
animationThreshold: options.animationThreshold ?? ANIMATION_FRAME_THRESHOLD,
|
|
847
|
-
debug: options.debug ?? false
|
|
875
|
+
debug: options.debug ?? false,
|
|
876
|
+
maxSegmentDuration: options.maxSegmentDuration ?? DEFAULT_MAX_SEGMENT_DURATION,
|
|
877
|
+
concurrency: options.concurrency ?? DEFAULT_SEGMENT_CONCURRENCY
|
|
848
878
|
};
|
|
849
879
|
}
|
|
850
880
|
async function resolveInput(options, workspacePath) {
|
|
@@ -1123,18 +1153,381 @@ var init_pruner = __esm({
|
|
|
1123
1153
|
}
|
|
1124
1154
|
});
|
|
1125
1155
|
|
|
1156
|
+
// src/utils/concurrency.ts
|
|
1157
|
+
function concurrencyLimit(limit) {
|
|
1158
|
+
limit = Math.max(1, limit);
|
|
1159
|
+
let active = 0;
|
|
1160
|
+
const queue = [];
|
|
1161
|
+
return async (fn) => {
|
|
1162
|
+
while (active >= limit) {
|
|
1163
|
+
await new Promise((resolve2) => queue.push(resolve2));
|
|
1164
|
+
}
|
|
1165
|
+
active++;
|
|
1166
|
+
try {
|
|
1167
|
+
return await fn();
|
|
1168
|
+
} finally {
|
|
1169
|
+
active--;
|
|
1170
|
+
queue.shift()?.();
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
var init_concurrency = __esm({
|
|
1175
|
+
"src/utils/concurrency.ts"() {
|
|
1176
|
+
"use strict";
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// src/core/segmenter.ts
|
|
1181
|
+
import { randomUUID } from "crypto";
|
|
1182
|
+
import { join as join5 } from "path";
|
|
1183
|
+
function shouldSegment(resolvedOptions, originalOptions) {
|
|
1184
|
+
if (resolvedOptions.mode === "frames") return false;
|
|
1185
|
+
if (originalOptions.mode === "file") {
|
|
1186
|
+
if (originalOptions.inputPath.toLowerCase().endsWith(".gif")) return false;
|
|
1187
|
+
}
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
|
|
1191
|
+
const effectiveFps = Math.max(0.5, Math.min(fps, maxFrames / totalDuration));
|
|
1192
|
+
if (totalDuration <= maxSegmentDuration) {
|
|
1193
|
+
return [
|
|
1194
|
+
{
|
|
1195
|
+
index: 0,
|
|
1196
|
+
startTime: 0,
|
|
1197
|
+
endTime: totalDuration,
|
|
1198
|
+
duration: totalDuration,
|
|
1199
|
+
allocatedFrames: Math.min(
|
|
1200
|
+
Math.ceil(effectiveFps * totalDuration),
|
|
1201
|
+
maxFrames
|
|
1202
|
+
),
|
|
1203
|
+
effectiveFps,
|
|
1204
|
+
overlapBefore: 0,
|
|
1205
|
+
overlapAfter: 0,
|
|
1206
|
+
extractStartTime: 0,
|
|
1207
|
+
extractDuration: totalDuration
|
|
1208
|
+
}
|
|
1209
|
+
];
|
|
1210
|
+
}
|
|
1211
|
+
const segmentCount = Math.ceil(totalDuration / maxSegmentDuration);
|
|
1212
|
+
const overlapTime = 1 / effectiveFps;
|
|
1213
|
+
const segments = [];
|
|
1214
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
1215
|
+
const startTime = i * maxSegmentDuration;
|
|
1216
|
+
const endTime = Math.min((i + 1) * maxSegmentDuration, totalDuration);
|
|
1217
|
+
const duration = endTime - startTime;
|
|
1218
|
+
const overlapBefore = i > 0 ? 1 : 0;
|
|
1219
|
+
const overlapAfter = i < segmentCount - 1 ? 1 : 0;
|
|
1220
|
+
const extractStartTime = Math.max(
|
|
1221
|
+
0,
|
|
1222
|
+
startTime - overlapBefore * overlapTime
|
|
1223
|
+
);
|
|
1224
|
+
const extractEndTime = Math.min(
|
|
1225
|
+
totalDuration,
|
|
1226
|
+
endTime + overlapAfter * overlapTime
|
|
1227
|
+
);
|
|
1228
|
+
const extractDuration = extractEndTime - extractStartTime;
|
|
1229
|
+
segments.push({
|
|
1230
|
+
index: i,
|
|
1231
|
+
startTime,
|
|
1232
|
+
endTime,
|
|
1233
|
+
duration,
|
|
1234
|
+
allocatedFrames: Math.ceil(effectiveFps * duration),
|
|
1235
|
+
effectiveFps,
|
|
1236
|
+
overlapBefore,
|
|
1237
|
+
overlapAfter,
|
|
1238
|
+
extractStartTime,
|
|
1239
|
+
extractDuration
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
const totalAllocated = segments.reduce(
|
|
1243
|
+
(sum, s) => sum + s.allocatedFrames,
|
|
1244
|
+
0
|
|
1245
|
+
);
|
|
1246
|
+
if (totalAllocated > maxFrames) {
|
|
1247
|
+
segments[segments.length - 1].allocatedFrames -= totalAllocated - maxFrames;
|
|
1248
|
+
}
|
|
1249
|
+
return segments;
|
|
1250
|
+
}
|
|
1251
|
+
function collectAllFrames(segmentResults) {
|
|
1252
|
+
const allFrames = [];
|
|
1253
|
+
for (const result of segmentResults) {
|
|
1254
|
+
for (const frame of result.frames) {
|
|
1255
|
+
allFrames.push({
|
|
1256
|
+
frame: {
|
|
1257
|
+
...frame,
|
|
1258
|
+
// Use extractStartTime for timestamp correction (Section 18 note 1)
|
|
1259
|
+
timestamp: frame.timestamp + result.segment.extractStartTime
|
|
1260
|
+
},
|
|
1261
|
+
segmentIndex: result.segment.index,
|
|
1262
|
+
localId: frame.id
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return allFrames;
|
|
1267
|
+
}
|
|
1268
|
+
function deduplicateFrames(frames, effectiveFps) {
|
|
1269
|
+
frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
|
|
1270
|
+
const dupThreshold = 1 / (effectiveFps * 2);
|
|
1271
|
+
const unique = [];
|
|
1272
|
+
for (const entry of frames) {
|
|
1273
|
+
if (unique.length > 0) {
|
|
1274
|
+
const last = unique[unique.length - 1];
|
|
1275
|
+
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
unique.push(entry);
|
|
1280
|
+
}
|
|
1281
|
+
return unique;
|
|
1282
|
+
}
|
|
1283
|
+
function remapFrameIds(uniqueFrames) {
|
|
1284
|
+
const globalIdMap = /* @__PURE__ */ new Map();
|
|
1285
|
+
const frames = uniqueFrames.map((entry, globalId) => {
|
|
1286
|
+
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
1287
|
+
return {
|
|
1288
|
+
id: globalId,
|
|
1289
|
+
timestamp: entry.frame.timestamp,
|
|
1290
|
+
extractPath: entry.frame.extractPath
|
|
1291
|
+
};
|
|
1292
|
+
});
|
|
1293
|
+
return { frames, globalIdMap };
|
|
1294
|
+
}
|
|
1295
|
+
function remapEdges(segmentResults, globalIdMap) {
|
|
1296
|
+
const edges = [];
|
|
1297
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1298
|
+
for (const result of segmentResults) {
|
|
1299
|
+
for (const edge of result.edges) {
|
|
1300
|
+
const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
|
|
1301
|
+
const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
|
|
1302
|
+
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1303
|
+
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1304
|
+
const existingIdx = edgeMap.get(edgeKey);
|
|
1305
|
+
if (existingIdx !== void 0) {
|
|
1306
|
+
if (edges[existingIdx].score < edge.score) {
|
|
1307
|
+
edges[existingIdx] = { sourceId: newSourceId, targetId: newTargetId, score: edge.score };
|
|
1308
|
+
}
|
|
1309
|
+
} else {
|
|
1310
|
+
edgeMap.set(edgeKey, edges.length);
|
|
1311
|
+
edges.push({ sourceId: newSourceId, targetId: newTargetId, score: edge.score });
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return edges;
|
|
1316
|
+
}
|
|
1317
|
+
function remapAnimations(segmentResults, globalIdMap) {
|
|
1318
|
+
const animations = [];
|
|
1319
|
+
for (const result of segmentResults) {
|
|
1320
|
+
for (const anim of result.animations) {
|
|
1321
|
+
const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
|
|
1322
|
+
const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
|
|
1323
|
+
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1324
|
+
animations.push({ ...anim, startFrameId: newStartId, endFrameId: newEndId });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return animations;
|
|
1328
|
+
}
|
|
1329
|
+
function mergeSegmentFrames(segmentResults) {
|
|
1330
|
+
if (segmentResults.length === 0) {
|
|
1331
|
+
return { frames: [], edges: [], animations: [] };
|
|
1332
|
+
}
|
|
1333
|
+
const effectiveFps = segmentResults[0].segment.effectiveFps;
|
|
1334
|
+
const allFrames = collectAllFrames(segmentResults);
|
|
1335
|
+
const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
|
|
1336
|
+
const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
|
|
1337
|
+
const edges = remapEdges(segmentResults, globalIdMap);
|
|
1338
|
+
const animations = remapAnimations(segmentResults, globalIdMap);
|
|
1339
|
+
return { frames, edges, animations };
|
|
1340
|
+
}
|
|
1341
|
+
function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
|
|
1342
|
+
return {
|
|
1343
|
+
options: {
|
|
1344
|
+
...resolvedOptions,
|
|
1345
|
+
fps: segment.effectiveFps,
|
|
1346
|
+
maxFrames: segment.allocatedFrames
|
|
1347
|
+
},
|
|
1348
|
+
workspacePath: segmentWorkspacePath,
|
|
1349
|
+
frames,
|
|
1350
|
+
graph: [],
|
|
1351
|
+
status: "ANALYZING",
|
|
1352
|
+
emitProgress: onProgress
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
async function processSegment(inputPath, segment, workspacePath, resolvedOptions, onProgress) {
|
|
1356
|
+
const framesDir = join5(workspacePath, "frames");
|
|
1357
|
+
const frames = await extractFramesForRange(
|
|
1358
|
+
inputPath,
|
|
1359
|
+
framesDir,
|
|
1360
|
+
segment.effectiveFps,
|
|
1361
|
+
resolvedOptions.scale,
|
|
1362
|
+
segment.extractStartTime,
|
|
1363
|
+
segment.extractDuration
|
|
1364
|
+
);
|
|
1365
|
+
if (frames.length < 2) {
|
|
1366
|
+
return { segment, frames, edges: [], animations: [] };
|
|
1367
|
+
}
|
|
1368
|
+
const ctx = buildSegmentContext(
|
|
1369
|
+
segment,
|
|
1370
|
+
frames,
|
|
1371
|
+
workspacePath,
|
|
1372
|
+
resolvedOptions,
|
|
1373
|
+
onProgress
|
|
1374
|
+
);
|
|
1375
|
+
const { edges, animations } = await analyzeFrames(ctx);
|
|
1376
|
+
return { segment, frames, edges, animations };
|
|
1377
|
+
}
|
|
1378
|
+
async function runSegmentedPipeline(options, resolvedOptions) {
|
|
1379
|
+
const pipelineStart = Date.now();
|
|
1380
|
+
const sessionId = randomUUID();
|
|
1381
|
+
let mainWorkspace = "";
|
|
1382
|
+
try {
|
|
1383
|
+
mainWorkspace = await createWorkspace(sessionId);
|
|
1384
|
+
logger.debug(`Segmented pipeline: workspace at ${mainWorkspace}`);
|
|
1385
|
+
const { resolvedInputPath } = await resolveInput(options, mainWorkspace);
|
|
1386
|
+
const inputPath = resolvedInputPath ?? resolvedOptions.inputPath;
|
|
1387
|
+
if (!inputPath) {
|
|
1388
|
+
throw new Error("No input path available for segmented pipeline");
|
|
1389
|
+
}
|
|
1390
|
+
const metadata = await getVideoMetadata(inputPath);
|
|
1391
|
+
const totalDuration = parseFloat(metadata.format?.duration ?? "0");
|
|
1392
|
+
if (totalDuration <= 0) {
|
|
1393
|
+
throw new Error(`Invalid video duration: ${totalDuration}`);
|
|
1394
|
+
}
|
|
1395
|
+
logger.debug(`Video duration: ${totalDuration}s`);
|
|
1396
|
+
const segments = computeSegmentPlan(
|
|
1397
|
+
totalDuration,
|
|
1398
|
+
resolvedOptions.maxSegmentDuration,
|
|
1399
|
+
resolvedOptions.maxFrames,
|
|
1400
|
+
resolvedOptions.fps
|
|
1401
|
+
);
|
|
1402
|
+
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1403
|
+
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1404
|
+
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1405
|
+
const weights = segments.map((s) => s.duration / totalDuration);
|
|
1406
|
+
const emitOverallProgress = (phase) => {
|
|
1407
|
+
if (!options.onProgress) return;
|
|
1408
|
+
const overall = weights.reduce(
|
|
1409
|
+
(sum, w, i) => sum + w * (segmentProgresses[i] ?? 0),
|
|
1410
|
+
0
|
|
1411
|
+
);
|
|
1412
|
+
options.onProgress(phase, Math.min(100, overall));
|
|
1413
|
+
};
|
|
1414
|
+
options.onProgress?.("EXTRACTING", 0);
|
|
1415
|
+
const results = await Promise.all(
|
|
1416
|
+
segments.map(
|
|
1417
|
+
(segment) => limit(async () => {
|
|
1418
|
+
const segWorkspace = await createSegmentWorkspace(
|
|
1419
|
+
mainWorkspace,
|
|
1420
|
+
segment.index
|
|
1421
|
+
);
|
|
1422
|
+
const result = await processSegment(
|
|
1423
|
+
inputPath,
|
|
1424
|
+
segment,
|
|
1425
|
+
segWorkspace,
|
|
1426
|
+
resolvedOptions,
|
|
1427
|
+
(percent) => {
|
|
1428
|
+
segmentProgresses[segment.index] = percent;
|
|
1429
|
+
emitOverallProgress("ANALYZING");
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
return result;
|
|
1433
|
+
})
|
|
1434
|
+
)
|
|
1435
|
+
);
|
|
1436
|
+
options.onProgress?.("ANALYZING", 100);
|
|
1437
|
+
const { frames, edges, animations } = mergeSegmentFrames(results);
|
|
1438
|
+
logger.debug(
|
|
1439
|
+
`Merged: ${frames.length} frames, ${edges.length} edges, ${animations.length} animations`
|
|
1440
|
+
);
|
|
1441
|
+
options.onProgress?.("PRUNING", 0);
|
|
1442
|
+
const survivingIds = pruneByThresholdWithCap(
|
|
1443
|
+
edges,
|
|
1444
|
+
frames,
|
|
1445
|
+
resolvedOptions.threshold,
|
|
1446
|
+
resolvedOptions.count
|
|
1447
|
+
);
|
|
1448
|
+
const prunedFrames = frames.filter((f) => survivingIds.has(f.id));
|
|
1449
|
+
options.onProgress?.("PRUNING", 100);
|
|
1450
|
+
options.onProgress?.("FINALIZING", 0);
|
|
1451
|
+
const ctx = {
|
|
1452
|
+
options: resolvedOptions,
|
|
1453
|
+
workspacePath: mainWorkspace,
|
|
1454
|
+
frames,
|
|
1455
|
+
graph: edges,
|
|
1456
|
+
animations,
|
|
1457
|
+
status: "FINALIZING",
|
|
1458
|
+
emitProgress: (percent) => options.onProgress?.("FINALIZING", percent)
|
|
1459
|
+
};
|
|
1460
|
+
let outputFiles = [];
|
|
1461
|
+
let outputBuffers;
|
|
1462
|
+
if (resolvedOptions.mode === "buffer" || resolvedOptions.mode === "frames") {
|
|
1463
|
+
outputBuffers = await readFramesAsBuffers(
|
|
1464
|
+
prunedFrames,
|
|
1465
|
+
resolvedOptions.quality
|
|
1466
|
+
);
|
|
1467
|
+
} else {
|
|
1468
|
+
outputFiles = await finalizeOutput(ctx, prunedFrames);
|
|
1469
|
+
}
|
|
1470
|
+
options.onProgress?.("FINALIZING", 100);
|
|
1471
|
+
logger.success(
|
|
1472
|
+
`Segmented pipeline: ${prunedFrames.length} scenes from ${frames.length} frames (${segments.length} segments)`
|
|
1473
|
+
);
|
|
1474
|
+
return {
|
|
1475
|
+
success: true,
|
|
1476
|
+
originalFramesCount: frames.length,
|
|
1477
|
+
prunedFramesCount: prunedFrames.length,
|
|
1478
|
+
outputFiles,
|
|
1479
|
+
outputBuffers,
|
|
1480
|
+
animations,
|
|
1481
|
+
video: {
|
|
1482
|
+
originalDurationMs: totalDuration * 1e3,
|
|
1483
|
+
fps: resolvedOptions.fps,
|
|
1484
|
+
resolution: {
|
|
1485
|
+
width: resolvedOptions.scale,
|
|
1486
|
+
height: Math.round(resolvedOptions.scale * 9 / 16)
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
executionTimeMs: Date.now() - pipelineStart
|
|
1490
|
+
};
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1493
|
+
logger.error(`Segmented pipeline failed: ${err.message}`);
|
|
1494
|
+
throw err;
|
|
1495
|
+
} finally {
|
|
1496
|
+
if (!resolvedOptions.debug) {
|
|
1497
|
+
await cleanupWorkspace(mainWorkspace);
|
|
1498
|
+
} else {
|
|
1499
|
+
logger.debug(`Debug mode: workspace preserved at ${mainWorkspace}`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
var init_segmenter = __esm({
|
|
1504
|
+
"src/core/segmenter.ts"() {
|
|
1505
|
+
"use strict";
|
|
1506
|
+
init_concurrency();
|
|
1507
|
+
init_logger();
|
|
1508
|
+
init_analyzer();
|
|
1509
|
+
init_extractor();
|
|
1510
|
+
init_input_resolver();
|
|
1511
|
+
init_pruner();
|
|
1512
|
+
init_workspace();
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1126
1516
|
// src/core/orchestrator.ts
|
|
1127
1517
|
var orchestrator_exports = {};
|
|
1128
1518
|
__export(orchestrator_exports, {
|
|
1129
1519
|
runPipeline: () => runPipeline
|
|
1130
1520
|
});
|
|
1131
|
-
import { randomUUID } from "crypto";
|
|
1521
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1132
1522
|
async function runPipeline(options) {
|
|
1133
|
-
const startTime = Date.now();
|
|
1134
|
-
const sessionId = randomUUID();
|
|
1135
1523
|
const debug = options.debug ?? false;
|
|
1136
1524
|
if (debug) setDebugMode(true);
|
|
1137
1525
|
const resolvedOptions = resolveOptions(options);
|
|
1526
|
+
if (shouldSegment(resolvedOptions, options)) {
|
|
1527
|
+
return runSegmentedPipeline(options, resolvedOptions);
|
|
1528
|
+
}
|
|
1529
|
+
const startTime = Date.now();
|
|
1530
|
+
const sessionId = randomUUID2();
|
|
1138
1531
|
const ctx = {
|
|
1139
1532
|
options: resolvedOptions,
|
|
1140
1533
|
workspacePath: "",
|
|
@@ -1236,6 +1629,7 @@ var init_orchestrator = __esm({
|
|
|
1236
1629
|
init_extractor();
|
|
1237
1630
|
init_input_resolver();
|
|
1238
1631
|
init_pruner();
|
|
1632
|
+
init_segmenter();
|
|
1239
1633
|
init_workspace();
|
|
1240
1634
|
}
|
|
1241
1635
|
});
|
|
@@ -1243,12 +1637,41 @@ var init_orchestrator = __esm({
|
|
|
1243
1637
|
// src/cli.ts
|
|
1244
1638
|
import { createRequire as createRequire2 } from "module";
|
|
1245
1639
|
import { Command } from "commander";
|
|
1246
|
-
|
|
1247
|
-
|
|
1640
|
+
|
|
1641
|
+
// ../shared/src/respond.ts
|
|
1642
|
+
function respond(command, data, startTime, version2) {
|
|
1643
|
+
const response = {
|
|
1644
|
+
ok: true,
|
|
1645
|
+
command,
|
|
1646
|
+
data,
|
|
1647
|
+
meta: {
|
|
1648
|
+
version: version2,
|
|
1649
|
+
durationMs: Date.now() - startTime,
|
|
1650
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
1654
|
+
}
|
|
1655
|
+
function respondError(command, code, message, startTime, version2, details) {
|
|
1656
|
+
const response = {
|
|
1657
|
+
ok: false,
|
|
1658
|
+
command,
|
|
1659
|
+
error: { code, message, ...details !== void 0 ? { details } : {} },
|
|
1660
|
+
meta: {
|
|
1661
|
+
version: version2,
|
|
1662
|
+
durationMs: Date.now() - startTime,
|
|
1663
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
1667
|
+
process.exitCode = 1;
|
|
1668
|
+
}
|
|
1248
1669
|
|
|
1249
1670
|
// src/commands/Sieve.tsx
|
|
1671
|
+
import { existsSync } from "fs";
|
|
1250
1672
|
import { Box as Box2, Text as Text3, useApp } from "ink";
|
|
1251
|
-
import {
|
|
1673
|
+
import { render } from "ink";
|
|
1674
|
+
import React, { useEffect, useState } from "react";
|
|
1252
1675
|
|
|
1253
1676
|
// src/components/PhaseStep.tsx
|
|
1254
1677
|
import { Box, Text as Text2 } from "ink";
|
|
@@ -1309,8 +1732,12 @@ var PhaseStep = ({ phase }) => {
|
|
|
1309
1732
|
] });
|
|
1310
1733
|
};
|
|
1311
1734
|
|
|
1735
|
+
// src/commands/Sieve.tsx
|
|
1736
|
+
init_constants();
|
|
1737
|
+
init_orchestrator();
|
|
1738
|
+
|
|
1312
1739
|
// src/core/run-in-worker.ts
|
|
1313
|
-
import { dirname, join as
|
|
1740
|
+
import { dirname, join as join6 } from "path";
|
|
1314
1741
|
import { fileURLToPath } from "url";
|
|
1315
1742
|
import { Worker } from "worker_threads";
|
|
1316
1743
|
async function runPipelineInWorker(options, onProgress) {
|
|
@@ -1319,7 +1746,7 @@ async function runPipelineInWorker(options, onProgress) {
|
|
|
1319
1746
|
const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_orchestrator(), orchestrator_exports));
|
|
1320
1747
|
return runPipeline2({ ...options, onProgress });
|
|
1321
1748
|
}
|
|
1322
|
-
const workerPath =
|
|
1749
|
+
const workerPath = join6(dirname(currentFile), "pipeline-worker.mjs");
|
|
1323
1750
|
return new Promise((resolve2, reject) => {
|
|
1324
1751
|
const worker = new Worker(workerPath, { workerData: options });
|
|
1325
1752
|
worker.on(
|
|
@@ -1347,6 +1774,145 @@ async function runPipelineInWorker(options, onProgress) {
|
|
|
1347
1774
|
|
|
1348
1775
|
// src/commands/Sieve.tsx
|
|
1349
1776
|
init_workspace();
|
|
1777
|
+
|
|
1778
|
+
// src/errors.ts
|
|
1779
|
+
var SieveErrorCode = {
|
|
1780
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
1781
|
+
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
1782
|
+
INVALID_FORMAT: "INVALID_FORMAT",
|
|
1783
|
+
PIPELINE_ERROR: "PIPELINE_ERROR",
|
|
1784
|
+
WORKER_ERROR: "WORKER_ERROR",
|
|
1785
|
+
UNKNOWN: "UNKNOWN"
|
|
1786
|
+
};
|
|
1787
|
+
function classifyError(error) {
|
|
1788
|
+
const msg = error.message.toLowerCase();
|
|
1789
|
+
if (error.code === "ENOENT" || msg.includes("not found")) {
|
|
1790
|
+
return SieveErrorCode.FILE_NOT_FOUND;
|
|
1791
|
+
} else if (msg.includes("no video stream") || msg.includes("invalid format")) {
|
|
1792
|
+
return SieveErrorCode.INVALID_FORMAT;
|
|
1793
|
+
} else if (msg.includes("worker")) {
|
|
1794
|
+
return SieveErrorCode.WORKER_ERROR;
|
|
1795
|
+
} else {
|
|
1796
|
+
return SieveErrorCode.PIPELINE_ERROR;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/utils/command-registry.ts
|
|
1801
|
+
var SIEVE_COMMAND = {
|
|
1802
|
+
name: "scene-sieve",
|
|
1803
|
+
description: "Extract key frames from video and GIF files",
|
|
1804
|
+
usage: "scene-sieve <input> [options]",
|
|
1805
|
+
arguments: [
|
|
1806
|
+
{
|
|
1807
|
+
name: "input",
|
|
1808
|
+
description: "Input video or GIF file path",
|
|
1809
|
+
required: true
|
|
1810
|
+
}
|
|
1811
|
+
],
|
|
1812
|
+
options: [
|
|
1813
|
+
{
|
|
1814
|
+
flag: "-n, --count <number>",
|
|
1815
|
+
description: "Max number of frames to keep (default: 20)",
|
|
1816
|
+
type: "number"
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
flag: "-t, --threshold <number>",
|
|
1820
|
+
description: "Normalized threshold 0~1 (default: 0.5; keeps frames above ratio of max change)",
|
|
1821
|
+
type: "number"
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
flag: "-o, --output <path>",
|
|
1825
|
+
description: "Output directory path",
|
|
1826
|
+
type: "string"
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
flag: "--fps <number>",
|
|
1830
|
+
description: "Max FPS for frame extraction",
|
|
1831
|
+
type: "number",
|
|
1832
|
+
default: "5"
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
flag: "-mf, --max-frames <number>",
|
|
1836
|
+
description: "Max frames to extract (auto-reduces FPS for long videos)",
|
|
1837
|
+
type: "number",
|
|
1838
|
+
default: "300"
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
flag: "-s, --scale <number>",
|
|
1842
|
+
description: "Scale size for vision analysis",
|
|
1843
|
+
type: "number",
|
|
1844
|
+
default: "720"
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
flag: "-q, --quality <number>",
|
|
1848
|
+
description: "JPEG output quality 1-100",
|
|
1849
|
+
type: "number",
|
|
1850
|
+
default: "80"
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
flag: "-it, --iou-threshold <number>",
|
|
1854
|
+
description: "IoU threshold for animation tracking (0-1) (default: 0.9)",
|
|
1855
|
+
type: "number"
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
flag: "-at, --anim-threshold <number>",
|
|
1859
|
+
description: "Min consecutive frames for animation (default: 5)",
|
|
1860
|
+
type: "number"
|
|
1861
|
+
},
|
|
1862
|
+
{
|
|
1863
|
+
flag: "--max-segment-duration <number>",
|
|
1864
|
+
description: "Max segment duration in seconds for long video splitting (default: 300)",
|
|
1865
|
+
type: "number"
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
flag: "--concurrency <number>",
|
|
1869
|
+
description: "Number of segments to process in parallel (default: 2)",
|
|
1870
|
+
type: "number"
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
flag: "--debug",
|
|
1874
|
+
description: "Enable debug mode (preserve temp workspace)",
|
|
1875
|
+
type: "boolean"
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
flag: "--json",
|
|
1879
|
+
description: "Output structured JSON to stdout",
|
|
1880
|
+
type: "boolean"
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
flag: "--describe",
|
|
1884
|
+
description: "Output JSON schema of available options",
|
|
1885
|
+
type: "boolean"
|
|
1886
|
+
}
|
|
1887
|
+
],
|
|
1888
|
+
examples: [
|
|
1889
|
+
"scene-sieve video.mp4",
|
|
1890
|
+
"scene-sieve video.mp4 -n 10",
|
|
1891
|
+
"scene-sieve video.mp4 -t 0.3 -o ./output",
|
|
1892
|
+
"scene-sieve video.mp4 --json",
|
|
1893
|
+
"scene-sieve --describe"
|
|
1894
|
+
]
|
|
1895
|
+
};
|
|
1896
|
+
|
|
1897
|
+
// src/utils/parse-options.ts
|
|
1898
|
+
function parsePipelineOptions(opts) {
|
|
1899
|
+
return {
|
|
1900
|
+
...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
|
|
1901
|
+
...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
|
|
1902
|
+
outputPath: opts.output,
|
|
1903
|
+
fps: parseInt(opts.fps, 10),
|
|
1904
|
+
maxFrames: parseInt(opts.maxFrames, 10),
|
|
1905
|
+
scale: parseInt(opts.scale, 10),
|
|
1906
|
+
quality: parseInt(opts.quality, 10),
|
|
1907
|
+
iouThreshold: opts.iouThreshold !== void 0 ? parseFloat(opts.iouThreshold) : void 0,
|
|
1908
|
+
animationThreshold: opts.animThreshold !== void 0 ? parseInt(opts.animThreshold, 10) : void 0,
|
|
1909
|
+
maxSegmentDuration: opts.maxSegmentDuration !== void 0 ? parseInt(opts.maxSegmentDuration, 10) : void 0,
|
|
1910
|
+
concurrency: opts.concurrency !== void 0 ? parseInt(opts.concurrency, 10) : void 0,
|
|
1911
|
+
debug: opts.debug ?? false
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// src/commands/Sieve.tsx
|
|
1350
1916
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1351
1917
|
var PHASE_DEFS = [
|
|
1352
1918
|
{ key: "INIT", label: "Initializing workspace", hasProgress: false },
|
|
@@ -1366,6 +1932,78 @@ function createInitialPhases() {
|
|
|
1366
1932
|
function phaseKeyToIndex(phase) {
|
|
1367
1933
|
return PHASE_DEFS.findIndex((d) => d.key === phase);
|
|
1368
1934
|
}
|
|
1935
|
+
function registerSieveCommand(program2, version2) {
|
|
1936
|
+
const cmd = SIEVE_COMMAND;
|
|
1937
|
+
program2.argument("<input>", cmd.arguments[0].description).option("-n, --count <number>", cmd.options.find((o) => o.flag.includes("--count")).description).option(
|
|
1938
|
+
"-t, --threshold <number>",
|
|
1939
|
+
cmd.options.find((o) => o.flag.includes("--threshold")).description
|
|
1940
|
+
).option("-o, --output <path>", cmd.options.find((o) => o.flag.includes("--output")).description).option("--fps <number>", cmd.options.find((o) => o.flag.includes("--fps")).description, String(DEFAULT_FPS)).option(
|
|
1941
|
+
"-mf, --max-frames <number>",
|
|
1942
|
+
cmd.options.find((o) => o.flag.includes("--max-frames")).description,
|
|
1943
|
+
String(DEFAULT_MAX_FRAMES)
|
|
1944
|
+
).option(
|
|
1945
|
+
"-s, --scale <number>",
|
|
1946
|
+
cmd.options.find((o) => o.flag.includes("--scale")).description,
|
|
1947
|
+
String(DEFAULT_SCALE)
|
|
1948
|
+
).option(
|
|
1949
|
+
"-q, --quality <number>",
|
|
1950
|
+
cmd.options.find((o) => o.flag.includes("--quality")).description,
|
|
1951
|
+
String(DEFAULT_QUALITY)
|
|
1952
|
+
).option(
|
|
1953
|
+
"-it, --iou-threshold <number>",
|
|
1954
|
+
cmd.options.find((o) => o.flag.includes("--iou-threshold")).description
|
|
1955
|
+
).option(
|
|
1956
|
+
"-at, --anim-threshold <number>",
|
|
1957
|
+
cmd.options.find((o) => o.flag.includes("--anim-threshold")).description
|
|
1958
|
+
).option(
|
|
1959
|
+
"--max-segment-duration <number>",
|
|
1960
|
+
cmd.options.find((o) => o.flag.includes("--max-segment-duration")).description
|
|
1961
|
+
).option(
|
|
1962
|
+
"--concurrency <number>",
|
|
1963
|
+
cmd.options.find((o) => o.flag.includes("--concurrency")).description
|
|
1964
|
+
).option("--debug", cmd.options.find((o) => o.flag.includes("--debug")).description).option("--json", cmd.options.find((o) => o.flag.includes("--json")).description).option("--describe", cmd.options.find((o) => o.flag.includes("--describe")).description).action(async (input, opts) => {
|
|
1965
|
+
const parsed = parsePipelineOptions(opts);
|
|
1966
|
+
if (opts.json) {
|
|
1967
|
+
const startTime = Date.now();
|
|
1968
|
+
if (!existsSync(input)) {
|
|
1969
|
+
respondError("extract", SieveErrorCode.FILE_NOT_FOUND, `File not found: ${input}`, startTime, version2);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
try {
|
|
1973
|
+
const result = await runPipeline({
|
|
1974
|
+
mode: "file",
|
|
1975
|
+
inputPath: input,
|
|
1976
|
+
...parsed,
|
|
1977
|
+
onProgress: (phase, percent) => {
|
|
1978
|
+
process.stderr.write(JSON.stringify({ phase, percent }) + "\n");
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
const data = {
|
|
1982
|
+
success: result.success,
|
|
1983
|
+
originalFrames: result.originalFramesCount,
|
|
1984
|
+
selectedFrames: result.prunedFramesCount,
|
|
1985
|
+
outputFiles: result.outputFiles,
|
|
1986
|
+
animations: result.animations ?? [],
|
|
1987
|
+
video: result.video ?? null
|
|
1988
|
+
};
|
|
1989
|
+
respond("extract", data, startTime, version2);
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1992
|
+
respondError("extract", classifyError(err), err.message, startTime, version2);
|
|
1993
|
+
}
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const { outputPath, ...viewOpts } = parsed;
|
|
1997
|
+
const { waitUntilExit } = render(
|
|
1998
|
+
React.createElement(SieveView, {
|
|
1999
|
+
input,
|
|
2000
|
+
...viewOpts,
|
|
2001
|
+
output: outputPath
|
|
2002
|
+
})
|
|
2003
|
+
);
|
|
2004
|
+
await waitUntilExit();
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
1369
2007
|
var SieveView = (props) => {
|
|
1370
2008
|
const { exit } = useApp();
|
|
1371
2009
|
const [phases, setPhases] = useState(createInitialPhases);
|
|
@@ -1397,6 +2035,8 @@ var SieveView = (props) => {
|
|
|
1397
2035
|
quality: props.quality,
|
|
1398
2036
|
iouThreshold: props.iouThreshold,
|
|
1399
2037
|
animationThreshold: props.animationThreshold,
|
|
2038
|
+
maxSegmentDuration: props.maxSegmentDuration,
|
|
2039
|
+
concurrency: props.concurrency,
|
|
1400
2040
|
debug: props.debug
|
|
1401
2041
|
},
|
|
1402
2042
|
(phase, percent) => {
|
|
@@ -1519,50 +2159,32 @@ var SieveView = (props) => {
|
|
|
1519
2159
|
};
|
|
1520
2160
|
|
|
1521
2161
|
// src/cli.ts
|
|
1522
|
-
init_constants();
|
|
1523
2162
|
var require3 = createRequire2(import.meta.url);
|
|
1524
2163
|
var { version } = require3("../package.json");
|
|
1525
2164
|
var program = new Command();
|
|
1526
|
-
program.name("scene-sieve").description("Extract key frames from video and GIF files").version(version)
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
).option(
|
|
1542
|
-
"-it, --iou-threshold <number>",
|
|
1543
|
-
`IoU threshold for animation tracking (0-1) (default: ${IOU_THRESHOLD})`
|
|
1544
|
-
).option(
|
|
1545
|
-
"-at, --anim-threshold <number>",
|
|
1546
|
-
`Min consecutive frames for animation (default: ${ANIMATION_FRAME_THRESHOLD})`
|
|
1547
|
-
).option("--debug", "Enable debug mode (preserve temp workspace)").action(async (input, opts) => {
|
|
1548
|
-
const { waitUntilExit } = render(
|
|
1549
|
-
React2.createElement(SieveView, {
|
|
1550
|
-
input,
|
|
1551
|
-
...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
|
|
1552
|
-
...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
|
|
1553
|
-
output: opts.output,
|
|
1554
|
-
fps: parseInt(opts.fps, 10),
|
|
1555
|
-
maxFrames: parseInt(opts.maxFrames, 10),
|
|
1556
|
-
scale: parseInt(opts.scale, 10),
|
|
1557
|
-
quality: parseInt(opts.quality, 10),
|
|
1558
|
-
iouThreshold: opts.iouThreshold !== void 0 ? parseFloat(opts.iouThreshold) : void 0,
|
|
1559
|
-
animationThreshold: opts.animThreshold !== void 0 ? parseInt(opts.animThreshold, 10) : void 0,
|
|
1560
|
-
debug: opts.debug ?? false
|
|
1561
|
-
})
|
|
2165
|
+
program.name("scene-sieve").description("Extract key frames from video and GIF files").version(version);
|
|
2166
|
+
registerSieveCommand(program, version);
|
|
2167
|
+
if (process.argv.includes("--describe")) {
|
|
2168
|
+
const startTime = Date.now();
|
|
2169
|
+
respond(
|
|
2170
|
+
"describe",
|
|
2171
|
+
{
|
|
2172
|
+
name: SIEVE_COMMAND.name,
|
|
2173
|
+
version,
|
|
2174
|
+
description: SIEVE_COMMAND.description,
|
|
2175
|
+
arguments: SIEVE_COMMAND.arguments,
|
|
2176
|
+
options: SIEVE_COMMAND.options
|
|
2177
|
+
},
|
|
2178
|
+
startTime,
|
|
2179
|
+
version
|
|
1562
2180
|
);
|
|
1563
|
-
|
|
1564
|
-
}
|
|
2181
|
+
process.exit(0);
|
|
2182
|
+
}
|
|
1565
2183
|
program.parseAsync(process.argv).catch((error) => {
|
|
1566
|
-
|
|
2184
|
+
if (process.argv.includes("--json")) {
|
|
2185
|
+
respondError("extract", SieveErrorCode.UNKNOWN, error.message, Date.now(), version);
|
|
2186
|
+
} else {
|
|
2187
|
+
console.error("Fatal error:", error.message);
|
|
2188
|
+
}
|
|
1567
2189
|
process.exit(1);
|
|
1568
2190
|
});
|