@lumy-pack/scene-sieve 0.0.9 → 0.0.11

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,6 +1290,9 @@ 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) {
@@ -1319,6 +1324,9 @@ function mergeSegmentFrames(segmentResults) {
1319
1324
  }
1320
1325
  }
1321
1326
  }
1327
+ return edges;
1328
+ }
1329
+ function remapAnimations(segmentResults, globalIdMap) {
1322
1330
  const animations = [];
1323
1331
  for (const result of segmentResults) {
1324
1332
  for (const anim of result.animations) {
@@ -1336,6 +1344,18 @@ function mergeSegmentFrames(segmentResults) {
1336
1344
  });
1337
1345
  }
1338
1346
  }
1347
+ return animations;
1348
+ }
1349
+ function mergeSegmentFrames(segmentResults) {
1350
+ if (segmentResults.length === 0) {
1351
+ return { frames: [], edges: [], animations: [] };
1352
+ }
1353
+ const effectiveFps = segmentResults[0].segment.effectiveFps;
1354
+ const allFrames = collectAllFrames(segmentResults);
1355
+ const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
1356
+ const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
1357
+ const edges = remapEdges(segmentResults, globalIdMap);
1358
+ const animations = remapAnimations(segmentResults, globalIdMap);
1339
1359
  return { frames, edges, animations };
1340
1360
  }
1341
1361
  function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
@@ -1637,12 +1657,41 @@ var init_orchestrator = __esm({
1637
1657
  // src/cli.ts
1638
1658
  import { createRequire as createRequire2 } from "module";
1639
1659
  import { Command } from "commander";
1640
- import { render } from "ink";
1641
- import React2 from "react";
1660
+
1661
+ // ../shared/src/respond.ts
1662
+ function respond(command, data, startTime, version2) {
1663
+ const response = {
1664
+ ok: true,
1665
+ command,
1666
+ data,
1667
+ meta: {
1668
+ version: version2,
1669
+ durationMs: Date.now() - startTime,
1670
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1671
+ }
1672
+ };
1673
+ process.stdout.write(JSON.stringify(response) + "\n");
1674
+ }
1675
+ function respondError(command, code, message, startTime, version2, details) {
1676
+ const response = {
1677
+ ok: false,
1678
+ command,
1679
+ error: { code, message, ...details !== void 0 ? { details } : {} },
1680
+ meta: {
1681
+ version: version2,
1682
+ durationMs: Date.now() - startTime,
1683
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1684
+ }
1685
+ };
1686
+ process.stdout.write(JSON.stringify(response) + "\n");
1687
+ process.exitCode = 1;
1688
+ }
1642
1689
 
1643
1690
  // src/commands/Sieve.tsx
1691
+ import { existsSync } from "fs";
1644
1692
  import { Box as Box2, Text as Text3, useApp } from "ink";
1645
- import { useEffect, useState } from "react";
1693
+ import { render } from "ink";
1694
+ import React, { useEffect, useState } from "react";
1646
1695
 
1647
1696
  // src/components/PhaseStep.tsx
1648
1697
  import { Box, Text as Text2 } from "ink";
@@ -1703,6 +1752,10 @@ var PhaseStep = ({ phase }) => {
1703
1752
  ] });
1704
1753
  };
1705
1754
 
1755
+ // src/commands/Sieve.tsx
1756
+ init_constants();
1757
+ init_orchestrator();
1758
+
1706
1759
  // src/core/run-in-worker.ts
1707
1760
  import { dirname, join as join6 } from "path";
1708
1761
  import { fileURLToPath } from "url";
@@ -1741,6 +1794,145 @@ async function runPipelineInWorker(options, onProgress) {
1741
1794
 
1742
1795
  // src/commands/Sieve.tsx
1743
1796
  init_workspace();
1797
+
1798
+ // src/errors.ts
1799
+ var SieveErrorCode = {
1800
+ INVALID_INPUT: "INVALID_INPUT",
1801
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
1802
+ INVALID_FORMAT: "INVALID_FORMAT",
1803
+ PIPELINE_ERROR: "PIPELINE_ERROR",
1804
+ WORKER_ERROR: "WORKER_ERROR",
1805
+ UNKNOWN: "UNKNOWN"
1806
+ };
1807
+ function classifyError(error) {
1808
+ const msg = error.message.toLowerCase();
1809
+ if (error.code === "ENOENT" || msg.includes("not found")) {
1810
+ return SieveErrorCode.FILE_NOT_FOUND;
1811
+ } else if (msg.includes("no video stream") || msg.includes("invalid format")) {
1812
+ return SieveErrorCode.INVALID_FORMAT;
1813
+ } else if (msg.includes("worker")) {
1814
+ return SieveErrorCode.WORKER_ERROR;
1815
+ } else {
1816
+ return SieveErrorCode.PIPELINE_ERROR;
1817
+ }
1818
+ }
1819
+
1820
+ // src/utils/command-registry.ts
1821
+ var SIEVE_COMMAND = {
1822
+ name: "scene-sieve",
1823
+ description: "Extract key frames from video and GIF files",
1824
+ usage: "scene-sieve <input> [options]",
1825
+ arguments: [
1826
+ {
1827
+ name: "input",
1828
+ description: "Input video or GIF file path",
1829
+ required: true
1830
+ }
1831
+ ],
1832
+ options: [
1833
+ {
1834
+ flag: "-n, --count <number>",
1835
+ description: "Max number of frames to keep (default: 20)",
1836
+ type: "number"
1837
+ },
1838
+ {
1839
+ flag: "-t, --threshold <number>",
1840
+ description: "Normalized threshold 0~1 (default: 0.5; keeps frames above ratio of max change)",
1841
+ type: "number"
1842
+ },
1843
+ {
1844
+ flag: "-o, --output <path>",
1845
+ description: "Output directory path",
1846
+ type: "string"
1847
+ },
1848
+ {
1849
+ flag: "--fps <number>",
1850
+ description: "Max FPS for frame extraction",
1851
+ type: "number",
1852
+ default: "5"
1853
+ },
1854
+ {
1855
+ flag: "-mf, --max-frames <number>",
1856
+ description: "Max frames to extract (auto-reduces FPS for long videos)",
1857
+ type: "number",
1858
+ default: "300"
1859
+ },
1860
+ {
1861
+ flag: "-s, --scale <number>",
1862
+ description: "Scale size for vision analysis",
1863
+ type: "number",
1864
+ default: "720"
1865
+ },
1866
+ {
1867
+ flag: "-q, --quality <number>",
1868
+ description: "JPEG output quality 1-100",
1869
+ type: "number",
1870
+ default: "80"
1871
+ },
1872
+ {
1873
+ flag: "-it, --iou-threshold <number>",
1874
+ description: "IoU threshold for animation tracking (0-1) (default: 0.9)",
1875
+ type: "number"
1876
+ },
1877
+ {
1878
+ flag: "-at, --anim-threshold <number>",
1879
+ description: "Min consecutive frames for animation (default: 5)",
1880
+ type: "number"
1881
+ },
1882
+ {
1883
+ flag: "--max-segment-duration <number>",
1884
+ description: "Max segment duration in seconds for long video splitting (default: 300)",
1885
+ type: "number"
1886
+ },
1887
+ {
1888
+ flag: "--concurrency <number>",
1889
+ description: "Number of segments to process in parallel (default: 2)",
1890
+ type: "number"
1891
+ },
1892
+ {
1893
+ flag: "--debug",
1894
+ description: "Enable debug mode (preserve temp workspace)",
1895
+ type: "boolean"
1896
+ },
1897
+ {
1898
+ flag: "--json",
1899
+ description: "Output structured JSON to stdout",
1900
+ type: "boolean"
1901
+ },
1902
+ {
1903
+ flag: "--describe",
1904
+ description: "Output JSON schema of available options",
1905
+ type: "boolean"
1906
+ }
1907
+ ],
1908
+ examples: [
1909
+ "scene-sieve video.mp4",
1910
+ "scene-sieve video.mp4 -n 10",
1911
+ "scene-sieve video.mp4 -t 0.3 -o ./output",
1912
+ "scene-sieve video.mp4 --json",
1913
+ "scene-sieve --describe"
1914
+ ]
1915
+ };
1916
+
1917
+ // src/utils/parse-options.ts
1918
+ function parsePipelineOptions(opts) {
1919
+ return {
1920
+ ...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
1921
+ ...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
1922
+ outputPath: opts.output,
1923
+ fps: parseInt(opts.fps, 10),
1924
+ maxFrames: parseInt(opts.maxFrames, 10),
1925
+ scale: parseInt(opts.scale, 10),
1926
+ quality: parseInt(opts.quality, 10),
1927
+ iouThreshold: opts.iouThreshold !== void 0 ? parseFloat(opts.iouThreshold) : void 0,
1928
+ animationThreshold: opts.animThreshold !== void 0 ? parseInt(opts.animThreshold, 10) : void 0,
1929
+ maxSegmentDuration: opts.maxSegmentDuration !== void 0 ? parseInt(opts.maxSegmentDuration, 10) : void 0,
1930
+ concurrency: opts.concurrency !== void 0 ? parseInt(opts.concurrency, 10) : void 0,
1931
+ debug: opts.debug ?? false
1932
+ };
1933
+ }
1934
+
1935
+ // src/commands/Sieve.tsx
1744
1936
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1745
1937
  var PHASE_DEFS = [
1746
1938
  { key: "INIT", label: "Initializing workspace", hasProgress: false },
@@ -1760,6 +1952,78 @@ function createInitialPhases() {
1760
1952
  function phaseKeyToIndex(phase) {
1761
1953
  return PHASE_DEFS.findIndex((d) => d.key === phase);
1762
1954
  }
1955
+ function registerSieveCommand(program2, version2) {
1956
+ const cmd = SIEVE_COMMAND;
1957
+ program2.argument("<input>", cmd.arguments[0].description).option("-n, --count <number>", cmd.options.find((o) => o.flag.includes("--count")).description).option(
1958
+ "-t, --threshold <number>",
1959
+ cmd.options.find((o) => o.flag.includes("--threshold")).description
1960
+ ).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(
1961
+ "-mf, --max-frames <number>",
1962
+ cmd.options.find((o) => o.flag.includes("--max-frames")).description,
1963
+ String(DEFAULT_MAX_FRAMES)
1964
+ ).option(
1965
+ "-s, --scale <number>",
1966
+ cmd.options.find((o) => o.flag.includes("--scale")).description,
1967
+ String(DEFAULT_SCALE)
1968
+ ).option(
1969
+ "-q, --quality <number>",
1970
+ cmd.options.find((o) => o.flag.includes("--quality")).description,
1971
+ String(DEFAULT_QUALITY)
1972
+ ).option(
1973
+ "-it, --iou-threshold <number>",
1974
+ cmd.options.find((o) => o.flag.includes("--iou-threshold")).description
1975
+ ).option(
1976
+ "-at, --anim-threshold <number>",
1977
+ cmd.options.find((o) => o.flag.includes("--anim-threshold")).description
1978
+ ).option(
1979
+ "--max-segment-duration <number>",
1980
+ cmd.options.find((o) => o.flag.includes("--max-segment-duration")).description
1981
+ ).option(
1982
+ "--concurrency <number>",
1983
+ cmd.options.find((o) => o.flag.includes("--concurrency")).description
1984
+ ).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) => {
1985
+ const parsed = parsePipelineOptions(opts);
1986
+ if (opts.json) {
1987
+ const startTime = Date.now();
1988
+ if (!existsSync(input)) {
1989
+ respondError("extract", SieveErrorCode.FILE_NOT_FOUND, `File not found: ${input}`, startTime, version2);
1990
+ return;
1991
+ }
1992
+ try {
1993
+ const result = await runPipeline({
1994
+ mode: "file",
1995
+ inputPath: input,
1996
+ ...parsed,
1997
+ onProgress: (phase, percent) => {
1998
+ process.stderr.write(JSON.stringify({ phase, percent }) + "\n");
1999
+ }
2000
+ });
2001
+ const data = {
2002
+ success: result.success,
2003
+ originalFrames: result.originalFramesCount,
2004
+ selectedFrames: result.prunedFramesCount,
2005
+ outputFiles: result.outputFiles,
2006
+ animations: result.animations ?? [],
2007
+ video: result.video ?? null
2008
+ };
2009
+ respond("extract", data, startTime, version2);
2010
+ } catch (error) {
2011
+ const err = error instanceof Error ? error : new Error(String(error));
2012
+ respondError("extract", classifyError(err), err.message, startTime, version2);
2013
+ }
2014
+ return;
2015
+ }
2016
+ const { outputPath, ...viewOpts } = parsed;
2017
+ const { waitUntilExit } = render(
2018
+ React.createElement(SieveView, {
2019
+ input,
2020
+ ...viewOpts,
2021
+ output: outputPath
2022
+ })
2023
+ );
2024
+ await waitUntilExit();
2025
+ });
2026
+ }
1763
2027
  var SieveView = (props) => {
1764
2028
  const { exit } = useApp();
1765
2029
  const [phases, setPhases] = useState(createInitialPhases);
@@ -1915,58 +2179,32 @@ var SieveView = (props) => {
1915
2179
  };
1916
2180
 
1917
2181
  // src/cli.ts
1918
- init_constants();
1919
2182
  var require3 = createRequire2(import.meta.url);
1920
2183
  var { version } = require3("../package.json");
1921
2184
  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
- })
2185
+ program.name("scene-sieve").description("Extract key frames from video and GIF files").version(version);
2186
+ registerSieveCommand(program, version);
2187
+ if (process.argv.includes("--describe")) {
2188
+ const startTime = Date.now();
2189
+ respond(
2190
+ "describe",
2191
+ {
2192
+ name: SIEVE_COMMAND.name,
2193
+ version,
2194
+ description: SIEVE_COMMAND.description,
2195
+ arguments: SIEVE_COMMAND.arguments,
2196
+ options: SIEVE_COMMAND.options
2197
+ },
2198
+ startTime,
2199
+ version
1966
2200
  );
1967
- await waitUntilExit();
1968
- });
2201
+ process.exit(0);
2202
+ }
1969
2203
  program.parseAsync(process.argv).catch((error) => {
1970
- console.error("Fatal error:", error.message);
2204
+ if (process.argv.includes("--json")) {
2205
+ respondError("extract", SieveErrorCode.UNKNOWN, error.message, Date.now(), version);
2206
+ } else {
2207
+ console.error("Fatal error:", error.message);
2208
+ }
1971
2209
  process.exit(1);
1972
2210
  });
@@ -1,3 +1,4 @@
1
+ import type { Command } from 'commander';
1
2
  import React from 'react';
2
3
  export interface SieveViewProps {
3
4
  input: string;
@@ -14,4 +15,5 @@ export interface SieveViewProps {
14
15
  concurrency?: number;
15
16
  debug: boolean;
16
17
  }
18
+ export declare function registerSieveCommand(program: Command, version: string): void;
17
19
  export declare const SieveView: React.FC<SieveViewProps>;
@@ -0,0 +1,10 @@
1
+ export declare const SieveErrorCode: {
2
+ readonly INVALID_INPUT: "INVALID_INPUT";
3
+ readonly FILE_NOT_FOUND: "FILE_NOT_FOUND";
4
+ readonly INVALID_FORMAT: "INVALID_FORMAT";
5
+ readonly PIPELINE_ERROR: "PIPELINE_ERROR";
6
+ readonly WORKER_ERROR: "WORKER_ERROR";
7
+ readonly UNKNOWN: "UNKNOWN";
8
+ };
9
+ export type SieveErrorCode = (typeof SieveErrorCode)[keyof typeof SieveErrorCode];
10
+ export declare function classifyError(error: Error): SieveErrorCode;
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,6 +1234,9 @@ 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) {
@@ -1263,6 +1268,9 @@ function mergeSegmentFrames(segmentResults) {
1263
1268
  }
1264
1269
  }
1265
1270
  }
1271
+ return edges;
1272
+ }
1273
+ function remapAnimations(segmentResults, globalIdMap) {
1266
1274
  const animations = [];
1267
1275
  for (const result of segmentResults) {
1268
1276
  for (const anim of result.animations) {
@@ -1280,6 +1288,18 @@ function mergeSegmentFrames(segmentResults) {
1280
1288
  });
1281
1289
  }
1282
1290
  }
1291
+ return animations;
1292
+ }
1293
+ function mergeSegmentFrames(segmentResults) {
1294
+ if (segmentResults.length === 0) {
1295
+ return { frames: [], edges: [], animations: [] };
1296
+ }
1297
+ const effectiveFps = segmentResults[0].segment.effectiveFps;
1298
+ const allFrames = collectAllFrames(segmentResults);
1299
+ const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
1300
+ const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
1301
+ const edges = remapEdges(segmentResults, globalIdMap);
1302
+ const animations = remapAnimations(segmentResults, globalIdMap);
1283
1303
  return { frames, edges, animations };
1284
1304
  }
1285
1305
  function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
package/dist/index.mjs CHANGED
@@ -1152,10 +1152,7 @@ function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
1152
1152
  }
1153
1153
  return segments;
1154
1154
  }
1155
- function mergeSegmentFrames(segmentResults) {
1156
- if (segmentResults.length === 0) {
1157
- return { frames: [], edges: [], animations: [] };
1158
- }
1155
+ function collectAllFrames(segmentResults) {
1159
1156
  const allFrames = [];
1160
1157
  for (const result of segmentResults) {
1161
1158
  for (const frame of result.frames) {
@@ -1170,19 +1167,24 @@ function mergeSegmentFrames(segmentResults) {
1170
1167
  });
1171
1168
  }
1172
1169
  }
1173
- allFrames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1174
- const effectiveFps = segmentResults[0].segment.effectiveFps;
1170
+ return allFrames;
1171
+ }
1172
+ function deduplicateFrames(frames, effectiveFps) {
1173
+ frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1175
1174
  const dupThreshold = 1 / (effectiveFps * 2);
1176
- const uniqueFrames = [];
1177
- for (const entry of allFrames) {
1178
- if (uniqueFrames.length > 0) {
1179
- const last = uniqueFrames[uniqueFrames.length - 1];
1175
+ const unique = [];
1176
+ for (const entry of frames) {
1177
+ if (unique.length > 0) {
1178
+ const last = unique[unique.length - 1];
1180
1179
  if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
1181
1180
  continue;
1182
1181
  }
1183
1182
  }
1184
- uniqueFrames.push(entry);
1183
+ unique.push(entry);
1185
1184
  }
1185
+ return unique;
1186
+ }
1187
+ function remapFrameIds(uniqueFrames) {
1186
1188
  const globalIdMap = /* @__PURE__ */ new Map();
1187
1189
  const frames = uniqueFrames.map((entry, globalId) => {
1188
1190
  globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
@@ -1192,6 +1194,9 @@ function mergeSegmentFrames(segmentResults) {
1192
1194
  extractPath: entry.frame.extractPath
1193
1195
  };
1194
1196
  });
1197
+ return { frames, globalIdMap };
1198
+ }
1199
+ function remapEdges(segmentResults, globalIdMap) {
1195
1200
  const edges = [];
1196
1201
  const edgeMap = /* @__PURE__ */ new Map();
1197
1202
  for (const result of segmentResults) {
@@ -1223,6 +1228,9 @@ function mergeSegmentFrames(segmentResults) {
1223
1228
  }
1224
1229
  }
1225
1230
  }
1231
+ return edges;
1232
+ }
1233
+ function remapAnimations(segmentResults, globalIdMap) {
1226
1234
  const animations = [];
1227
1235
  for (const result of segmentResults) {
1228
1236
  for (const anim of result.animations) {
@@ -1240,6 +1248,18 @@ function mergeSegmentFrames(segmentResults) {
1240
1248
  });
1241
1249
  }
1242
1250
  }
1251
+ return animations;
1252
+ }
1253
+ function mergeSegmentFrames(segmentResults) {
1254
+ if (segmentResults.length === 0) {
1255
+ return { frames: [], edges: [], animations: [] };
1256
+ }
1257
+ const effectiveFps = segmentResults[0].segment.effectiveFps;
1258
+ const allFrames = collectAllFrames(segmentResults);
1259
+ const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
1260
+ const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
1261
+ const edges = remapEdges(segmentResults, globalIdMap);
1262
+ const animations = remapAnimations(segmentResults, globalIdMap);
1243
1263
  return { frames, edges, animations };
1244
1264
  }
1245
1265
  function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
@@ -1155,10 +1155,7 @@ function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
1155
1155
  }
1156
1156
  return segments;
1157
1157
  }
1158
- function mergeSegmentFrames(segmentResults) {
1159
- if (segmentResults.length === 0) {
1160
- return { frames: [], edges: [], animations: [] };
1161
- }
1158
+ function collectAllFrames(segmentResults) {
1162
1159
  const allFrames = [];
1163
1160
  for (const result of segmentResults) {
1164
1161
  for (const frame of result.frames) {
@@ -1173,19 +1170,24 @@ function mergeSegmentFrames(segmentResults) {
1173
1170
  });
1174
1171
  }
1175
1172
  }
1176
- allFrames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1177
- const effectiveFps = segmentResults[0].segment.effectiveFps;
1173
+ return allFrames;
1174
+ }
1175
+ function deduplicateFrames(frames, effectiveFps) {
1176
+ frames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
1178
1177
  const dupThreshold = 1 / (effectiveFps * 2);
1179
- const uniqueFrames = [];
1180
- for (const entry of allFrames) {
1181
- if (uniqueFrames.length > 0) {
1182
- const last = uniqueFrames[uniqueFrames.length - 1];
1178
+ const unique = [];
1179
+ for (const entry of frames) {
1180
+ if (unique.length > 0) {
1181
+ const last = unique[unique.length - 1];
1183
1182
  if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
1184
1183
  continue;
1185
1184
  }
1186
1185
  }
1187
- uniqueFrames.push(entry);
1186
+ unique.push(entry);
1188
1187
  }
1188
+ return unique;
1189
+ }
1190
+ function remapFrameIds(uniqueFrames) {
1189
1191
  const globalIdMap = /* @__PURE__ */ new Map();
1190
1192
  const frames = uniqueFrames.map((entry, globalId) => {
1191
1193
  globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
@@ -1195,6 +1197,9 @@ function mergeSegmentFrames(segmentResults) {
1195
1197
  extractPath: entry.frame.extractPath
1196
1198
  };
1197
1199
  });
1200
+ return { frames, globalIdMap };
1201
+ }
1202
+ function remapEdges(segmentResults, globalIdMap) {
1198
1203
  const edges = [];
1199
1204
  const edgeMap = /* @__PURE__ */ new Map();
1200
1205
  for (const result of segmentResults) {
@@ -1226,6 +1231,9 @@ function mergeSegmentFrames(segmentResults) {
1226
1231
  }
1227
1232
  }
1228
1233
  }
1234
+ return edges;
1235
+ }
1236
+ function remapAnimations(segmentResults, globalIdMap) {
1229
1237
  const animations = [];
1230
1238
  for (const result of segmentResults) {
1231
1239
  for (const anim of result.animations) {
@@ -1243,6 +1251,18 @@ function mergeSegmentFrames(segmentResults) {
1243
1251
  });
1244
1252
  }
1245
1253
  }
1254
+ return animations;
1255
+ }
1256
+ function mergeSegmentFrames(segmentResults) {
1257
+ if (segmentResults.length === 0) {
1258
+ return { frames: [], edges: [], animations: [] };
1259
+ }
1260
+ const effectiveFps = segmentResults[0].segment.effectiveFps;
1261
+ const allFrames = collectAllFrames(segmentResults);
1262
+ const uniqueFrames = deduplicateFrames(allFrames, effectiveFps);
1263
+ const { frames, globalIdMap } = remapFrameIds(uniqueFrames);
1264
+ const edges = remapEdges(segmentResults, globalIdMap);
1265
+ const animations = remapAnimations(segmentResults, globalIdMap);
1246
1266
  return { frames, edges, animations };
1247
1267
  }
1248
1268
  function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Central command registry for scene-sieve CLI.
3
+ * Single source of truth for command metadata, used by --describe and --help.
4
+ */
5
+ export interface CommandOption {
6
+ flag: string;
7
+ description: string;
8
+ type?: 'boolean' | 'string' | 'number';
9
+ default?: string;
10
+ }
11
+ export interface CommandArgument {
12
+ name: string;
13
+ description: string;
14
+ required: boolean;
15
+ }
16
+ export interface CommandInfo {
17
+ name: string;
18
+ description: string;
19
+ usage: string;
20
+ arguments?: CommandArgument[];
21
+ options?: CommandOption[];
22
+ examples?: string[];
23
+ }
24
+ export declare const SIEVE_COMMAND: CommandInfo;
@@ -0,0 +1,21 @@
1
+ import type { SieveOptionsBase } from '../types/index.js';
2
+ /**
3
+ * Common pipeline options parsed from CLI opts (Commander string values → typed values).
4
+ * Does NOT include mode-specific fields (mode, inputPath, onProgress).
5
+ */
6
+ export type ParsedPipelineOptions = Pick<Required<SieveOptionsBase>, 'fps' | 'maxFrames' | 'scale' | 'quality' | 'debug'> & Pick<SieveOptionsBase, 'count' | 'threshold' | 'outputPath' | 'iouThreshold' | 'animationThreshold' | 'maxSegmentDuration' | 'concurrency'>;
7
+ export interface RawCliOptions {
8
+ count?: string;
9
+ threshold?: string;
10
+ output?: string;
11
+ fps: string;
12
+ maxFrames: string;
13
+ scale: string;
14
+ quality: string;
15
+ iouThreshold?: string;
16
+ animThreshold?: string;
17
+ maxSegmentDuration?: string;
18
+ concurrency?: string;
19
+ debug?: boolean;
20
+ }
21
+ export declare function parsePipelineOptions(opts: RawCliOptions): ParsedPipelineOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumy-pack/scene-sieve",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "CLI tool for extracting key frames from video and GIF files",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,7 +33,7 @@
33
33
  "main": "dist/index.cjs",
34
34
  "module": "dist/index.mjs",
35
35
  "types": "dist/index.d.ts",
36
- "bin": "./dist/cli.mjs",
36
+ "bin": "dist/cli.mjs",
37
37
  "files": [
38
38
  "dist",
39
39
  "!dist/tsconfig.tsbuildinfo"
@@ -66,6 +66,7 @@
66
66
  "sharp": "^0.33.0"
67
67
  },
68
68
  "devDependencies": {
69
+ "@lumy-pack/shared": "0.0.1",
69
70
  "@types/node": "^20.11.0",
70
71
  "@types/react": "^18.0.0",
71
72
  "@vitest/coverage-v8": "^3.2.4"