@ttt-productions/media-processing-core 0.8.15 → 0.8.17

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.
Files changed (132) hide show
  1. package/dist/audio/audio-processor.d.ts +2 -2
  2. package/dist/audio/audio-processor.d.ts.map +1 -1
  3. package/dist/audio/audio-processor.js +62 -48
  4. package/dist/audio/audio-processor.js.map +1 -1
  5. package/dist/audio/probe.d.ts +1 -1
  6. package/dist/audio/probe.d.ts.map +1 -1
  7. package/dist/audio/probe.js +6 -3
  8. package/dist/audio/probe.js.map +1 -1
  9. package/dist/duration/media-duration.d.ts +2 -2
  10. package/dist/duration/media-duration.d.ts.map +1 -1
  11. package/dist/duration/media-duration.js +23 -21
  12. package/dist/duration/media-duration.js.map +1 -1
  13. package/dist/image/formats.d.ts +2 -2
  14. package/dist/image/formats.js +25 -25
  15. package/dist/image/formats.js.map +1 -1
  16. package/dist/image/image-processor.d.ts +2 -2
  17. package/dist/image/image-processor.d.ts.map +1 -1
  18. package/dist/image/image-processor.js +47 -37
  19. package/dist/image/image-processor.js.map +1 -1
  20. package/dist/image/resize.d.ts +2 -2
  21. package/dist/image/resize.d.ts.map +1 -1
  22. package/dist/image/resize.js +13 -13
  23. package/dist/image/resize.js.map +1 -1
  24. package/dist/index.d.ts +19 -16
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +18 -16
  27. package/dist/index.js.map +1 -1
  28. package/dist/inspection/inspect-media.d.ts +13 -13
  29. package/dist/inspection/inspect-media.d.ts.map +1 -1
  30. package/dist/inspection/inspect-media.js +196 -140
  31. package/dist/inspection/inspect-media.js.map +1 -1
  32. package/dist/io/fs.d.ts +1 -1
  33. package/dist/io/fs.js +4 -4
  34. package/dist/io/fs.js.map +1 -1
  35. package/dist/io/local-input.d.ts +1 -1
  36. package/dist/io/local-input.d.ts.map +1 -1
  37. package/dist/io/local-input.js +15 -15
  38. package/dist/io/local-input.js.map +1 -1
  39. package/dist/io/types.d.ts.map +1 -1
  40. package/dist/io/types.js.map +1 -1
  41. package/dist/moderation/merge.d.ts +1 -1
  42. package/dist/moderation/merge.d.ts.map +1 -1
  43. package/dist/moderation/merge.js +1 -1
  44. package/dist/moderation/merge.js.map +1 -1
  45. package/dist/moderation/types.d.ts +3 -3
  46. package/dist/moderation/types.js.map +1 -1
  47. package/dist/process-media.d.ts +2 -3
  48. package/dist/process-media.d.ts.map +1 -1
  49. package/dist/process-media.js +9 -9
  50. package/dist/process-media.js.map +1 -1
  51. package/dist/processing/errors.d.ts.map +1 -1
  52. package/dist/processing/errors.js +1 -1
  53. package/dist/processing/errors.js.map +1 -1
  54. package/dist/processing/result.d.ts +2 -2
  55. package/dist/processing/result.js +1 -1
  56. package/dist/processing/result.js.map +1 -1
  57. package/dist/run-pipeline.d.ts +5 -5
  58. package/dist/run-pipeline.d.ts.map +1 -1
  59. package/dist/run-pipeline.js +70 -54
  60. package/dist/run-pipeline.js.map +1 -1
  61. package/dist/server/download-token.d.ts +14 -0
  62. package/dist/server/download-token.d.ts.map +1 -0
  63. package/dist/server/download-token.js +19 -0
  64. package/dist/server/download-token.js.map +1 -0
  65. package/dist/server/firebase-media-io.d.ts +2 -2
  66. package/dist/server/firebase-media-io.js +1 -1
  67. package/dist/server/firebase-media-io.js.map +1 -1
  68. package/dist/server/gcs-errors.js +2 -2
  69. package/dist/server/gcs-errors.js.map +1 -1
  70. package/dist/server/index.d.ts +7 -5
  71. package/dist/server/index.d.ts.map +1 -1
  72. package/dist/server/index.js +7 -5
  73. package/dist/server/index.js.map +1 -1
  74. package/dist/server/pending-media.d.ts +2 -2
  75. package/dist/server/pending-media.d.ts.map +1 -1
  76. package/dist/server/pending-media.js +12 -6
  77. package/dist/server/pending-media.js.map +1 -1
  78. package/dist/server/r2-retry.d.ts +4 -4
  79. package/dist/server/r2-retry.d.ts.map +1 -1
  80. package/dist/server/r2-retry.js +48 -47
  81. package/dist/server/r2-retry.js.map +1 -1
  82. package/dist/server/staged-object.d.ts +2 -2
  83. package/dist/server/staged-object.d.ts.map +1 -1
  84. package/dist/server/staged-object.js +4 -4
  85. package/dist/server/staged-object.js.map +1 -1
  86. package/dist/server/storage-ops.d.ts +3 -5
  87. package/dist/server/storage-ops.d.ts.map +1 -1
  88. package/dist/server/storage-ops.js +101 -97
  89. package/dist/server/storage-ops.js.map +1 -1
  90. package/dist/types.d.ts +2 -2
  91. package/dist/types.js.map +1 -1
  92. package/dist/utils/log.d.ts.map +1 -1
  93. package/dist/utils/log.js +3 -3
  94. package/dist/utils/log.js.map +1 -1
  95. package/dist/utils/paths.d.ts.map +1 -1
  96. package/dist/utils/paths.js +1 -4
  97. package/dist/utils/paths.js.map +1 -1
  98. package/dist/utils/safe-path.js +8 -8
  99. package/dist/utils/safe-path.js.map +1 -1
  100. package/dist/validation/validate-duration.d.ts.map +1 -1
  101. package/dist/validation/validate-duration.js.map +1 -1
  102. package/dist/validation/validate-mime.d.ts.map +1 -1
  103. package/dist/validation/validate-mime.js.map +1 -1
  104. package/dist/validation/validate-size.d.ts.map +1 -1
  105. package/dist/validation/validate-size.js.map +1 -1
  106. package/dist/video/ffmpeg.d.ts +1 -1
  107. package/dist/video/ffmpeg.d.ts.map +1 -1
  108. package/dist/video/ffmpeg.js +51 -43
  109. package/dist/video/ffmpeg.js.map +1 -1
  110. package/dist/video/probe.d.ts +1 -1
  111. package/dist/video/probe.d.ts.map +1 -1
  112. package/dist/video/probe.js +10 -7
  113. package/dist/video/probe.js.map +1 -1
  114. package/dist/video/video-processor.d.ts +2 -2
  115. package/dist/video/video-processor.d.ts.map +1 -1
  116. package/dist/video/video-processor.js +123 -81
  117. package/dist/video/video-processor.js.map +1 -1
  118. package/dist/workspace/temp.js +4 -4
  119. package/dist/workspace/temp.js.map +1 -1
  120. package/package.json +2 -2
  121. package/dist/audio/index.d.ts +0 -3
  122. package/dist/audio/index.d.ts.map +0 -1
  123. package/dist/audio/index.js +0 -3
  124. package/dist/audio/index.js.map +0 -1
  125. package/dist/image/index.d.ts +0 -4
  126. package/dist/image/index.d.ts.map +0 -1
  127. package/dist/image/index.js +0 -4
  128. package/dist/image/index.js.map +0 -1
  129. package/dist/video/index.d.ts +0 -4
  130. package/dist/video/index.d.ts.map +0 -1
  131. package/dist/video/index.js +0 -4
  132. package/dist/video/index.js.map +0 -1
@@ -25,13 +25,13 @@
25
25
  // probes as a one-frame AV1 `video` stream);
26
26
  // - FAIL-CLOSED: anything unproven is `indeterminate` with the
27
27
  // `strict-video-fallback` safety plan — never audio, never publishable.
28
- import { open } from "node:fs/promises";
29
- import { z } from "zod";
30
- import { MediaInspectionResultSchema, } from "@ttt-productions/media-schemas";
31
- import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from "../video/ffmpeg.js";
32
- import { measureDurationSec } from "../duration/media-duration.js";
28
+ import { open } from 'node:fs/promises';
29
+ import { z } from 'zod';
30
+ import { MediaInspectionResultSchema, } from '@ttt-productions/media-schemas';
31
+ import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';
32
+ import { measureDurationSec } from '../duration/media-duration.js';
33
33
  /** Bump on any classification-rule change (rides every decision event). */
34
- export const INSPECTOR_VERSION = "mpc-inspect-2";
34
+ export const INSPECTOR_VERSION = 'mpc-inspect-2';
35
35
  const SIGNATURE_READ_BYTES = 64 * 1024;
36
36
  const PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback
37
37
  const ANALYZE_DURATION_US = 10 * 1000 * 1000;
@@ -48,42 +48,56 @@ const ProbeStreamSchema = z
48
48
  .loose();
49
49
  const ProbeJsonSchema = z
50
50
  .object({
51
- format: z.object({ format_name: z.string().max(200).optional() }).loose().optional(),
51
+ format: z
52
+ .object({ format_name: z.string().max(200).optional() })
53
+ .loose()
54
+ .optional(),
52
55
  streams: z.array(ProbeStreamSchema).max(256).optional(),
53
56
  })
54
57
  .loose();
55
58
  // ---- codec normalization (bounded — raw strings never escape) ----
56
59
  const CODEC_MAP = {
57
- h264: "h264",
58
- hevc: "hevc",
59
- vp8: "vp8",
60
- vp9: "vp9",
61
- av1: "av1",
62
- theora: "theora",
63
- mpeg4: "mpeg4",
64
- mjpeg: "mjpeg",
65
- aac: "aac",
66
- alac: "alac",
67
- opus: "opus",
68
- vorbis: "vorbis",
69
- mp3: "mp3",
70
- flac: "flac",
60
+ h264: 'h264',
61
+ hevc: 'hevc',
62
+ vp8: 'vp8',
63
+ vp9: 'vp9',
64
+ av1: 'av1',
65
+ theora: 'theora',
66
+ mpeg4: 'mpeg4',
67
+ mjpeg: 'mjpeg',
68
+ aac: 'aac',
69
+ alac: 'alac',
70
+ opus: 'opus',
71
+ vorbis: 'vorbis',
72
+ mp3: 'mp3',
73
+ flac: 'flac',
71
74
  };
72
75
  function normalizeCodec(name) {
73
76
  if (!name)
74
- return "other";
77
+ return 'other';
75
78
  const n = name.toLowerCase();
76
79
  if (CODEC_MAP[n])
77
80
  return CODEC_MAP[n];
78
- if (n.startsWith("pcm_"))
79
- return "pcm";
80
- if (n.startsWith("amr"))
81
- return "amr";
82
- return "other";
81
+ if (n.startsWith('pcm_'))
82
+ return 'pcm';
83
+ if (n.startsWith('amr'))
84
+ return 'amr';
85
+ return 'other';
83
86
  }
84
87
  /** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via
85
88
  * decoder proof BEFORE the timed-video rule — see the AVIF note above. */
86
- const ISOBMFF_IMAGE_BRANDS = new Set(["avif", "avis", "heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1"]);
89
+ const ISOBMFF_IMAGE_BRANDS = new Set([
90
+ 'avif',
91
+ 'avis',
92
+ 'heic',
93
+ 'heix',
94
+ 'heim',
95
+ 'heis',
96
+ 'hevc',
97
+ 'hevx',
98
+ 'mif1',
99
+ 'msf1',
100
+ ]);
87
101
  function ascii(b, off, s) {
88
102
  if (off + s.length > b.length)
89
103
  return false;
@@ -94,58 +108,62 @@ function ascii(b, off, s) {
94
108
  }
95
109
  export function detectSignatureFamily(b) {
96
110
  if (b.length < 12)
97
- return { family: "unknown" };
111
+ return { family: 'unknown' };
98
112
  // images (single-format signatures)
99
113
  if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
100
- return { family: "image", formatId: "jpeg", container: "jpeg" };
101
- if (b[0] === 0x89 && ascii(b, 1, "PNG"))
102
- return { family: "image", formatId: "png", container: "png" };
103
- if (ascii(b, 0, "GIF8"))
104
- return { family: "image", formatId: "gif", container: "gif" };
105
- if (ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP"))
106
- return { family: "image", formatId: "webp", container: "webp" };
114
+ return { family: 'image', formatId: 'jpeg', container: 'jpeg' };
115
+ if (b[0] === 0x89 && ascii(b, 1, 'PNG'))
116
+ return { family: 'image', formatId: 'png', container: 'png' };
117
+ if (ascii(b, 0, 'GIF8'))
118
+ return { family: 'image', formatId: 'gif', container: 'gif' };
119
+ if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WEBP'))
120
+ return { family: 'image', formatId: 'webp', container: 'webp' };
107
121
  if (b[0] === 0x42 && b[1] === 0x4d)
108
- return { family: "image", formatId: "bmp", container: "bmp" };
109
- if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) || (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {
110
- return { family: "image", formatId: "tiff", container: "tiff" };
122
+ return { family: 'image', formatId: 'bmp', container: 'bmp' };
123
+ if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) ||
124
+ (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {
125
+ return { family: 'image', formatId: 'tiff', container: 'tiff' };
111
126
  }
112
127
  // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).
113
128
  {
114
129
  const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))
115
- .toString("latin1")
130
+ .toString('latin1')
116
131
  .toLowerCase();
117
- if (prefix.includes("<svg") && (prefix.trimStart().startsWith("<?xml") || prefix.trimStart().startsWith("<svg") || prefix.includes("<!doctype svg"))) {
118
- return { family: "svg" };
132
+ if (prefix.includes('<svg') &&
133
+ (prefix.trimStart().startsWith('<?xml') ||
134
+ prefix.trimStart().startsWith('<svg') ||
135
+ prefix.includes('<!doctype svg'))) {
136
+ return { family: 'svg' };
119
137
  }
120
138
  }
121
139
  // containers
122
140
  if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
123
- return { family: "ebml" };
124
- if (ascii(b, 4, "ftyp")) {
141
+ return { family: 'ebml' };
142
+ if (ascii(b, 4, 'ftyp')) {
125
143
  const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();
126
- return { family: "isobmff", brand };
144
+ return { family: 'isobmff', brand };
127
145
  }
128
- if (ascii(b, 0, "OggS"))
129
- return { family: "ogg" };
130
- if (ascii(b, 0, "RIFF") && ascii(b, 8, "AVI "))
131
- return { family: "avi" };
132
- if (ascii(b, 0, "RIFF") && ascii(b, 8, "WAVE"))
133
- return { family: "wav" };
134
- if (ascii(b, 0, "ID3"))
135
- return { family: "mp3" };
146
+ if (ascii(b, 0, 'OggS'))
147
+ return { family: 'ogg' };
148
+ if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'AVI '))
149
+ return { family: 'avi' };
150
+ if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WAVE'))
151
+ return { family: 'wav' };
152
+ if (ascii(b, 0, 'ID3'))
153
+ return { family: 'mp3' };
136
154
  if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0)
137
- return { family: "mp3" };
138
- if (ascii(b, 0, "fLaC"))
139
- return { family: "flac" };
140
- return { family: "unknown" };
155
+ return { family: 'mp3' };
156
+ if (ascii(b, 0, 'fLaC'))
157
+ return { family: 'flac' };
158
+ return { family: 'unknown' };
141
159
  }
142
160
  let cachedFfprobeVersion;
143
161
  async function getFfprobeVersion() {
144
162
  if (cachedFfprobeVersion !== undefined)
145
163
  return cachedFfprobeVersion ?? undefined;
146
164
  try {
147
- const r = await runCmd("ffprobe", ["-version"], { timeoutMs: 5_000 });
148
- const first = (r.stdout || r.stderr).split(/\r?\n/)[0]?.trim() ?? "";
165
+ const r = await runCmd('ffprobe', ['-version'], { timeoutMs: 5_000 });
166
+ const first = (r.stdout || r.stderr).split(/\r?\n/)[0]?.trim() ?? '';
149
167
  cachedFfprobeVersion = first ? first.slice(0, 120) : null;
150
168
  }
151
169
  catch {
@@ -156,40 +174,42 @@ async function getFfprobeVersion() {
156
174
  async function defaultProbe(localPath, opts) {
157
175
  let r;
158
176
  try {
159
- r = await runCmd("ffprobe", [
160
- "-v",
161
- "error",
162
- "-print_format",
163
- "json",
164
- "-show_format",
165
- "-show_streams",
166
- "-probesize",
177
+ r = await runCmd('ffprobe', [
178
+ '-v',
179
+ 'error',
180
+ '-print_format',
181
+ 'json',
182
+ '-show_format',
183
+ '-show_streams',
184
+ '-probesize',
167
185
  String(PROBE_SIZE_BYTES),
168
- "-analyzeduration",
186
+ '-analyzeduration',
169
187
  String(ANALYZE_DURATION_US),
170
188
  localPath,
171
189
  ], { timeoutMs: opts.timeoutMs, signal: opts.signal });
172
190
  }
173
191
  catch {
174
- return { ok: false, reason: "failed" }; // spawn failure (ffprobe missing) — fail closed
192
+ return { ok: false, reason: 'failed' }; // spawn failure (ffprobe missing) — fail closed
175
193
  }
176
194
  if (r.timedOut)
177
- return { ok: false, reason: "timeout" };
195
+ return { ok: false, reason: 'timeout' };
178
196
  if (r.truncated)
179
- return { ok: false, reason: "truncated" };
197
+ return { ok: false, reason: 'truncated' };
180
198
  if (r.code !== 0)
181
- return { ok: false, reason: "failed" };
199
+ return { ok: false, reason: 'failed' };
182
200
  try {
183
- return { ok: true, json: JSON.parse(r.stdout || "{}") };
201
+ return { ok: true, json: JSON.parse(r.stdout || '{}') };
184
202
  }
185
203
  catch {
186
- return { ok: false, reason: "bad_json" };
204
+ return { ok: false, reason: 'bad_json' };
187
205
  }
188
206
  }
189
207
  async function defaultDecodeImage(localPath) {
190
208
  try {
191
- const sharp = (await import("sharp")).default;
192
- const meta = await sharp(localPath, { limitInputPixels: 268402689 /* sharp default ~16k x 16k */ }).metadata();
209
+ const sharp = (await import('sharp')).default;
210
+ const meta = await sharp(localPath, {
211
+ limitInputPixels: 268402689 /* sharp default ~16k x 16k */,
212
+ }).metadata();
193
213
  if (!meta.width || !meta.height)
194
214
  return null;
195
215
  return { frames: Math.max(1, meta.pages ?? 1) };
@@ -203,7 +223,13 @@ function baseResult(partial, ffprobeVersion) {
203
223
  const result = {
204
224
  inspectorVersion: INSPECTOR_VERSION,
205
225
  ...(ffprobeVersion ? { ffprobeVersion } : {}),
206
- streams: partial.streams ?? { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 },
226
+ streams: partial.streams ?? {
227
+ audio: 0,
228
+ timedVideo: 0,
229
+ attachedPictures: 0,
230
+ imageFrames: 0,
231
+ auxiliary: 0,
232
+ },
207
233
  codecs: partial.codecs ?? { audio: [], video: [] },
208
234
  status: partial.status,
209
235
  container: partial.container,
@@ -217,9 +243,9 @@ function baseResult(partial, ffprobeVersion) {
217
243
  }
218
244
  function indeterminate(container, reasonCode, ffprobeVersion, formatId) {
219
245
  return baseResult({
220
- status: "indeterminate",
246
+ status: 'indeterminate',
221
247
  container,
222
- safetyPlan: "strict-video-fallback",
248
+ safetyPlan: 'strict-video-fallback',
223
249
  reasonCode,
224
250
  ...(formatId ? { formatId } : {}),
225
251
  }, ffprobeVersion);
@@ -232,7 +258,7 @@ export async function inspectMedia(args) {
232
258
  // 1) bounded signature read (header only)
233
259
  let head;
234
260
  try {
235
- const fh = await open(args.localPath, "r");
261
+ const fh = await open(args.localPath, 'r');
236
262
  try {
237
263
  const buf = Buffer.alloc(SIGNATURE_READ_BYTES);
238
264
  const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);
@@ -243,89 +269,113 @@ export async function inspectMedia(args) {
243
269
  }
244
270
  }
245
271
  catch {
246
- return indeterminate("unknown", "read_failed", ffprobeVersion);
272
+ return indeterminate('unknown', 'read_failed', ffprobeVersion);
247
273
  }
248
274
  const sig = detectSignatureFamily(head);
249
275
  // 2) family routing
250
276
  switch (sig.family) {
251
- case "unknown":
252
- return indeterminate("unknown", "unrecognized_signature", ffprobeVersion);
253
- case "svg":
277
+ case 'unknown':
278
+ return indeterminate('unknown', 'unrecognized_signature', ffprobeVersion);
279
+ case 'svg':
254
280
  // Scriptable XML "image" — structurally recognized, deliberately NOT a
255
281
  // processable format: it can carry script and no sanitize/rasterize step
256
282
  // exists. Never classified by decoder capability alone.
257
283
  return baseResult({
258
- status: "unsupported",
259
- container: "svg",
260
- formatId: "svg",
261
- safetyPlan: "strict-video-fallback",
262
- reasonCode: "unsupported_format:svg",
284
+ status: 'unsupported',
285
+ container: 'svg',
286
+ formatId: 'svg',
287
+ safetyPlan: 'strict-video-fallback',
288
+ reasonCode: 'unsupported_format:svg',
263
289
  }, ffprobeVersion);
264
- case "image": {
290
+ case 'image': {
265
291
  // Single-format image signature: decoder proof required — a signature is
266
292
  // a candidate, not a classification.
267
293
  const decoded = await decodeImage(args.localPath);
268
294
  if (!decoded)
269
- return baseResult({ status: "malformed", container: sig.container, formatId: sig.formatId, safetyPlan: "strict-video-fallback", reasonCode: "image_decode_failed" }, ffprobeVersion);
295
+ return baseResult({
296
+ status: 'malformed',
297
+ container: sig.container,
298
+ formatId: sig.formatId,
299
+ safetyPlan: 'strict-video-fallback',
300
+ reasonCode: 'image_decode_failed',
301
+ }, ffprobeVersion);
270
302
  const frames = decoded.frames;
271
303
  return baseResult({
272
- status: "definitive",
273
- canonicalKind: "image",
304
+ status: 'definitive',
305
+ canonicalKind: 'image',
274
306
  container: sig.container,
275
307
  formatId: sig.formatId,
276
- streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },
277
- safetyPlan: frames > 1 ? "animated-image-frames" : "still-image",
278
- reasonCode: "ok",
308
+ streams: {
309
+ audio: 0,
310
+ timedVideo: 0,
311
+ attachedPictures: 0,
312
+ imageFrames: frames,
313
+ auxiliary: 0,
314
+ },
315
+ safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',
316
+ reasonCode: 'ok',
279
317
  }, ffprobeVersion);
280
318
  }
281
- case "isobmff":
319
+ case 'isobmff':
282
320
  if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {
283
321
  // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid
284
322
  // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +
285
323
  // successful bounded decode + no ordinary timed A/V presentation.
286
324
  const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });
287
325
  const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;
288
- const streams = parsed?.success ? parsed.data.streams ?? [] : null;
289
- const hasAudio = streams ? streams.some((s) => s.codec_type === "audio") : true; // unknown ⇒ assume worst
290
- const formatId = sig.brand.startsWith("avi") ? "avif" : "heic";
326
+ const streams = parsed?.success ? (parsed.data.streams ?? []) : null;
327
+ const hasAudio = streams ? streams.some((s) => s.codec_type === 'audio') : true; // unknown ⇒ assume worst
328
+ const formatId = sig.brand.startsWith('avi') ? 'avif' : 'heic';
291
329
  const decoded = await decodeImage(args.localPath);
292
330
  if (!decoded || hasAudio) {
293
- if (formatId === "heic" && !decoded && !hasAudio) {
331
+ if (formatId === 'heic' && !decoded && !hasAudio) {
294
332
  // Recognized HEIC-family that the runtime cannot decode: explicit
295
333
  // unsupported-image (launch policy) — NEVER relabeled video.
296
- return baseResult({ status: "unsupported", container: "heif", formatId, safetyPlan: "strict-video-fallback", reasonCode: "unsupported_format:heic" }, ffprobeVersion);
334
+ return baseResult({
335
+ status: 'unsupported',
336
+ container: 'heif',
337
+ formatId,
338
+ safetyPlan: 'strict-video-fallback',
339
+ reasonCode: 'unsupported_format:heic',
340
+ }, ffprobeVersion);
297
341
  }
298
342
  // Undecodable/ambiguous image-brand file, or one carrying audio (a
299
343
  // real timed presentation wearing an image brand): fail toward the
300
344
  // stricter visual path.
301
- return indeterminate("heif", decoded ? "image_brand_with_audio" : "image_decode_failed", ffprobeVersion, formatId);
345
+ return indeterminate('heif', decoded ? 'image_brand_with_audio' : 'image_decode_failed', ffprobeVersion, formatId);
302
346
  }
303
347
  const frames = decoded.frames;
304
348
  return baseResult({
305
- status: "definitive",
306
- canonicalKind: "image",
307
- container: "heif",
349
+ status: 'definitive',
350
+ canonicalKind: 'image',
351
+ container: 'heif',
308
352
  formatId,
309
- streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },
310
- safetyPlan: frames > 1 ? "animated-image-frames" : "still-image",
311
- reasonCode: "ok",
353
+ streams: {
354
+ audio: 0,
355
+ timedVideo: 0,
356
+ attachedPictures: 0,
357
+ imageFrames: frames,
358
+ auxiliary: 0,
359
+ },
360
+ safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',
361
+ reasonCode: 'ok',
312
362
  }, ffprobeVersion);
313
363
  }
314
- return classifyAvContainer(args, "isobmff", "isobmff", probe, timeoutMs, ffprobeVersion);
315
- case "ebml":
316
- return classifyAvContainer(args, "webm", "webm", probe, timeoutMs, ffprobeVersion);
317
- case "ogg":
318
- return classifyAvContainer(args, "ogg", "ogg", probe, timeoutMs, ffprobeVersion);
319
- case "avi":
364
+ return classifyAvContainer(args, 'isobmff', 'isobmff', probe, timeoutMs, ffprobeVersion);
365
+ case 'ebml':
366
+ return classifyAvContainer(args, 'webm', 'webm', probe, timeoutMs, ffprobeVersion);
367
+ case 'ogg':
368
+ return classifyAvContainer(args, 'ogg', 'ogg', probe, timeoutMs, ffprobeVersion);
369
+ case 'avi':
320
370
  // Real container ffprobe parses; no registry entry (no origin enables it
321
371
  // today) — the stream truth still classifies it, policy rejects by format.
322
- return classifyAvContainer(args, "avi", undefined, probe, timeoutMs, ffprobeVersion);
323
- case "wav":
324
- return classifyAvContainer(args, "wav", "wav", probe, timeoutMs, ffprobeVersion);
325
- case "mp3":
326
- return classifyAvContainer(args, "mp3", "mp3", probe, timeoutMs, ffprobeVersion);
327
- case "flac":
328
- return classifyAvContainer(args, "flac", "flac", probe, timeoutMs, ffprobeVersion);
372
+ return classifyAvContainer(args, 'avi', undefined, probe, timeoutMs, ffprobeVersion);
373
+ case 'wav':
374
+ return classifyAvContainer(args, 'wav', 'wav', probe, timeoutMs, ffprobeVersion);
375
+ case 'mp3':
376
+ return classifyAvContainer(args, 'mp3', 'mp3', probe, timeoutMs, ffprobeVersion);
377
+ case 'flac':
378
+ return classifyAvContainer(args, 'flac', 'flac', probe, timeoutMs, ffprobeVersion);
329
379
  }
330
380
  }
331
381
  async function classifyAvContainer(args, container, formatId, probe, timeoutMs, ffprobeVersion) {
@@ -335,21 +385,27 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
335
385
  }
336
386
  const parsed = ProbeJsonSchema.safeParse(probed.json);
337
387
  if (!parsed.success)
338
- return indeterminate(container, "probe_schema_invalid", ffprobeVersion, formatId);
388
+ return indeterminate(container, 'probe_schema_invalid', ffprobeVersion, formatId);
339
389
  const rawStreams = parsed.data.streams ?? [];
340
390
  if (rawStreams.length === 0)
341
- return indeterminate(container, "no_streams", ffprobeVersion, formatId);
342
- const counts = { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 };
391
+ return indeterminate(container, 'no_streams', ffprobeVersion, formatId);
392
+ const counts = {
393
+ audio: 0,
394
+ timedVideo: 0,
395
+ attachedPictures: 0,
396
+ imageFrames: 0,
397
+ auxiliary: 0,
398
+ };
343
399
  const audioCodecs = new Set();
344
400
  const videoCodecs = new Set();
345
401
  let sawUnknownType = false;
346
402
  for (const s of rawStreams) {
347
- const type = s.codec_type ?? "";
348
- if (type === "audio") {
403
+ const type = s.codec_type ?? '';
404
+ if (type === 'audio') {
349
405
  counts.audio++;
350
406
  audioCodecs.add(normalizeCodec(s.codec_name));
351
407
  }
352
- else if (type === "video") {
408
+ else if (type === 'video') {
353
409
  // An attached picture (cover art) is NOT a timed presentation — but the
354
410
  // disposition flag is untrusted container data, so it is COUNTED and the
355
411
  // safety plan forces every such picture through the image hash path
@@ -362,7 +418,7 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
362
418
  videoCodecs.add(normalizeCodec(s.codec_name));
363
419
  }
364
420
  }
365
- else if (type === "subtitle" || type === "data" || type === "attachment") {
421
+ else if (type === 'subtitle' || type === 'data' || type === 'attachment') {
366
422
  counts.auxiliary++;
367
423
  }
368
424
  else {
@@ -377,41 +433,41 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
377
433
  // classification table (fail-closed)
378
434
  if (counts.timedVideo > 0) {
379
435
  return baseResult({
380
- status: "definitive",
381
- canonicalKind: "video",
436
+ status: 'definitive',
437
+ canonicalKind: 'video',
382
438
  container,
383
439
  ...(formatId ? { formatId } : {}),
384
440
  streams: counts,
385
441
  codecs,
386
442
  ...duration,
387
- safetyPlan: "video-frames",
388
- reasonCode: "ok",
443
+ safetyPlan: 'video-frames',
444
+ reasonCode: 'ok',
389
445
  }, ffprobeVersion);
390
446
  }
391
447
  if (counts.audio > 0 && !sawUnknownType) {
392
448
  return baseResult({
393
- status: "definitive",
394
- canonicalKind: "audio",
449
+ status: 'definitive',
450
+ canonicalKind: 'audio',
395
451
  container,
396
452
  ...(formatId ? { formatId } : {}),
397
453
  streams: counts,
398
454
  codecs,
399
455
  ...duration,
400
- safetyPlan: counts.attachedPictures > 0 ? "audio-plus-artwork" : "audio-only",
401
- reasonCode: "ok",
456
+ safetyPlan: counts.attachedPictures > 0 ? 'audio-plus-artwork' : 'audio-only',
457
+ reasonCode: 'ok',
402
458
  }, ffprobeVersion);
403
459
  }
404
460
  // auxiliary-only, unknown stream types alongside audio, or nothing usable:
405
461
  // never audio — the strict visual fallback owns it.
406
462
  return baseResult({
407
- status: "indeterminate",
463
+ status: 'indeterminate',
408
464
  container,
409
465
  ...(formatId ? { formatId } : {}),
410
466
  streams: counts,
411
467
  codecs,
412
468
  ...duration,
413
- safetyPlan: "strict-video-fallback",
414
- reasonCode: sawUnknownType ? "unknown_stream_type" : "auxiliary_only",
469
+ safetyPlan: 'strict-video-fallback',
470
+ reasonCode: sawUnknownType ? 'unknown_stream_type' : 'auxiliary_only',
415
471
  }, ffprobeVersion);
416
472
  }
417
473
  //# sourceMappingURL=inspect-media.js.map