@lumy-pack/scene-sieve 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,86 +1,95 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { createRequire as createRequire2 } from "module";
5
- import { Command } from "commander";
6
-
7
- // src/core/orchestrator.ts
8
- import { randomUUID } from "crypto";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
9
11
 
10
12
  // src/utils/logger.ts
11
13
  import pc from "picocolors";
12
- var debugMode = false;
13
14
  function setDebugMode(enabled) {
14
15
  debugMode = enabled;
15
16
  }
16
17
  function timestamp() {
17
18
  return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
18
19
  }
19
- var logger = {
20
- info(message) {
21
- console.log(`${pc.blue("info")} ${message}`);
22
- },
23
- success(message) {
24
- console.log(`
20
+ var debugMode, logger;
21
+ var init_logger = __esm({
22
+ "src/utils/logger.ts"() {
23
+ "use strict";
24
+ debugMode = false;
25
+ logger = {
26
+ info(message) {
27
+ console.log(`${pc.blue("info")} ${message}`);
28
+ },
29
+ success(message) {
30
+ console.log(`
25
31
  ${pc.green("done")} ${message}`);
26
- },
27
- warn(message) {
28
- console.warn(`${pc.yellow("warn")} ${message}`);
29
- },
30
- error(message) {
31
- console.error(`${pc.red("error")} ${message}`);
32
- },
33
- debug(message) {
34
- if (debugMode) {
35
- console.log(`${pc.gray(`[${timestamp()}] debug`)} ${message}`);
36
- }
32
+ },
33
+ warn(message) {
34
+ console.warn(`${pc.yellow("warn")} ${message}`);
35
+ },
36
+ error(message) {
37
+ console.error(`${pc.red("error")} ${message}`);
38
+ },
39
+ debug(message) {
40
+ if (debugMode) {
41
+ console.log(`${pc.gray(`[${timestamp()}] debug`)} ${message}`);
42
+ }
43
+ }
44
+ };
37
45
  }
38
- };
39
-
40
- // src/core/analyzer.ts
41
- import { createRequire } from "module";
42
- import sharp from "sharp";
46
+ });
43
47
 
44
48
  // src/constants.ts
45
49
  import { tmpdir } from "os";
46
50
  import { join } from "path";
47
- var APP_NAME = "scene-sieve";
48
- var DEFAULT_COUNT = 20;
49
- var DEFAULT_THRESHOLD = 0.5;
50
- var DEFAULT_FPS = 5;
51
- var DEFAULT_SCALE = 720;
52
- var DEFAULT_QUALITY = 80;
53
- var NORMALIZATION_PERCENTILE = 0.9;
54
- var WORKSPACE_PREFIX = `${APP_NAME}-`;
55
- var TEMP_BASE_DIR = tmpdir();
56
- var SUPPORTED_VIDEO_EXTENSIONS = [
57
- ".mp4",
58
- ".mov",
59
- ".avi",
60
- ".mkv",
61
- ".webm"
62
- ];
63
- var SUPPORTED_GIF_EXTENSIONS = [".gif"];
64
- var FRAME_OUTPUT_EXTENSION = ".jpg";
65
- var OPENCV_BATCH_SIZE = 10;
66
- var MIN_IFRAME_COUNT = 3;
67
- var DBSCAN_ALPHA = 0.03;
68
- var DBSCAN_MIN_PTS = 4;
69
- var IOU_THRESHOLD = 0.9;
70
- var DECAY_LAMBDA = 0.95;
71
- var ANIMATION_FRAME_THRESHOLD = 5;
72
- var MATCH_DISTANCE_THRESHOLD = 0.25;
73
- var PIXELDIFF_GAUSSIAN_KERNEL = 3;
74
- var PIXELDIFF_BINARY_THRESHOLD = 30;
75
- var PIXELDIFF_CONTOUR_MIN_AREA = 100;
76
- var PIXELDIFF_SAMPLE_SPACING = 8;
77
51
  function getTempWorkspaceDir(sessionId) {
78
52
  return join(TEMP_BASE_DIR, `${WORKSPACE_PREFIX}${sessionId}`);
79
53
  }
54
+ var APP_NAME, DEFAULT_COUNT, DEFAULT_THRESHOLD, DEFAULT_FPS, DEFAULT_SCALE, DEFAULT_QUALITY, DEFAULT_MAX_FRAMES, NORMALIZATION_PERCENTILE, WORKSPACE_PREFIX, TEMP_BASE_DIR, SUPPORTED_VIDEO_EXTENSIONS, SUPPORTED_GIF_EXTENSIONS, FRAME_OUTPUT_EXTENSION, FRAME_FILENAME_PATTERN, OPENCV_BATCH_SIZE, DBSCAN_ALPHA, DBSCAN_MIN_PTS, IOU_THRESHOLD, DECAY_LAMBDA, ANIMATION_FRAME_THRESHOLD, MATCH_DISTANCE_THRESHOLD, PIXELDIFF_GAUSSIAN_KERNEL, PIXELDIFF_BINARY_THRESHOLD, PIXELDIFF_CONTOUR_MIN_AREA, PIXELDIFF_SAMPLE_SPACING;
55
+ var init_constants = __esm({
56
+ "src/constants.ts"() {
57
+ "use strict";
58
+ APP_NAME = "scene-sieve";
59
+ DEFAULT_COUNT = 20;
60
+ DEFAULT_THRESHOLD = 0.5;
61
+ DEFAULT_FPS = 5;
62
+ DEFAULT_SCALE = 720;
63
+ DEFAULT_QUALITY = 80;
64
+ DEFAULT_MAX_FRAMES = 300;
65
+ NORMALIZATION_PERCENTILE = 0.9;
66
+ WORKSPACE_PREFIX = `${APP_NAME}-`;
67
+ TEMP_BASE_DIR = tmpdir();
68
+ SUPPORTED_VIDEO_EXTENSIONS = [
69
+ ".mp4",
70
+ ".mov",
71
+ ".avi",
72
+ ".mkv",
73
+ ".webm"
74
+ ];
75
+ SUPPORTED_GIF_EXTENSIONS = [".gif"];
76
+ FRAME_OUTPUT_EXTENSION = ".jpg";
77
+ FRAME_FILENAME_PATTERN = "frame_%06d.jpg";
78
+ OPENCV_BATCH_SIZE = 10;
79
+ DBSCAN_ALPHA = 0.03;
80
+ DBSCAN_MIN_PTS = 4;
81
+ IOU_THRESHOLD = 0.9;
82
+ DECAY_LAMBDA = 0.95;
83
+ ANIMATION_FRAME_THRESHOLD = 5;
84
+ MATCH_DISTANCE_THRESHOLD = 0.25;
85
+ PIXELDIFF_GAUSSIAN_KERNEL = 3;
86
+ PIXELDIFF_BINARY_THRESHOLD = 30;
87
+ PIXELDIFF_CONTOUR_MIN_AREA = 100;
88
+ PIXELDIFF_SAMPLE_SPACING = 8;
89
+ }
90
+ });
80
91
 
81
92
  // src/core/dbscan.ts
82
- var UNVISITED = -2;
83
- var NOISE = -1;
84
93
  function dbscan(points, imageWidth, imageHeight, alpha, minPts) {
85
94
  if (points.length === 0) {
86
95
  return { labels: [], boundingBoxes: [] };
@@ -155,11 +164,19 @@ function findNeighbors(points, idx, epsSquared) {
155
164
  }
156
165
  return neighbors;
157
166
  }
167
+ var UNVISITED, NOISE;
168
+ var init_dbscan = __esm({
169
+ "src/core/dbscan.ts"() {
170
+ "use strict";
171
+ init_constants();
172
+ UNVISITED = -2;
173
+ NOISE = -1;
174
+ }
175
+ });
158
176
 
159
177
  // src/core/analyzer.ts
160
- var OPENCV_INIT_TIMEOUT_MS = 3e4;
161
- var require2 = createRequire(import.meta.url);
162
- var cvReady = null;
178
+ import { createRequire } from "module";
179
+ import sharp from "sharp";
163
180
  async function ensureOpenCV() {
164
181
  if (!cvReady) {
165
182
  cvReady = (async () => {
@@ -201,67 +218,6 @@ function computeIoU(a, b) {
201
218
  const union = aArea + bArea - intersection;
202
219
  return union === 0 ? 0 : intersection / union;
203
220
  }
204
- var IoUTracker = class {
205
- regions = [];
206
- update(boxes, pairIndex) {
207
- const animationIndices = /* @__PURE__ */ new Set();
208
- const matched = /* @__PURE__ */ new Set();
209
- for (let bi = 0; bi < boxes.length; bi++) {
210
- const box = boxes[bi];
211
- let bestIoU = 0;
212
- let bestRegionIdx = -1;
213
- for (let ri = 0; ri < this.regions.length; ri++) {
214
- if (matched.has(ri)) continue;
215
- const iou = computeIoU(box, this.regions[ri].box);
216
- if (iou > bestIoU) {
217
- bestIoU = iou;
218
- bestRegionIdx = ri;
219
- }
220
- }
221
- if (bestIoU > IOU_THRESHOLD && bestRegionIdx !== -1) {
222
- const region = this.regions[bestRegionIdx];
223
- const gap = pairIndex - region.lastSeen;
224
- region.box = box;
225
- region.consecutiveCount++;
226
- region.lastSeen = pairIndex;
227
- region.weight *= Math.pow(DECAY_LAMBDA, gap);
228
- matched.add(bestRegionIdx);
229
- if (region.consecutiveCount >= ANIMATION_FRAME_THRESHOLD) {
230
- animationIndices.add(bi);
231
- }
232
- } else {
233
- this.regions.push({
234
- box,
235
- consecutiveCount: 1,
236
- lastSeen: pairIndex,
237
- weight: 1
238
- });
239
- }
240
- }
241
- for (let ri = 0; ri < this.regions.length; ri++) {
242
- if (!matched.has(ri)) {
243
- const gap = pairIndex - this.regions[ri].lastSeen;
244
- this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
245
- }
246
- }
247
- this.regions = this.regions.filter((r) => r.weight > 0.01);
248
- return animationIndices;
249
- }
250
- getAnimationWeight(boxIndex, boxes) {
251
- if (boxIndex >= boxes.length) return 0;
252
- const box = boxes[boxIndex];
253
- let maxWeight = 0;
254
- for (const region of this.regions) {
255
- if (region.consecutiveCount >= ANIMATION_FRAME_THRESHOLD) {
256
- const iou = computeIoU(box, region.box);
257
- if (iou > IOU_THRESHOLD) {
258
- maxWeight = Math.max(maxWeight, region.weight);
259
- }
260
- }
261
- }
262
- return maxWeight;
263
- }
264
- };
265
221
  async function computeAKAZEDiff(cvLib, frame1, frame2) {
266
222
  const cv = cvLib;
267
223
  const mat1 = new cv.Mat(frame1.height, frame1.width, cv.CV_8UC1);
@@ -476,13 +432,17 @@ async function analyzeBatch(cvLib, frames, scale, tracker, pairOffset) {
476
432
  }
477
433
  async function analyzeFrames(ctx) {
478
434
  const { frames } = ctx;
479
- if (frames.length < 2) return [];
435
+ if (frames.length < 2) return { edges: [], animations: [] };
480
436
  logger.debug(
481
437
  `Analyzing ${frames.length} frames in batches of ${OPENCV_BATCH_SIZE}`
482
438
  );
483
439
  const cvLib = await ensureOpenCV();
484
440
  const edges = [];
485
- const tracker = new IoUTracker();
441
+ const tracker = new IoUTracker(
442
+ ctx.options.fps,
443
+ ctx.options.iouThreshold,
444
+ ctx.options.animationThreshold
445
+ );
486
446
  const scale = ctx.options.scale;
487
447
  for (let i = 0; i < frames.length - 1; i += OPENCV_BATCH_SIZE) {
488
448
  const batchEnd = Math.min(i + OPENCV_BATCH_SIZE + 1, frames.length);
@@ -495,16 +455,120 @@ async function analyzeFrames(ctx) {
495
455
  );
496
456
  ctx.emitProgress(progress);
497
457
  }
498
- logger.debug(`Computed ${edges.length} score edges`);
499
- return edges;
458
+ const animations = tracker.flushAndGetAnimations();
459
+ logger.debug(
460
+ `Computed ${edges.length} score edges and ${animations.length} animations`
461
+ );
462
+ return { edges, animations };
500
463
  }
501
-
502
- // src/core/extractor.ts
503
- import { readdir } from "fs/promises";
504
- import { join as join2 } from "path";
505
- import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
506
- import { execa } from "execa";
507
- import ffmpegPath from "ffmpeg-static";
464
+ var OPENCV_INIT_TIMEOUT_MS, require2, cvReady, IoUTracker;
465
+ var init_analyzer = __esm({
466
+ "src/core/analyzer.ts"() {
467
+ "use strict";
468
+ init_constants();
469
+ init_logger();
470
+ init_dbscan();
471
+ OPENCV_INIT_TIMEOUT_MS = 3e4;
472
+ require2 = createRequire(import.meta.url);
473
+ cvReady = null;
474
+ IoUTracker = class {
475
+ constructor(fps = DEFAULT_FPS, iouThreshold = IOU_THRESHOLD, animationThreshold = ANIMATION_FRAME_THRESHOLD) {
476
+ this.fps = fps;
477
+ this.iouThreshold = iouThreshold;
478
+ this.animationThreshold = animationThreshold;
479
+ }
480
+ regions = [];
481
+ extractedAnimations = [];
482
+ update(boxes, pairIndex) {
483
+ const animationIndices = /* @__PURE__ */ new Set();
484
+ const matched = /* @__PURE__ */ new Set();
485
+ for (let bi = 0; bi < boxes.length; bi++) {
486
+ const box = boxes[bi];
487
+ let bestIoU = 0;
488
+ let bestRegionIdx = -1;
489
+ for (let ri = 0; ri < this.regions.length; ri++) {
490
+ if (matched.has(ri)) continue;
491
+ const iou = computeIoU(box, this.regions[ri].box);
492
+ if (iou > bestIoU) {
493
+ bestIoU = iou;
494
+ bestRegionIdx = ri;
495
+ }
496
+ }
497
+ if (bestIoU > this.iouThreshold && bestRegionIdx !== -1) {
498
+ const region = this.regions[bestRegionIdx];
499
+ const gap = pairIndex - region.lastSeen;
500
+ region.box = box;
501
+ region.consecutiveCount++;
502
+ region.lastSeen = pairIndex;
503
+ region.weight *= Math.pow(DECAY_LAMBDA, gap);
504
+ matched.add(bestRegionIdx);
505
+ if (region.consecutiveCount >= this.animationThreshold) {
506
+ animationIndices.add(bi);
507
+ }
508
+ } else {
509
+ this.regions.push({
510
+ box,
511
+ consecutiveCount: 1,
512
+ firstSeen: pairIndex,
513
+ lastSeen: pairIndex,
514
+ weight: 1
515
+ });
516
+ }
517
+ }
518
+ for (let ri = 0; ri < this.regions.length; ri++) {
519
+ if (!matched.has(ri)) {
520
+ const gap = pairIndex - this.regions[ri].lastSeen;
521
+ this.regions[ri].weight *= Math.pow(DECAY_LAMBDA, gap);
522
+ }
523
+ }
524
+ for (let i = 0; i < this.regions.length; i++) {
525
+ const region = this.regions[i];
526
+ if (region.weight <= 0.01 && !matched.has(i)) {
527
+ this.collectAnimation(region);
528
+ }
529
+ }
530
+ this.regions = this.regions.filter(
531
+ (r, i) => r.weight > 0.01 || matched.has(i)
532
+ );
533
+ return animationIndices;
534
+ }
535
+ collectAnimation(region) {
536
+ if (region.consecutiveCount >= this.animationThreshold) {
537
+ const durationMs = region.consecutiveCount / this.fps * 1e3;
538
+ this.extractedAnimations.push({
539
+ type: "loading_spinner",
540
+ // 기본값으로 loading_spinner 사용
541
+ boundingBox: region.box,
542
+ startFrameId: region.firstSeen,
543
+ endFrameId: region.lastSeen,
544
+ durationMs
545
+ });
546
+ }
547
+ }
548
+ flushAndGetAnimations() {
549
+ for (const region of this.regions) {
550
+ this.collectAnimation(region);
551
+ }
552
+ this.regions = [];
553
+ return this.extractedAnimations;
554
+ }
555
+ getAnimationWeight(boxIndex, boxes) {
556
+ if (boxIndex >= boxes.length) return 0;
557
+ const box = boxes[boxIndex];
558
+ let maxWeight = 0;
559
+ for (const region of this.regions) {
560
+ if (region.consecutiveCount >= this.animationThreshold) {
561
+ const iou = computeIoU(box, region.box);
562
+ if (iou > this.iouThreshold) {
563
+ maxWeight = Math.max(maxWeight, region.weight);
564
+ }
565
+ }
566
+ }
567
+ return maxWeight;
568
+ }
569
+ };
570
+ }
571
+ });
508
572
 
509
573
  // src/utils/paths.ts
510
574
  import { mkdir, stat } from "fs/promises";
@@ -539,11 +603,21 @@ function deriveOutputPath(inputPath) {
539
603
  function isSupportedFile(filePath, extensions) {
540
604
  return extensions.includes(extname(filePath).toLowerCase());
541
605
  }
606
+ var init_paths = __esm({
607
+ "src/utils/paths.ts"() {
608
+ "use strict";
609
+ }
610
+ });
542
611
 
543
612
  // src/core/extractor.ts
613
+ import { readdir } from "fs/promises";
614
+ import { join as join2 } from "path";
615
+ import { path as ffprobePath } from "@ffprobe-installer/ffprobe";
616
+ import { execa } from "execa";
617
+ import ffmpegPath from "ffmpeg-static";
544
618
  async function extractFrames(ctx) {
545
619
  const framesDir = join2(ctx.workspacePath, "frames");
546
- const { inputPath, fps, scale } = ctx.options;
620
+ const { inputPath, fps, maxFrames, scale } = ctx.options;
547
621
  if (!inputPath) {
548
622
  throw new Error("inputPath is required for frame extraction");
549
623
  }
@@ -560,41 +634,23 @@ async function extractFrames(ctx) {
560
634
  }
561
635
  logger.debug(`Extracting frames from: ${inputPath}`);
562
636
  await ensureDir(framesDir);
563
- const isGif = isSupportedFile(inputPath, SUPPORTED_GIF_EXTENSIONS);
564
- let frames;
565
- if (isGif) {
566
- logger.debug("GIF detected \u2014 using FPS extraction");
567
- frames = await extractByFps(inputPath, framesDir, fps, scale);
568
- } else {
569
- frames = await extractIFrames(inputPath, framesDir, scale);
570
- if (frames.length < MIN_IFRAME_COUNT) {
571
- logger.debug(
572
- `Insufficient I-frames (${frames.length}), falling back to FPS mode`
573
- );
574
- frames = await extractByFps(inputPath, framesDir, fps, scale);
575
- }
637
+ let effectiveFps = fps;
638
+ const duration = await getVideoDuration(inputPath).catch(() => 0);
639
+ if (duration > 0) {
640
+ const fpsCap = maxFrames / duration;
641
+ effectiveFps = Math.min(fps, fpsCap);
642
+ effectiveFps = Math.max(0.5, effectiveFps);
643
+ logger.debug(
644
+ `Duration: ${duration.toFixed(1)}s, FPS: ${fps} \u2192 effective: ${effectiveFps.toFixed(2)} (maxFrames: ${maxFrames})`
645
+ );
576
646
  }
647
+ const frames = await extractByFps(inputPath, framesDir, effectiveFps, scale);
577
648
  ctx.emitProgress(100);
578
649
  logger.debug(`Extracted ${frames.length} frames`);
579
650
  return frames;
580
651
  }
581
- async function extractIFrames(inputPath, outputDir, scale) {
582
- const outputPattern = join2(outputDir, "frame_%06d.jpg");
583
- await execa(ffmpegPath, [
584
- "-i",
585
- inputPath,
586
- "-vf",
587
- `select='eq(pict_type,I)',scale=-1:${scale}`,
588
- "-vsync",
589
- "vfr",
590
- "-q:v",
591
- "2",
592
- outputPattern
593
- ]);
594
- return buildFrameList(outputDir, inputPath);
595
- }
596
652
  async function extractByFps(inputPath, outputDir, fps, scale) {
597
- const outputPattern = join2(outputDir, "frame_%06d.jpg");
653
+ const outputPattern = join2(outputDir, FRAME_FILENAME_PATTERN);
598
654
  await execa(ffmpegPath, [
599
655
  "-i",
600
656
  inputPath,
@@ -638,12 +694,17 @@ async function buildFrameList(framesDir, inputPath) {
638
694
  extractPath: join2(framesDir, file)
639
695
  }));
640
696
  }
641
-
642
- // src/core/input-resolver.ts
643
- import { join as join4 } from "path";
697
+ var init_extractor = __esm({
698
+ "src/core/extractor.ts"() {
699
+ "use strict";
700
+ init_constants();
701
+ init_logger();
702
+ init_paths();
703
+ }
704
+ });
644
705
 
645
706
  // src/core/workspace.ts
646
- import { rename, rm, writeFile } from "fs/promises";
707
+ import { readdir as readdir2, rename, rm, stat as stat2, writeFile } from "fs/promises";
647
708
  import { join as join3 } from "path";
648
709
  import sharp2 from "sharp";
649
710
  async function createWorkspace(sessionId) {
@@ -657,13 +718,44 @@ async function finalizeOutput(ctx, selectedFrames) {
657
718
  const outputPath = ctx.options.outputPath;
658
719
  const quality = ctx.options.quality;
659
720
  const outputFiles = [];
721
+ const framesMetadata = [];
722
+ const totalFramesCount = ctx.frames.length;
723
+ const padding = Math.max(4, String(totalFramesCount).length);
660
724
  for (let i = 0; i < selectedFrames.length; i++) {
661
725
  const frame = selectedFrames[i];
662
- const destName = `scene_${String(i + 1).padStart(3, "0")}.jpg`;
663
- const destPath = join3(stagingDir, destName);
726
+ const fileName = `frame_${String(frame.id + 1).padStart(padding, "0")}.jpg`;
727
+ const destPath = join3(stagingDir, fileName);
664
728
  await sharp2(frame.extractPath).jpeg({ quality, mozjpeg: true }).toFile(destPath);
665
- outputFiles.push(join3(outputPath, destName));
729
+ outputFiles.push(join3(outputPath, fileName));
730
+ framesMetadata.push({
731
+ step: i + 1,
732
+ fileName,
733
+ frameId: frame.id + 1,
734
+ timestampMs: Math.round(frame.timestamp * 1e3)
735
+ });
666
736
  }
737
+ const metadata = {
738
+ video: {
739
+ originalDurationMs: Math.round(
740
+ (ctx.frames.length > 0 ? ctx.frames[ctx.frames.length - 1].timestamp : 0) * 1e3
741
+ ),
742
+ fps: ctx.options.fps,
743
+ resolution: {
744
+ width: ctx.options.scale,
745
+ height: Math.round(ctx.options.scale * 9 / 16)
746
+ }
747
+ },
748
+ frames: framesMetadata,
749
+ animations: (ctx.animations || []).map((anim) => ({
750
+ ...anim,
751
+ startFrameId: anim.startFrameId + 1,
752
+ endFrameId: anim.endFrameId + 1,
753
+ durationMs: Math.round(anim.durationMs)
754
+ }))
755
+ };
756
+ const metadataPath = join3(stagingDir, ".metadata.json");
757
+ await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
758
+ outputFiles.push(join3(outputPath, ".metadata.json"));
667
759
  await ensureDir(join3(outputPath, ".."));
668
760
  await rm(outputPath, { recursive: true, force: true });
669
761
  await rename(stagingDir, outputPath);
@@ -676,6 +768,21 @@ async function cleanupWorkspace(workspacePath) {
676
768
  } catch {
677
769
  }
678
770
  }
771
+ async function cleanupStaleWorkspaces() {
772
+ const entries = await readdir2(TEMP_BASE_DIR);
773
+ const now = Date.now();
774
+ for (const entry of entries) {
775
+ if (!entry.startsWith(WORKSPACE_PREFIX)) continue;
776
+ const fullPath = join3(TEMP_BASE_DIR, entry);
777
+ try {
778
+ const info = await stat2(fullPath);
779
+ if (info.isDirectory() && now - info.mtimeMs > STALE_THRESHOLD_MS) {
780
+ await rm(fullPath, { recursive: true, force: true });
781
+ }
782
+ } catch {
783
+ }
784
+ }
785
+ }
679
786
  async function writeInputBuffer(buffer, workspacePath) {
680
787
  const inputDir = join3(workspacePath, "input");
681
788
  await ensureDir(inputDir);
@@ -702,8 +809,18 @@ async function readFramesAsBuffers(frameNodes, quality) {
702
809
  )
703
810
  );
704
811
  }
812
+ var STALE_THRESHOLD_MS;
813
+ var init_workspace = __esm({
814
+ "src/core/workspace.ts"() {
815
+ "use strict";
816
+ init_constants();
817
+ init_paths();
818
+ STALE_THRESHOLD_MS = 60 * 60 * 1e3;
819
+ }
820
+ });
705
821
 
706
822
  // src/core/input-resolver.ts
823
+ import { join as join4 } from "path";
707
824
  function resolveOptions(options) {
708
825
  const mode = options.mode;
709
826
  const inputPath = mode === "file" ? resolveAbsolute(options.inputPath) : void 0;
@@ -723,8 +840,11 @@ function resolveOptions(options) {
723
840
  pruneMode,
724
841
  outputPath,
725
842
  fps: options.fps ?? DEFAULT_FPS,
843
+ maxFrames: options.maxFrames ?? DEFAULT_MAX_FRAMES,
726
844
  scale: options.scale ?? DEFAULT_SCALE,
727
845
  quality: options.quality ?? DEFAULT_QUALITY,
846
+ iouThreshold: options.iouThreshold ?? IOU_THRESHOLD,
847
+ animationThreshold: options.animationThreshold ?? ANIMATION_FRAME_THRESHOLD,
728
848
  debug: options.debug ?? false
729
849
  };
730
850
  }
@@ -748,50 +868,64 @@ async function resolveInput(options, workspacePath) {
748
868
  }
749
869
  throw new Error(`Unsupported input mode: ${options.mode}`);
750
870
  }
871
+ var init_input_resolver = __esm({
872
+ "src/core/input-resolver.ts"() {
873
+ "use strict";
874
+ init_constants();
875
+ init_paths();
876
+ init_workspace();
877
+ }
878
+ });
751
879
 
752
880
  // src/utils/min-heap.ts
753
- var MinHeap = class {
754
- h = [];
755
- get size() {
756
- return this.h.length;
757
- }
758
- push(entry) {
759
- this.h.push(entry);
760
- this.siftUp(this.h.length - 1);
761
- }
762
- pop() {
763
- const n = this.h.length;
764
- if (n === 0) return void 0;
765
- const top = this.h[0];
766
- const last = this.h.pop();
767
- if (n > 1) {
768
- this.h[0] = last;
769
- this.siftDown(0);
770
- }
771
- return top;
772
- }
773
- siftUp(i) {
774
- while (i > 0) {
775
- const p = i - 1 >> 1;
776
- if (this.h[p].score <= this.h[i].score) break;
777
- [this.h[p], this.h[i]] = [this.h[i], this.h[p]];
778
- i = p;
779
- }
780
- }
781
- siftDown(i) {
782
- const n = this.h.length;
783
- for (; ; ) {
784
- let m = i;
785
- const l = 2 * i + 1;
786
- const r = 2 * i + 2;
787
- if (l < n && this.h[l].score < this.h[m].score) m = l;
788
- if (r < n && this.h[r].score < this.h[m].score) m = r;
789
- if (m === i) break;
790
- [this.h[m], this.h[i]] = [this.h[i], this.h[m]];
791
- i = m;
792
- }
881
+ var MinHeap;
882
+ var init_min_heap = __esm({
883
+ "src/utils/min-heap.ts"() {
884
+ "use strict";
885
+ MinHeap = class {
886
+ h = [];
887
+ get size() {
888
+ return this.h.length;
889
+ }
890
+ push(entry) {
891
+ this.h.push(entry);
892
+ this.siftUp(this.h.length - 1);
893
+ }
894
+ pop() {
895
+ const n = this.h.length;
896
+ if (n === 0) return void 0;
897
+ const top = this.h[0];
898
+ const last = this.h.pop();
899
+ if (n > 1) {
900
+ this.h[0] = last;
901
+ this.siftDown(0);
902
+ }
903
+ return top;
904
+ }
905
+ siftUp(i) {
906
+ while (i > 0) {
907
+ const p = i - 1 >> 1;
908
+ if (this.h[p].score <= this.h[i].score) break;
909
+ [this.h[p], this.h[i]] = [this.h[i], this.h[p]];
910
+ i = p;
911
+ }
912
+ }
913
+ siftDown(i) {
914
+ const n = this.h.length;
915
+ for (; ; ) {
916
+ let m = i;
917
+ const l = 2 * i + 1;
918
+ const r = 2 * i + 2;
919
+ if (l < n && this.h[l].score < this.h[m].score) m = l;
920
+ if (r < n && this.h[r].score < this.h[m].score) m = r;
921
+ if (m === i) break;
922
+ [this.h[m], this.h[i]] = [this.h[i], this.h[m]];
923
+ i = m;
924
+ }
925
+ }
926
+ };
793
927
  }
794
- };
928
+ });
795
929
 
796
930
  // src/core/pruner.ts
797
931
  function pruneTo(graph, frames, targetCount) {
@@ -953,8 +1087,20 @@ function pruneByThresholdWithCap(graph, frames, threshold, maxCount) {
953
1087
  }
954
1088
  return pruneTo(syntheticEdges, survivingFrames, maxCount);
955
1089
  }
1090
+ var init_pruner = __esm({
1091
+ "src/core/pruner.ts"() {
1092
+ "use strict";
1093
+ init_constants();
1094
+ init_min_heap();
1095
+ }
1096
+ });
956
1097
 
957
1098
  // src/core/orchestrator.ts
1099
+ var orchestrator_exports = {};
1100
+ __export(orchestrator_exports, {
1101
+ runPipeline: () => runPipeline
1102
+ });
1103
+ import { randomUUID } from "crypto";
958
1104
  async function runPipeline(options) {
959
1105
  const startTime = Date.now();
960
1106
  const sessionId = randomUUID();
@@ -995,7 +1141,9 @@ async function runPipeline(options) {
995
1141
  }
996
1142
  ctx.emitProgress(100);
997
1143
  ctx.status = "ANALYZING";
998
- ctx.graph = await analyzeFrames(ctx);
1144
+ const { edges, animations } = await analyzeFrames(ctx);
1145
+ ctx.graph = edges;
1146
+ ctx.animations = animations;
999
1147
  ctx.status = "PRUNING";
1000
1148
  const survivingIds = pruneByThresholdWithCap(
1001
1149
  ctx.graph,
@@ -1028,6 +1176,15 @@ async function runPipeline(options) {
1028
1176
  prunedFramesCount: prunedFrames.length,
1029
1177
  outputFiles,
1030
1178
  outputBuffers,
1179
+ animations: ctx.animations,
1180
+ video: {
1181
+ originalDurationMs: ctx.frames.length / ctx.options.fps * 1e3,
1182
+ fps: ctx.options.fps,
1183
+ resolution: {
1184
+ width: ctx.options.scale,
1185
+ height: Math.round(ctx.options.scale * 9 / 16)
1186
+ }
1187
+ },
1031
1188
  executionTimeMs: Date.now() - startTime
1032
1189
  };
1033
1190
  } catch (error) {
@@ -1043,58 +1200,339 @@ async function runPipeline(options) {
1043
1200
  }
1044
1201
  }
1045
1202
  }
1203
+ var init_orchestrator = __esm({
1204
+ "src/core/orchestrator.ts"() {
1205
+ "use strict";
1206
+ init_logger();
1207
+ init_analyzer();
1208
+ init_extractor();
1209
+ init_input_resolver();
1210
+ init_pruner();
1211
+ init_workspace();
1212
+ }
1213
+ });
1046
1214
 
1047
1215
  // src/cli.ts
1216
+ import { createRequire as createRequire2 } from "module";
1217
+ import { Command } from "commander";
1218
+ import { render } from "ink";
1219
+ import React2 from "react";
1220
+
1221
+ // src/commands/Sieve.tsx
1222
+ import { Box as Box2, Text as Text3, useApp } from "ink";
1223
+ import { useEffect, useState } from "react";
1224
+
1225
+ // src/components/PhaseStep.tsx
1226
+ import { Box, Text as Text2 } from "ink";
1227
+ import Spinner from "ink-spinner";
1228
+
1229
+ // src/components/ProgressBar.tsx
1230
+ import { Text } from "ink";
1231
+ import { jsx, jsxs } from "react/jsx-runtime";
1232
+ var ProgressBar = ({
1233
+ percent,
1234
+ width = 30
1235
+ }) => {
1236
+ const clamped = Math.max(0, Math.min(100, percent));
1237
+ const filled = Math.round(width * (clamped / 100));
1238
+ const empty = width - filled;
1239
+ return /* @__PURE__ */ jsxs(Text, { children: [
1240
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2588".repeat(filled) }),
1241
+ /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2591".repeat(empty) }),
1242
+ /* @__PURE__ */ jsxs(Text, { children: [
1243
+ " ",
1244
+ clamped,
1245
+ "%"
1246
+ ] })
1247
+ ] });
1248
+ };
1249
+
1250
+ // src/components/PhaseStep.tsx
1251
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1252
+ var PhaseStep = ({ phase }) => {
1253
+ const icon = (() => {
1254
+ switch (phase.status) {
1255
+ case "done":
1256
+ return /* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u2713" });
1257
+ case "running":
1258
+ return /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) });
1259
+ case "failed":
1260
+ return /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717" });
1261
+ default:
1262
+ return /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u25CB" });
1263
+ }
1264
+ })();
1265
+ const duration = phase.status === "done" && phase.durationMs !== void 0 ? `Done (${Math.round(phase.durationMs / 1e3)}s)` : "";
1266
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
1267
+ /* @__PURE__ */ jsxs2(Text2, { children: [
1268
+ " ",
1269
+ icon,
1270
+ " ",
1271
+ phase.label,
1272
+ duration ? /* @__PURE__ */ jsxs2(Text2, { color: "gray", children: [
1273
+ " ",
1274
+ duration
1275
+ ] }) : null
1276
+ ] }),
1277
+ phase.status === "running" && phase.hasProgress && phase.percent > 0 && /* @__PURE__ */ jsxs2(Text2, { children: [
1278
+ " ",
1279
+ /* @__PURE__ */ jsx2(ProgressBar, { percent: phase.percent })
1280
+ ] })
1281
+ ] });
1282
+ };
1283
+
1284
+ // src/core/run-in-worker.ts
1285
+ import { dirname, join as join5 } from "path";
1286
+ import { fileURLToPath } from "url";
1287
+ import { Worker } from "worker_threads";
1288
+ async function runPipelineInWorker(options, onProgress) {
1289
+ const currentFile = fileURLToPath(import.meta.url);
1290
+ if (!currentFile.endsWith(".mjs")) {
1291
+ const { runPipeline: runPipeline2 } = await Promise.resolve().then(() => (init_orchestrator(), orchestrator_exports));
1292
+ return runPipeline2({ ...options, onProgress });
1293
+ }
1294
+ const workerPath = join5(dirname(currentFile), "pipeline-worker.mjs");
1295
+ return new Promise((resolve2, reject) => {
1296
+ const worker = new Worker(workerPath, { workerData: options });
1297
+ worker.on(
1298
+ "message",
1299
+ (msg) => {
1300
+ if (msg.type === "progress" && msg.phase && msg.percent !== void 0) {
1301
+ onProgress(msg.phase, msg.percent);
1302
+ } else if (msg.type === "result") {
1303
+ resolve2(msg.result);
1304
+ worker.terminate();
1305
+ } else if (msg.type === "error") {
1306
+ reject(new Error(msg.message));
1307
+ worker.terminate();
1308
+ }
1309
+ }
1310
+ );
1311
+ worker.on("error", reject);
1312
+ worker.on("exit", (code) => {
1313
+ if (code !== 0 && code !== 1) {
1314
+ reject(new Error(`Worker exited with code ${code}`));
1315
+ }
1316
+ });
1317
+ });
1318
+ }
1319
+
1320
+ // src/commands/Sieve.tsx
1321
+ init_workspace();
1322
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1323
+ var PHASE_DEFS = [
1324
+ { key: "INIT", label: "Initializing workspace", hasProgress: false },
1325
+ { key: "EXTRACTING", label: "Extracting frames", hasProgress: false },
1326
+ { key: "ANALYZING", label: "Analyzing frame similarity", hasProgress: true },
1327
+ { key: "PRUNING", label: "Pruning similar frames", hasProgress: false },
1328
+ { key: "FINALIZING", label: "Finalizing output", hasProgress: false }
1329
+ ];
1330
+ function createInitialPhases() {
1331
+ return PHASE_DEFS.map((def) => ({
1332
+ label: def.label,
1333
+ status: "pending",
1334
+ hasProgress: def.hasProgress,
1335
+ percent: 0
1336
+ }));
1337
+ }
1338
+ function phaseKeyToIndex(phase) {
1339
+ return PHASE_DEFS.findIndex((d) => d.key === phase);
1340
+ }
1341
+ var SieveView = (props) => {
1342
+ const { exit } = useApp();
1343
+ const [phases, setPhases] = useState(createInitialPhases);
1344
+ const [result, setResult] = useState(null);
1345
+ const [error, setError] = useState(null);
1346
+ useEffect(() => {
1347
+ const phaseStartTimes = PHASE_DEFS.map(() => 0);
1348
+ let currentPhaseKey = "";
1349
+ (async () => {
1350
+ try {
1351
+ await cleanupStaleWorkspaces().catch(() => {
1352
+ });
1353
+ phaseStartTimes[0] = Date.now();
1354
+ setPhases((prev) => {
1355
+ const next = [...prev];
1356
+ next[0] = { ...next[0], status: "running" };
1357
+ return next;
1358
+ });
1359
+ const res = await runPipelineInWorker(
1360
+ {
1361
+ mode: "file",
1362
+ inputPath: props.input,
1363
+ ...props.threshold !== void 0 ? { threshold: props.threshold } : {},
1364
+ ...props.count !== void 0 ? { count: props.count } : {},
1365
+ outputPath: props.output,
1366
+ fps: props.fps,
1367
+ maxFrames: props.maxFrames,
1368
+ scale: props.scale,
1369
+ quality: props.quality,
1370
+ iouThreshold: props.iouThreshold,
1371
+ animationThreshold: props.animationThreshold,
1372
+ debug: props.debug
1373
+ },
1374
+ (phase, percent) => {
1375
+ const phaseIdx = phaseKeyToIndex(phase);
1376
+ if (phaseIdx < 0) return;
1377
+ if (phase !== currentPhaseKey) {
1378
+ const now2 = Date.now();
1379
+ currentPhaseKey = phase;
1380
+ phaseStartTimes[phaseIdx] = now2;
1381
+ setPhases((prev) => {
1382
+ const next = [...prev];
1383
+ for (let i = 0; i < next.length; i++) {
1384
+ if (i < phaseIdx) {
1385
+ if (next[i].status !== "done") {
1386
+ next[i] = {
1387
+ ...next[i],
1388
+ status: "done",
1389
+ percent: 100,
1390
+ durationMs: phaseStartTimes[i] ? now2 - phaseStartTimes[i] : 0
1391
+ };
1392
+ }
1393
+ } else if (i === phaseIdx) {
1394
+ next[i] = { ...next[i], status: "running", percent: 0 };
1395
+ }
1396
+ }
1397
+ return next;
1398
+ });
1399
+ }
1400
+ setPhases((prev) => {
1401
+ const next = [...prev];
1402
+ if (next[phaseIdx].status === "running") {
1403
+ next[phaseIdx] = {
1404
+ ...next[phaseIdx],
1405
+ percent: Math.round(percent)
1406
+ };
1407
+ }
1408
+ return next;
1409
+ });
1410
+ }
1411
+ );
1412
+ const now = Date.now();
1413
+ setPhases(
1414
+ (prev) => prev.map((p, i) => {
1415
+ if (p.status !== "done") {
1416
+ return {
1417
+ ...p,
1418
+ status: "done",
1419
+ percent: 100,
1420
+ durationMs: phaseStartTimes[i] ? now - phaseStartTimes[i] : 0
1421
+ };
1422
+ }
1423
+ return p;
1424
+ })
1425
+ );
1426
+ setResult(res);
1427
+ setTimeout(() => exit(), 100);
1428
+ } catch (err) {
1429
+ const now = Date.now();
1430
+ setPhases((prev) => {
1431
+ const next = [...prev];
1432
+ for (let i = 0; i < next.length; i++) {
1433
+ if (next[i].status === "running") {
1434
+ next[i] = {
1435
+ ...next[i],
1436
+ status: "failed",
1437
+ durationMs: phaseStartTimes[i] ? now - phaseStartTimes[i] : 0
1438
+ };
1439
+ }
1440
+ }
1441
+ return next;
1442
+ });
1443
+ setError(err instanceof Error ? err.message : String(err));
1444
+ setTimeout(() => exit(), 100);
1445
+ }
1446
+ })();
1447
+ }, []);
1448
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
1449
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
1450
+ "\u25B8 scene-sieve",
1451
+ " \u2014 ",
1452
+ props.input.split("/").pop()
1453
+ ] }),
1454
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
1455
+ phases.map((phase, i) => /* @__PURE__ */ jsx3(PhaseStep, { phase }, i)),
1456
+ error && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
1457
+ "\u2717 Failed \u2014 ",
1458
+ error
1459
+ ] }) }),
1460
+ result && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
1461
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: " \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1462
+ /* @__PURE__ */ jsxs3(Text3, { color: "green", bold: true, children: [
1463
+ "\u2713 Done",
1464
+ " \u2014 ",
1465
+ result.originalFramesCount,
1466
+ " frames \u2192",
1467
+ " ",
1468
+ result.prunedFramesCount,
1469
+ " scenes (",
1470
+ (result.executionTimeMs / 1e3).toFixed(1),
1471
+ "s)"
1472
+ ] }),
1473
+ result.animations && result.animations.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: "blue", children: [
1474
+ "\u2139 Found",
1475
+ " ",
1476
+ result.animations.length,
1477
+ " animations (recorded in .metadata.json)"
1478
+ ] }),
1479
+ props.debug && result.outputFiles.length > 0 && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
1480
+ /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
1481
+ "Output: ",
1482
+ result.outputFiles[0]?.replace(/\/[^/]+$/, "/")
1483
+ ] }),
1484
+ result.outputFiles.map((f, i) => /* @__PURE__ */ jsxs3(Text3, { color: "gray", children: [
1485
+ " - ",
1486
+ f
1487
+ ] }, i))
1488
+ ] })
1489
+ ] })
1490
+ ] });
1491
+ };
1492
+
1493
+ // src/cli.ts
1494
+ init_constants();
1048
1495
  var require3 = createRequire2(import.meta.url);
1049
1496
  var { version } = require3("../package.json");
1050
1497
  var program = new Command();
1051
1498
  program.name("scene-sieve").description("Extract key frames from video and GIF files").version(version).argument("<input>", "Input video or GIF file path").option("-n, --count <number>", "Max number of frames to keep (default: 20)").option(
1052
1499
  "-t, --threshold <number>",
1053
1500
  "Normalized threshold 0~1 (default: 0.5; keeps frames above ratio of max change)"
1054
- ).option("-o, --output <path>", "Output directory path").option("--fps <number>", "Fallback FPS for frame extraction", "5").option("-s, --scale <number>", "Scale size for vision analysis", "720").option("-q, --quality <number>", "JPEG output quality 1-100", "80").option("--debug", "Enable debug mode (preserve temp workspace)").action(async (input, opts) => {
1055
- const { default: ora } = await import("ora");
1056
- const { default: cliProgress } = await import("cli-progress");
1057
- const spinner = ora("Initializing...").start();
1058
- try {
1059
- spinner.stop();
1060
- const bar = new cliProgress.SingleBar({
1061
- format: "{phase} |{bar}| {percentage}%",
1062
- barCompleteChar: "\u2588",
1063
- barIncompleteChar: "\u2591",
1064
- hideCursor: true
1065
- });
1066
- bar.start(100, 0, { phase: "EXTRACTING" });
1067
- const result = await runPipeline({
1068
- mode: "file",
1069
- inputPath: input,
1501
+ ).option("-o, --output <path>", "Output directory path").option("--fps <number>", "Max FPS for frame extraction", String(DEFAULT_FPS)).option(
1502
+ "-mf, --max-frames <number>",
1503
+ "Max frames to extract (auto-reduces FPS for long videos)",
1504
+ String(DEFAULT_MAX_FRAMES)
1505
+ ).option(
1506
+ "-s, --scale <number>",
1507
+ "Scale size for vision analysis",
1508
+ String(DEFAULT_SCALE)
1509
+ ).option(
1510
+ "-q, --quality <number>",
1511
+ "JPEG output quality 1-100",
1512
+ String(DEFAULT_QUALITY)
1513
+ ).option(
1514
+ "-it, --iou-threshold <number>",
1515
+ `IoU threshold for animation tracking (0-1) (default: ${IOU_THRESHOLD})`
1516
+ ).option(
1517
+ "-at, --anim-threshold <number>",
1518
+ `Min consecutive frames for animation (default: ${ANIMATION_FRAME_THRESHOLD})`
1519
+ ).option("--debug", "Enable debug mode (preserve temp workspace)").action(async (input, opts) => {
1520
+ const { waitUntilExit } = render(
1521
+ React2.createElement(SieveView, {
1522
+ input,
1070
1523
  ...opts.threshold !== void 0 ? { threshold: parseFloat(opts.threshold) } : {},
1071
1524
  ...opts.count !== void 0 ? { count: parseInt(opts.count, 10) } : {},
1072
- outputPath: opts.output,
1525
+ output: opts.output,
1073
1526
  fps: parseInt(opts.fps, 10),
1527
+ maxFrames: parseInt(opts.maxFrames, 10),
1074
1528
  scale: parseInt(opts.scale, 10),
1075
1529
  quality: parseInt(opts.quality, 10),
1076
- debug: opts.debug ?? false,
1077
- onProgress: (phase, percent) => {
1078
- bar.update(Math.round(percent), { phase });
1079
- }
1080
- });
1081
- bar.stop();
1082
- console.log(
1083
- `
1084
- Done! ${result.originalFramesCount} frames -> ${result.prunedFramesCount} scenes (${result.executionTimeMs}ms)`
1085
- );
1086
- if (opts.debug) {
1087
- console.log(
1088
- `Output: ${result.outputFiles[0]?.replace(/\/[^/]+$/, "/")}`
1089
- );
1090
- result.outputFiles.forEach((f) => console.log(` - ${f}`));
1091
- }
1092
- } catch (error) {
1093
- spinner.fail(
1094
- `Failed: ${error instanceof Error ? error.message : String(error)}`
1095
- );
1096
- process.exit(1);
1097
- }
1530
+ iouThreshold: opts.iouThreshold !== void 0 ? parseFloat(opts.iouThreshold) : void 0,
1531
+ animationThreshold: opts.animThreshold !== void 0 ? parseInt(opts.animThreshold, 10) : void 0,
1532
+ debug: opts.debug ?? false
1533
+ })
1534
+ );
1535
+ await waitUntilExit();
1098
1536
  });
1099
1537
  program.parseAsync(process.argv).catch((error) => {
1100
1538
  console.error("Fatal error:", error.message);