@lumy-pack/scene-sieve 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/cli.mjs +459 -23
- package/dist/commands/Sieve.d.ts +2 -0
- package/dist/constants.d.ts +8 -1
- package/dist/core/extractor.d.ts +23 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/segmenter.d.ts +38 -0
- package/dist/core/workspace.d.ts +1 -0
- package/dist/index.cjs +423 -19
- package/dist/index.mjs +423 -19
- package/dist/pipeline-worker.mjs +423 -19
- package/dist/types/index.d.ts +22 -0
- package/dist/utils/concurrency.d.ts +5 -0
- package/dist/utils/math.d.ts +27 -0
- package/package.json +1 -1
package/dist/pipeline-worker.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { parentPort, workerData } from "worker_threads";
|
|
3
3
|
|
|
4
4
|
// src/core/orchestrator.ts
|
|
5
|
-
import { randomUUID } from "crypto";
|
|
5
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
6
6
|
|
|
7
7
|
// src/utils/logger.ts
|
|
8
8
|
import pc from "picocolors";
|
|
@@ -48,7 +48,10 @@ var DEFAULT_FPS = 5;
|
|
|
48
48
|
var DEFAULT_SCALE = 720;
|
|
49
49
|
var DEFAULT_QUALITY = 80;
|
|
50
50
|
var DEFAULT_MAX_FRAMES = 300;
|
|
51
|
-
var
|
|
51
|
+
var NORMALIZATION_LOGISTIC_K = 3;
|
|
52
|
+
var NORMALIZATION_ALPHA = 0.4;
|
|
53
|
+
var NORMALIZATION_MAD_COEFFICIENT = 1.4826;
|
|
54
|
+
var NORMALIZATION_MIN_SAMPLE_SIZE = 10;
|
|
52
55
|
var WORKSPACE_PREFIX = `${APP_NAME}-`;
|
|
53
56
|
var TEMP_BASE_DIR = tmpdir();
|
|
54
57
|
var FRAME_OUTPUT_EXTENSION = ".jpg";
|
|
@@ -64,6 +67,8 @@ var PIXELDIFF_GAUSSIAN_KERNEL = 3;
|
|
|
64
67
|
var PIXELDIFF_BINARY_THRESHOLD = 30;
|
|
65
68
|
var PIXELDIFF_CONTOUR_MIN_AREA = 100;
|
|
66
69
|
var PIXELDIFF_SAMPLE_SPACING = 8;
|
|
70
|
+
var DEFAULT_MAX_SEGMENT_DURATION = 300;
|
|
71
|
+
var DEFAULT_SEGMENT_CONCURRENCY = 2;
|
|
67
72
|
function getTempWorkspaceDir(sessionId) {
|
|
68
73
|
return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
|
|
69
74
|
}
|
|
@@ -656,6 +661,23 @@ async function buildFrameList(framesDir, duration) {
|
|
|
656
661
|
extractPath: join2(framesDir, file)
|
|
657
662
|
}));
|
|
658
663
|
}
|
|
664
|
+
async function extractFramesForRange(inputPath, outputDir, fps, scale, startTime, duration) {
|
|
665
|
+
const outputPattern = join2(outputDir, FRAME_FILENAME_PATTERN);
|
|
666
|
+
await execa(ffmpegPath, [
|
|
667
|
+
"-ss",
|
|
668
|
+
String(startTime),
|
|
669
|
+
"-i",
|
|
670
|
+
inputPath,
|
|
671
|
+
"-t",
|
|
672
|
+
String(duration),
|
|
673
|
+
"-vf",
|
|
674
|
+
`fps=${fps},scale=-1:${scale}`,
|
|
675
|
+
"-q:v",
|
|
676
|
+
"2",
|
|
677
|
+
outputPattern
|
|
678
|
+
]);
|
|
679
|
+
return buildFrameList(outputDir, duration);
|
|
680
|
+
}
|
|
659
681
|
|
|
660
682
|
// src/core/input-resolver.ts
|
|
661
683
|
import { join as join4 } from "path";
|
|
@@ -718,6 +740,15 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
718
740
|
await rename(stagingDir, outputPath);
|
|
719
741
|
return outputFiles;
|
|
720
742
|
}
|
|
743
|
+
async function createSegmentWorkspace(parentWorkspacePath, segmentIndex) {
|
|
744
|
+
const segmentPath = join3(
|
|
745
|
+
parentWorkspacePath,
|
|
746
|
+
"segments",
|
|
747
|
+
String(segmentIndex)
|
|
748
|
+
);
|
|
749
|
+
await ensureDir(join3(segmentPath, "frames"));
|
|
750
|
+
return segmentPath;
|
|
751
|
+
}
|
|
721
752
|
async function cleanupWorkspace(workspacePath) {
|
|
722
753
|
if (!workspacePath) return;
|
|
723
754
|
try {
|
|
@@ -778,7 +809,9 @@ function resolveOptions(options2) {
|
|
|
778
809
|
quality: options2.quality ?? DEFAULT_QUALITY,
|
|
779
810
|
iouThreshold: options2.iouThreshold ?? IOU_THRESHOLD,
|
|
780
811
|
animationThreshold: options2.animationThreshold ?? ANIMATION_FRAME_THRESHOLD,
|
|
781
|
-
debug: options2.debug ?? false
|
|
812
|
+
debug: options2.debug ?? false,
|
|
813
|
+
maxSegmentDuration: options2.maxSegmentDuration ?? DEFAULT_MAX_SEGMENT_DURATION,
|
|
814
|
+
concurrency: options2.concurrency ?? DEFAULT_SEGMENT_CONCURRENCY
|
|
782
815
|
};
|
|
783
816
|
}
|
|
784
817
|
async function resolveInput(options2, workspacePath) {
|
|
@@ -802,6 +835,43 @@ async function resolveInput(options2, workspacePath) {
|
|
|
802
835
|
throw new Error(`Unsupported input mode: ${options2.mode}`);
|
|
803
836
|
}
|
|
804
837
|
|
|
838
|
+
// src/utils/math.ts
|
|
839
|
+
function normalizeScores(items) {
|
|
840
|
+
if (items.length === 0) return [];
|
|
841
|
+
const safeScores = items.map(
|
|
842
|
+
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
843
|
+
);
|
|
844
|
+
const positiveScores = safeScores.filter((s) => s > 0);
|
|
845
|
+
if (positiveScores.length === 0) return safeScores;
|
|
846
|
+
const sorted = [...positiveScores].sort((a, b) => a - b);
|
|
847
|
+
if (positiveScores.length <= NORMALIZATION_MIN_SAMPLE_SIZE) {
|
|
848
|
+
const min = sorted[0];
|
|
849
|
+
const max = sorted[sorted.length - 1];
|
|
850
|
+
if (max === min) return safeScores.map((s) => s > 0 ? 1 : 0);
|
|
851
|
+
return safeScores.map(
|
|
852
|
+
(s) => s <= 0 ? 0 : Math.max(0, Math.min((s - min) / (max - min), 1))
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
856
|
+
const absoluteDiffs = positiveScores.map((v) => Math.abs(v - median));
|
|
857
|
+
const mad = [...absoluteDiffs].sort((a, b) => a - b)[Math.floor(absoluteDiffs.length / 2)];
|
|
858
|
+
const scale = mad === 0 ? median : mad * NORMALIZATION_MAD_COEFFICIENT;
|
|
859
|
+
const logisticZ = safeScores.map((s) => {
|
|
860
|
+
if (s <= 0) return 0;
|
|
861
|
+
if (scale === 0) return 1;
|
|
862
|
+
const z = (s - median) / scale;
|
|
863
|
+
return 1 / (1 + Math.exp(-NORMALIZATION_LOGISTIC_K * z));
|
|
864
|
+
});
|
|
865
|
+
const cdf = safeScores.map((s) => {
|
|
866
|
+
if (s <= 0) return 0;
|
|
867
|
+
const rank = sorted.findIndex((v) => v >= s);
|
|
868
|
+
return rank / sorted.length;
|
|
869
|
+
});
|
|
870
|
+
return logisticZ.map(
|
|
871
|
+
(z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
805
875
|
// src/utils/min-heap.ts
|
|
806
876
|
var MinHeap = class {
|
|
807
877
|
h = [];
|
|
@@ -896,20 +966,6 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
896
966
|
}
|
|
897
967
|
return surviving;
|
|
898
968
|
}
|
|
899
|
-
function normalizeScores(graph) {
|
|
900
|
-
if (graph.length === 0) return [];
|
|
901
|
-
const safeScores = graph.map(
|
|
902
|
-
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
903
|
-
);
|
|
904
|
-
const sorted = [...safeScores].filter((s) => s > 0).sort((a, b) => a - b);
|
|
905
|
-
if (sorted.length === 0) return safeScores;
|
|
906
|
-
const pIdx = Math.min(
|
|
907
|
-
Math.floor(sorted.length * NORMALIZATION_PERCENTILE),
|
|
908
|
-
sorted.length - 1
|
|
909
|
-
);
|
|
910
|
-
const refScore = sorted[pIdx];
|
|
911
|
-
return safeScores.map((s) => Math.min(s / refScore, 1));
|
|
912
|
-
}
|
|
913
969
|
function suppressConsecutiveRuns(graph, passingIndices, normalizedScores) {
|
|
914
970
|
const result = /* @__PURE__ */ new Set();
|
|
915
971
|
let runStart = 0;
|
|
@@ -1007,13 +1063,361 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1007
1063
|
return pruneTo(syntheticEdges, survivingFrames, maxCount);
|
|
1008
1064
|
}
|
|
1009
1065
|
|
|
1066
|
+
// src/core/segmenter.ts
|
|
1067
|
+
import { randomUUID } from "crypto";
|
|
1068
|
+
import { join as join5 } from "path";
|
|
1069
|
+
|
|
1070
|
+
// src/utils/concurrency.ts
|
|
1071
|
+
function concurrencyLimit(limit) {
|
|
1072
|
+
limit = Math.max(1, limit);
|
|
1073
|
+
let active = 0;
|
|
1074
|
+
const queue = [];
|
|
1075
|
+
return async (fn) => {
|
|
1076
|
+
while (active >= limit) {
|
|
1077
|
+
await new Promise((resolve2) => queue.push(resolve2));
|
|
1078
|
+
}
|
|
1079
|
+
active++;
|
|
1080
|
+
try {
|
|
1081
|
+
return await fn();
|
|
1082
|
+
} finally {
|
|
1083
|
+
active--;
|
|
1084
|
+
queue.shift()?.();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/core/segmenter.ts
|
|
1090
|
+
function shouldSegment(resolvedOptions, originalOptions) {
|
|
1091
|
+
if (resolvedOptions.mode === "frames") return false;
|
|
1092
|
+
if (originalOptions.mode === "file") {
|
|
1093
|
+
if (originalOptions.inputPath.toLowerCase().endsWith(".gif")) return false;
|
|
1094
|
+
}
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
function computeSegmentPlan(totalDuration, maxSegmentDuration, maxFrames, fps) {
|
|
1098
|
+
const effectiveFps = Math.max(0.5, Math.min(fps, maxFrames / totalDuration));
|
|
1099
|
+
if (totalDuration <= maxSegmentDuration) {
|
|
1100
|
+
return [
|
|
1101
|
+
{
|
|
1102
|
+
index: 0,
|
|
1103
|
+
startTime: 0,
|
|
1104
|
+
endTime: totalDuration,
|
|
1105
|
+
duration: totalDuration,
|
|
1106
|
+
allocatedFrames: Math.min(
|
|
1107
|
+
Math.ceil(effectiveFps * totalDuration),
|
|
1108
|
+
maxFrames
|
|
1109
|
+
),
|
|
1110
|
+
effectiveFps,
|
|
1111
|
+
overlapBefore: 0,
|
|
1112
|
+
overlapAfter: 0,
|
|
1113
|
+
extractStartTime: 0,
|
|
1114
|
+
extractDuration: totalDuration
|
|
1115
|
+
}
|
|
1116
|
+
];
|
|
1117
|
+
}
|
|
1118
|
+
const segmentCount = Math.ceil(totalDuration / maxSegmentDuration);
|
|
1119
|
+
const overlapTime = 1 / effectiveFps;
|
|
1120
|
+
const segments = [];
|
|
1121
|
+
for (let i = 0; i < segmentCount; i++) {
|
|
1122
|
+
const startTime = i * maxSegmentDuration;
|
|
1123
|
+
const endTime = Math.min((i + 1) * maxSegmentDuration, totalDuration);
|
|
1124
|
+
const duration = endTime - startTime;
|
|
1125
|
+
const overlapBefore = i > 0 ? 1 : 0;
|
|
1126
|
+
const overlapAfter = i < segmentCount - 1 ? 1 : 0;
|
|
1127
|
+
const extractStartTime = Math.max(
|
|
1128
|
+
0,
|
|
1129
|
+
startTime - overlapBefore * overlapTime
|
|
1130
|
+
);
|
|
1131
|
+
const extractEndTime = Math.min(
|
|
1132
|
+
totalDuration,
|
|
1133
|
+
endTime + overlapAfter * overlapTime
|
|
1134
|
+
);
|
|
1135
|
+
const extractDuration = extractEndTime - extractStartTime;
|
|
1136
|
+
segments.push({
|
|
1137
|
+
index: i,
|
|
1138
|
+
startTime,
|
|
1139
|
+
endTime,
|
|
1140
|
+
duration,
|
|
1141
|
+
allocatedFrames: Math.ceil(effectiveFps * duration),
|
|
1142
|
+
effectiveFps,
|
|
1143
|
+
overlapBefore,
|
|
1144
|
+
overlapAfter,
|
|
1145
|
+
extractStartTime,
|
|
1146
|
+
extractDuration
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
const totalAllocated = segments.reduce(
|
|
1150
|
+
(sum, s) => sum + s.allocatedFrames,
|
|
1151
|
+
0
|
|
1152
|
+
);
|
|
1153
|
+
if (totalAllocated > maxFrames) {
|
|
1154
|
+
segments[segments.length - 1].allocatedFrames -= totalAllocated - maxFrames;
|
|
1155
|
+
}
|
|
1156
|
+
return segments;
|
|
1157
|
+
}
|
|
1158
|
+
function mergeSegmentFrames(segmentResults) {
|
|
1159
|
+
if (segmentResults.length === 0) {
|
|
1160
|
+
return { frames: [], edges: [], animations: [] };
|
|
1161
|
+
}
|
|
1162
|
+
const allFrames = [];
|
|
1163
|
+
for (const result of segmentResults) {
|
|
1164
|
+
for (const frame of result.frames) {
|
|
1165
|
+
allFrames.push({
|
|
1166
|
+
frame: {
|
|
1167
|
+
...frame,
|
|
1168
|
+
// Use extractStartTime for timestamp correction (Section 18 note 1)
|
|
1169
|
+
timestamp: frame.timestamp + result.segment.extractStartTime
|
|
1170
|
+
},
|
|
1171
|
+
segmentIndex: result.segment.index,
|
|
1172
|
+
localId: frame.id
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
allFrames.sort((a, b) => a.frame.timestamp - b.frame.timestamp);
|
|
1177
|
+
const effectiveFps = segmentResults[0].segment.effectiveFps;
|
|
1178
|
+
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];
|
|
1183
|
+
if (Math.abs(entry.frame.timestamp - last.frame.timestamp) < dupThreshold) {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
uniqueFrames.push(entry);
|
|
1188
|
+
}
|
|
1189
|
+
const globalIdMap = /* @__PURE__ */ new Map();
|
|
1190
|
+
const frames = uniqueFrames.map((entry, globalId) => {
|
|
1191
|
+
globalIdMap.set(`${entry.segmentIndex}:${entry.localId}`, globalId);
|
|
1192
|
+
return {
|
|
1193
|
+
id: globalId,
|
|
1194
|
+
timestamp: entry.frame.timestamp,
|
|
1195
|
+
extractPath: entry.frame.extractPath
|
|
1196
|
+
};
|
|
1197
|
+
});
|
|
1198
|
+
const edges = [];
|
|
1199
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1200
|
+
for (const result of segmentResults) {
|
|
1201
|
+
for (const edge of result.edges) {
|
|
1202
|
+
const newSourceId = globalIdMap.get(
|
|
1203
|
+
`${result.segment.index}:${edge.sourceId}`
|
|
1204
|
+
);
|
|
1205
|
+
const newTargetId = globalIdMap.get(
|
|
1206
|
+
`${result.segment.index}:${edge.targetId}`
|
|
1207
|
+
);
|
|
1208
|
+
if (newSourceId === void 0 || newTargetId === void 0) continue;
|
|
1209
|
+
const edgeKey = `${newSourceId}-${newTargetId}`;
|
|
1210
|
+
const existingIdx = edgeMap.get(edgeKey);
|
|
1211
|
+
if (existingIdx !== void 0) {
|
|
1212
|
+
if (edges[existingIdx].score < edge.score) {
|
|
1213
|
+
edges[existingIdx] = {
|
|
1214
|
+
sourceId: newSourceId,
|
|
1215
|
+
targetId: newTargetId,
|
|
1216
|
+
score: edge.score
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
edgeMap.set(edgeKey, edges.length);
|
|
1221
|
+
edges.push({
|
|
1222
|
+
sourceId: newSourceId,
|
|
1223
|
+
targetId: newTargetId,
|
|
1224
|
+
score: edge.score
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const animations = [];
|
|
1230
|
+
for (const result of segmentResults) {
|
|
1231
|
+
for (const anim of result.animations) {
|
|
1232
|
+
const newStartId = globalIdMap.get(
|
|
1233
|
+
`${result.segment.index}:${anim.startFrameId}`
|
|
1234
|
+
);
|
|
1235
|
+
const newEndId = globalIdMap.get(
|
|
1236
|
+
`${result.segment.index}:${anim.endFrameId}`
|
|
1237
|
+
);
|
|
1238
|
+
if (newStartId === void 0 || newEndId === void 0) continue;
|
|
1239
|
+
animations.push({
|
|
1240
|
+
...anim,
|
|
1241
|
+
startFrameId: newStartId,
|
|
1242
|
+
endFrameId: newEndId
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return { frames, edges, animations };
|
|
1247
|
+
}
|
|
1248
|
+
function buildSegmentContext(segment, frames, segmentWorkspacePath, resolvedOptions, onProgress) {
|
|
1249
|
+
return {
|
|
1250
|
+
options: {
|
|
1251
|
+
...resolvedOptions,
|
|
1252
|
+
fps: segment.effectiveFps,
|
|
1253
|
+
maxFrames: segment.allocatedFrames
|
|
1254
|
+
},
|
|
1255
|
+
workspacePath: segmentWorkspacePath,
|
|
1256
|
+
frames,
|
|
1257
|
+
graph: [],
|
|
1258
|
+
status: "ANALYZING",
|
|
1259
|
+
emitProgress: onProgress
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
async function processSegment(inputPath, segment, workspacePath, resolvedOptions, onProgress) {
|
|
1263
|
+
const framesDir = join5(workspacePath, "frames");
|
|
1264
|
+
const frames = await extractFramesForRange(
|
|
1265
|
+
inputPath,
|
|
1266
|
+
framesDir,
|
|
1267
|
+
segment.effectiveFps,
|
|
1268
|
+
resolvedOptions.scale,
|
|
1269
|
+
segment.extractStartTime,
|
|
1270
|
+
segment.extractDuration
|
|
1271
|
+
);
|
|
1272
|
+
if (frames.length < 2) {
|
|
1273
|
+
return { segment, frames, edges: [], animations: [] };
|
|
1274
|
+
}
|
|
1275
|
+
const ctx = buildSegmentContext(
|
|
1276
|
+
segment,
|
|
1277
|
+
frames,
|
|
1278
|
+
workspacePath,
|
|
1279
|
+
resolvedOptions,
|
|
1280
|
+
onProgress
|
|
1281
|
+
);
|
|
1282
|
+
const { edges, animations } = await analyzeFrames(ctx);
|
|
1283
|
+
return { segment, frames, edges, animations };
|
|
1284
|
+
}
|
|
1285
|
+
async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
1286
|
+
const pipelineStart = Date.now();
|
|
1287
|
+
const sessionId = randomUUID();
|
|
1288
|
+
let mainWorkspace = "";
|
|
1289
|
+
try {
|
|
1290
|
+
mainWorkspace = await createWorkspace(sessionId);
|
|
1291
|
+
logger.debug(`Segmented pipeline: workspace at ${mainWorkspace}`);
|
|
1292
|
+
const { resolvedInputPath } = await resolveInput(options2, mainWorkspace);
|
|
1293
|
+
const inputPath = resolvedInputPath ?? resolvedOptions.inputPath;
|
|
1294
|
+
if (!inputPath) {
|
|
1295
|
+
throw new Error("No input path available for segmented pipeline");
|
|
1296
|
+
}
|
|
1297
|
+
const metadata = await getVideoMetadata(inputPath);
|
|
1298
|
+
const totalDuration = parseFloat(metadata.format?.duration ?? "0");
|
|
1299
|
+
if (totalDuration <= 0) {
|
|
1300
|
+
throw new Error(`Invalid video duration: ${totalDuration}`);
|
|
1301
|
+
}
|
|
1302
|
+
logger.debug(`Video duration: ${totalDuration}s`);
|
|
1303
|
+
const segments = computeSegmentPlan(
|
|
1304
|
+
totalDuration,
|
|
1305
|
+
resolvedOptions.maxSegmentDuration,
|
|
1306
|
+
resolvedOptions.maxFrames,
|
|
1307
|
+
resolvedOptions.fps
|
|
1308
|
+
);
|
|
1309
|
+
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1310
|
+
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1311
|
+
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1312
|
+
const weights = segments.map((s) => s.duration / totalDuration);
|
|
1313
|
+
const emitOverallProgress = (phase) => {
|
|
1314
|
+
if (!options2.onProgress) return;
|
|
1315
|
+
const overall = weights.reduce(
|
|
1316
|
+
(sum, w, i) => sum + w * (segmentProgresses[i] ?? 0),
|
|
1317
|
+
0
|
|
1318
|
+
);
|
|
1319
|
+
options2.onProgress(phase, Math.min(100, overall));
|
|
1320
|
+
};
|
|
1321
|
+
options2.onProgress?.("EXTRACTING", 0);
|
|
1322
|
+
const results = await Promise.all(
|
|
1323
|
+
segments.map(
|
|
1324
|
+
(segment) => limit(async () => {
|
|
1325
|
+
const segWorkspace = await createSegmentWorkspace(
|
|
1326
|
+
mainWorkspace,
|
|
1327
|
+
segment.index
|
|
1328
|
+
);
|
|
1329
|
+
const result = await processSegment(
|
|
1330
|
+
inputPath,
|
|
1331
|
+
segment,
|
|
1332
|
+
segWorkspace,
|
|
1333
|
+
resolvedOptions,
|
|
1334
|
+
(percent) => {
|
|
1335
|
+
segmentProgresses[segment.index] = percent;
|
|
1336
|
+
emitOverallProgress("ANALYZING");
|
|
1337
|
+
}
|
|
1338
|
+
);
|
|
1339
|
+
return result;
|
|
1340
|
+
})
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1343
|
+
options2.onProgress?.("ANALYZING", 100);
|
|
1344
|
+
const { frames, edges, animations } = mergeSegmentFrames(results);
|
|
1345
|
+
logger.debug(
|
|
1346
|
+
`Merged: ${frames.length} frames, ${edges.length} edges, ${animations.length} animations`
|
|
1347
|
+
);
|
|
1348
|
+
options2.onProgress?.("PRUNING", 0);
|
|
1349
|
+
const survivingIds = pruneByThresholdWithCap(
|
|
1350
|
+
edges,
|
|
1351
|
+
frames,
|
|
1352
|
+
resolvedOptions.threshold,
|
|
1353
|
+
resolvedOptions.count
|
|
1354
|
+
);
|
|
1355
|
+
const prunedFrames = frames.filter((f) => survivingIds.has(f.id));
|
|
1356
|
+
options2.onProgress?.("PRUNING", 100);
|
|
1357
|
+
options2.onProgress?.("FINALIZING", 0);
|
|
1358
|
+
const ctx = {
|
|
1359
|
+
options: resolvedOptions,
|
|
1360
|
+
workspacePath: mainWorkspace,
|
|
1361
|
+
frames,
|
|
1362
|
+
graph: edges,
|
|
1363
|
+
animations,
|
|
1364
|
+
status: "FINALIZING",
|
|
1365
|
+
emitProgress: (percent) => options2.onProgress?.("FINALIZING", percent)
|
|
1366
|
+
};
|
|
1367
|
+
let outputFiles = [];
|
|
1368
|
+
let outputBuffers;
|
|
1369
|
+
if (resolvedOptions.mode === "buffer" || resolvedOptions.mode === "frames") {
|
|
1370
|
+
outputBuffers = await readFramesAsBuffers(
|
|
1371
|
+
prunedFrames,
|
|
1372
|
+
resolvedOptions.quality
|
|
1373
|
+
);
|
|
1374
|
+
} else {
|
|
1375
|
+
outputFiles = await finalizeOutput(ctx, prunedFrames);
|
|
1376
|
+
}
|
|
1377
|
+
options2.onProgress?.("FINALIZING", 100);
|
|
1378
|
+
logger.success(
|
|
1379
|
+
`Segmented pipeline: ${prunedFrames.length} scenes from ${frames.length} frames (${segments.length} segments)`
|
|
1380
|
+
);
|
|
1381
|
+
return {
|
|
1382
|
+
success: true,
|
|
1383
|
+
originalFramesCount: frames.length,
|
|
1384
|
+
prunedFramesCount: prunedFrames.length,
|
|
1385
|
+
outputFiles,
|
|
1386
|
+
outputBuffers,
|
|
1387
|
+
animations,
|
|
1388
|
+
video: {
|
|
1389
|
+
originalDurationMs: totalDuration * 1e3,
|
|
1390
|
+
fps: resolvedOptions.fps,
|
|
1391
|
+
resolution: {
|
|
1392
|
+
width: resolvedOptions.scale,
|
|
1393
|
+
height: Math.round(resolvedOptions.scale * 9 / 16)
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1396
|
+
executionTimeMs: Date.now() - pipelineStart
|
|
1397
|
+
};
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1400
|
+
logger.error(`Segmented pipeline failed: ${err.message}`);
|
|
1401
|
+
throw err;
|
|
1402
|
+
} finally {
|
|
1403
|
+
if (!resolvedOptions.debug) {
|
|
1404
|
+
await cleanupWorkspace(mainWorkspace);
|
|
1405
|
+
} else {
|
|
1406
|
+
logger.debug(`Debug mode: workspace preserved at ${mainWorkspace}`);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1010
1411
|
// src/core/orchestrator.ts
|
|
1011
1412
|
async function runPipeline(options2) {
|
|
1012
|
-
const startTime = Date.now();
|
|
1013
|
-
const sessionId = randomUUID();
|
|
1014
1413
|
const debug = options2.debug ?? false;
|
|
1015
1414
|
if (debug) setDebugMode(true);
|
|
1016
1415
|
const resolvedOptions = resolveOptions(options2);
|
|
1416
|
+
if (shouldSegment(resolvedOptions, options2)) {
|
|
1417
|
+
return runSegmentedPipeline(options2, resolvedOptions);
|
|
1418
|
+
}
|
|
1419
|
+
const startTime = Date.now();
|
|
1420
|
+
const sessionId = randomUUID2();
|
|
1017
1421
|
const ctx = {
|
|
1018
1422
|
options: resolvedOptions,
|
|
1019
1423
|
workspacePath: "",
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface SieveOptionsBase {
|
|
|
21
21
|
animationThreshold?: number;
|
|
22
22
|
debug?: boolean;
|
|
23
23
|
onProgress?: (phase: ProgressPhase, percent: number) => void;
|
|
24
|
+
maxSegmentDuration?: number;
|
|
25
|
+
concurrency?: number;
|
|
24
26
|
}
|
|
25
27
|
export type SieveOptions = SieveOptionsBase & SieveInput;
|
|
26
28
|
export interface ResolvedOptions {
|
|
@@ -37,6 +39,8 @@ export interface ResolvedOptions {
|
|
|
37
39
|
iouThreshold: number;
|
|
38
40
|
animationThreshold: number;
|
|
39
41
|
debug: boolean;
|
|
42
|
+
maxSegmentDuration: number;
|
|
43
|
+
concurrency: number;
|
|
40
44
|
}
|
|
41
45
|
export interface SieveResult {
|
|
42
46
|
success: boolean;
|
|
@@ -105,3 +109,21 @@ export interface AnalysisResult {
|
|
|
105
109
|
edges: ScoreEdge[];
|
|
106
110
|
animations: AnimationMetadata[];
|
|
107
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,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[];
|