@lumy-pack/scene-sieve 0.0.11 → 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.mjs +46 -24
- package/dist/index.cjs +38 -22
- package/dist/index.mjs +38 -22
- package/dist/pipeline-worker.mjs +38 -22
- package/package.json +3 -1
package/dist/cli.mjs
CHANGED
|
@@ -173,6 +173,7 @@ var init_dbscan = __esm({
|
|
|
173
173
|
|
|
174
174
|
// src/core/analyzer.ts
|
|
175
175
|
import { createRequire } from "module";
|
|
176
|
+
import { filter, map } from "@winglet/common-utils";
|
|
176
177
|
import sharp from "sharp";
|
|
177
178
|
async function ensureOpenCV() {
|
|
178
179
|
if (!cvReady) {
|
|
@@ -356,7 +357,7 @@ function computeInformationGain(clusters, clusterPoints, imageArea, animationInd
|
|
|
356
357
|
async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
357
358
|
const edges = [];
|
|
358
359
|
const preprocessed = await Promise.all(
|
|
359
|
-
|
|
360
|
+
map(frames, (f) => preprocessFrame(f.extractPath, scale))
|
|
360
361
|
);
|
|
361
362
|
const imageWidth = preprocessed[0]?.width ?? scale;
|
|
362
363
|
const imageHeight = preprocessed[0]?.height ?? Math.round(scale * 9 / 16);
|
|
@@ -398,7 +399,8 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
398
399
|
}
|
|
399
400
|
}
|
|
400
401
|
const animationIndices = tracker.update(clusters, pairIndex);
|
|
401
|
-
const animationWeights =
|
|
402
|
+
const animationWeights = map(
|
|
403
|
+
clusters,
|
|
402
404
|
(_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0
|
|
403
405
|
);
|
|
404
406
|
const score = computeInformationGain(
|
|
@@ -524,7 +526,8 @@ var init_analyzer = __esm({
|
|
|
524
526
|
this.collectAnimation(region);
|
|
525
527
|
}
|
|
526
528
|
}
|
|
527
|
-
this.regions =
|
|
529
|
+
this.regions = filter(
|
|
530
|
+
this.regions,
|
|
528
531
|
(r, i) => r.weight > 0.01 || matched.has(i)
|
|
529
532
|
);
|
|
530
533
|
return animationIndices;
|
|
@@ -607,6 +610,7 @@ var init_paths = __esm({
|
|
|
607
610
|
import { readdir } from "fs/promises";
|
|
608
611
|
import { join as join2 } from "path";
|
|
609
612
|
import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
|
|
613
|
+
import { filter as filter2, map as map2 } from "@winglet/common-utils";
|
|
610
614
|
import { execa } from "execa";
|
|
611
615
|
import ffmpegPath from "ffmpeg-static";
|
|
612
616
|
async function extractFrames(ctx) {
|
|
@@ -685,11 +689,11 @@ async function getVideoMetadata(inputPath) {
|
|
|
685
689
|
}
|
|
686
690
|
async function buildFrameList(framesDir, duration) {
|
|
687
691
|
const files = await readdir(framesDir);
|
|
688
|
-
const jpgFiles = files
|
|
692
|
+
const jpgFiles = filter2(files, (f) => f.endsWith(".jpg")).sort();
|
|
689
693
|
if (jpgFiles.length === 0) {
|
|
690
694
|
return [];
|
|
691
695
|
}
|
|
692
|
-
return jpgFiles
|
|
696
|
+
return map2(jpgFiles, (file, index) => ({
|
|
693
697
|
id: index,
|
|
694
698
|
timestamp: duration > 0 && jpgFiles.length > 1 ? duration * index / (jpgFiles.length - 1) : index,
|
|
695
699
|
extractPath: join2(framesDir, file)
|
|
@@ -724,6 +728,7 @@ var init_extractor = __esm({
|
|
|
724
728
|
// src/core/workspace.ts
|
|
725
729
|
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
726
730
|
import { join as join3 } from "path";
|
|
731
|
+
import { map as map3 } from "@winglet/common-utils";
|
|
727
732
|
import sharp2 from "sharp";
|
|
728
733
|
async function createWorkspace(sessionId) {
|
|
729
734
|
const workspacePath = getTempWorkspaceDir(sessionId);
|
|
@@ -764,7 +769,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
764
769
|
}
|
|
765
770
|
},
|
|
766
771
|
frames: framesMetadata,
|
|
767
|
-
animations: (ctx.animations || []
|
|
772
|
+
animations: map3(ctx.animations || [], (anim) => ({
|
|
768
773
|
...anim,
|
|
769
774
|
startFrameId: anim.startFrameId + 1,
|
|
770
775
|
endFrameId: anim.endFrameId + 1,
|
|
@@ -831,7 +836,8 @@ async function writeInputFrames(frames, workspacePath) {
|
|
|
831
836
|
}
|
|
832
837
|
async function readFramesAsBuffers(frameNodes, quality) {
|
|
833
838
|
return Promise.all(
|
|
834
|
-
|
|
839
|
+
map3(
|
|
840
|
+
frameNodes,
|
|
835
841
|
(f) => sharp2(f.extractPath).jpeg({ quality, mozjpeg: true }).toBuffer()
|
|
836
842
|
)
|
|
837
843
|
);
|
|
@@ -907,38 +913,42 @@ var init_input_resolver = __esm({
|
|
|
907
913
|
});
|
|
908
914
|
|
|
909
915
|
// src/utils/math.ts
|
|
916
|
+
import { filter as filter3, map as map4 } from "@winglet/common-utils";
|
|
910
917
|
function normalizeScores(items) {
|
|
911
918
|
if (items.length === 0) return [];
|
|
912
|
-
const safeScores =
|
|
919
|
+
const safeScores = map4(
|
|
920
|
+
items,
|
|
913
921
|
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
914
922
|
);
|
|
915
|
-
const positiveScores = safeScores
|
|
923
|
+
const positiveScores = filter3(safeScores, (s) => s > 0);
|
|
916
924
|
if (positiveScores.length === 0) return safeScores;
|
|
917
925
|
const sorted = [...positiveScores].sort((a, b) => a - b);
|
|
918
926
|
if (positiveScores.length <= NORMALIZATION_MIN_SAMPLE_SIZE) {
|
|
919
927
|
const min = sorted[0];
|
|
920
928
|
const max = sorted[sorted.length - 1];
|
|
921
|
-
if (max === min) return safeScores
|
|
922
|
-
return
|
|
929
|
+
if (max === min) return map4(safeScores, (s) => s > 0 ? 1 : 0);
|
|
930
|
+
return map4(
|
|
931
|
+
safeScores,
|
|
923
932
|
(s) => s <= 0 ? 0 : Math.max(0, Math.min((s - min) / (max - min), 1))
|
|
924
933
|
);
|
|
925
934
|
}
|
|
926
935
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
927
|
-
const absoluteDiffs = positiveScores
|
|
936
|
+
const absoluteDiffs = map4(positiveScores, (v) => Math.abs(v - median));
|
|
928
937
|
const mad = [...absoluteDiffs].sort((a, b) => a - b)[Math.floor(absoluteDiffs.length / 2)];
|
|
929
938
|
const scale = mad === 0 ? median : mad * NORMALIZATION_MAD_COEFFICIENT;
|
|
930
|
-
const logisticZ = safeScores
|
|
939
|
+
const logisticZ = map4(safeScores, (s) => {
|
|
931
940
|
if (s <= 0) return 0;
|
|
932
941
|
if (scale === 0) return 1;
|
|
933
942
|
const z = (s - median) / scale;
|
|
934
943
|
return 1 / (1 + Math.exp(-NORMALIZATION_LOGISTIC_K * z));
|
|
935
944
|
});
|
|
936
|
-
const cdf = safeScores
|
|
945
|
+
const cdf = map4(safeScores, (s) => {
|
|
937
946
|
if (s <= 0) return 0;
|
|
938
947
|
const rank = sorted.findIndex((v) => v >= s);
|
|
939
948
|
return rank / sorted.length;
|
|
940
949
|
});
|
|
941
|
-
return
|
|
950
|
+
return map4(
|
|
951
|
+
logisticZ,
|
|
942
952
|
(z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA
|
|
943
953
|
);
|
|
944
954
|
}
|
|
@@ -1000,9 +1010,10 @@ var init_min_heap = __esm({
|
|
|
1000
1010
|
});
|
|
1001
1011
|
|
|
1002
1012
|
// src/core/pruner.ts
|
|
1013
|
+
import { filter as filter4, map as map5 } from "@winglet/common-utils";
|
|
1003
1014
|
function pruneTo(graph, frames, targetCount) {
|
|
1004
1015
|
if (frames.length <= targetCount) {
|
|
1005
|
-
return new Set(frames
|
|
1016
|
+
return new Set(map5(frames, (f) => f.id));
|
|
1006
1017
|
}
|
|
1007
1018
|
const prev = /* @__PURE__ */ new Map();
|
|
1008
1019
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -1020,7 +1031,7 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
1020
1031
|
tgtId: edge.targetId
|
|
1021
1032
|
});
|
|
1022
1033
|
}
|
|
1023
|
-
const surviving = new Set(frames
|
|
1034
|
+
const surviving = new Set(map5(frames, (f) => f.id));
|
|
1024
1035
|
const firstId = frames[0].id;
|
|
1025
1036
|
const lastId = frames[frames.length - 1].id;
|
|
1026
1037
|
while (surviving.size > targetCount && heap.size > 0) {
|
|
@@ -1113,7 +1124,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1113
1124
|
if (thresholdSurvivors.size <= maxCount) {
|
|
1114
1125
|
return thresholdSurvivors;
|
|
1115
1126
|
}
|
|
1116
|
-
const survivingFrames = frames
|
|
1127
|
+
const survivingFrames = filter4(frames, (f) => thresholdSurvivors.has(f.id));
|
|
1117
1128
|
const idToOrigIdx = /* @__PURE__ */ new Map();
|
|
1118
1129
|
for (let i = 0; i < frames.length; i++) {
|
|
1119
1130
|
idToOrigIdx.set(frames[i].id, i);
|
|
@@ -1180,6 +1191,7 @@ var init_concurrency = __esm({
|
|
|
1180
1191
|
// src/core/segmenter.ts
|
|
1181
1192
|
import { randomUUID } from "crypto";
|
|
1182
1193
|
import { join as join5 } from "path";
|
|
1194
|
+
import { filter as filter5, map as map6 } from "@winglet/common-utils";
|
|
1183
1195
|
function shouldSegment(resolvedOptions, originalOptions) {
|
|
1184
1196
|
if (resolvedOptions.mode === "frames") return false;
|
|
1185
1197
|
if (originalOptions.mode === "file") {
|
|
@@ -1422,7 +1434,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1422
1434
|
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1423
1435
|
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1424
1436
|
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1425
|
-
const weights = segments
|
|
1437
|
+
const weights = map6(segments, (s) => s.duration / totalDuration);
|
|
1426
1438
|
const emitOverallProgress = (phase) => {
|
|
1427
1439
|
if (!options.onProgress) return;
|
|
1428
1440
|
const overall = weights.reduce(
|
|
@@ -1433,7 +1445,8 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1433
1445
|
};
|
|
1434
1446
|
options.onProgress?.("EXTRACTING", 0);
|
|
1435
1447
|
const results = await Promise.all(
|
|
1436
|
-
|
|
1448
|
+
map6(
|
|
1449
|
+
segments,
|
|
1437
1450
|
(segment) => limit(async () => {
|
|
1438
1451
|
const segWorkspace = await createSegmentWorkspace(
|
|
1439
1452
|
mainWorkspace,
|
|
@@ -1465,7 +1478,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1465
1478
|
resolvedOptions.threshold,
|
|
1466
1479
|
resolvedOptions.count
|
|
1467
1480
|
);
|
|
1468
|
-
const prunedFrames = frames
|
|
1481
|
+
const prunedFrames = filter5(frames, (f) => survivingIds.has(f.id));
|
|
1469
1482
|
options.onProgress?.("PRUNING", 100);
|
|
1470
1483
|
options.onProgress?.("FINALIZING", 0);
|
|
1471
1484
|
const ctx = {
|
|
@@ -1539,6 +1552,7 @@ __export(orchestrator_exports, {
|
|
|
1539
1552
|
runPipeline: () => runPipeline
|
|
1540
1553
|
});
|
|
1541
1554
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1555
|
+
import { filter as filter6 } from "@winglet/common-utils";
|
|
1542
1556
|
async function runPipeline(options) {
|
|
1543
1557
|
const debug = options.debug ?? false;
|
|
1544
1558
|
if (debug) setDebugMode(true);
|
|
@@ -1592,7 +1606,7 @@ async function runPipeline(options) {
|
|
|
1592
1606
|
resolvedOptions.threshold,
|
|
1593
1607
|
resolvedOptions.count
|
|
1594
1608
|
);
|
|
1595
|
-
const prunedFrames = ctx.frames
|
|
1609
|
+
const prunedFrames = filter6(ctx.frames, (f) => survivingIds.has(f.id));
|
|
1596
1610
|
ctx.emitProgress(100);
|
|
1597
1611
|
ctx.status = "FINALIZING";
|
|
1598
1612
|
let outputFiles = [];
|
|
@@ -1656,7 +1670,6 @@ var init_orchestrator = __esm({
|
|
|
1656
1670
|
|
|
1657
1671
|
// src/cli.ts
|
|
1658
1672
|
import { createRequire as createRequire2 } from "module";
|
|
1659
|
-
import { Command } from "commander";
|
|
1660
1673
|
|
|
1661
1674
|
// ../shared/src/respond.ts
|
|
1662
1675
|
function respond(command, data, startTime, version2) {
|
|
@@ -1687,6 +1700,9 @@ function respondError(command, code, message, startTime, version2, details) {
|
|
|
1687
1700
|
process.exitCode = 1;
|
|
1688
1701
|
}
|
|
1689
1702
|
|
|
1703
|
+
// src/cli.ts
|
|
1704
|
+
import { Command } from "commander";
|
|
1705
|
+
|
|
1690
1706
|
// src/commands/Sieve.tsx
|
|
1691
1707
|
import { existsSync } from "fs";
|
|
1692
1708
|
import { Box as Box2, Text as Text3, useApp } from "ink";
|
|
@@ -2202,7 +2218,13 @@ if (process.argv.includes("--describe")) {
|
|
|
2202
2218
|
}
|
|
2203
2219
|
program.parseAsync(process.argv).catch((error) => {
|
|
2204
2220
|
if (process.argv.includes("--json")) {
|
|
2205
|
-
respondError(
|
|
2221
|
+
respondError(
|
|
2222
|
+
"extract",
|
|
2223
|
+
SieveErrorCode.UNKNOWN,
|
|
2224
|
+
error.message,
|
|
2225
|
+
Date.now(),
|
|
2226
|
+
version
|
|
2227
|
+
);
|
|
2206
2228
|
} else {
|
|
2207
2229
|
console.error("Fatal error:", error.message);
|
|
2208
2230
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
|
40
40
|
|
|
41
41
|
// src/core/orchestrator.ts
|
|
42
42
|
var import_node_crypto2 = require("crypto");
|
|
43
|
+
var import_common_utils7 = require("@winglet/common-utils");
|
|
43
44
|
|
|
44
45
|
// src/utils/logger.ts
|
|
45
46
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
@@ -73,6 +74,7 @@ ${import_picocolors.default.green("done")} ${message}`);
|
|
|
73
74
|
|
|
74
75
|
// src/core/analyzer.ts
|
|
75
76
|
var import_node_module = require("module");
|
|
77
|
+
var import_common_utils = require("@winglet/common-utils");
|
|
76
78
|
var import_sharp = __toESM(require("sharp"), 1);
|
|
77
79
|
|
|
78
80
|
// src/constants.ts
|
|
@@ -289,7 +291,8 @@ var IoUTracker = class {
|
|
|
289
291
|
this.collectAnimation(region);
|
|
290
292
|
}
|
|
291
293
|
}
|
|
292
|
-
this.regions =
|
|
294
|
+
this.regions = (0, import_common_utils.filter)(
|
|
295
|
+
this.regions,
|
|
293
296
|
(r, i) => r.weight > 0.01 || matched.has(i)
|
|
294
297
|
);
|
|
295
298
|
return animationIndices;
|
|
@@ -470,7 +473,7 @@ function computeInformationGain(clusters, clusterPoints, imageArea, animationInd
|
|
|
470
473
|
async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
471
474
|
const edges = [];
|
|
472
475
|
const preprocessed = await Promise.all(
|
|
473
|
-
|
|
476
|
+
(0, import_common_utils.map)(frames, (f) => preprocessFrame(f.extractPath, scale))
|
|
474
477
|
);
|
|
475
478
|
const imageWidth = preprocessed[0]?.width ?? scale;
|
|
476
479
|
const imageHeight = preprocessed[0]?.height ?? Math.round(scale * 9 / 16);
|
|
@@ -512,7 +515,8 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
517
|
const animationIndices = tracker.update(clusters, pairIndex);
|
|
515
|
-
const animationWeights =
|
|
518
|
+
const animationWeights = (0, import_common_utils.map)(
|
|
519
|
+
clusters,
|
|
516
520
|
(_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0
|
|
517
521
|
);
|
|
518
522
|
const score = computeInformationGain(
|
|
@@ -577,6 +581,7 @@ async function analyzeFrames(ctx) {
|
|
|
577
581
|
var import_promises2 = require("fs/promises");
|
|
578
582
|
var import_node_path3 = require("path");
|
|
579
583
|
var import_ffprobe = require("@ffprobe-installer/ffprobe");
|
|
584
|
+
var import_common_utils2 = require("@winglet/common-utils");
|
|
580
585
|
var import_execa = require("execa");
|
|
581
586
|
var import_ffmpeg_static = __toESM(require("ffmpeg-static"), 1);
|
|
582
587
|
|
|
@@ -688,11 +693,11 @@ async function getVideoMetadata(inputPath) {
|
|
|
688
693
|
}
|
|
689
694
|
async function buildFrameList(framesDir, duration) {
|
|
690
695
|
const files = await (0, import_promises2.readdir)(framesDir);
|
|
691
|
-
const jpgFiles =
|
|
696
|
+
const jpgFiles = (0, import_common_utils2.filter)(files, (f) => f.endsWith(".jpg")).sort();
|
|
692
697
|
if (jpgFiles.length === 0) {
|
|
693
698
|
return [];
|
|
694
699
|
}
|
|
695
|
-
return
|
|
700
|
+
return (0, import_common_utils2.map)(jpgFiles, (file, index) => ({
|
|
696
701
|
id: index,
|
|
697
702
|
timestamp: duration > 0 && jpgFiles.length > 1 ? duration * index / (jpgFiles.length - 1) : index,
|
|
698
703
|
extractPath: (0, import_node_path3.join)(framesDir, file)
|
|
@@ -722,6 +727,7 @@ var import_node_path5 = require("path");
|
|
|
722
727
|
// src/core/workspace.ts
|
|
723
728
|
var import_promises3 = require("fs/promises");
|
|
724
729
|
var import_node_path4 = require("path");
|
|
730
|
+
var import_common_utils3 = require("@winglet/common-utils");
|
|
725
731
|
var import_sharp2 = __toESM(require("sharp"), 1);
|
|
726
732
|
async function createWorkspace(sessionId) {
|
|
727
733
|
const workspacePath = getTempWorkspaceDir(sessionId);
|
|
@@ -762,7 +768,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
762
768
|
}
|
|
763
769
|
},
|
|
764
770
|
frames: framesMetadata,
|
|
765
|
-
animations: (ctx.animations || []
|
|
771
|
+
animations: (0, import_common_utils3.map)(ctx.animations || [], (anim) => ({
|
|
766
772
|
...anim,
|
|
767
773
|
startFrameId: anim.startFrameId + 1,
|
|
768
774
|
endFrameId: anim.endFrameId + 1,
|
|
@@ -815,7 +821,8 @@ async function writeInputFrames(frames, workspacePath) {
|
|
|
815
821
|
}
|
|
816
822
|
async function readFramesAsBuffers(frameNodes, quality) {
|
|
817
823
|
return Promise.all(
|
|
818
|
-
|
|
824
|
+
(0, import_common_utils3.map)(
|
|
825
|
+
frameNodes,
|
|
819
826
|
(f) => (0, import_sharp2.default)(f.extractPath).jpeg({ quality, mozjpeg: true }).toBuffer()
|
|
820
827
|
)
|
|
821
828
|
);
|
|
@@ -872,39 +879,46 @@ async function resolveInput(options, workspacePath) {
|
|
|
872
879
|
throw new Error(`Unsupported input mode: ${options.mode}`);
|
|
873
880
|
}
|
|
874
881
|
|
|
882
|
+
// src/core/pruner.ts
|
|
883
|
+
var import_common_utils5 = require("@winglet/common-utils");
|
|
884
|
+
|
|
875
885
|
// src/utils/math.ts
|
|
886
|
+
var import_common_utils4 = require("@winglet/common-utils");
|
|
876
887
|
function normalizeScores(items) {
|
|
877
888
|
if (items.length === 0) return [];
|
|
878
|
-
const safeScores =
|
|
889
|
+
const safeScores = (0, import_common_utils4.map)(
|
|
890
|
+
items,
|
|
879
891
|
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
880
892
|
);
|
|
881
|
-
const positiveScores =
|
|
893
|
+
const positiveScores = (0, import_common_utils4.filter)(safeScores, (s) => s > 0);
|
|
882
894
|
if (positiveScores.length === 0) return safeScores;
|
|
883
895
|
const sorted = [...positiveScores].sort((a, b) => a - b);
|
|
884
896
|
if (positiveScores.length <= NORMALIZATION_MIN_SAMPLE_SIZE) {
|
|
885
897
|
const min = sorted[0];
|
|
886
898
|
const max = sorted[sorted.length - 1];
|
|
887
|
-
if (max === min) return
|
|
888
|
-
return
|
|
899
|
+
if (max === min) return (0, import_common_utils4.map)(safeScores, (s) => s > 0 ? 1 : 0);
|
|
900
|
+
return (0, import_common_utils4.map)(
|
|
901
|
+
safeScores,
|
|
889
902
|
(s) => s <= 0 ? 0 : Math.max(0, Math.min((s - min) / (max - min), 1))
|
|
890
903
|
);
|
|
891
904
|
}
|
|
892
905
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
893
|
-
const absoluteDiffs =
|
|
906
|
+
const absoluteDiffs = (0, import_common_utils4.map)(positiveScores, (v) => Math.abs(v - median));
|
|
894
907
|
const mad = [...absoluteDiffs].sort((a, b) => a - b)[Math.floor(absoluteDiffs.length / 2)];
|
|
895
908
|
const scale = mad === 0 ? median : mad * NORMALIZATION_MAD_COEFFICIENT;
|
|
896
|
-
const logisticZ =
|
|
909
|
+
const logisticZ = (0, import_common_utils4.map)(safeScores, (s) => {
|
|
897
910
|
if (s <= 0) return 0;
|
|
898
911
|
if (scale === 0) return 1;
|
|
899
912
|
const z = (s - median) / scale;
|
|
900
913
|
return 1 / (1 + Math.exp(-NORMALIZATION_LOGISTIC_K * z));
|
|
901
914
|
});
|
|
902
|
-
const cdf =
|
|
915
|
+
const cdf = (0, import_common_utils4.map)(safeScores, (s) => {
|
|
903
916
|
if (s <= 0) return 0;
|
|
904
917
|
const rank = sorted.findIndex((v) => v >= s);
|
|
905
918
|
return rank / sorted.length;
|
|
906
919
|
});
|
|
907
|
-
return
|
|
920
|
+
return (0, import_common_utils4.map)(
|
|
921
|
+
logisticZ,
|
|
908
922
|
(z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA
|
|
909
923
|
);
|
|
910
924
|
}
|
|
@@ -956,7 +970,7 @@ var MinHeap = class {
|
|
|
956
970
|
// src/core/pruner.ts
|
|
957
971
|
function pruneTo(graph, frames, targetCount) {
|
|
958
972
|
if (frames.length <= targetCount) {
|
|
959
|
-
return new Set(
|
|
973
|
+
return new Set((0, import_common_utils5.map)(frames, (f) => f.id));
|
|
960
974
|
}
|
|
961
975
|
const prev = /* @__PURE__ */ new Map();
|
|
962
976
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -974,7 +988,7 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
974
988
|
tgtId: edge.targetId
|
|
975
989
|
});
|
|
976
990
|
}
|
|
977
|
-
const surviving = new Set(
|
|
991
|
+
const surviving = new Set((0, import_common_utils5.map)(frames, (f) => f.id));
|
|
978
992
|
const firstId = frames[0].id;
|
|
979
993
|
const lastId = frames[frames.length - 1].id;
|
|
980
994
|
while (surviving.size > targetCount && heap.size > 0) {
|
|
@@ -1067,7 +1081,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1067
1081
|
if (thresholdSurvivors.size <= maxCount) {
|
|
1068
1082
|
return thresholdSurvivors;
|
|
1069
1083
|
}
|
|
1070
|
-
const survivingFrames =
|
|
1084
|
+
const survivingFrames = (0, import_common_utils5.filter)(frames, (f) => thresholdSurvivors.has(f.id));
|
|
1071
1085
|
const idToOrigIdx = /* @__PURE__ */ new Map();
|
|
1072
1086
|
for (let i = 0; i < frames.length; i++) {
|
|
1073
1087
|
idToOrigIdx.set(frames[i].id, i);
|
|
@@ -1103,6 +1117,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1103
1117
|
// src/core/segmenter.ts
|
|
1104
1118
|
var import_node_crypto = require("crypto");
|
|
1105
1119
|
var import_node_path6 = require("path");
|
|
1120
|
+
var import_common_utils6 = require("@winglet/common-utils");
|
|
1106
1121
|
|
|
1107
1122
|
// src/utils/concurrency.ts
|
|
1108
1123
|
function concurrencyLimit(limit) {
|
|
@@ -1366,7 +1381,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1366
1381
|
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1367
1382
|
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1368
1383
|
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1369
|
-
const weights =
|
|
1384
|
+
const weights = (0, import_common_utils6.map)(segments, (s) => s.duration / totalDuration);
|
|
1370
1385
|
const emitOverallProgress = (phase) => {
|
|
1371
1386
|
if (!options.onProgress) return;
|
|
1372
1387
|
const overall = weights.reduce(
|
|
@@ -1377,7 +1392,8 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1377
1392
|
};
|
|
1378
1393
|
options.onProgress?.("EXTRACTING", 0);
|
|
1379
1394
|
const results = await Promise.all(
|
|
1380
|
-
|
|
1395
|
+
(0, import_common_utils6.map)(
|
|
1396
|
+
segments,
|
|
1381
1397
|
(segment) => limit(async () => {
|
|
1382
1398
|
const segWorkspace = await createSegmentWorkspace(
|
|
1383
1399
|
mainWorkspace,
|
|
@@ -1409,7 +1425,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1409
1425
|
resolvedOptions.threshold,
|
|
1410
1426
|
resolvedOptions.count
|
|
1411
1427
|
);
|
|
1412
|
-
const prunedFrames =
|
|
1428
|
+
const prunedFrames = (0, import_common_utils6.filter)(frames, (f) => survivingIds.has(f.id));
|
|
1413
1429
|
options.onProgress?.("PRUNING", 100);
|
|
1414
1430
|
options.onProgress?.("FINALIZING", 0);
|
|
1415
1431
|
const ctx = {
|
|
@@ -1519,7 +1535,7 @@ async function runPipeline(options) {
|
|
|
1519
1535
|
resolvedOptions.threshold,
|
|
1520
1536
|
resolvedOptions.count
|
|
1521
1537
|
);
|
|
1522
|
-
const prunedFrames = ctx.frames
|
|
1538
|
+
const prunedFrames = (0, import_common_utils7.filter)(ctx.frames, (f) => survivingIds.has(f.id));
|
|
1523
1539
|
ctx.emitProgress(100);
|
|
1524
1540
|
ctx.status = "FINALIZING";
|
|
1525
1541
|
let outputFiles = [];
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/core/orchestrator.ts
|
|
2
2
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3
|
+
import { filter as filter6 } from "@winglet/common-utils";
|
|
3
4
|
|
|
4
5
|
// src/utils/logger.ts
|
|
5
6
|
import pc from "picocolors";
|
|
@@ -33,6 +34,7 @@ ${pc.green("done")} ${message}`);
|
|
|
33
34
|
|
|
34
35
|
// src/core/analyzer.ts
|
|
35
36
|
import { createRequire } from "module";
|
|
37
|
+
import { filter, map } from "@winglet/common-utils";
|
|
36
38
|
import sharp from "sharp";
|
|
37
39
|
|
|
38
40
|
// src/constants.ts
|
|
@@ -249,7 +251,8 @@ var IoUTracker = class {
|
|
|
249
251
|
this.collectAnimation(region);
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
|
-
this.regions =
|
|
254
|
+
this.regions = filter(
|
|
255
|
+
this.regions,
|
|
253
256
|
(r, i) => r.weight > 0.01 || matched.has(i)
|
|
254
257
|
);
|
|
255
258
|
return animationIndices;
|
|
@@ -430,7 +433,7 @@ function computeInformationGain(clusters, clusterPoints, imageArea, animationInd
|
|
|
430
433
|
async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
431
434
|
const edges = [];
|
|
432
435
|
const preprocessed = await Promise.all(
|
|
433
|
-
|
|
436
|
+
map(frames, (f) => preprocessFrame(f.extractPath, scale))
|
|
434
437
|
);
|
|
435
438
|
const imageWidth = preprocessed[0]?.width ?? scale;
|
|
436
439
|
const imageHeight = preprocessed[0]?.height ?? Math.round(scale * 9 / 16);
|
|
@@ -472,7 +475,8 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
|
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
477
|
const animationIndices = tracker.update(clusters, pairIndex);
|
|
475
|
-
const animationWeights =
|
|
478
|
+
const animationWeights = map(
|
|
479
|
+
clusters,
|
|
476
480
|
(_, ci) => animationIndices.has(ci) ? tracker.getAnimationWeight(ci, clusters) : 0
|
|
477
481
|
);
|
|
478
482
|
const score = computeInformationGain(
|
|
@@ -537,6 +541,7 @@ async function analyzeFrames(ctx) {
|
|
|
537
541
|
import { readdir } from "fs/promises";
|
|
538
542
|
import { join as join2 } from "path";
|
|
539
543
|
import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
|
|
544
|
+
import { filter as filter2, map as map2 } from "@winglet/common-utils";
|
|
540
545
|
import { execa } from "execa";
|
|
541
546
|
import ffmpegPath from "ffmpeg-static";
|
|
542
547
|
|
|
@@ -648,11 +653,11 @@ async function getVideoMetadata(inputPath) {
|
|
|
648
653
|
}
|
|
649
654
|
async function buildFrameList(framesDir, duration) {
|
|
650
655
|
const files = await readdir(framesDir);
|
|
651
|
-
const jpgFiles = files
|
|
656
|
+
const jpgFiles = filter2(files, (f) => f.endsWith(".jpg")).sort();
|
|
652
657
|
if (jpgFiles.length === 0) {
|
|
653
658
|
return [];
|
|
654
659
|
}
|
|
655
|
-
return jpgFiles
|
|
660
|
+
return map2(jpgFiles, (file, index) => ({
|
|
656
661
|
id: index,
|
|
657
662
|
timestamp: duration > 0 && jpgFiles.length > 1 ? duration * index / (jpgFiles.length - 1) : index,
|
|
658
663
|
extractPath: join2(framesDir, file)
|
|
@@ -682,6 +687,7 @@ import { join as join4 } from "path";
|
|
|
682
687
|
// src/core/workspace.ts
|
|
683
688
|
import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
684
689
|
import { join as join3 } from "path";
|
|
690
|
+
import { map as map3 } from "@winglet/common-utils";
|
|
685
691
|
import sharp2 from "sharp";
|
|
686
692
|
async function createWorkspace(sessionId) {
|
|
687
693
|
const workspacePath = getTempWorkspaceDir(sessionId);
|
|
@@ -722,7 +728,7 @@ async function finalizeOutput(ctx, selectedFrames) {
|
|
|
722
728
|
}
|
|
723
729
|
},
|
|
724
730
|
frames: framesMetadata,
|
|
725
|
-
animations: (ctx.animations || []
|
|
731
|
+
animations: map3(ctx.animations || [], (anim) => ({
|
|
726
732
|
...anim,
|
|
727
733
|
startFrameId: anim.startFrameId + 1,
|
|
728
734
|
endFrameId: anim.endFrameId + 1,
|
|
@@ -775,7 +781,8 @@ async function writeInputFrames(frames, workspacePath) {
|
|
|
775
781
|
}
|
|
776
782
|
async function readFramesAsBuffers(frameNodes, quality) {
|
|
777
783
|
return Promise.all(
|
|
778
|
-
|
|
784
|
+
map3(
|
|
785
|
+
frameNodes,
|
|
779
786
|
(f) => sharp2(f.extractPath).jpeg({ quality, mozjpeg: true }).toBuffer()
|
|
780
787
|
)
|
|
781
788
|
);
|
|
@@ -832,39 +839,46 @@ async function resolveInput(options, workspacePath) {
|
|
|
832
839
|
throw new Error(`Unsupported input mode: ${options.mode}`);
|
|
833
840
|
}
|
|
834
841
|
|
|
842
|
+
// src/core/pruner.ts
|
|
843
|
+
import { filter as filter4, map as map5 } from "@winglet/common-utils";
|
|
844
|
+
|
|
835
845
|
// src/utils/math.ts
|
|
846
|
+
import { filter as filter3, map as map4 } from "@winglet/common-utils";
|
|
836
847
|
function normalizeScores(items) {
|
|
837
848
|
if (items.length === 0) return [];
|
|
838
|
-
const safeScores =
|
|
849
|
+
const safeScores = map4(
|
|
850
|
+
items,
|
|
839
851
|
(e) => Number.isFinite(e.score) && e.score > 0 ? e.score : 0
|
|
840
852
|
);
|
|
841
|
-
const positiveScores = safeScores
|
|
853
|
+
const positiveScores = filter3(safeScores, (s) => s > 0);
|
|
842
854
|
if (positiveScores.length === 0) return safeScores;
|
|
843
855
|
const sorted = [...positiveScores].sort((a, b) => a - b);
|
|
844
856
|
if (positiveScores.length <= NORMALIZATION_MIN_SAMPLE_SIZE) {
|
|
845
857
|
const min = sorted[0];
|
|
846
858
|
const max = sorted[sorted.length - 1];
|
|
847
|
-
if (max === min) return safeScores
|
|
848
|
-
return
|
|
859
|
+
if (max === min) return map4(safeScores, (s) => s > 0 ? 1 : 0);
|
|
860
|
+
return map4(
|
|
861
|
+
safeScores,
|
|
849
862
|
(s) => s <= 0 ? 0 : Math.max(0, Math.min((s - min) / (max - min), 1))
|
|
850
863
|
);
|
|
851
864
|
}
|
|
852
865
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
853
|
-
const absoluteDiffs = positiveScores
|
|
866
|
+
const absoluteDiffs = map4(positiveScores, (v) => Math.abs(v - median));
|
|
854
867
|
const mad = [...absoluteDiffs].sort((a, b) => a - b)[Math.floor(absoluteDiffs.length / 2)];
|
|
855
868
|
const scale = mad === 0 ? median : mad * NORMALIZATION_MAD_COEFFICIENT;
|
|
856
|
-
const logisticZ = safeScores
|
|
869
|
+
const logisticZ = map4(safeScores, (s) => {
|
|
857
870
|
if (s <= 0) return 0;
|
|
858
871
|
if (scale === 0) return 1;
|
|
859
872
|
const z = (s - median) / scale;
|
|
860
873
|
return 1 / (1 + Math.exp(-NORMALIZATION_LOGISTIC_K * z));
|
|
861
874
|
});
|
|
862
|
-
const cdf = safeScores
|
|
875
|
+
const cdf = map4(safeScores, (s) => {
|
|
863
876
|
if (s <= 0) return 0;
|
|
864
877
|
const rank = sorted.findIndex((v) => v >= s);
|
|
865
878
|
return rank / sorted.length;
|
|
866
879
|
});
|
|
867
|
-
return
|
|
880
|
+
return map4(
|
|
881
|
+
logisticZ,
|
|
868
882
|
(z, i) => z * (1 - NORMALIZATION_ALPHA) + cdf[i] * NORMALIZATION_ALPHA
|
|
869
883
|
);
|
|
870
884
|
}
|
|
@@ -916,7 +930,7 @@ var MinHeap = class {
|
|
|
916
930
|
// src/core/pruner.ts
|
|
917
931
|
function pruneTo(graph, frames, targetCount) {
|
|
918
932
|
if (frames.length <= targetCount) {
|
|
919
|
-
return new Set(frames
|
|
933
|
+
return new Set(map5(frames, (f) => f.id));
|
|
920
934
|
}
|
|
921
935
|
const prev = /* @__PURE__ */ new Map();
|
|
922
936
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -934,7 +948,7 @@ function pruneTo(graph, frames, targetCount) {
|
|
|
934
948
|
tgtId: edge.targetId
|
|
935
949
|
});
|
|
936
950
|
}
|
|
937
|
-
const surviving = new Set(frames
|
|
951
|
+
const surviving = new Set(map5(frames, (f) => f.id));
|
|
938
952
|
const firstId = frames[0].id;
|
|
939
953
|
const lastId = frames[frames.length - 1].id;
|
|
940
954
|
while (surviving.size > targetCount && heap.size > 0) {
|
|
@@ -1027,7 +1041,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1027
1041
|
if (thresholdSurvivors.size <= maxCount) {
|
|
1028
1042
|
return thresholdSurvivors;
|
|
1029
1043
|
}
|
|
1030
|
-
const survivingFrames = frames
|
|
1044
|
+
const survivingFrames = filter4(frames, (f) => thresholdSurvivors.has(f.id));
|
|
1031
1045
|
const idToOrigIdx = /* @__PURE__ */ new Map();
|
|
1032
1046
|
for (let i = 0; i < frames.length; i++) {
|
|
1033
1047
|
idToOrigIdx.set(frames[i].id, i);
|
|
@@ -1063,6 +1077,7 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
|
|
|
1063
1077
|
// src/core/segmenter.ts
|
|
1064
1078
|
import { randomUUID } from "crypto";
|
|
1065
1079
|
import { join as join5 } from "path";
|
|
1080
|
+
import { filter as filter5, map as map6 } from "@winglet/common-utils";
|
|
1066
1081
|
|
|
1067
1082
|
// src/utils/concurrency.ts
|
|
1068
1083
|
function concurrencyLimit(limit) {
|
|
@@ -1326,7 +1341,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1326
1341
|
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1327
1342
|
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1328
1343
|
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1329
|
-
const weights = segments
|
|
1344
|
+
const weights = map6(segments, (s) => s.duration / totalDuration);
|
|
1330
1345
|
const emitOverallProgress = (phase) => {
|
|
1331
1346
|
if (!options.onProgress) return;
|
|
1332
1347
|
const overall = weights.reduce(
|
|
@@ -1337,7 +1352,8 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1337
1352
|
};
|
|
1338
1353
|
options.onProgress?.("EXTRACTING", 0);
|
|
1339
1354
|
const results = await Promise.all(
|
|
1340
|
-
|
|
1355
|
+
map6(
|
|
1356
|
+
segments,
|
|
1341
1357
|
(segment) => limit(async () => {
|
|
1342
1358
|
const segWorkspace = await createSegmentWorkspace(
|
|
1343
1359
|
mainWorkspace,
|
|
@@ -1369,7 +1385,7 @@ async function runSegmentedPipeline(options, resolvedOptions) {
|
|
|
1369
1385
|
resolvedOptions.threshold,
|
|
1370
1386
|
resolvedOptions.count
|
|
1371
1387
|
);
|
|
1372
|
-
const prunedFrames = frames
|
|
1388
|
+
const prunedFrames = filter5(frames, (f) => survivingIds.has(f.id));
|
|
1373
1389
|
options.onProgress?.("PRUNING", 100);
|
|
1374
1390
|
options.onProgress?.("FINALIZING", 0);
|
|
1375
1391
|
const ctx = {
|
|
@@ -1479,7 +1495,7 @@ async function runPipeline(options) {
|
|
|
1479
1495
|
resolvedOptions.threshold,
|
|
1480
1496
|
resolvedOptions.count
|
|
1481
1497
|
);
|
|
1482
|
-
const prunedFrames = ctx.frames
|
|
1498
|
+
const prunedFrames = filter6(ctx.frames, (f) => survivingIds.has(f.id));
|
|
1483
1499
|
ctx.emitProgress(100);
|
|
1484
1500
|
ctx.status = "FINALIZING";
|
|
1485
1501
|
let outputFiles = [];
|
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) {
|
|
@@ -1329,7 +1344,7 @@ async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
|
1329
1344
|
logger.debug(`Segment plan: ${segments.length} segments`);
|
|
1330
1345
|
const limit = concurrencyLimit(resolvedOptions.concurrency);
|
|
1331
1346
|
const segmentProgresses = new Array(segments.length).fill(0);
|
|
1332
|
-
const weights = segments
|
|
1347
|
+
const weights = map6(segments, (s) => s.duration / totalDuration);
|
|
1333
1348
|
const emitOverallProgress = (phase) => {
|
|
1334
1349
|
if (!options2.onProgress) return;
|
|
1335
1350
|
const overall = weights.reduce(
|
|
@@ -1340,7 +1355,8 @@ async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
|
1340
1355
|
};
|
|
1341
1356
|
options2.onProgress?.("EXTRACTING", 0);
|
|
1342
1357
|
const results = await Promise.all(
|
|
1343
|
-
|
|
1358
|
+
map6(
|
|
1359
|
+
segments,
|
|
1344
1360
|
(segment) => limit(async () => {
|
|
1345
1361
|
const segWorkspace = await createSegmentWorkspace(
|
|
1346
1362
|
mainWorkspace,
|
|
@@ -1372,7 +1388,7 @@ async function runSegmentedPipeline(options2, resolvedOptions) {
|
|
|
1372
1388
|
resolvedOptions.threshold,
|
|
1373
1389
|
resolvedOptions.count
|
|
1374
1390
|
);
|
|
1375
|
-
const prunedFrames = frames
|
|
1391
|
+
const prunedFrames = filter5(frames, (f) => survivingIds.has(f.id));
|
|
1376
1392
|
options2.onProgress?.("PRUNING", 100);
|
|
1377
1393
|
options2.onProgress?.("FINALIZING", 0);
|
|
1378
1394
|
const ctx = {
|
|
@@ -1482,7 +1498,7 @@ async function runPipeline(options2) {
|
|
|
1482
1498
|
resolvedOptions.threshold,
|
|
1483
1499
|
resolvedOptions.count
|
|
1484
1500
|
);
|
|
1485
|
-
const prunedFrames = ctx.frames
|
|
1501
|
+
const prunedFrames = filter6(ctx.frames, (f) => survivingIds.has(f.id));
|
|
1486
1502
|
ctx.emitProgress(100);
|
|
1487
1503
|
ctx.status = "FINALIZING";
|
|
1488
1504
|
let outputFiles = [];
|
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",
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@ffprobe-installer/ffprobe": "^1.4.1",
|
|
58
58
|
"@techstark/opencv-js": "4.12.0-release.1",
|
|
59
|
+
"@winglet/common-utils": "^0.11.2",
|
|
60
|
+
"@winglet/react-utils": "^0.11.2",
|
|
59
61
|
"commander": "^12.1.0",
|
|
60
62
|
"execa": "^9.5.0",
|
|
61
63
|
"ffmpeg-static": "^5.2.0",
|