@ttt-productions/media-processing-core 0.8.16 → 0.8.18

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