@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 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 mergeSegmentFrames(segmentResults) {
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.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1270
- const effectiveFps = segmentResults[0].segment.effectiveFps;
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 uniqueFrames = [];
1273
- for (const entry of allFrames) {
1274
- if (uniqueFrames.length > 0) {
1275
- const last = uniqueFrames[uniqueFrames.length - 1];
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
- uniqueFrames.push(entry);
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
- `${result.segment.index}:${edge.sourceId}`
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
- `${result.segment.index}:${anim.startFrameId}`
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
- import { render } from "ink";
1641
- import React2 from "react";
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 { useEffect, useState } from "react";
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).argument("<input>", "Input video or GIF file path").option("-n, --count <number>", "Max number of frames to keep (default: 20)").option(
1923
- "-t, --threshold <number>",
1924
- "Normalized threshold 0~1 (default: 0.5; keeps frames above ratio of max change)"
1925
- ).option("-o, --output <path>", "Output directory path").option("--fps <number>", "Max FPS for frame extraction", String(DEFAULT_FPS)).option(
1926
- "-mf, --max-frames <number>",
1927
- "Max frames to extract (auto-reduces FPS for long videos)",
1928
- String(DEFAULT_MAX_FRAMES)
1929
- ).option(
1930
- "-s, --scale <number>",
1931
- "Scale size for vision analysis",
1932
- String(DEFAULT_SCALE)
1933
- ).option(
1934
- "-q, --quality <number>",
1935
- "JPEG output quality 1-100",
1936
- String(DEFAULT_QUALITY)
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
- await waitUntilExit();
1968
- });
2181
+ process.exit(0);
2182
+ }
1969
2183
  program.parseAsync(process.argv).catch((error) => {
1970
- console.error("Fatal error:", error.message);
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 mergeSegmentFrames(segmentResults) {
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.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1214
- const effectiveFps = segmentResults[0].segment.effectiveFps;
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 uniqueFrames = [];
1217
- for (const entry of allFrames) {
1218
- if (uniqueFrames.length > 0) {
1219
- const last = uniqueFrames[uniqueFrames.length - 1];
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
- uniqueFrames.push(entry);
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
- `${result.segment.index}:${edge.sourceId}`
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
- `${result.segment.index}:${anim.startFrameId}`
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) {