@lumy-pack/scene-sieve 0.0.10 → 0.0.12
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.d.ts +1 -0
- package/dist/cli.mjs +73 -31
- package/dist/commands/Sieve.d.ts +19 -0
- package/dist/components/PhaseStep.d.ts +14 -0
- package/dist/components/ProgressBar.d.ts +7 -0
- package/dist/constants.d.ts +32 -0
- package/dist/core/analyzer.d.ts +62 -0
- package/dist/core/dbscan.d.ts +10 -0
- package/dist/core/extractor.d.ts +30 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/input-resolver.d.ts +13 -0
- package/dist/core/orchestrator.d.ts +2 -0
- package/dist/core/pipeline-worker.d.ts +1 -0
- package/dist/core/pruner.d.ts +61 -0
- package/dist/core/run-in-worker.d.ts +9 -0
- package/dist/core/segmenter.d.ts +38 -0
- package/dist/core/workspace.d.ts +25 -0
- package/dist/errors.d.ts +10 -0
- package/dist/index.cjs +65 -29
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +65 -29
- package/dist/pipeline-worker.mjs +65 -29
- package/dist/types/index.d.ts +129 -0
- package/dist/utils/command-registry.d.ts +24 -0
- package/dist/utils/concurrency.d.ts +5 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/math.d.ts +27 -0
- package/dist/utils/min-heap.d.ts +16 -0
- package/dist/utils/parse-options.d.ts +21 -0
- package/dist/utils/paths.d.ts +18 -0
- package/package.json +4 -2
package/dist/pipeline-worker.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { parentPort, workerData } from "worker_threads";
|
|
|
3
3
|
|
|
4
4
|
// src/core/orchestrator.ts
|
|
5
5
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6
|
+
import { filter as filter6 } from "@winglet/common-utils";
|
|
6
7
|
|
|
7
8
|
// src/utils/logger.ts
|
|
8
9
|
import pc from "picocolors";
|
|
@@ -36,6 +37,7 @@ ${pc.green("done")} ${message}`);
|
|
|
36
37
|
|
|
37
38
|
// src/core/analyzer.ts
|
|
38
39
|
import { createRequire } from "module";
|
|
40
|
+
import { filter, map } from "@winglet/common-utils";
|
|
39
41
|
import sharp from "sharp";
|
|
40
42
|
|
|
41
43
|
// src/constants.ts
|
|
@@ -252,7 +254,8 @@ var IoUTracker = class {
|
|
|
252
254
|
this.collectAnimation(region);
|
|
253
255
|
}
|
|
254
256
|
}
|
|
255
|
-
this.regions =
|
|
257
|
+
this.regions = filter(
|
|
258
|
+
this.regions,
|
|
256
259
|
(r, i) => r.weight > 0.01 || matched.has(i)
|
|
257
260
|
);
|
|
258
261
|
return animationIndices;
|
|
@@ -433,7 +436,7 @@ function computeInformationGain(clusters, clusterPoints, imageArea, animationInd
|
|
|
433
436
|
async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
434
437
|
const edges = [];
|
|
435
438
|
const preprocessed = await Promise.all(
|
|
436
|
-
|
|
439
|
+
map(frames, (f) => preprocessFrame(f.extractPath, scale))
|
|
437
440
|
);
|
|
438
441
|
const imageWidth = preprocessed[0]?.width ?? scale;
|
|
439
442
|
const imageHeight = preprocessed[0]?.height ?? Math.round(scale * 9 / 16);
|
|
@@ -475,7 +478,8 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
475
478
|
}
|
|
476
479
|
}
|
|
477
480
|
const animationIndices = tracker.update(clusters, pairIndex);
|
|
478
|
-
const animationWeights =
|
|
481
|
+
const animationWeights = map(
|
|
482
|
+
clusters,
|
|
479
483
|
(_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0
|
|
480
484
|
);
|
|
481
485
|
const score = computeInformationGain(
|
|
@@ -540,6 +544,7 @@ async function analyzeFrames(ctx) {
|
|
|
540
544
|
import { readdir } from "fs/promises";
|
|
541
545
|
import { join as join2 } from "path";
|
|
542
546
|
import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
|
|
547
|
+
import { filter as filter2, map as map2 } from "@winglet/common-utils";
|
|
543
548
|
import { execa } from "execa";
|
|
544
549
|
import ffmpegPath from "ffmpeg-static";
|
|
545
550
|
|
|
@@ -651,11 +656,11 @@ async function getVideoMetadata(inputPath) {
|
|
|
651
656
|
}
|
|
652
657
|
async function buildFrameList(framesDir, duration) {
|
|
653
658
|
const files = await readdir(framesDir);
|
|
654
|
-
const jpgFiles = files
|
|
659
|
+
const jpgFiles = filter2(files, (f) => f.endsWith(".jpg")).sort();
|
|
655
660
|
if (jpgFiles.length === 0) {
|
|
656
661
|
return [];
|
|
657
662
|
}
|
|
658
|
-
return jpgFiles
|
|
663
|
+
return map2(jpgFiles, (file, index) => ({
|
|
659
664
|
id: index,
|
|
660
665
|
timestamp: duration > 0 && jpgFiles.length > 1 ? duration * index / (jpgFiles.length - 1) : index,
|
|
661
666
|
extractPath: join2(framesDir, file)
|
|
@@ -685,6 +690,7 @@ import { join as join4 } from "path";
|
|
|
685
690
|
// src/core/workspace.ts
|
|
686
691
|
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
687
692
|
import { join as join3 } from "path";
|
|
693
|
+
import { map as map3 } from "@winglet/common-utils";
|
|
688
694
|
import sharp2 from "sharp";
|
|
689
695
|
async function createWorkspace(sessionId) {
|
|
690
696
|
const workspacePath = getTempWorkspaceDir(sessionId);
|
|
@@ -725,7 +731,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
725
731
|
}
|
|
726
732
|
},
|
|
727
733
|
frames: framesMetadata,
|
|
728
|
-
animations: (ctx.animations || []
|
|
734
|
+
animations: map3(ctx.animations || [], (anim) => ({
|
|
729
735
|
...anim,
|
|
730
736
|
startFrameId: anim.startFrameId + 1,
|
|
731
737
|
endFrameId: anim.endFrameId + 1,
|
|
@@ -778,7 +784,8 @@ async function writeInputFrames(frames, workspacePath) {
|
|
|
778
784
|
}
|
|
779
785
|
async function readFramesAsBuffers(frameNodes, quality) {
|
|
780
786
|
return Promise.all(
|
|
781
|
-
|
|
787
|
+
map3(
|
|
788
|
+
frameNodes,
|
|
782
789
|
(f) => sharp2(f.extractPath).jpeg({ quality, mozjpeg: true }).toBuffer()
|
|
783
790
|
)
|
|
784
791
|
);
|
|
@@ -835,39 +842,46 @@ async function resolveInput(options2, workspacePath) {
|
|
|
835
842
|
throw new Error(`Unsupported input mode: ${options2.mode}`);
|
|
836
843
|
}
|
|
837
844
|
|
|
845
|
+
// src/core/pruner.ts
|
|
846
|
+
import { filter as filter4, map as map5 } from "@winglet/common-utils";
|
|
847
|
+
|
|
838
848
|
// src/utils/math.ts
|
|
849
|
+
import { filter as filter3, map as map4 } from "@winglet/common-utils";
|
|
839
850
|
function normalizeScores(items) {
|
|
840
851
|
if (items.length === 0) return [];
|
|
841
|
-
const safeScores =
|
|
852
|
+
const safeScores = map4(
|
|
853
|
+
items,
|
|
842
854
|
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
843
855
|
);
|
|
844
|
-
const positiveScores = safeScores
|
|
856
|
+
const positiveScores = filter3(safeScores, (s) => s > 0);
|
|
845
857
|
if (positiveScores.length === 0) return safeScores;
|
|
846
858
|
const sorted = [...positiveScores].sort((a, b) => a - b);
|
|
847
859
|
if (positiveScores.length <= NORMALIZATION_MIN_SAMPLE_SIZE) {
|
|
848
860
|
const min = sorted[0];
|
|
849
861
|
const max = sorted[sorted.length - 1];
|
|
850
|
-
if (max === min) return safeScores
|
|
851
|
-
return
|
|
862
|
+
if (max === min) return map4(safeScores, (s) => s > 0 ? 1 : 0);
|
|
863
|
+
return map4(
|
|
864
|
+
safeScores,
|
|
852
865
|
(s) => s <= 0 ? 0 : Math.max(0, Math.min((s - min) / (max - min), 1))
|
|
853
866
|
);
|
|
854
867
|
}
|
|
855
868
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
856
|
-
const absoluteDiffs = positiveScores
|
|
869
|
+
const absoluteDiffs = map4(positiveScores, (v) => Math.abs(v - median));
|
|
857
870
|
const mad = [...absoluteDiffs].sort((a, b) => a - b)[Math.floor(absoluteDiffs.length / 2)];
|
|
858
871
|
const scale = mad === 0 ? median : mad * NORMALIZATION_MAD_COEFFICIENT;
|
|
859
|
-
const logisticZ = safeScores
|
|
872
|
+
const logisticZ = map4(safeScores, (s) => {
|
|
860
873
|
if (s <= 0) return 0;
|
|
861
874
|
if (scale === 0) return 1;
|
|
862
875
|
const z = (s - median) / scale;
|
|
863
876
|
return 1 / (1 + Math.exp(-NORMALIZATION_LOGISTIC_K * z));
|
|
864
877
|
});
|
|
865
|
-
const cdf = safeScores
|
|
878
|
+
const cdf = map4(safeScores, (s) => {
|
|
866
879
|
if (s <= 0) return 0;
|
|
867
880
|
const rank = sorted.findIndex((v) => v >= s);
|
|
868
881
|
return rank / sorted.length;
|
|
869
882
|
});
|
|
870
|
-
return
|
|
883
|
+
return map4(
|
|
884
|
+
logisticZ,
|
|
871
885
|
(z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA
|
|
872
886
|
);
|
|
873
887
|
}
|
|
@@ -919,7 +933,7 @@ var MinHeap = class {
|
|
|
919
933
|
// src/core/pruner.ts
|
|
920
934
|
function pruneTo(graph, frames, targetCount) {
|
|
921
935
|
if (frames.length <= targetCount) {
|
|
922
|
-
return new Set(frames
|
|
936
|
+
return new Set(map5(frames, (f) => f.id));
|
|
923
937
|
}
|
|
924
938
|
const prev = /* @__PURE__ */ new Map();
|
|
925
939
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -937,7 +951,7 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
937
951
|
tgtId: edge.targetId
|
|
938
952
|
});
|
|
939
953
|
}
|
|
940
|
-
const surviving = new Set(frames
|
|
954
|
+
const surviving = new Set(map5(frames, (f) => f.id));
|
|
941
955
|
const firstId = frames[0].id;
|
|
942
956
|
const lastId = frames[frames.length - 1].id;
|
|
943
957
|
while (surviving.size > targetCount && heap.size > 0) {
|
|
@@ -1030,7 +1044,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1030
1044
|
if (thresholdSurvivors.size <= maxCount) {
|
|
1031
1045
|
return thresholdSurvivors;
|
|
1032
1046
|
}
|
|
1033
|
-
const survivingFrames = frames
|
|
1047
|
+
const survivingFrames = filter4(frames, (f) => thresholdSurvivors.has(f.id));
|
|
1034
1048
|
const idToOrigIdx = /* @__PURE__ */ new Map();
|
|
1035
1049
|
for (let i = 0; i < frames.length; i++) {
|
|
1036
1050
|
idToOrigIdx.set(frames[i].id, i);
|
|
@@ -1066,6 +1080,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1066
1080
|
// src/core/segmenter.ts
|
|
1067
1081
|
import { randomUUID } from "crypto";
|
|
1068
1082
|
import { join as join5 } from "path";
|
|
1083
|
+
import { filter as filter5, map as map6 } from "@winglet/common-utils";
|
|
1069
1084
|
|
|
1070
1085
|
// src/utils/concurrency.ts
|
|
1071
1086
|
function concurrencyLimit(limit) {
|
|
@@ -1204,18 +1219,30 @@ function remapEdges(segmentResults, globalIdMap) {
|
|
|
1204
1219
|
const edgeMap = /* @__PURE__ */ new Map();
|
|
1205
1220
|
for (const result of segmentResults) {
|
|
1206
1221
|
for (const edge of result.edges) {
|
|
1207
|
-
const newSourceId = globalIdMap.get(
|
|
1208
|
-
|
|
1222
|
+
const newSourceId = globalIdMap.get(
|
|
1223
|
+
`${result.segment.index}:${edge.sourceId}`
|
|
1224
|
+
);
|
|
1225
|
+
const newTargetId = globalIdMap.get(
|
|
1226
|
+
`${result.segment.index}:${edge.targetId}`
|
|
1227
|
+
);
|
|
1209
1228
|
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1210
1229
|
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1211
1230
|
const existingIdx = edgeMap.get(edgeKey);
|
|
1212
1231
|
if (existingIdx !== void 0) {
|
|
1213
1232
|
if (edges[existingIdx].score < edge.score) {
|
|
1214
|
-
edges[existingIdx] = {
|
|
1233
|
+
edges[existingIdx] = {
|
|
1234
|
+
sourceId: newSourceId,
|
|
1235
|
+
targetId: newTargetId,
|
|
1236
|
+
score: edge.score
|
|
1237
|
+
};
|
|
1215
1238
|
}
|
|
1216
1239
|
} else {
|
|
1217
1240
|
edgeMap.set(edgeKey, edges.length);
|
|
1218
|
-
edges.push({
|
|
1241
|
+
edges.push({
|
|
1242
|
+
sourceId: newSourceId,
|
|
1243
|
+
targetId: newTargetId,
|
|
1244
|
+
score: edge.score
|
|
1245
|
+
});
|
|
1219
1246
|
}
|
|
1220
1247
|
}
|
|
1221
1248
|
}
|
|
@@ -1225,10 +1252,18 @@ function remapAnimations(segmentResults, globalIdMap) {
|
|
|
1225
1252
|
const animations = [];
|
|
1226
1253
|
for (const result of segmentResults) {
|
|
1227
1254
|
for (const anim of result.animations) {
|
|
1228
|
-
const newStartId = globalIdMap.get(
|
|
1229
|
-
|
|
1255
|
+
const newStartId = globalIdMap.get(
|
|
1256
|
+
`${result.segment.index}:${anim.startFrameId}`
|
|
1257
|
+
);
|
|
1258
|
+
const newEndId = globalIdMap.get(
|
|
1259
|
+
`${result.segment.index}:${anim.endFrameId}`
|
|
1260
|
+
);
|
|
1230
1261
|
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1231
|
-
animations.push({
|
|
1262
|
+
animations.push({
|
|
1263
|
+
...anim,
|
|
1264
|
+
startFrameId: newStartId,
|
|
1265
|
+
endFrameId: newEndId
|
|
1266
|
+
});
|
|
1232
1267
|
}
|
|
1233
1268
|
}
|
|
1234
1269
|
return animations;
|
|
@@ -1309,7 +1344,7 @@ async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
|
1309
1344
|
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1310
1345
|
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1311
1346
|
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1312
|
-
const weights = segments
|
|
1347
|
+
const weights = map6(segments, (s) => s.duration / totalDuration);
|
|
1313
1348
|
const emitOverallProgress = (phase) => {
|
|
1314
1349
|
if (!options2.onProgress) return;
|
|
1315
1350
|
const overall = weights.reduce(
|
|
@@ -1320,7 +1355,8 @@ async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
|
1320
1355
|
};
|
|
1321
1356
|
options2.onProgress?.("EXTRACTING", 0);
|
|
1322
1357
|
const results = await Promise.all(
|
|
1323
|
-
|
|
1358
|
+
map6(
|
|
1359
|
+
segments,
|
|
1324
1360
|
(segment) => limit(async () => {
|
|
1325
1361
|
const segWorkspace = await createSegmentWorkspace(
|
|
1326
1362
|
mainWorkspace,
|
|
@@ -1352,7 +1388,7 @@ async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
|
1352
1388
|
resolvedOptions.threshold,
|
|
1353
1389
|
resolvedOptions.count
|
|
1354
1390
|
);
|
|
1355
|
-
const prunedFrames = frames
|
|
1391
|
+
const prunedFrames = filter5(frames, (f) => survivingIds.has(f.id));
|
|
1356
1392
|
options2.onProgress?.("PRUNING", 100);
|
|
1357
1393
|
options2.onProgress?.("FINALIZING", 0);
|
|
1358
1394
|
const ctx = {
|
|
@@ -1462,7 +1498,7 @@ async function runPipeline(options2) {
|
|
|
1462
1498
|
resolvedOptions.threshold,
|
|
1463
1499
|
resolvedOptions.count
|
|
1464
1500
|
);
|
|
1465
|
-
const prunedFrames = ctx.frames
|
|
1501
|
+
const prunedFrames = filter6(ctx.frames, (f) => survivingIds.has(f.id));
|
|
1466
1502
|
ctx.emitProgress(100);
|
|
1467
1503
|
ctx.status = "FINALIZING";
|
|
1468
1504
|
let outputFiles = [];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export type ProgressPhase = 'EXTRACTING' | 'ANALYZING' | 'PRUNING' | 'FINALIZING';
|
|
2
|
+
export type SieveInput = {
|
|
3
|
+
mode: 'file';
|
|
4
|
+
inputPath: string;
|
|
5
|
+
} | {
|
|
6
|
+
mode: 'buffer';
|
|
7
|
+
inputBuffer: Buffer;
|
|
8
|
+
} | {
|
|
9
|
+
mode: 'frames';
|
|
10
|
+
inputFrames: Buffer[];
|
|
11
|
+
};
|
|
12
|
+
export interface SieveOptionsBase {
|
|
13
|
+
count?: number;
|
|
14
|
+
threshold?: number;
|
|
15
|
+
outputPath?: string;
|
|
16
|
+
fps?: number;
|
|
17
|
+
maxFrames?: number;
|
|
18
|
+
scale?: number;
|
|
19
|
+
quality?: number;
|
|
20
|
+
iouThreshold?: number;
|
|
21
|
+
animationThreshold?: number;
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
onProgress?: (phase: ProgressPhase, percent: number) => void;
|
|
24
|
+
maxSegmentDuration?: number;
|
|
25
|
+
concurrency?: number;
|
|
26
|
+
}
|
|
27
|
+
export type SieveOptions = SieveOptionsBase & SieveInput;
|
|
28
|
+
export interface ResolvedOptions {
|
|
29
|
+
mode: 'file' | 'buffer' | 'frames';
|
|
30
|
+
inputPath?: string;
|
|
31
|
+
count: number;
|
|
32
|
+
threshold: number;
|
|
33
|
+
pruneMode: 'threshold-with-cap';
|
|
34
|
+
outputPath: string;
|
|
35
|
+
fps: number;
|
|
36
|
+
maxFrames: number;
|
|
37
|
+
scale: number;
|
|
38
|
+
quality: number;
|
|
39
|
+
iouThreshold: number;
|
|
40
|
+
animationThreshold: number;
|
|
41
|
+
debug: boolean;
|
|
42
|
+
maxSegmentDuration: number;
|
|
43
|
+
concurrency: number;
|
|
44
|
+
}
|
|
45
|
+
export interface SieveResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
originalFramesCount: number;
|
|
48
|
+
prunedFramesCount: number;
|
|
49
|
+
outputFiles: string[];
|
|
50
|
+
outputBuffers?: Buffer[];
|
|
51
|
+
animations?: AnimationMetadata[];
|
|
52
|
+
video?: VideoMetadata;
|
|
53
|
+
executionTimeMs: number;
|
|
54
|
+
}
|
|
55
|
+
export interface AnimationMetadata {
|
|
56
|
+
type: string;
|
|
57
|
+
boundingBox: BoundingBox;
|
|
58
|
+
startFrameId: number;
|
|
59
|
+
endFrameId: number;
|
|
60
|
+
durationMs: number;
|
|
61
|
+
}
|
|
62
|
+
export interface VideoMetadata {
|
|
63
|
+
originalDurationMs: number;
|
|
64
|
+
fps: number;
|
|
65
|
+
resolution: {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export interface FrameNode {
|
|
71
|
+
id: number;
|
|
72
|
+
timestamp: number;
|
|
73
|
+
extractPath: string;
|
|
74
|
+
}
|
|
75
|
+
export interface ScoreEdge {
|
|
76
|
+
sourceId: number;
|
|
77
|
+
targetId: number;
|
|
78
|
+
/**
|
|
79
|
+
* Information gain score (G(t)) between adjacent frames.
|
|
80
|
+
* Higher values = greater visual change (state transition) = should be preserved.
|
|
81
|
+
* Lower values = similar frames (little change) = candidates for pruning.
|
|
82
|
+
*
|
|
83
|
+
* Pruner removes frames with the LOWEST scores first (greedy ascending).
|
|
84
|
+
* Maps directly to G(t) from the vision analysis pipeline.
|
|
85
|
+
*/
|
|
86
|
+
score: number;
|
|
87
|
+
}
|
|
88
|
+
export interface BoundingBox {
|
|
89
|
+
x: number;
|
|
90
|
+
y: number;
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
}
|
|
94
|
+
export interface DBSCANResult {
|
|
95
|
+
labels: number[];
|
|
96
|
+
boundingBoxes: BoundingBox[];
|
|
97
|
+
}
|
|
98
|
+
export interface ProcessContext {
|
|
99
|
+
options: ResolvedOptions;
|
|
100
|
+
workspacePath: string;
|
|
101
|
+
frames: FrameNode[];
|
|
102
|
+
graph: ScoreEdge[];
|
|
103
|
+
animations?: AnimationMetadata[];
|
|
104
|
+
status: 'INIT' | ProgressPhase | 'SUCCESS' | 'FAILED';
|
|
105
|
+
emitProgress: (percent: number) => void;
|
|
106
|
+
error?: Error;
|
|
107
|
+
}
|
|
108
|
+
export interface AnalysisResult {
|
|
109
|
+
edges: ScoreEdge[];
|
|
110
|
+
animations: AnimationMetadata[];
|
|
111
|
+
}
|
|
112
|
+
export interface SegmentPlan {
|
|
113
|
+
index: number;
|
|
114
|
+
startTime: number;
|
|
115
|
+
endTime: number;
|
|
116
|
+
duration: number;
|
|
117
|
+
allocatedFrames: number;
|
|
118
|
+
effectiveFps: number;
|
|
119
|
+
overlapBefore: number;
|
|
120
|
+
overlapAfter: number;
|
|
121
|
+
extractStartTime: number;
|
|
122
|
+
extractDuration: number;
|
|
123
|
+
}
|
|
124
|
+
export interface SegmentResult {
|
|
125
|
+
segment: SegmentPlan;
|
|
126
|
+
frames: FrameNode[];
|
|
127
|
+
edges: ScoreEdge[];
|
|
128
|
+
animations: AnimationMetadata[];
|
|
129
|
+
}
|
|
@@ -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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for objects that have a numeric score.
|
|
3
|
+
*/
|
|
4
|
+
export interface ScoredItem {
|
|
5
|
+
score: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Normalize raw scores to [0, 1] range via Robust Hybrid Normalization.
|
|
9
|
+
*
|
|
10
|
+
* This model combines two mathematical approaches to provide a stable "relative" threshold:
|
|
11
|
+
*
|
|
12
|
+
* 1. Logistic-Robust-Z (Intensity):
|
|
13
|
+
* Calculates Z-scores using Median and Median Absolute Deviation (MAD).
|
|
14
|
+
* Maps these to a sigmoid (logistic) curve. This suppresses noise (scores near median)
|
|
15
|
+
* and highlights significant signals (outliers) without letting extreme outliers
|
|
16
|
+
* crush other meaningful transitions.
|
|
17
|
+
*
|
|
18
|
+
* 2. CDF / Percentile Rank (Relative Position):
|
|
19
|
+
* Maps each score to its percentile rank in the sequence. This ensures that 't'
|
|
20
|
+
* always has a consistent meaning as a "relative rank" regardless of absolute values.
|
|
21
|
+
*
|
|
22
|
+
* The final score is a weighted sum (NORMALIZATION_ALPHA) of both.
|
|
23
|
+
*
|
|
24
|
+
* @param items - Array of items with scores to normalize
|
|
25
|
+
* @returns normalized scores array (same length as input)
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeScores<T extends ScoredItem>(items: T[]): number[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic binary min-heap.
|
|
3
|
+
*
|
|
4
|
+
* Elements are ordered by a numeric `score` field.
|
|
5
|
+
* push/pop: O(log N), size: O(1).
|
|
6
|
+
*/
|
|
7
|
+
export declare class MinHeap<T extends {
|
|
8
|
+
score: number;
|
|
9
|
+
}> {
|
|
10
|
+
private readonly h;
|
|
11
|
+
get size(): number;
|
|
12
|
+
push(entry: T): void;
|
|
13
|
+
pop(): T | undefined;
|
|
14
|
+
private siftUp;
|
|
15
|
+
private siftDown;
|
|
16
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function ensureDir(dirPath: string): Promise<void>;
|
|
2
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
3
|
+
/**
|
|
4
|
+
* Expand leading ~ to homedir. Node's path.resolve() does not expand ~,
|
|
5
|
+
* so paths like ~/Desktop/foo depend on process.cwd() and can produce
|
|
6
|
+
* different results when run from different directories.
|
|
7
|
+
*/
|
|
8
|
+
export declare function expandTilde(p: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve path to absolute. Expands ~ to homedir first so that the result
|
|
11
|
+
* does not depend on process.cwd().
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveAbsolute(p: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Derive default output directory name from input file path.
|
|
16
|
+
* e.g., /path/to/video.mp4 -> /path/to/video_scenes
|
|
17
|
+
*/
|
|
18
|
+
export declare function deriveOutputPath(inputPath: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumy-pack/scene-sieve",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "CLI tool for extracting key frames from video and GIF files",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -55,8 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@ffprobe-installer/ffprobe": "^1.4.1",
|
|
58
|
-
"@lumy-pack/shared": "0.0.1",
|
|
59
58
|
"@techstark/opencv-js": "4.12.0-release.1",
|
|
59
|
+
"@winglet/common-utils": "^0.11.2",
|
|
60
|
+
"@winglet/react-utils": "^0.11.2",
|
|
60
61
|
"commander": "^12.1.0",
|
|
61
62
|
"execa": "^9.5.0",
|
|
62
63
|
"ffmpeg-static": "^5.2.0",
|
|
@@ -67,6 +68,7 @@
|
|
|
67
68
|
"sharp": "^0.33.0"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
71
|
+
"@lumy-pack/shared": "0.0.1",
|
|
70
72
|
"@types/node": "^20.11.0",
|
|
71
73
|
"@types/react": "^18.0.0",
|
|
72
74
|
"@vitest/coverage-v8": "^3.2.4"
|