@lumy-pack/scene-sieve 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +343 -125
- package/dist/index.cjs +38 -38
- package/dist/index.mjs +38 -38
- package/dist/pipeline-worker.mjs +38 -38
- package/package.json +3 -2
- package/dist/cli.d.ts +0 -1
- package/dist/commands/Sieve.d.ts +0 -17
- package/dist/components/PhaseStep.d.ts +0 -14
- package/dist/components/ProgressBar.d.ts +0 -7
- package/dist/constants.d.ts +0 -32
- package/dist/core/analyzer.d.ts +0 -62
- package/dist/core/dbscan.d.ts +0 -10
- package/dist/core/extractor.d.ts +0 -30
- package/dist/core/index.d.ts +0 -9
- 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/segmenter.d.ts +0 -38
- package/dist/core/workspace.d.ts +0 -25
- package/dist/index.d.ts +0 -2
- package/dist/types/index.d.ts +0 -129
- package/dist/utils/concurrency.d.ts +0 -5
- 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,42 +9,6 @@ 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";
|
|
@@ -86,6 +50,42 @@ var init_constants = __esm({
|
|
|
86
50
|
}
|
|
87
51
|
});
|
|
88
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
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
89
|
// src/core/dbscan.ts
|
|
90
90
|
function dbscan(points, imageWidth, imageHeight, alpha, minPts) {
|
|
91
91
|
if (points.length === 0) {
|
|
@@ -1248,10 +1248,7 @@ function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
return segments;
|
|
1250
1250
|
}
|
|
1251
|
-
function
|
|
1252
|
-
if (segmentResults.length === 0) {
|
|
1253
|
-
return { frames: [], edges: [], animations: [] };
|
|
1254
|
-
}
|
|
1251
|
+
function collectAllFrames(segmentResults) {
|
|
1255
1252
|
const allFrames = [];
|
|
1256
1253
|
for (const result of segmentResults) {
|
|
1257
1254
|
for (const frame of result.frames) {
|
|
@@ -1266,19 +1263,24 @@ function mergeSegmentFrames(segmentResults) {
|
|
|
1266
1263
|
});
|
|
1267
1264
|
}
|
|
1268
1265
|
}
|
|
1269
|
-
allFrames
|
|
1270
|
-
|
|
1266
|
+
return allFrames;
|
|
1267
|
+
}
|
|
1268
|
+
function deduplicateFrames(frames, effectiveFps) {
|
|
1269
|
+
frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
|
|
1271
1270
|
const dupThreshold = 1 / (effectiveFps * 2);
|
|
1272
|
-
const
|
|
1273
|
-
for (const entry of
|
|
1274
|
-
if (
|
|
1275
|
-
const last =
|
|
1271
|
+
const unique = [];
|
|
1272
|
+
for (const entry of frames) {
|
|
1273
|
+
if (unique.length > 0) {
|
|
1274
|
+
const last = unique[unique.length - 1];
|
|
1276
1275
|
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
|
|
1277
1276
|
continue;
|
|
1278
1277
|
}
|
|
1279
1278
|
}
|
|
1280
|
-
|
|
1279
|
+
unique.push(entry);
|
|
1281
1280
|
}
|
|
1281
|
+
return unique;
|
|
1282
|
+
}
|
|
1283
|
+
function remapFrameIds(uniqueFrames) {
|
|
1282
1284
|
const globalIdMap = /* @__PURE__ */ new Map();
|
|
1283
1285
|
const frames = uniqueFrames.map((entry, globalId) => {
|
|
1284
1286
|
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
@@ -1288,54 +1290,52 @@ function mergeSegmentFrames(segmentResults) {
|
|
|
1288
1290
|
extractPath: entry.frame.extractPath
|
|
1289
1291
|
};
|
|
1290
1292
|
});
|
|
1293
|
+
return { frames, globalIdMap };
|
|
1294
|
+
}
|
|
1295
|
+
function remapEdges(segmentResults, globalIdMap) {
|
|
1291
1296
|
const edges = [];
|
|
1292
1297
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
1293
1298
|
for (const result of segmentResults) {
|
|
1294
1299
|
for (const edge of result.edges) {
|
|
1295
|
-
const newSourceId = globalIdMap.get(
|
|
1296
|
-
|
|
1297
|
-
);
|
|
1298
|
-
const newTargetId = globalIdMap.get(
|
|
1299
|
-
`${result.segment.index}:${edge.targetId}`
|
|
1300
|
-
);
|
|
1300
|
+
const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
|
|
1301
|
+
const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
|
|
1301
1302
|
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1302
1303
|
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1303
1304
|
const existingIdx = edgeMap.get(edgeKey);
|
|
1304
1305
|
if (existingIdx !== void 0) {
|
|
1305
1306
|
if (edges[existingIdx].score < edge.score) {
|
|
1306
|
-
edges[existingIdx] = {
|
|
1307
|
-
sourceId: newSourceId,
|
|
1308
|
-
targetId: newTargetId,
|
|
1309
|
-
score: edge.score
|
|
1310
|
-
};
|
|
1307
|
+
edges[existingIdx] = { sourceId: newSourceId, targetId: newTargetId, score: edge.score };
|
|
1311
1308
|
}
|
|
1312
1309
|
} else {
|
|
1313
1310
|
edgeMap.set(edgeKey, edges.length);
|
|
1314
|
-
edges.push({
|
|
1315
|
-
sourceId: newSourceId,
|
|
1316
|
-
targetId: newTargetId,
|
|
1317
|
-
score: edge.score
|
|
1318
|
-
});
|
|
1311
|
+
edges.push({ sourceId: newSourceId, targetId: newTargetId, score: edge.score });
|
|
1319
1312
|
}
|
|
1320
1313
|
}
|
|
1321
1314
|
}
|
|
1315
|
+
return edges;
|
|
1316
|
+
}
|
|
1317
|
+
function remapAnimations(segmentResults, globalIdMap) {
|
|
1322
1318
|
const animations = [];
|
|
1323
1319
|
for (const result of segmentResults) {
|
|
1324
1320
|
for (const anim of result.animations) {
|
|
1325
|
-
const newStartId = globalIdMap.get(
|
|
1326
|
-
|
|
1327
|
-
);
|
|
1328
|
-
const newEndId = globalIdMap.get(
|
|
1329
|
-
`${result.segment.index}:${anim.endFrameId}`
|
|
1330
|
-
);
|
|
1321
|
+
const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
|
|
1322
|
+
const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
|
|
1331
1323
|
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1332
|
-
animations.push({
|
|
1333
|
-
...anim,
|
|
1334
|
-
startFrameId: newStartId,
|
|
1335
|
-
endFrameId: newEndId
|
|
1336
|
-
});
|
|
1324
|
+
animations.push({ ...anim, startFrameId: newStartId, endFrameId: newEndId });
|
|
1337
1325
|
}
|
|
1338
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
1339
|
return { frames, edges, animations };
|
|
1340
1340
|
}
|
|
1341
1341
|
function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
|
|
@@ -1637,12 +1637,41 @@ var init_orchestrator = __esm({
|
|
|
1637
1637
|
// src/cli.ts
|
|
1638
1638
|
import { createRequire as createRequire2 } from "module";
|
|
1639
1639
|
import { Command } from "commander";
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
+
}
|
|
1642
1669
|
|
|
1643
1670
|
// src/commands/Sieve.tsx
|
|
1671
|
+
import { existsSync } from "fs";
|
|
1644
1672
|
import { Box as Box2, Text as Text3, useApp } from "ink";
|
|
1645
|
-
import {
|
|
1673
|
+
import { render } from "ink";
|
|
1674
|
+
import React, { useEffect, useState } from "react";
|
|
1646
1675
|
|
|
1647
1676
|
// src/components/PhaseStep.tsx
|
|
1648
1677
|
import { Box, Text as Text2 } from "ink";
|
|
@@ -1703,6 +1732,10 @@ var PhaseStep = ({ phase }) => {
|
|
|
1703
1732
|
] });
|
|
1704
1733
|
};
|
|
1705
1734
|
|
|
1735
|
+
// src/commands/Sieve.tsx
|
|
1736
|
+
init_constants();
|
|
1737
|
+
init_orchestrator();
|
|
1738
|
+
|
|
1706
1739
|
// src/core/run-in-worker.ts
|
|
1707
1740
|
import { dirname, join as join6 } from "path";
|
|
1708
1741
|
import { fileURLToPath } from "url";
|
|
@@ -1741,6 +1774,145 @@ async function runPipelineInWorker(options, onProgress) {
|
|
|
1741
1774
|
|
|
1742
1775
|
// src/commands/Sieve.tsx
|
|
1743
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
|
|
1744
1916
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1745
1917
|
var PHASE_DEFS = [
|
|
1746
1918
|
{ key: "INIT", label: "Initializing workspace", hasProgress: false },
|
|
@@ -1760,6 +1932,78 @@ function createInitialPhases() {
|
|
|
1760
1932
|
function phaseKeyToIndex(phase) {
|
|
1761
1933
|
return PHASE_DEFS.findIndex((d) => d.key === phase);
|
|
1762
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
|
+
}
|
|
1763
2007
|
var SieveView = (props) => {
|
|
1764
2008
|
const { exit } = useApp();
|
|
1765
2009
|
const [phases, setPhases] = useState(createInitialPhases);
|
|
@@ -1915,58 +2159,32 @@ var SieveView = (props) => {
|
|
|
1915
2159
|
};
|
|
1916
2160
|
|
|
1917
2161
|
// src/cli.ts
|
|
1918
|
-
init_constants();
|
|
1919
2162
|
var require3 = createRequire2(import.meta.url);
|
|
1920
2163
|
var { version } = require3("../package.json");
|
|
1921
2164
|
var program = new Command();
|
|
1922
|
-
program.name("scene-sieve").description("Extract key frames from video and GIF files").version(version)
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
).option(
|
|
1938
|
-
"-it, --iou-threshold <number>",
|
|
1939
|
-
`IoU threshold for animation tracking (0-1) (default: ${IOU_THRESHOLD})`
|
|
1940
|
-
).option(
|
|
1941
|
-
"-at, --anim-threshold <number>",
|
|
1942
|
-
`Min consecutive frames for animation (default: ${ANIMATION_FRAME_THRESHOLD})`
|
|
1943
|
-
).option(
|
|
1944
|
-
"--max-segment-duration <number>",
|
|
1945
|
-
`Max segment duration in seconds for long video splitting (default: ${DEFAULT_MAX_SEGMENT_DURATION})`
|
|
1946
|
-
).option(
|
|
1947
|
-
"--concurrency <number>",
|
|
1948
|
-
`Number of segments to process in parallel (default: ${DEFAULT_SEGMENT_CONCURRENCY})`
|
|
1949
|
-
).option("--debug", "Enable debug mode (preserve temp workspace)").action(async (input, opts) => {
|
|
1950
|
-
const { waitUntilExit } = render(
|
|
1951
|
-
React2.createElement(SieveView, {
|
|
1952
|
-
input,
|
|
1953
|
-
...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
|
|
1954
|
-
...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
|
|
1955
|
-
output: opts.output,
|
|
1956
|
-
fps: parseInt(opts.fps, 10),
|
|
1957
|
-
maxFrames: parseInt(opts.maxFrames, 10),
|
|
1958
|
-
scale: parseInt(opts.scale, 10),
|
|
1959
|
-
quality: parseInt(opts.quality, 10),
|
|
1960
|
-
iouThreshold: opts.iouThreshold !== void 0 ? parseFloat(opts.iouThreshold) : void 0,
|
|
1961
|
-
animationThreshold: opts.animThreshold !== void 0 ? parseInt(opts.animThreshold, 10) : void 0,
|
|
1962
|
-
maxSegmentDuration: opts.maxSegmentDuration !== void 0 ? parseInt(opts.maxSegmentDuration, 10) : void 0,
|
|
1963
|
-
concurrency: opts.concurrency !== void 0 ? parseInt(opts.concurrency, 10) : void 0,
|
|
1964
|
-
debug: opts.debug ?? false
|
|
1965
|
-
})
|
|
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
|
|
1966
2180
|
);
|
|
1967
|
-
|
|
1968
|
-
}
|
|
2181
|
+
process.exit(0);
|
|
2182
|
+
}
|
|
1969
2183
|
program.parseAsync(process.argv).catch((error) => {
|
|
1970
|
-
|
|
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
|
+
}
|
|
1971
2189
|
process.exit(1);
|
|
1972
2190
|
});
|
package/dist/index.cjs
CHANGED
|
@@ -1192,10 +1192,7 @@ function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
|
|
|
1192
1192
|
}
|
|
1193
1193
|
return segments;
|
|
1194
1194
|
}
|
|
1195
|
-
function
|
|
1196
|
-
if (segmentResults.length === 0) {
|
|
1197
|
-
return { frames: [], edges: [], animations: [] };
|
|
1198
|
-
}
|
|
1195
|
+
function collectAllFrames(segmentResults) {
|
|
1199
1196
|
const allFrames = [];
|
|
1200
1197
|
for (const result of segmentResults) {
|
|
1201
1198
|
for (const frame of result.frames) {
|
|
@@ -1210,19 +1207,24 @@ function mergeSegmentFrames(segmentResults) {
|
|
|
1210
1207
|
});
|
|
1211
1208
|
}
|
|
1212
1209
|
}
|
|
1213
|
-
allFrames
|
|
1214
|
-
|
|
1210
|
+
return allFrames;
|
|
1211
|
+
}
|
|
1212
|
+
function deduplicateFrames(frames, effectiveFps) {
|
|
1213
|
+
frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
|
|
1215
1214
|
const dupThreshold = 1 / (effectiveFps * 2);
|
|
1216
|
-
const
|
|
1217
|
-
for (const entry of
|
|
1218
|
-
if (
|
|
1219
|
-
const last =
|
|
1215
|
+
const unique = [];
|
|
1216
|
+
for (const entry of frames) {
|
|
1217
|
+
if (unique.length > 0) {
|
|
1218
|
+
const last = unique[unique.length - 1];
|
|
1220
1219
|
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
|
|
1221
1220
|
continue;
|
|
1222
1221
|
}
|
|
1223
1222
|
}
|
|
1224
|
-
|
|
1223
|
+
unique.push(entry);
|
|
1225
1224
|
}
|
|
1225
|
+
return unique;
|
|
1226
|
+
}
|
|
1227
|
+
function remapFrameIds(uniqueFrames) {
|
|
1226
1228
|
const globalIdMap = /* @__PURE__ */ new Map();
|
|
1227
1229
|
const frames = uniqueFrames.map((entry, globalId) => {
|
|
1228
1230
|
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
@@ -1232,54 +1234,52 @@ function mergeSegmentFrames(segmentResults) {
|
|
|
1232
1234
|
extractPath: entry.frame.extractPath
|
|
1233
1235
|
};
|
|
1234
1236
|
});
|
|
1237
|
+
return { frames, globalIdMap };
|
|
1238
|
+
}
|
|
1239
|
+
function remapEdges(segmentResults, globalIdMap) {
|
|
1235
1240
|
const edges = [];
|
|
1236
1241
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
1237
1242
|
for (const result of segmentResults) {
|
|
1238
1243
|
for (const edge of result.edges) {
|
|
1239
|
-
const newSourceId = globalIdMap.get(
|
|
1240
|
-
|
|
1241
|
-
);
|
|
1242
|
-
const newTargetId = globalIdMap.get(
|
|
1243
|
-
`${result.segment.index}:${edge.targetId}`
|
|
1244
|
-
);
|
|
1244
|
+
const newSourceId = globalIdMap.get(`${result.segment.index}:${edge.sourceId}`);
|
|
1245
|
+
const newTargetId = globalIdMap.get(`${result.segment.index}:${edge.targetId}`);
|
|
1245
1246
|
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1246
1247
|
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1247
1248
|
const existingIdx = edgeMap.get(edgeKey);
|
|
1248
1249
|
if (existingIdx !== void 0) {
|
|
1249
1250
|
if (edges[existingIdx].score < edge.score) {
|
|
1250
|
-
edges[existingIdx] = {
|
|
1251
|
-
sourceId: newSourceId,
|
|
1252
|
-
targetId: newTargetId,
|
|
1253
|
-
score: edge.score
|
|
1254
|
-
};
|
|
1251
|
+
edges[existingIdx] = { sourceId: newSourceId, targetId: newTargetId, score: edge.score };
|
|
1255
1252
|
}
|
|
1256
1253
|
} else {
|
|
1257
1254
|
edgeMap.set(edgeKey, edges.length);
|
|
1258
|
-
edges.push({
|
|
1259
|
-
sourceId: newSourceId,
|
|
1260
|
-
targetId: newTargetId,
|
|
1261
|
-
score: edge.score
|
|
1262
|
-
});
|
|
1255
|
+
edges.push({ sourceId: newSourceId, targetId: newTargetId, score: edge.score });
|
|
1263
1256
|
}
|
|
1264
1257
|
}
|
|
1265
1258
|
}
|
|
1259
|
+
return edges;
|
|
1260
|
+
}
|
|
1261
|
+
function remapAnimations(segmentResults, globalIdMap) {
|
|
1266
1262
|
const animations = [];
|
|
1267
1263
|
for (const result of segmentResults) {
|
|
1268
1264
|
for (const anim of result.animations) {
|
|
1269
|
-
const newStartId = globalIdMap.get(
|
|
1270
|
-
|
|
1271
|
-
);
|
|
1272
|
-
const newEndId = globalIdMap.get(
|
|
1273
|
-
`${result.segment.index}:${anim.endFrameId}`
|
|
1274
|
-
);
|
|
1265
|
+
const newStartId = globalIdMap.get(`${result.segment.index}:${anim.startFrameId}`);
|
|
1266
|
+
const newEndId = globalIdMap.get(`${result.segment.index}:${anim.endFrameId}`);
|
|
1275
1267
|
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1276
|
-
animations.push({
|
|
1277
|
-
...anim,
|
|
1278
|
-
startFrameId: newStartId,
|
|
1279
|
-
endFrameId: newEndId
|
|
1280
|
-
});
|
|
1268
|
+
animations.push({ ...anim, startFrameId: newStartId, endFrameId: newEndId });
|
|
1281
1269
|
}
|
|
1282
1270
|
}
|
|
1271
|
+
return animations;
|
|
1272
|
+
}
|
|
1273
|
+
function mergeSegmentFrames(segmentResults) {
|
|
1274
|
+
if (segmentResults.length === 0) {
|
|
1275
|
+
return { frames: [], edges: [], animations: [] };
|
|
1276
|
+
}
|
|
1277
|
+
const effectiveFps = segmentResults[0].segment.effectiveFps;
|
|
1278
|
+
const allFrames = collectAllFrames(segmentResults);
|
|
1279
|
+
const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
|
|
1280
|
+
const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
|
|
1281
|
+
const edges = remapEdges(segmentResults, globalIdMap);
|
|
1282
|
+
const animations = remapAnimations(segmentResults, globalIdMap);
|
|
1283
1283
|
return { frames, edges, animations };
|
|
1284
1284
|
}
|
|
1285
1285
|
function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
|