visual-ai-assertions 0.9.0 → 0.10.0

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/index.js CHANGED
@@ -1241,10 +1241,65 @@ async function normalizeImage(input) {
1241
1241
  };
1242
1242
  }
1243
1243
 
1244
+ // src/core/debug-frames.ts
1245
+ import { randomBytes } from "crypto";
1246
+ import { mkdir, writeFile } from "fs/promises";
1247
+ import { join, resolve } from "path";
1248
+ var DEBUG_FRAMES_ENV = "VISUAL_AI_DEBUG_FRAMES";
1249
+ var DEBUG_FRAMES_DIR_ENV = "VISUAL_AI_DEBUG_FRAMES_DIR";
1250
+ var DEFAULT_DIR_NAME = "visual-ai-debug-frames";
1251
+ function isEnabled(env) {
1252
+ const raw = env[DEBUG_FRAMES_ENV];
1253
+ if (raw === void 0 || raw === "") return false;
1254
+ const lower = raw.toLowerCase();
1255
+ return lower === "true" || lower === "1";
1256
+ }
1257
+ function timestampSlug(date) {
1258
+ return date.toISOString().replace(/[:.]/g, "-");
1259
+ }
1260
+ function paddedIndex(value, total) {
1261
+ const width = Math.max(2, String(total - 1).length);
1262
+ return String(value).padStart(width, "0");
1263
+ }
1264
+ function extensionFromMimeType(mimeType) {
1265
+ if (mimeType === "image/png") return ".png";
1266
+ if (mimeType === "image/webp") return ".webp";
1267
+ return ".jpg";
1268
+ }
1269
+ async function saveDebugFrames(frames, env = process.env) {
1270
+ if (!isEnabled(env)) return void 0;
1271
+ if (frames.length === 0) return void 0;
1272
+ const baseDir = env[DEBUG_FRAMES_DIR_ENV]?.trim() || DEFAULT_DIR_NAME;
1273
+ const runDir = resolve(baseDir, `${timestampSlug(/* @__PURE__ */ new Date())}-${randomBytes(3).toString("hex")}`);
1274
+ try {
1275
+ await mkdir(runDir, { recursive: true });
1276
+ await Promise.all(
1277
+ frames.map((frame) => {
1278
+ const idx = paddedIndex(frame.index, frames.length);
1279
+ const ts = frame.timestampSeconds.toFixed(2);
1280
+ const ext = extensionFromMimeType(frame.mimeType);
1281
+ const filename = `frame-${idx}-t${ts}s${ext}`;
1282
+ return writeFile(join(runDir, filename), frame.data);
1283
+ })
1284
+ );
1285
+ } catch (err) {
1286
+ process.stderr.write(
1287
+ `[visual-ai-assertions] warning: failed to save debug frames to ${runDir}: ${err instanceof Error ? err.message : String(err)}
1288
+ `
1289
+ );
1290
+ return void 0;
1291
+ }
1292
+ process.stderr.write(
1293
+ `[visual-ai-assertions] Saved ${frames.length} debug frame(s) to ${runDir}
1294
+ `
1295
+ );
1296
+ return runDir;
1297
+ }
1298
+
1244
1299
  // src/core/video.ts
1245
- import { mkdtemp, readFile as readFile2, readdir, rm, writeFile } from "fs/promises";
1300
+ import { mkdtemp, readFile as readFile2, readdir, rm, writeFile as writeFile2 } from "fs/promises";
1246
1301
  import { tmpdir } from "os";
1247
- import { extname as extname2, join } from "path";
1302
+ import { extname as extname2, join as join2 } from "path";
1248
1303
  var FRAME_MAX_DIMENSION = 1568;
1249
1304
  var DEFAULT_FPS = 1;
1250
1305
  var DEFAULT_MAX_FRAMES = 10;
@@ -1343,11 +1398,11 @@ async function resolveVideoToPath(input) {
1343
1398
  return writeBufferToTemp(buf, mimeType);
1344
1399
  }
1345
1400
  async function writeBufferToTemp(data, mimeType) {
1346
- const dir = await mkdtemp(join(tmpdir(), "visual-ai-video-"));
1401
+ const dir = await mkdtemp(join2(tmpdir(), "visual-ai-video-"));
1347
1402
  try {
1348
1403
  const ext = extensionFor(mimeType);
1349
- const path = join(dir, `input${ext}`);
1350
- await writeFile(path, data);
1404
+ const path = join2(dir, `input${ext}`);
1405
+ await writeFile2(path, data);
1351
1406
  return {
1352
1407
  path,
1353
1408
  mimeType,
@@ -1389,7 +1444,7 @@ async function loadFfmpegFactory() {
1389
1444
  const code = err?.code;
1390
1445
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
1391
1446
  throw new VisualAIVideoError(
1392
- "Video support requires fluent-ffmpeg. Install it with: pnpm add -D fluent-ffmpeg @ffmpeg-installer/ffmpeg @ffprobe-installer/ffprobe @types/fluent-ffmpeg"
1447
+ "Could not load fluent-ffmpeg. It ships as a dependency of visual-ai-assertions, so this usually means the install was pruned or the platform-specific binary is unavailable. Reinstall the package or run: pnpm add fluent-ffmpeg @ffmpeg-installer/ffmpeg @ffprobe-installer/ffprobe"
1393
1448
  );
1394
1449
  }
1395
1450
  throw new VisualAIVideoError(
@@ -1434,7 +1489,7 @@ async function loadFfmpegFactory() {
1434
1489
  }
1435
1490
  async function probeDurationSeconds(videoPath) {
1436
1491
  const ffmpeg = await loadFfmpegFactory();
1437
- return new Promise((resolve, reject) => {
1492
+ return new Promise((resolve2, reject) => {
1438
1493
  let settled = false;
1439
1494
  const finish = (fn) => {
1440
1495
  if (settled) return;
@@ -1471,7 +1526,7 @@ async function probeDurationSeconds(videoPath) {
1471
1526
  return;
1472
1527
  }
1473
1528
  finish(() => {
1474
- resolve(duration);
1529
+ resolve2(duration);
1475
1530
  });
1476
1531
  });
1477
1532
  });
@@ -1503,10 +1558,10 @@ async function extractFrames(videoPath, options = {}) {
1503
1558
  `Video duration ${durationSeconds.toFixed(2)}s exceeds limit of ${maxDurationSeconds}s. Pass { maxDurationSeconds: N } to override, or trim the source video.`
1504
1559
  );
1505
1560
  }
1506
- const outputDir = await mkdtemp(join(tmpdir(), "visual-ai-frames-"));
1561
+ const outputDir = await mkdtemp(join2(tmpdir(), "visual-ai-frames-"));
1507
1562
  try {
1508
1563
  const filter = `fps=${fps},scale='if(gt(iw,ih),min(${FRAME_MAX_DIMENSION},iw),-2)':'if(gt(iw,ih),-2,min(${FRAME_MAX_DIMENSION},ih))':flags=area`;
1509
- await new Promise((resolve, reject) => {
1564
+ await new Promise((resolve2, reject) => {
1510
1565
  let settled = false;
1511
1566
  const cmd = ffmpeg(videoPath);
1512
1567
  const finish = (fn) => {
@@ -1528,9 +1583,9 @@ async function extractFrames(videoPath, options = {}) {
1528
1583
  );
1529
1584
  });
1530
1585
  }, FFMPEG_RUN_TIMEOUT_MS);
1531
- cmd.outputOptions(["-vf", filter, "-vframes", String(maxFrames), "-q:v", "3"]).output(join(outputDir, "frame-%04d.jpg")).on("end", () => {
1586
+ cmd.outputOptions(["-vf", filter, "-vframes", String(maxFrames), "-q:v", "3"]).output(join2(outputDir, "frame-%04d.jpg")).on("end", () => {
1532
1587
  finish(() => {
1533
- resolve();
1588
+ resolve2();
1534
1589
  });
1535
1590
  }).on("error", (err) => {
1536
1591
  finish(() => {
@@ -1546,7 +1601,7 @@ async function extractFrames(videoPath, options = {}) {
1546
1601
  }
1547
1602
  const frames = await Promise.all(
1548
1603
  files.map(async (name, index) => {
1549
- const data = await readFile2(join(outputDir, name));
1604
+ const data = await readFile2(join2(outputDir, name));
1550
1605
  const timestampSeconds = Math.min(durationSeconds, (index + 0.5) / fps);
1551
1606
  let cachedBase64;
1552
1607
  return {
@@ -1602,6 +1657,7 @@ async function normalizeMedia(input, videoOptions) {
1602
1657
  const { path, cleanup } = await resolveVideoToPath(input);
1603
1658
  try {
1604
1659
  const { frames, durationSeconds } = await extractFrames(path, videoOptions);
1660
+ await saveDebugFrames(frames);
1605
1661
  return { kind: "video", frames, durationSeconds };
1606
1662
  } finally {
1607
1663
  try {