@ttt-productions/media-processing-core 0.8.4 → 0.8.5

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.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from "./audio/index.js";
8
8
  export * from "./audio/audio-processor.js";
9
9
  export * from "./io/types.js";
10
10
  export * from "./run-pipeline.js";
11
+ export * from "./inspection/inspect-media.js";
11
12
  export * from "./io/fs.js";
12
13
  export * from "./moderation/types.js";
13
14
  export * from "./moderation/merge.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,YAAY,CAAC;AAE3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,YAAY,CAAC;AAE3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./audio/index.js"; // Not ./audio.js
8
8
  export * from "./audio/audio-processor.js";
9
9
  export * from "./io/types.js";
10
10
  export * from "./run-pipeline.js";
11
+ export * from "./inspection/inspect-media.js";
11
12
  export * from "./io/fs.js";
12
13
  export * from "./moderation/types.js";
13
14
  export * from "./moderation/merge.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,YAAY,CAAC;AAE3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC","sourcesContent":["export * from \"./image/index.js\"; // Not ./image.js\nexport * from \"./image/image-processor.js\";\n\nexport * from \"./video/index.js\"; // Not ./video.js\nexport * from \"./video/video-processor.js\";\n\nexport * from \"./process-media.js\";\nexport * from \"./workspace/temp.js\";\n\nexport * from \"./audio/index.js\"; // Not ./audio.js\nexport * from \"./audio/audio-processor.js\";\n\nexport * from \"./io/types.js\";\nexport * from \"./run-pipeline.js\";\n\nexport * from \"./io/fs.js\";\n\nexport * from \"./moderation/types.js\";\nexport * from \"./moderation/merge.js\";"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,kBAAkB,CAAC,CAAW,iBAAiB;AAC7D,cAAc,4BAA4B,CAAC;AAE3C,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC;AAElC,cAAc,+BAA+B,CAAC;AAE9C,cAAc,YAAY,CAAC;AAE3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC","sourcesContent":["export * from \"./image/index.js\"; // Not ./image.js\nexport * from \"./image/image-processor.js\";\n\nexport * from \"./video/index.js\"; // Not ./video.js\nexport * from \"./video/video-processor.js\";\n\nexport * from \"./process-media.js\";\nexport * from \"./workspace/temp.js\";\n\nexport * from \"./audio/index.js\"; // Not ./audio.js\nexport * from \"./audio/audio-processor.js\";\n\nexport * from \"./io/types.js\";\nexport * from \"./run-pipeline.js\";\n\nexport * from \"./inspection/inspect-media.js\";\n\nexport * from \"./io/fs.js\";\n\nexport * from \"./moderation/types.js\";\nexport * from \"./moderation/merge.js\";"]}
@@ -0,0 +1,57 @@
1
+ import { type MediaInspectionResult, type MediaFormatId } from "@ttt-productions/media-schemas";
2
+ /** Bump on any classification-rule change (rides every decision event). */
3
+ export declare const INSPECTOR_VERSION = "mpc-inspect-1";
4
+ type SignatureFamily = {
5
+ family: "image";
6
+ formatId: MediaFormatId;
7
+ container: string;
8
+ } | {
9
+ family: "svg";
10
+ } | {
11
+ family: "ebml";
12
+ } | {
13
+ family: "isobmff";
14
+ brand: string;
15
+ } | {
16
+ family: "ogg";
17
+ } | {
18
+ family: "avi";
19
+ } | {
20
+ family: "wav";
21
+ } | {
22
+ family: "mp3";
23
+ } | {
24
+ family: "flac";
25
+ } | {
26
+ family: "unknown";
27
+ };
28
+ export declare function detectSignatureFamily(b: Uint8Array): SignatureFamily;
29
+ export interface InspectMediaDeps {
30
+ /** Probe the local file; default shells to the hardened `ffprobe`. */
31
+ probe?: (localPath: string, opts: {
32
+ timeoutMs: number;
33
+ signal?: AbortSignal;
34
+ }) => Promise<{
35
+ ok: true;
36
+ json: unknown;
37
+ } | {
38
+ ok: false;
39
+ reason: "timeout" | "truncated" | "failed" | "bad_json";
40
+ }>;
41
+ /** Decode-prove an image; default uses sharp metadata. Returns frame count
42
+ * (pages) on success, null on decode failure. */
43
+ decodeImage?: (localPath: string) => Promise<{
44
+ frames: number;
45
+ } | null>;
46
+ /** Injected ffprobe version (default: cached `ffprobe -version` first line). */
47
+ ffprobeVersion?: string;
48
+ }
49
+ export interface InspectMediaArgs {
50
+ localPath: string;
51
+ timeoutMs?: number;
52
+ signal?: AbortSignal;
53
+ deps?: InspectMediaDeps;
54
+ }
55
+ export declare function inspectMedia(args: InspectMediaArgs): Promise<MediaInspectionResult>;
56
+ export {};
57
+ //# sourceMappingURL=inspect-media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect-media.d.ts","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"AA8BA,OAAO,EAEL,KAAK,qBAAqB,EAG1B,KAAK,aAAa,EACnB,MAAM,gCAAgC,CAAC;AAGxC,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AA2DjD,KAAK,eAAe,GAChB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAY1B,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,UAAU,GAAG,eAAe,CAiCpE;AAID,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CACrF;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAC3B;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAA;KAAE,CACzE,CAAC;IACF;sDACkD;IAClD,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxE,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAyGD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA0HzF"}
@@ -0,0 +1,412 @@
1
+ // =============================================================================
2
+ // Canonical media content inspection (canonical-upload-content-classification)
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 (reviewed 2026-07-31; see the consuming app's
15
+ // CODE_CHANGE_canonical_upload_content_classification.md):
16
+ // - family selection by a bounded 64 KiB SIGNATURE read (header only —
17
+ // semantic elements are NEVER searched for in arbitrary payload; that
18
+ // flat-scan heuristic caused the WebM audio misclassification twice);
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 { runCmd } from "../video/ffmpeg.js";
32
+ /** Bump on any classification-rule change (rides every decision event). */
33
+ export const INSPECTOR_VERSION = "mpc-inspect-1";
34
+ const SIGNATURE_READ_BYTES = 64 * 1024;
35
+ const DEFAULT_PROBE_TIMEOUT_MS = 20_000;
36
+ const PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback
37
+ const ANALYZE_DURATION_US = 10 * 1000 * 1000;
38
+ // ---- ffprobe JSON boundary (strict; anything else is malformed) ----
39
+ const ProbeStreamSchema = z
40
+ .object({
41
+ codec_type: z.string().max(32).optional(),
42
+ codec_name: z.string().max(64).optional(),
43
+ disposition: z
44
+ .object({ attached_pic: z.number().int().min(0).max(1).optional() })
45
+ .loose()
46
+ .optional(),
47
+ nb_frames: z.string().max(20).optional(),
48
+ duration: z.string().max(32).optional(),
49
+ })
50
+ .loose();
51
+ const ProbeJsonSchema = z
52
+ .object({
53
+ format: z.object({ format_name: z.string().max(200).optional() }).loose().optional(),
54
+ streams: z.array(ProbeStreamSchema).max(256).optional(),
55
+ })
56
+ .loose();
57
+ // ---- codec normalization (bounded — raw strings never escape) ----
58
+ const CODEC_MAP = {
59
+ h264: "h264",
60
+ hevc: "hevc",
61
+ vp8: "vp8",
62
+ vp9: "vp9",
63
+ av1: "av1",
64
+ theora: "theora",
65
+ mpeg4: "mpeg4",
66
+ mjpeg: "mjpeg",
67
+ aac: "aac",
68
+ alac: "alac",
69
+ opus: "opus",
70
+ vorbis: "vorbis",
71
+ mp3: "mp3",
72
+ flac: "flac",
73
+ };
74
+ function normalizeCodec(name) {
75
+ if (!name)
76
+ return "other";
77
+ const n = name.toLowerCase();
78
+ if (CODEC_MAP[n])
79
+ return CODEC_MAP[n];
80
+ if (n.startsWith("pcm_"))
81
+ return "pcm";
82
+ if (n.startsWith("amr"))
83
+ return "amr";
84
+ return "other";
85
+ }
86
+ /** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via
87
+ * decoder proof BEFORE the timed-video rule — see the AVIF note above. */
88
+ const ISOBMFF_IMAGE_BRANDS = new Set(["avif", "avis", "heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1"]);
89
+ function ascii(b, off, s) {
90
+ if (off + s.length > b.length)
91
+ return false;
92
+ for (let i = 0; i < s.length; i++)
93
+ if (b[off + i] !== s.charCodeAt(i))
94
+ return false;
95
+ return true;
96
+ }
97
+ export function detectSignatureFamily(b) {
98
+ if (b.length < 12)
99
+ return { family: "unknown" };
100
+ // images (single-format signatures)
101
+ if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
102
+ return { family: "image", formatId: "jpeg", container: "jpeg" };
103
+ if (b[0] === 0x89 && ascii(b, 1, "PNG"))
104
+ return { family: "image", formatId: "png", container: "png" };
105
+ if (ascii(b, 0, "GIF8"))
106
+ return { family: "image", formatId: "gif", container: "gif" };
107
+ if (ascii(b, 0, "RIFF") && ascii(b, 8, "WEBP"))
108
+ return { family: "image", formatId: "webp", container: "webp" };
109
+ if (b[0] === 0x42 && b[1] === 0x4d)
110
+ return { family: "image", formatId: "bmp", container: "bmp" };
111
+ if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) || (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {
112
+ return { family: "image", formatId: "tiff", container: "tiff" };
113
+ }
114
+ // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).
115
+ {
116
+ const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))
117
+ .toString("latin1")
118
+ .toLowerCase();
119
+ if (prefix.includes("<svg") && (prefix.trimStart().startsWith("<?xml") || prefix.trimStart().startsWith("<svg") || prefix.includes("<!doctype svg"))) {
120
+ return { family: "svg" };
121
+ }
122
+ }
123
+ // containers
124
+ if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
125
+ return { family: "ebml" };
126
+ if (ascii(b, 4, "ftyp")) {
127
+ const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();
128
+ return { family: "isobmff", brand };
129
+ }
130
+ if (ascii(b, 0, "OggS"))
131
+ return { family: "ogg" };
132
+ if (ascii(b, 0, "RIFF") && ascii(b, 8, "AVI "))
133
+ return { family: "avi" };
134
+ if (ascii(b, 0, "RIFF") && ascii(b, 8, "WAVE"))
135
+ return { family: "wav" };
136
+ if (ascii(b, 0, "ID3"))
137
+ return { family: "mp3" };
138
+ if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0)
139
+ return { family: "mp3" };
140
+ if (ascii(b, 0, "fLaC"))
141
+ return { family: "flac" };
142
+ return { family: "unknown" };
143
+ }
144
+ let cachedFfprobeVersion;
145
+ async function getFfprobeVersion() {
146
+ if (cachedFfprobeVersion !== undefined)
147
+ return cachedFfprobeVersion ?? undefined;
148
+ try {
149
+ const r = await runCmd("ffprobe", ["-version"], { timeoutMs: 5_000 });
150
+ const first = (r.stdout || r.stderr).split(/\r?\n/)[0]?.trim() ?? "";
151
+ cachedFfprobeVersion = first ? first.slice(0, 120) : null;
152
+ }
153
+ catch {
154
+ cachedFfprobeVersion = null;
155
+ }
156
+ return cachedFfprobeVersion ?? undefined;
157
+ }
158
+ async function defaultProbe(localPath, opts) {
159
+ let r;
160
+ try {
161
+ r = await runCmd("ffprobe", [
162
+ "-v",
163
+ "error",
164
+ "-print_format",
165
+ "json",
166
+ "-show_format",
167
+ "-show_streams",
168
+ "-probesize",
169
+ String(PROBE_SIZE_BYTES),
170
+ "-analyzeduration",
171
+ String(ANALYZE_DURATION_US),
172
+ localPath,
173
+ ], { timeoutMs: opts.timeoutMs, signal: opts.signal });
174
+ }
175
+ catch {
176
+ return { ok: false, reason: "failed" }; // spawn failure (ffprobe missing) — fail closed
177
+ }
178
+ if (r.timedOut)
179
+ return { ok: false, reason: "timeout" };
180
+ if (r.truncated)
181
+ return { ok: false, reason: "truncated" };
182
+ if (r.code !== 0)
183
+ return { ok: false, reason: "failed" };
184
+ try {
185
+ return { ok: true, json: JSON.parse(r.stdout || "{}") };
186
+ }
187
+ catch {
188
+ return { ok: false, reason: "bad_json" };
189
+ }
190
+ }
191
+ async function defaultDecodeImage(localPath) {
192
+ try {
193
+ const sharp = (await import("sharp")).default;
194
+ const meta = await sharp(localPath, { limitInputPixels: 268402689 /* sharp default ~16k x 16k */ }).metadata();
195
+ if (!meta.width || !meta.height)
196
+ return null;
197
+ return { frames: Math.max(1, meta.pages ?? 1) };
198
+ }
199
+ catch {
200
+ return null;
201
+ }
202
+ }
203
+ // ---- result builders ----
204
+ function baseResult(partial, ffprobeVersion) {
205
+ const result = {
206
+ inspectorVersion: INSPECTOR_VERSION,
207
+ ...(ffprobeVersion ? { ffprobeVersion } : {}),
208
+ streams: partial.streams ?? { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 },
209
+ codecs: partial.codecs ?? { audio: [], video: [] },
210
+ status: partial.status,
211
+ container: partial.container,
212
+ safetyPlan: partial.safetyPlan,
213
+ reasonCode: partial.reasonCode,
214
+ ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),
215
+ ...(partial.formatId ? { formatId: partial.formatId } : {}),
216
+ };
217
+ return MediaInspectionResultSchema.parse(result);
218
+ }
219
+ function indeterminate(container, reasonCode, ffprobeVersion, formatId) {
220
+ return baseResult({
221
+ status: "indeterminate",
222
+ container,
223
+ safetyPlan: "strict-video-fallback",
224
+ reasonCode,
225
+ ...(formatId ? { formatId } : {}),
226
+ }, ffprobeVersion);
227
+ }
228
+ export async function inspectMedia(args) {
229
+ const timeoutMs = args.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
230
+ const probe = args.deps?.probe ?? defaultProbe;
231
+ const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;
232
+ const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());
233
+ // 1) bounded signature read (header only)
234
+ let head;
235
+ try {
236
+ const fh = await open(args.localPath, "r");
237
+ try {
238
+ const buf = Buffer.alloc(SIGNATURE_READ_BYTES);
239
+ const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);
240
+ head = buf.subarray(0, bytesRead);
241
+ }
242
+ finally {
243
+ await fh.close();
244
+ }
245
+ }
246
+ catch {
247
+ return indeterminate("unknown", "read_failed", ffprobeVersion);
248
+ }
249
+ const sig = detectSignatureFamily(head);
250
+ // 2) family routing
251
+ switch (sig.family) {
252
+ case "unknown":
253
+ return indeterminate("unknown", "unrecognized_signature", ffprobeVersion);
254
+ case "svg":
255
+ // Scriptable XML "image" — structurally recognized, deliberately NOT a
256
+ // processable format until a sanitize/rasterize design exists (launch
257
+ // policy 2026-07-31). Never classified by decoder capability alone.
258
+ return baseResult({
259
+ status: "unsupported",
260
+ container: "svg",
261
+ formatId: "svg",
262
+ safetyPlan: "strict-video-fallback",
263
+ reasonCode: "unsupported_format:svg",
264
+ }, ffprobeVersion);
265
+ case "image": {
266
+ // Single-format image signature: decoder proof required — a signature is
267
+ // a candidate, not a classification.
268
+ const decoded = await decodeImage(args.localPath);
269
+ if (!decoded)
270
+ return baseResult({ status: "malformed", container: sig.container, formatId: sig.formatId, safetyPlan: "strict-video-fallback", reasonCode: "image_decode_failed" }, ffprobeVersion);
271
+ const frames = decoded.frames;
272
+ return baseResult({
273
+ status: "definitive",
274
+ canonicalKind: "image",
275
+ container: sig.container,
276
+ formatId: sig.formatId,
277
+ streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },
278
+ safetyPlan: frames > 1 ? "animated-image-frames" : "still-image",
279
+ reasonCode: "ok",
280
+ }, ffprobeVersion);
281
+ }
282
+ case "isobmff":
283
+ if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {
284
+ // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid
285
+ // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +
286
+ // successful bounded decode + no ordinary timed A/V presentation.
287
+ const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });
288
+ const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;
289
+ const streams = parsed?.success ? parsed.data.streams ?? [] : null;
290
+ const hasAudio = streams ? streams.some((s) => s.codec_type === "audio") : true; // unknown ⇒ assume worst
291
+ const formatId = sig.brand.startsWith("avi") ? "avif" : "heic";
292
+ const decoded = await decodeImage(args.localPath);
293
+ if (!decoded || hasAudio) {
294
+ if (formatId === "heic" && !decoded && !hasAudio) {
295
+ // Recognized HEIC-family that the runtime cannot decode: explicit
296
+ // unsupported-image (launch policy) — NEVER relabeled video.
297
+ return baseResult({ status: "unsupported", container: "heif", formatId, safetyPlan: "strict-video-fallback", reasonCode: "unsupported_format:heic" }, ffprobeVersion);
298
+ }
299
+ // Undecodable/ambiguous image-brand file, or one carrying audio (a
300
+ // real timed presentation wearing an image brand): fail toward the
301
+ // stricter visual path.
302
+ return indeterminate("heif", decoded ? "image_brand_with_audio" : "image_decode_failed", ffprobeVersion, formatId);
303
+ }
304
+ const frames = decoded.frames;
305
+ return baseResult({
306
+ status: "definitive",
307
+ canonicalKind: "image",
308
+ container: "heif",
309
+ formatId,
310
+ streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },
311
+ safetyPlan: frames > 1 ? "animated-image-frames" : "still-image",
312
+ reasonCode: "ok",
313
+ }, ffprobeVersion);
314
+ }
315
+ return classifyAvContainer(args, "isobmff", "isobmff", probe, timeoutMs, ffprobeVersion);
316
+ case "ebml":
317
+ return classifyAvContainer(args, "webm", "webm", probe, timeoutMs, ffprobeVersion);
318
+ case "ogg":
319
+ return classifyAvContainer(args, "ogg", "ogg", probe, timeoutMs, ffprobeVersion);
320
+ case "avi":
321
+ // Real container ffprobe parses; no registry entry (no origin enables it
322
+ // today) — the stream truth still classifies it, policy rejects by format.
323
+ return classifyAvContainer(args, "avi", undefined, probe, timeoutMs, ffprobeVersion);
324
+ case "wav":
325
+ return classifyAvContainer(args, "wav", "wav", probe, timeoutMs, ffprobeVersion);
326
+ case "mp3":
327
+ return classifyAvContainer(args, "mp3", "mp3", probe, timeoutMs, ffprobeVersion);
328
+ case "flac":
329
+ return classifyAvContainer(args, "flac", "flac", probe, timeoutMs, ffprobeVersion);
330
+ }
331
+ }
332
+ async function classifyAvContainer(args, container, formatId, probe, timeoutMs, ffprobeVersion) {
333
+ const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });
334
+ if (!probed.ok) {
335
+ return indeterminate(container, `probe_${probed.reason}`, ffprobeVersion, formatId);
336
+ }
337
+ const parsed = ProbeJsonSchema.safeParse(probed.json);
338
+ if (!parsed.success)
339
+ return indeterminate(container, "probe_schema_invalid", ffprobeVersion, formatId);
340
+ const rawStreams = parsed.data.streams ?? [];
341
+ if (rawStreams.length === 0)
342
+ return indeterminate(container, "no_streams", ffprobeVersion, formatId);
343
+ const counts = { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 };
344
+ const audioCodecs = new Set();
345
+ const videoCodecs = new Set();
346
+ let sawUnknownType = false;
347
+ for (const s of rawStreams) {
348
+ const type = s.codec_type ?? "";
349
+ if (type === "audio") {
350
+ counts.audio++;
351
+ audioCodecs.add(normalizeCodec(s.codec_name));
352
+ }
353
+ else if (type === "video") {
354
+ // An attached picture (cover art) is NOT a timed presentation — but the
355
+ // disposition flag is untrusted container data, so it is COUNTED and the
356
+ // safety plan forces every such picture through the image hash path
357
+ // before the audio guard may return clean.
358
+ if (s.disposition?.attached_pic === 1) {
359
+ counts.attachedPictures++;
360
+ }
361
+ else {
362
+ counts.timedVideo++;
363
+ videoCodecs.add(normalizeCodec(s.codec_name));
364
+ }
365
+ }
366
+ else if (type === "subtitle" || type === "data" || type === "attachment") {
367
+ counts.auxiliary++;
368
+ }
369
+ else {
370
+ counts.auxiliary++;
371
+ sawUnknownType = true;
372
+ }
373
+ }
374
+ const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };
375
+ // classification table (fail-closed)
376
+ if (counts.timedVideo > 0) {
377
+ return baseResult({
378
+ status: "definitive",
379
+ canonicalKind: "video",
380
+ container,
381
+ ...(formatId ? { formatId } : {}),
382
+ streams: counts,
383
+ codecs,
384
+ safetyPlan: "video-frames",
385
+ reasonCode: "ok",
386
+ }, ffprobeVersion);
387
+ }
388
+ if (counts.audio > 0 && !sawUnknownType) {
389
+ return baseResult({
390
+ status: "definitive",
391
+ canonicalKind: "audio",
392
+ container,
393
+ ...(formatId ? { formatId } : {}),
394
+ streams: counts,
395
+ codecs,
396
+ safetyPlan: counts.attachedPictures > 0 ? "audio-plus-artwork" : "audio-only",
397
+ reasonCode: "ok",
398
+ }, ffprobeVersion);
399
+ }
400
+ // auxiliary-only, unknown stream types alongside audio, or nothing usable:
401
+ // never audio — the strict visual fallback owns it.
402
+ return baseResult({
403
+ status: "indeterminate",
404
+ container,
405
+ ...(formatId ? { formatId } : {}),
406
+ streams: counts,
407
+ codecs,
408
+ safetyPlan: "strict-video-fallback",
409
+ reasonCode: sawUnknownType ? "unknown_stream_type" : "auxiliary_only",
410
+ }, ffprobeVersion);
411
+ }
412
+ //# sourceMappingURL=inspect-media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspect-media.js","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"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","sourcesContent":["// =============================================================================\n// Canonical media content inspection (canonical-upload-content-classification)\n// =============================================================================\n//\n// The ONE server-side classification authority. Consumes a LOCAL file (the\n// pipeline's generation-pinned temp download), establishes what the bytes\n// actually contain, and returns the bounded `MediaInspectionResult` contract\n// from @ttt-productions/media-schemas. Everything downstream — origin/kind\n// validation, processing-spec selection, the persisted asset kind, and the\n// safety (PhotoDNA) routing — derives from this one result. Client MIME,\n// filename, extension, container brand, and payload-byte heuristics are never\n// consulted.\n//\n// Design (reviewed 2026-07-31; see the consuming app's\n// CODE_CHANGE_canonical_upload_content_classification.md):\n// - family selection by a bounded 64 KiB SIGNATURE read (header only —\n// semantic elements are NEVER searched for in arbitrary payload; that\n// flat-scan heuristic caused the WebM audio misclassification twice);\n// - shared A/V containers (EBML/ISO-BMFF/Ogg/AVI) classified by a hardened\n// `ffprobe` stream table (libavformat's structural parsers), with timeout,\n// kill-on-abort, output cap, and strict schema validation;\n// - image-family proof by DECODER (sharp) — a signature or `ftyp` brand is a\n// candidate, not proof, and image-family ISO-BMFF (AVIF/HEIC) is resolved\n// BEFORE the ordinary \"any timed video stream wins\" rule (a valid AVIF\n// probes as a one-frame AV1 `video` stream);\n// - FAIL-CLOSED: anything unproven is `indeterminate` with the\n// `strict-video-fallback` safety plan — never audio, never publishable.\n\nimport { open } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport {\n MediaInspectionResultSchema,\n type MediaInspectionResult,\n type MediaInspectionStreams,\n type NormalizedCodecId,\n type MediaFormatId,\n} from \"@ttt-productions/media-schemas\";\nimport { runCmd } from \"../video/ffmpeg.js\";\n\n/** Bump on any classification-rule change (rides every decision event). */\nexport const INSPECTOR_VERSION = \"mpc-inspect-1\";\n\nconst SIGNATURE_READ_BYTES = 64 * 1024;\nconst DEFAULT_PROBE_TIMEOUT_MS = 20_000;\nconst PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback\nconst ANALYZE_DURATION_US = 10 * 1000 * 1000;\n\n// ---- ffprobe JSON boundary (strict; anything else is malformed) ----\n\nconst ProbeStreamSchema = z\n .object({\n codec_type: z.string().max(32).optional(),\n codec_name: z.string().max(64).optional(),\n disposition: z\n .object({ attached_pic: z.number().int().min(0).max(1).optional() })\n .loose()\n .optional(),\n nb_frames: z.string().max(20).optional(),\n duration: z.string().max(32).optional(),\n })\n .loose();\n\nconst ProbeJsonSchema = z\n .object({\n format: z.object({ format_name: z.string().max(200).optional() }).loose().optional(),\n streams: z.array(ProbeStreamSchema).max(256).optional(),\n })\n .loose();\n\n// ---- codec normalization (bounded — raw strings never escape) ----\n\nconst CODEC_MAP: Record<string, NormalizedCodecId> = {\n h264: \"h264\",\n hevc: \"hevc\",\n vp8: \"vp8\",\n vp9: \"vp9\",\n av1: \"av1\",\n theora: \"theora\",\n mpeg4: \"mpeg4\",\n mjpeg: \"mjpeg\",\n aac: \"aac\",\n alac: \"alac\",\n opus: \"opus\",\n vorbis: \"vorbis\",\n mp3: \"mp3\",\n flac: \"flac\",\n};\n\nfunction normalizeCodec(name: string | undefined): NormalizedCodecId {\n if (!name) return \"other\";\n const n = name.toLowerCase();\n if (CODEC_MAP[n]) return CODEC_MAP[n];\n if (n.startsWith(\"pcm_\")) return \"pcm\";\n if (n.startsWith(\"amr\")) return \"amr\";\n return \"other\";\n}\n\n// ---- signature families ----\n\ntype SignatureFamily =\n | { family: \"image\"; formatId: MediaFormatId; container: string }\n | { family: \"svg\" }\n | { family: \"ebml\" }\n | { family: \"isobmff\"; brand: string }\n | { family: \"ogg\" }\n | { family: \"avi\" }\n | { family: \"wav\" }\n | { family: \"mp3\" }\n | { family: \"flac\" }\n | { family: \"unknown\" };\n\n/** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via\n * decoder proof BEFORE the timed-video rule — see the AVIF note above. */\nconst ISOBMFF_IMAGE_BRANDS = new Set([\"avif\", \"avis\", \"heic\", \"heix\", \"heim\", \"heis\", \"hevc\", \"hevx\", \"mif1\", \"msf1\"]);\n\nfunction ascii(b: Uint8Array, off: number, s: string): boolean {\n if (off + s.length > b.length) return false;\n for (let i = 0; i < s.length; i++) if (b[off + i] !== s.charCodeAt(i)) return false;\n return true;\n}\n\nexport function detectSignatureFamily(b: Uint8Array): SignatureFamily {\n if (b.length < 12) return { family: \"unknown\" };\n // images (single-format signatures)\n if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return { family: \"image\", formatId: \"jpeg\", container: \"jpeg\" };\n if (b[0] === 0x89 && ascii(b, 1, \"PNG\")) return { family: \"image\", formatId: \"png\", container: \"png\" };\n if (ascii(b, 0, \"GIF8\")) return { family: \"image\", formatId: \"gif\", container: \"gif\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"WEBP\")) return { family: \"image\", formatId: \"webp\", container: \"webp\" };\n if (b[0] === 0x42 && b[1] === 0x4d) return { family: \"image\", formatId: \"bmp\", container: \"bmp\" };\n if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) || (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {\n return { family: \"image\", formatId: \"tiff\", container: \"tiff\" };\n }\n // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).\n {\n const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))\n .toString(\"latin1\")\n .toLowerCase();\n if (prefix.includes(\"<svg\") && (prefix.trimStart().startsWith(\"<?xml\") || prefix.trimStart().startsWith(\"<svg\") || prefix.includes(\"<!doctype svg\"))) {\n return { family: \"svg\" };\n }\n }\n // containers\n if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3) return { family: \"ebml\" };\n if (ascii(b, 4, \"ftyp\")) {\n const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();\n return { family: \"isobmff\", brand };\n }\n if (ascii(b, 0, \"OggS\")) return { family: \"ogg\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"AVI \")) return { family: \"avi\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"WAVE\")) return { family: \"wav\" };\n if (ascii(b, 0, \"ID3\")) return { family: \"mp3\" };\n if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0) return { family: \"mp3\" };\n if (ascii(b, 0, \"fLaC\")) return { family: \"flac\" };\n return { family: \"unknown\" };\n}\n\n// ---- dependency seams (tests inject; production uses the real tools) ----\n\nexport interface InspectMediaDeps {\n /** Probe the local file; default shells to the hardened `ffprobe`. */\n probe?: (localPath: string, opts: { timeoutMs: number; signal?: AbortSignal }) => Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: \"timeout\" | \"truncated\" | \"failed\" | \"bad_json\" }\n >;\n /** Decode-prove an image; default uses sharp metadata. Returns frame count\n * (pages) on success, null on decode failure. */\n decodeImage?: (localPath: string) => Promise<{ frames: number } | null>;\n /** Injected ffprobe version (default: cached `ffprobe -version` first line). */\n ffprobeVersion?: string;\n}\n\nlet cachedFfprobeVersion: string | null | undefined;\n\nasync function getFfprobeVersion(): Promise<string | undefined> {\n if (cachedFfprobeVersion !== undefined) return cachedFfprobeVersion ?? undefined;\n try {\n const r = await runCmd(\"ffprobe\", [\"-version\"], { timeoutMs: 5_000 });\n const first = (r.stdout || r.stderr).split(/\\r?\\n/)[0]?.trim() ?? \"\";\n cachedFfprobeVersion = first ? first.slice(0, 120) : null;\n } catch {\n cachedFfprobeVersion = null;\n }\n return cachedFfprobeVersion ?? undefined;\n}\n\nasync function defaultProbe(\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n): Promise<{ ok: true; json: unknown } | { ok: false; reason: \"timeout\" | \"truncated\" | \"failed\" | \"bad_json\" }> {\n let r;\n try {\n r = await runCmd(\n \"ffprobe\",\n [\n \"-v\",\n \"error\",\n \"-print_format\",\n \"json\",\n \"-show_format\",\n \"-show_streams\",\n \"-probesize\",\n String(PROBE_SIZE_BYTES),\n \"-analyzeduration\",\n String(ANALYZE_DURATION_US),\n localPath,\n ],\n { timeoutMs: opts.timeoutMs, signal: opts.signal },\n );\n } catch {\n return { ok: false, reason: \"failed\" }; // spawn failure (ffprobe missing) — fail closed\n }\n if (r.timedOut) return { ok: false, reason: \"timeout\" };\n if (r.truncated) return { ok: false, reason: \"truncated\" };\n if (r.code !== 0) return { ok: false, reason: \"failed\" };\n try {\n return { ok: true, json: JSON.parse(r.stdout || \"{}\") };\n } catch {\n return { ok: false, reason: \"bad_json\" };\n }\n}\n\nasync function defaultDecodeImage(localPath: string): Promise<{ frames: number } | null> {\n try {\n const sharp = (await import(\"sharp\")).default;\n const meta = await sharp(localPath, { limitInputPixels: 268402689 /* sharp default ~16k x 16k */ }).metadata();\n if (!meta.width || !meta.height) return null;\n return { frames: Math.max(1, meta.pages ?? 1) };\n } catch {\n return null;\n }\n}\n\n// ---- result builders ----\n\nfunction baseResult(\n partial: Omit<MediaInspectionResult, \"inspectorVersion\" | \"streams\" | \"codecs\"> &\n Partial<Pick<MediaInspectionResult, \"streams\" | \"codecs\">>,\n ffprobeVersion: string | undefined,\n): MediaInspectionResult {\n const result: MediaInspectionResult = {\n inspectorVersion: INSPECTOR_VERSION,\n ...(ffprobeVersion ? { ffprobeVersion } : {}),\n streams: partial.streams ?? { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 },\n codecs: partial.codecs ?? { audio: [], video: [] },\n status: partial.status,\n container: partial.container,\n safetyPlan: partial.safetyPlan,\n reasonCode: partial.reasonCode,\n ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),\n ...(partial.formatId ? { formatId: partial.formatId } : {}),\n };\n return MediaInspectionResultSchema.parse(result);\n}\n\nfunction indeterminate(\n container: string,\n reasonCode: string,\n ffprobeVersion: string | undefined,\n formatId?: MediaFormatId,\n): MediaInspectionResult {\n return baseResult(\n {\n status: \"indeterminate\",\n container,\n safetyPlan: \"strict-video-fallback\",\n reasonCode,\n ...(formatId ? { formatId } : {}),\n },\n ffprobeVersion,\n );\n}\n\n// ---- the inspector ----\n\nexport interface InspectMediaArgs {\n localPath: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n deps?: InspectMediaDeps;\n}\n\nexport async function inspectMedia(args: InspectMediaArgs): Promise<MediaInspectionResult> {\n const timeoutMs = args.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;\n const probe = args.deps?.probe ?? defaultProbe;\n const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;\n const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());\n\n // 1) bounded signature read (header only)\n let head: Uint8Array;\n try {\n const fh = await open(args.localPath, \"r\");\n try {\n const buf = Buffer.alloc(SIGNATURE_READ_BYTES);\n const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);\n head = buf.subarray(0, bytesRead);\n } finally {\n await fh.close();\n }\n } catch {\n return indeterminate(\"unknown\", \"read_failed\", ffprobeVersion);\n }\n\n const sig = detectSignatureFamily(head);\n\n // 2) family routing\n switch (sig.family) {\n case \"unknown\":\n return indeterminate(\"unknown\", \"unrecognized_signature\", ffprobeVersion);\n\n case \"svg\":\n // Scriptable XML \"image\" — structurally recognized, deliberately NOT a\n // processable format until a sanitize/rasterize design exists (launch\n // policy 2026-07-31). Never classified by decoder capability alone.\n return baseResult(\n {\n status: \"unsupported\",\n container: \"svg\",\n formatId: \"svg\",\n safetyPlan: \"strict-video-fallback\",\n reasonCode: \"unsupported_format:svg\",\n },\n ffprobeVersion,\n );\n\n case \"image\": {\n // Single-format image signature: decoder proof required — a signature is\n // a candidate, not a classification.\n const decoded = await decodeImage(args.localPath);\n if (!decoded) return baseResult(\n { status: \"malformed\", container: sig.container, formatId: sig.formatId, safetyPlan: \"strict-video-fallback\", reasonCode: \"image_decode_failed\" },\n ffprobeVersion,\n );\n const frames = decoded.frames;\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"image\",\n container: sig.container,\n formatId: sig.formatId,\n streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },\n safetyPlan: frames > 1 ? \"animated-image-frames\" : \"still-image\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n\n case \"isobmff\":\n if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {\n // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid\n // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +\n // successful bounded decode + no ordinary timed A/V presentation.\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;\n const streams = parsed?.success ? parsed.data.streams ?? [] : null;\n const hasAudio = streams ? streams.some((s) => s.codec_type === \"audio\") : true; // unknown ⇒ assume worst\n const formatId: MediaFormatId | undefined = sig.brand.startsWith(\"avi\") ? \"avif\" : \"heic\";\n const decoded = await decodeImage(args.localPath);\n if (!decoded || hasAudio) {\n if (formatId === \"heic\" && !decoded && !hasAudio) {\n // Recognized HEIC-family that the runtime cannot decode: explicit\n // unsupported-image (launch policy) — NEVER relabeled video.\n return baseResult(\n { status: \"unsupported\", container: \"heif\", formatId, safetyPlan: \"strict-video-fallback\", reasonCode: \"unsupported_format:heic\" },\n ffprobeVersion,\n );\n }\n // Undecodable/ambiguous image-brand file, or one carrying audio (a\n // real timed presentation wearing an image brand): fail toward the\n // stricter visual path.\n return indeterminate(\"heif\", decoded ? \"image_brand_with_audio\" : \"image_decode_failed\", ffprobeVersion, formatId);\n }\n const frames = decoded.frames;\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"image\",\n container: \"heif\",\n formatId,\n streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },\n safetyPlan: frames > 1 ? \"animated-image-frames\" : \"still-image\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n return classifyAvContainer(args, \"isobmff\", \"isobmff\", probe, timeoutMs, ffprobeVersion);\n\n case \"ebml\":\n return classifyAvContainer(args, \"webm\", \"webm\", probe, timeoutMs, ffprobeVersion);\n case \"ogg\":\n return classifyAvContainer(args, \"ogg\", \"ogg\", probe, timeoutMs, ffprobeVersion);\n case \"avi\":\n // Real container ffprobe parses; no registry entry (no origin enables it\n // today) — the stream truth still classifies it, policy rejects by format.\n return classifyAvContainer(args, \"avi\", undefined, probe, timeoutMs, ffprobeVersion);\n case \"wav\":\n return classifyAvContainer(args, \"wav\", \"wav\", probe, timeoutMs, ffprobeVersion);\n case \"mp3\":\n return classifyAvContainer(args, \"mp3\", \"mp3\", probe, timeoutMs, ffprobeVersion);\n case \"flac\":\n return classifyAvContainer(args, \"flac\", \"flac\", probe, timeoutMs, ffprobeVersion);\n }\n}\n\nasync function classifyAvContainer(\n args: InspectMediaArgs,\n container: string,\n formatId: MediaFormatId | undefined,\n probe: NonNullable<InspectMediaDeps[\"probe\"]>,\n timeoutMs: number,\n ffprobeVersion: string | undefined,\n): Promise<MediaInspectionResult> {\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n if (!probed.ok) {\n return indeterminate(container, `probe_${probed.reason}`, ffprobeVersion, formatId);\n }\n const parsed = ProbeJsonSchema.safeParse(probed.json);\n if (!parsed.success) return indeterminate(container, \"probe_schema_invalid\", ffprobeVersion, formatId);\n\n const rawStreams = parsed.data.streams ?? [];\n if (rawStreams.length === 0) return indeterminate(container, \"no_streams\", ffprobeVersion, formatId);\n\n const counts: MediaInspectionStreams = { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 };\n const audioCodecs = new Set<NormalizedCodecId>();\n const videoCodecs = new Set<NormalizedCodecId>();\n let sawUnknownType = false;\n\n for (const s of rawStreams) {\n const type = s.codec_type ?? \"\";\n if (type === \"audio\") {\n counts.audio++;\n audioCodecs.add(normalizeCodec(s.codec_name));\n } else if (type === \"video\") {\n // An attached picture (cover art) is NOT a timed presentation — but the\n // disposition flag is untrusted container data, so it is COUNTED and the\n // safety plan forces every such picture through the image hash path\n // before the audio guard may return clean.\n if (s.disposition?.attached_pic === 1) {\n counts.attachedPictures++;\n } else {\n counts.timedVideo++;\n videoCodecs.add(normalizeCodec(s.codec_name));\n }\n } else if (type === \"subtitle\" || type === \"data\" || type === \"attachment\") {\n counts.auxiliary++;\n } else {\n counts.auxiliary++;\n sawUnknownType = true;\n }\n }\n\n const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };\n\n // classification table (fail-closed)\n if (counts.timedVideo > 0) {\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"video\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n safetyPlan: \"video-frames\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n if (counts.audio > 0 && !sawUnknownType) {\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"audio\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n safetyPlan: counts.attachedPictures > 0 ? \"audio-plus-artwork\" : \"audio-only\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n // auxiliary-only, unknown stream types alongside audio, or nothing usable:\n // never audio — the strict visual fallback owns it.\n return baseResult(\n {\n status: \"indeterminate\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n safetyPlan: \"strict-video-fallback\",\n reasonCode: sawUnknownType ? \"unknown_stream_type\" : \"auxiliary_only\",\n },\n ffprobeVersion,\n );\n}\n"]}
@@ -1,4 +1,5 @@
1
- import type { MediaProcessingResult, MediaProcessingSpec } from "@ttt-productions/media-schemas";
1
+ import type { MediaProcessingResult, MediaProcessingSpec, MediaInspectionResult, MediaProcessingError } from "@ttt-productions/media-schemas";
2
+ import { type InspectMediaDeps } from "./inspection/inspect-media.js";
2
3
  import type { MediaIO } from "./io/types.js";
3
4
  import type { ModerationAdapter } from "./moderation/types.js";
4
5
  export interface RunPipelineArgs {
@@ -8,6 +9,33 @@ export interface RunPipelineArgs {
8
9
  moderation?: ModerationAdapter;
9
10
  signal?: AbortSignal;
10
11
  onProgress?: (p: import("./types.js").MediaPipelineProgress) => void;
12
+ /**
13
+ * CANONICAL INSPECTION SEAM (canonical-upload-content-classification).
14
+ * When present, the pipeline runs `inspectMedia` on the just-downloaded temp
15
+ * input — BEFORE moderation and processing — and calls this policy hook with
16
+ * the bounded result. The hook either:
17
+ * - returns `{ spec }` — the caller's policy adapter selected the
18
+ * processing spec FROM the inspection (the byte-derived kind, never the
19
+ * client MIME); the pipeline continues with that spec; or
20
+ * - returns `{ reject }` — a typed terminal (`kind_mismatch`,
21
+ * `unsupported_format`, `unsupported_codec`, …); the pipeline stops
22
+ * before any transcode/moderation work.
23
+ * The inspection result rides the returned `MediaProcessingResult.inspection`
24
+ * either way, so the finalizer hands the SAME object to the safety gate —
25
+ * one authority, no re-detection. Callers that omit the hook get the
26
+ * pre-existing behavior unchanged.
27
+ */
28
+ resolveAfterInspection?: (inspection: MediaInspectionResult) => Promise<{
29
+ spec: MediaProcessingSpec;
30
+ } | {
31
+ reject: MediaProcessingError;
32
+ }> | ({
33
+ spec: MediaProcessingSpec;
34
+ } | {
35
+ reject: MediaProcessingError;
36
+ });
37
+ /** Inspection tool overrides (tests). */
38
+ inspectionDeps?: InspectMediaDeps;
11
39
  }
12
40
  export declare function runMediaPipeline(args: RunPipelineArgs): Promise<MediaProcessingResult>;
13
41
  //# sourceMappingURL=run-pipeline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-pipeline.d.ts","sourceRoot":"","sources":["../src/run-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,qBAAqB,EACrB,mBAAmB,EAEpB,MAAM,gCAAgC,CAAC;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAK/D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,YAAY,EAAE,qBAAqB,KAAK,IAAI,CAAC;CACpE;AAiBD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA2M5F"}
1
+ {"version":3,"file":"run-pipeline.d.ts","sourceRoot":"","sources":["../src/run-pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,qBAAqB,EACrB,mBAAmB,EAEnB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACpF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAK/D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,YAAY,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACrE;;;;;;;;;;;;;;;OAeG;IACH,sBAAsB,CAAC,EAAE,CACvB,UAAU,EAAE,qBAAqB,KAC9B,OAAO,CAAC;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,oBAAoB,CAAA;KAAE,CAAC,GAAG,CAAC;QAAE,IAAI,EAAE,mBAAmB,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,oBAAoB,CAAA;KAAE,CAAC,CAAC;IACpJ,yCAAyC;IACzC,cAAc,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAiBD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAgP5F"}
@@ -1,6 +1,7 @@
1
1
  import { parseMediaProcessingSpec } from "@ttt-productions/media-schemas";
2
2
  import { createTempWorkspace } from "./workspace/temp.js";
3
3
  import { processMedia } from "./process-media.js";
4
+ import { inspectMedia } from "./inspection/inspect-media.js";
4
5
  import { mergeModeration } from "./moderation/merge.js";
5
6
  import path from "node:path";
6
7
  import { stat } from "node:fs/promises";
@@ -20,7 +21,11 @@ function attachProvider(m, provider) {
20
21
  }
21
22
  export async function runMediaPipeline(args) {
22
23
  let { spec } = args;
23
- const { io, outputBaseName = "media", moderation, signal, onProgress } = args;
24
+ const { io, outputBaseName = "media", moderation, signal, onProgress, resolveAfterInspection } = args;
25
+ // Carried onto every return path once produced — the finalizer must hand the
26
+ // SAME inspection object to the safety gate (one classification authority).
27
+ let inspection;
28
+ const withInspection = (r) => inspection ? { ...r, inspection } : r;
24
29
  if (signal?.aborted) {
25
30
  return {
26
31
  ok: false,
@@ -53,6 +58,38 @@ export async function runMediaPipeline(args) {
53
58
  onProgress?.({ phase: "read_input", percent: 0 });
54
59
  await io.input.readToFile(inputPath);
55
60
  onProgress?.({ phase: "read_input", percent: 1 });
61
+ // CANONICAL INSPECTION (before moderation and processing): classify the
62
+ // just-downloaded bytes structurally and let the caller's policy adapter
63
+ // pick the spec from the result. See RunPipelineArgs.resolveAfterInspection.
64
+ if (resolveAfterInspection) {
65
+ inspection = await inspectMedia({
66
+ localPath: inputPath,
67
+ ...(signal ? { signal } : {}),
68
+ ...(args.inspectionDeps ? { deps: args.inspectionDeps } : {}),
69
+ });
70
+ const resolution = await resolveAfterInspection(inspection);
71
+ if ("reject" in resolution) {
72
+ return withInspection({
73
+ ok: false,
74
+ mediaType: mediaTypeFromSpecKind(spec.kind),
75
+ error: resolution.reject,
76
+ });
77
+ }
78
+ try {
79
+ spec = parseMediaProcessingSpec(resolution.spec);
80
+ }
81
+ catch (e) {
82
+ return withInspection({
83
+ ok: false,
84
+ mediaType: mediaTypeFromSpecKind(spec.kind),
85
+ error: {
86
+ code: "invalid_spec",
87
+ message: "Invalid MediaProcessingSpec from resolveAfterInspection.",
88
+ details: { issues: e?.issues ?? e?.message ?? String(e) },
89
+ },
90
+ });
91
+ }
92
+ }
56
93
  // PRE moderation (original)
57
94
  let mIn;
58
95
  if (moderation?.moderateInput) {
@@ -73,7 +110,7 @@ export async function runMediaPipeline(args) {
73
110
  // "error") on the result — callers route that to their retryable
74
111
  // moderation lane and land the row failed rather than publishing it.
75
112
  if (mIn?.status === "rejected" || mIn?.status === "error") {
76
- return {
113
+ return withInspection({
77
114
  ok: false,
78
115
  mediaType: mediaTypeFromSpecKind(spec.kind),
79
116
  moderation: mIn,
@@ -88,7 +125,7 @@ export async function runMediaPipeline(args) {
88
125
  message: "Rejected by moderation.",
89
126
  details: { provider: mIn.provider, reasons: mIn.reasons, findings: mIn.findings },
90
127
  },
91
- };
128
+ });
92
129
  }
93
130
  }
94
131
  const result = await processMedia(spec, {
@@ -110,7 +147,7 @@ export async function runMediaPipeline(args) {
110
147
  if (!o.sizeBytes)
111
148
  o.sizeBytes = size;
112
149
  if (size > perFileLimit) {
113
- return {
150
+ return withInspection({
114
151
  ok: false,
115
152
  mediaType: mediaTypeFromSpecKind(spec.kind),
116
153
  error: {
@@ -118,10 +155,10 @@ export async function runMediaPipeline(args) {
118
155
  message: "Processed output exceeds allowed size.",
119
156
  details: { key: o.key, sizeBytes: size, limitBytes: perFileLimit },
120
157
  },
121
- };
158
+ });
122
159
  }
123
160
  if (total > totalLimit) {
124
- return {
161
+ return withInspection({
125
162
  ok: false,
126
163
  mediaType: mediaTypeFromSpecKind(spec.kind),
127
164
  error: {
@@ -129,7 +166,7 @@ export async function runMediaPipeline(args) {
129
166
  message: "Total processed outputs exceed allowed size.",
130
167
  details: { totalBytes: total, limitBytes: totalLimit },
131
168
  },
132
- };
169
+ });
133
170
  }
134
171
  }
135
172
  }
@@ -137,7 +174,7 @@ export async function runMediaPipeline(args) {
137
174
  if (mIn)
138
175
  result.moderation = mIn;
139
176
  onProgress?.({ phase: "done", percent: result.ok ? 1 : 0 });
140
- return result;
177
+ return withInspection(result);
141
178
  }
142
179
  // POST moderation (processed outputs) — runs on the LOCAL output paths
143
180
  // BEFORE any final persistence, so rejected output never reaches the
@@ -159,7 +196,7 @@ export async function runMediaPipeline(args) {
159
196
  // publish unscanned output. Ordering is load-bearing — see the
160
197
  // consuming app's media-serving design docs.
161
198
  if (mOut?.status === "rejected" || mOut?.status === "error") {
162
- return {
199
+ return withInspection({
163
200
  ok: false,
164
201
  mediaType: result.mediaType,
165
202
  moderation: mOut,
@@ -174,7 +211,7 @@ export async function runMediaPipeline(args) {
174
211
  message: "Rejected by moderation.",
175
212
  details: { provider: mOut.provider, reasons: mOut.reasons, findings: mOut.findings },
176
213
  },
177
- };
214
+ });
178
215
  }
179
216
  }
180
217
  // Persist outputs — only after input AND output moderation both passed.
@@ -183,7 +220,7 @@ export async function runMediaPipeline(args) {
183
220
  let outIdx = 0;
184
221
  for (const out of result.outputs) {
185
222
  if (signal?.aborted) {
186
- return { ok: false, mediaType: result.mediaType, error: { code: "processing_canceled", message: "Processing canceled." } };
223
+ return withInspection({ ok: false, mediaType: result.mediaType, error: { code: "processing_canceled", message: "Processing canceled." } });
187
224
  }
188
225
  onProgress?.({ phase: "persist_outputs", percent: totalOut ? outIdx / totalOut : 0, detail: { key: out.key } });
189
226
  if (!out.path)
@@ -200,7 +237,7 @@ export async function runMediaPipeline(args) {
200
237
  if (merged)
201
238
  result.moderation = merged;
202
239
  onProgress?.({ phase: "done", percent: 1 });
203
- return result;
240
+ return withInspection(result);
204
241
  }
205
242
  finally {
206
243
  await ws.cleanup();
@@ -1 +1 @@
1
- {"version":3,"file":"run-pipeline.js","sourceRoot":"","sources":["../src/run-pipeline.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaProcessingResult,\n MediaProcessingSpec,\n MediaModerationResult,\n } from \"@ttt-productions/media-schemas\";\nimport { parseMediaProcessingSpec } from \"@ttt-productions/media-schemas\";\n import { createTempWorkspace } from \"./workspace/temp.js\";\n import { processMedia } from \"./process-media.js\";\n import type { MediaIO } from \"./io/types.js\";\n import type { ModerationAdapter } from \"./moderation/types.js\";\n import { mergeModeration } from \"./moderation/merge.js\";\n import path from \"node:path\";\n import { stat } from \"node:fs/promises\";\n \n export interface RunPipelineArgs {\n spec: MediaProcessingSpec;\n io: MediaIO;\n outputBaseName?: string;\n moderation?: ModerationAdapter;\n signal?: AbortSignal;\n onProgress?: (p: import(\"./types.js\").MediaPipelineProgress) => void;\n }\n \n function mediaTypeFromSpecKind(kind: MediaProcessingSpec[\"kind\"]): \"image\" | \"video\" | \"audio\" | \"other\" {\n if (kind === \"image\") return \"image\";\n if (kind === \"video\") return \"video\";\n if (kind === \"audio\") return \"audio\";\n return \"other\";\n }\n \n function attachProvider(\n m: MediaModerationResult | null | undefined,\n provider?: string\n ): MediaModerationResult | null | undefined {\n if (!m) return m;\n return { ...m, provider: m.provider ?? provider, reviewedAt: m.reviewedAt ?? Date.now() };\n }\n \n export async function runMediaPipeline(args: RunPipelineArgs): Promise<MediaProcessingResult> {\n let { spec } = args;\n const { io, outputBaseName = \"media\", moderation, signal, onProgress } = args;\n\n if (signal?.aborted) {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: { code: \"processing_canceled\", message: \"Processing canceled.\" },\n };\n }\n\n // Runtime contract validation (fast-fail with actionable errors).\n try {\n spec = parseMediaProcessingSpec(spec);\n } catch (e: any) {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"invalid_spec\",\n message: \"Invalid MediaProcessingSpec.\",\n details: { issues: e?.issues ?? e?.message ?? String(e) },\n },\n };\n }\n \n const ws = await createTempWorkspace();\n \n try {\n const inputPath = path.join(ws.dir, \"input\");\n const outputBasePath = path.join(ws.dir, outputBaseName);\n \n if (signal?.aborted) {\n return { ok: false, mediaType: mediaTypeFromSpecKind(spec.kind), error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n onProgress?.({ phase: \"read_input\", percent: 0 });\n await io.input.readToFile(inputPath);\n onProgress?.({ phase: \"read_input\", percent: 1 });\n \n // PRE moderation (original)\n let mIn: MediaModerationResult | null | undefined;\n if (moderation?.moderateInput) {\n onProgress?.({ phase: \"moderation_input\", percent: 0 });\n mIn = attachProvider(\n await moderation.moderateInput({\n spec,\n kind: spec.kind,\n localPath: inputPath,\n mime: io.input.mime,\n }),\n moderation.provider\n );\n onProgress?.({ phase: \"moderation_input\", percent: 1 });\n \n // Fail closed on both \"rejected\" (policy violation) and \"error\" (the\n // moderation check itself did not complete — quota/outage/API failure).\n // A non-passing check must NEVER fall through to transcode + persist:\n // published bytes must always have been scanned. An \"error\" is a system\n // failure, not a content rejection, so it returns the distinct,\n // retryable `processing_failed` code AND carries `moderation` (status\n // \"error\") on the result — callers route that to their retryable\n // moderation lane and land the row failed rather than publishing it.\n if (mIn?.status === \"rejected\" || mIn?.status === \"error\") {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n moderation: mIn,\n error:\n mIn.status === \"error\"\n ? {\n code: \"processing_failed\",\n message: \"Moderation check did not complete.\",\n details: { provider: mIn.provider, reason: \"moderation_error\" },\n }\n : {\n code: \"rejected\",\n message: \"Rejected by moderation.\",\n details: { provider: mIn.provider, reasons: mIn.reasons, findings: mIn.findings },\n },\n };\n }\n }\n \n const result = await processMedia(spec, {\n inputPath,\n outputBasePath,\n inputMime: io.input.mime,\n }, { signal, onProgress });\n\n // Output size enforcement (processed outputs)\n if (result.ok && result.outputs?.length) {\n const perFileLimit = spec.maxOutputBytes ?? spec.maxBytes ?? 200 * 1024 * 1024; // 200MB default\n const totalLimit = spec.maxTotalOutputBytes ?? spec.maxOutputBytes ?? spec.maxBytes ?? 400 * 1024 * 1024; // 400MB default\n\n let total = 0;\n for (const o of result.outputs) {\n if (!o.path) continue;\n const s = await stat(o.path);\n const size = Number(s.size ?? 0);\n total += size;\n if (!o.sizeBytes) o.sizeBytes = size;\n if (size > perFileLimit) {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"too_large\",\n message: \"Processed output exceeds allowed size.\",\n details: { key: o.key, sizeBytes: size, limitBytes: perFileLimit },\n },\n };\n }\n if (total > totalLimit) {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"too_large\",\n message: \"Total processed outputs exceed allowed size.\",\n details: { totalBytes: total, limitBytes: totalLimit },\n },\n };\n }\n }\n }\n \n if (!result.ok || !result.outputs?.length) {\n if (mIn) result.moderation = mIn;\n onProgress?.({ phase: \"done\", percent: result.ok ? 1 : 0 });\n return result;\n }\n \n // POST moderation (processed outputs) — runs on the LOCAL output paths\n // BEFORE any final persistence, so rejected output never reaches the\n // final store (R2 / emulator). Ordering is load-bearing — see\n // the consuming app's media-serving design docs.\n let mOut: MediaModerationResult | null | undefined;\n if (moderation?.moderateOutput) {\n onProgress?.({ phase: \"moderation_output\", percent: 0 });\n mOut = attachProvider(\n await moderation.moderateOutput({\n spec,\n kind: spec.kind,\n outputs: (result.outputs ?? [])\n .filter((o) => !!o.path)\n .map((o) => ({ key: o.key, localPath: o.path!, mime: o.mime })),\n }),\n moderation.provider\n );\n onProgress?.({ phase: \"moderation_output\", percent: 1 });\n\n // Same fail-closed posture as the input check: block both \"rejected\"\n // and \"error\" before any persistence, so an incomplete check can never\n // publish unscanned output. Ordering is load-bearing — see the\n // consuming app's media-serving design docs.\n if (mOut?.status === \"rejected\" || mOut?.status === \"error\") {\n return {\n ok: false,\n mediaType: result.mediaType,\n moderation: mOut,\n error:\n mOut.status === \"error\"\n ? {\n code: \"processing_failed\",\n message: \"Moderation check did not complete.\",\n details: { provider: mOut.provider, reason: \"moderation_error\" },\n }\n : {\n code: \"rejected\",\n message: \"Rejected by moderation.\",\n details: { provider: mOut.provider, reasons: mOut.reasons, findings: mOut.findings },\n },\n };\n }\n }\n\n // Persist outputs — only after input AND output moderation both passed.\n onProgress?.({ phase: \"persist_outputs\", percent: 0 });\n const totalOut = result.outputs.length;\n let outIdx = 0;\n for (const out of result.outputs) {\n if (signal?.aborted) {\n return { ok: false, mediaType: result.mediaType, error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n onProgress?.({ phase: \"persist_outputs\", percent: totalOut ? outIdx / totalOut : 0, detail: { key: out.key } });\n if (!out.path) continue;\n const persisted = await io.output.writeFromFile(out.path, out.key);\n if (persisted.url) out.url = persisted.url;\n if (persisted.path) out.path = persisted.path;\n outIdx += 1;\n }\n onProgress?.({ phase: \"persist_outputs\", percent: 1 });\n\n const merged = mergeModeration(mIn ?? undefined, mOut ?? undefined);\n if (merged) result.moderation = merged;\n\n onProgress?.({ phase: \"done\", percent: 1 });\n \n return result;\n } finally {\n await ws.cleanup();\n }\n }\n "]}
1
+ {"version":3,"file":"run-pipeline.js","sourceRoot":"","sources":["../src/run-pipeline.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAyB,MAAM,+BAA+B,CAAC;AAGpF,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAgCxC,SAAS,qBAAqB,CAAC,IAAiC;IAC9D,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACrC,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACrC,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACrC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,cAAc,CACrB,CAA2C,EAC3C,QAAiB;IAEjB,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AAC5F,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAqB;IAC1D,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,sBAAsB,EAAE,GAAG,IAAI,CAAC;IACtG,6EAA6E;IAC7E,4EAA4E;IAC5E,IAAI,UAA6C,CAAC;IAClD,MAAM,cAAc,GAAG,CAAC,CAAwB,EAAyB,EAAE,CACzE,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAExC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACpB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;YAC3C,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,sBAAsB,EAAE;SACxE,CAAC;IACJ,CAAC;IAED,kEAAkE;IAClE,IAAI,CAAC;QACH,IAAI,GAAG,wBAAwB,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;YAC3C,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,8BAA8B;gBACvC,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,IAAI,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE;aAC1D;SACF,CAAC;IACJ,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,mBAAmB,EAAE,CAAC;IAEvC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAEzD,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,sBAAsB,EAAE,EAAE,CAAC;QAC7I,CAAC;QACD,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QACrC,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QAElD,wEAAwE;QACxE,yEAAyE;QACzE,6EAA6E;QAC7E,IAAI,sBAAsB,EAAE,CAAC;YAC3B,UAAU,GAAG,MAAM,YAAY,CAAC;gBAC9B,SAAS,EAAE,SAAS;gBACpB,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7B,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC9D,CAAC,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,sBAAsB,CAAC,UAAU,CAAC,CAAC;YAC5D,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;gBAC3B,OAAO,cAAc,CAAC;oBACpB,EAAE,EAAE,KAAK;oBACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;oBAC3C,KAAK,EAAE,UAAU,CAAC,MAAM;iBACzB,CAAC,CAAC;YACL,CAAC;YACD,IAAI,CAAC;gBACH,IAAI,GAAG,wBAAwB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACnD,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBAChB,OAAO,cAAc,CAAC;oBACpB,EAAE,EAAE,KAAK;oBACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;oBAC3C,KAAK,EAAE;wBACL,IAAI,EAAE,cAAc;wBACpB,OAAO,EAAE,0DAA0D;wBACnE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,IAAI,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE;qBAC1D;iBACF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,4BAA4B;QAC5B,IAAI,GAA6C,CAAC;QAClD,IAAI,UAAU,EAAE,aAAa,EAAE,CAAC;YAC9B,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YACxD,GAAG,GAAG,cAAc,CAClB,MAAM,UAAU,CAAC,aAAa,CAAC;gBAC7B,IAAI;gBACJ,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,SAAS,EAAE,SAAS;gBACpB,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI;aACpB,CAAC,EACF,UAAU,CAAC,QAAQ,CACpB,CAAC;YACF,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YAExD,qEAAqE;YACrE,wEAAwE;YACxE,sEAAsE;YACtE,wEAAwE;YACxE,gEAAgE;YAChE,sEAAsE;YACtE,iEAAiE;YACjE,qEAAqE;YACrE,IAAI,GAAG,EAAE,MAAM,KAAK,UAAU,IAAI,GAAG,EAAE,MAAM,KAAK,OAAO,EAAE,CAAC;gBAC1D,OAAO,cAAc,CAAC;oBACpB,EAAE,EAAE,KAAK;oBACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;oBAC3C,UAAU,EAAE,GAAG;oBACf,KAAK,EACH,GAAG,CAAC,MAAM,KAAK,OAAO;wBACpB,CAAC,CAAC;4BACE,IAAI,EAAE,mBAAmB;4BACzB,OAAO,EAAE,oCAAoC;4BAC7C,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE;yBAChE;wBACH,CAAC,CAAC;4BACE,IAAI,EAAE,UAAU;4BAChB,OAAO,EAAE,yBAAyB;4BAClC,OAAO,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE;yBAClF;iBACR,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE;YACtC,SAAS;YACT,cAAc;YACd,SAAS,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI;SACzB,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QAE3B,8CAA8C;QAC9C,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;YACxC,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,gBAAgB;YAChG,MAAM,UAAU,GAAG,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,gBAAgB;YAE1H,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/B,IAAI,CAAC,CAAC,CAAC,IAAI;oBAAE,SAAS;gBACtB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;gBACjC,KAAK,IAAI,IAAI,CAAC;gBACd,IAAI,CAAC,CAAC,CAAC,SAAS;oBAAE,CAAC,CAAC,SAAS,GAAG,IAAI,CAAC;gBACrC,IAAI,IAAI,GAAG,YAAY,EAAE,CAAC;oBACxB,OAAO,cAAc,CAAC;wBACpB,EAAE,EAAE,KAAK;wBACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;wBAC3C,KAAK,EAAE;4BACL,IAAI,EAAE,WAAW;4BACjB,OAAO,EAAE,wCAAwC;4BACjD,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE;yBACnE;qBACF,CAAC,CAAC;gBACL,CAAC;gBACD,IAAI,KAAK,GAAG,UAAU,EAAE,CAAC;oBACvB,OAAO,cAAc,CAAC;wBACpB,EAAE,EAAE,KAAK;wBACT,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC;wBAC3C,KAAK,EAAE;4BACL,IAAI,EAAE,WAAW;4BACjB,OAAO,EAAE,8CAA8C;4BACvD,OAAO,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE;yBACvD;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;YAC1C,IAAI,GAAG;gBAAE,MAAM,CAAC,UAAU,GAAG,GAAG,CAAC;YACjC,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC5D,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC;QAED,uEAAuE;QACvE,qEAAqE;QACrE,8DAA8D;QAC9D,iDAAiD;QACjD,IAAI,IAA8C,CAAC;QACnD,IAAI,UAAU,EAAE,cAAc,EAAE,CAAC;YAC/B,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YACzD,IAAI,GAAG,cAAc,CACnB,MAAM,UAAU,CAAC,cAAc,CAAC;gBAC9B,IAAI;gBACJ,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;qBAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;qBACvB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,IAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;aAClE,CAAC,EACF,UAAU,CAAC,QAAQ,CACpB,CAAC;YACF,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YAEzD,qEAAqE;YACrE,uEAAuE;YACvE,+DAA+D;YAC/D,6CAA6C;YAC7C,IAAI,IAAI,EAAE,MAAM,KAAK,UAAU,IAAI,IAAI,EAAE,MAAM,KAAK,OAAO,EAAE,CAAC;gBAC5D,OAAO,cAAc,CAAC;oBACpB,EAAE,EAAE,KAAK;oBACT,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,UAAU,EAAE,IAAI;oBAChB,KAAK,EACH,IAAI,CAAC,MAAM,KAAK,OAAO;wBACrB,CAAC,CAAC;4BACE,IAAI,EAAE,mBAAmB;4BACzB,OAAO,EAAE,oCAAoC;4BAC7C,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE;yBACjE;wBACH,CAAC,CAAC;4BACE,IAAI,EAAE,UAAU;4BAChB,OAAO,EAAE,yBAAyB;4BAClC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE;yBACrF;iBACR,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,wEAAwE;QACxE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;QACvC,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACpB,OAAO,cAAc,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,sBAAsB,EAAE,EAAE,CAAC,CAAC;YAC7I,CAAC;YACD,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAChH,IAAI,CAAC,GAAG,CAAC,IAAI;gBAAE,SAAS;YACxB,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;YACnE,IAAI,SAAS,CAAC,GAAG;gBAAE,GAAG,CAAC,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC;YAC3C,IAAI,SAAS,CAAC,IAAI;gBAAE,GAAG,CAAC,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC;YAC9C,MAAM,IAAI,CAAC,CAAC;QACd,CAAC;QACD,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QAEvD,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,IAAI,SAAS,EAAE,IAAI,IAAI,SAAS,CAAC,CAAC;QACpE,IAAI,MAAM;YAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC;QAEvC,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QAE5C,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC;IACrB,CAAC;AACH,CAAC","sourcesContent":["import type {\n MediaProcessingResult,\n MediaProcessingSpec,\n MediaModerationResult,\n MediaInspectionResult,\n MediaProcessingError,\n } from \"@ttt-productions/media-schemas\";\nimport { parseMediaProcessingSpec } from \"@ttt-productions/media-schemas\";\n import { createTempWorkspace } from \"./workspace/temp.js\";\n import { processMedia } from \"./process-media.js\";\n import { inspectMedia, type InspectMediaDeps } from \"./inspection/inspect-media.js\";\n import type { MediaIO } from \"./io/types.js\";\n import type { ModerationAdapter } from \"./moderation/types.js\";\n import { mergeModeration } from \"./moderation/merge.js\";\n import path from \"node:path\";\n import { stat } from \"node:fs/promises\";\n\n export interface RunPipelineArgs {\n spec: MediaProcessingSpec;\n io: MediaIO;\n outputBaseName?: string;\n moderation?: ModerationAdapter;\n signal?: AbortSignal;\n onProgress?: (p: import(\"./types.js\").MediaPipelineProgress) => void;\n /**\n * CANONICAL INSPECTION SEAM (canonical-upload-content-classification).\n * When present, the pipeline runs `inspectMedia` on the just-downloaded temp\n * input — BEFORE moderation and processing — and calls this policy hook with\n * the bounded result. The hook either:\n * - returns `{ spec }` — the caller's policy adapter selected the\n * processing spec FROM the inspection (the byte-derived kind, never the\n * client MIME); the pipeline continues with that spec; or\n * - returns `{ reject }` — a typed terminal (`kind_mismatch`,\n * `unsupported_format`, `unsupported_codec`, …); the pipeline stops\n * before any transcode/moderation work.\n * The inspection result rides the returned `MediaProcessingResult.inspection`\n * either way, so the finalizer hands the SAME object to the safety gate —\n * one authority, no re-detection. Callers that omit the hook get the\n * pre-existing behavior unchanged.\n */\n resolveAfterInspection?: (\n inspection: MediaInspectionResult,\n ) => Promise<{ spec: MediaProcessingSpec } | { reject: MediaProcessingError }> | ({ spec: MediaProcessingSpec } | { reject: MediaProcessingError });\n /** Inspection tool overrides (tests). */\n inspectionDeps?: InspectMediaDeps;\n }\n \n function mediaTypeFromSpecKind(kind: MediaProcessingSpec[\"kind\"]): \"image\" | \"video\" | \"audio\" | \"other\" {\n if (kind === \"image\") return \"image\";\n if (kind === \"video\") return \"video\";\n if (kind === \"audio\") return \"audio\";\n return \"other\";\n }\n \n function attachProvider(\n m: MediaModerationResult | null | undefined,\n provider?: string\n ): MediaModerationResult | null | undefined {\n if (!m) return m;\n return { ...m, provider: m.provider ?? provider, reviewedAt: m.reviewedAt ?? Date.now() };\n }\n \n export async function runMediaPipeline(args: RunPipelineArgs): Promise<MediaProcessingResult> {\n let { spec } = args;\n const { io, outputBaseName = \"media\", moderation, signal, onProgress, resolveAfterInspection } = args;\n // Carried onto every return path once produced — the finalizer must hand the\n // SAME inspection object to the safety gate (one classification authority).\n let inspection: MediaInspectionResult | undefined;\n const withInspection = (r: MediaProcessingResult): MediaProcessingResult =>\n inspection ? { ...r, inspection } : r;\n\n if (signal?.aborted) {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: { code: \"processing_canceled\", message: \"Processing canceled.\" },\n };\n }\n\n // Runtime contract validation (fast-fail with actionable errors).\n try {\n spec = parseMediaProcessingSpec(spec);\n } catch (e: any) {\n return {\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"invalid_spec\",\n message: \"Invalid MediaProcessingSpec.\",\n details: { issues: e?.issues ?? e?.message ?? String(e) },\n },\n };\n }\n \n const ws = await createTempWorkspace();\n \n try {\n const inputPath = path.join(ws.dir, \"input\");\n const outputBasePath = path.join(ws.dir, outputBaseName);\n \n if (signal?.aborted) {\n return { ok: false, mediaType: mediaTypeFromSpecKind(spec.kind), error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n onProgress?.({ phase: \"read_input\", percent: 0 });\n await io.input.readToFile(inputPath);\n onProgress?.({ phase: \"read_input\", percent: 1 });\n\n // CANONICAL INSPECTION (before moderation and processing): classify the\n // just-downloaded bytes structurally and let the caller's policy adapter\n // pick the spec from the result. See RunPipelineArgs.resolveAfterInspection.\n if (resolveAfterInspection) {\n inspection = await inspectMedia({\n localPath: inputPath,\n ...(signal ? { signal } : {}),\n ...(args.inspectionDeps ? { deps: args.inspectionDeps } : {}),\n });\n const resolution = await resolveAfterInspection(inspection);\n if (\"reject\" in resolution) {\n return withInspection({\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: resolution.reject,\n });\n }\n try {\n spec = parseMediaProcessingSpec(resolution.spec);\n } catch (e: any) {\n return withInspection({\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"invalid_spec\",\n message: \"Invalid MediaProcessingSpec from resolveAfterInspection.\",\n details: { issues: e?.issues ?? e?.message ?? String(e) },\n },\n });\n }\n }\n\n // PRE moderation (original)\n let mIn: MediaModerationResult | null | undefined;\n if (moderation?.moderateInput) {\n onProgress?.({ phase: \"moderation_input\", percent: 0 });\n mIn = attachProvider(\n await moderation.moderateInput({\n spec,\n kind: spec.kind,\n localPath: inputPath,\n mime: io.input.mime,\n }),\n moderation.provider\n );\n onProgress?.({ phase: \"moderation_input\", percent: 1 });\n \n // Fail closed on both \"rejected\" (policy violation) and \"error\" (the\n // moderation check itself did not complete — quota/outage/API failure).\n // A non-passing check must NEVER fall through to transcode + persist:\n // published bytes must always have been scanned. An \"error\" is a system\n // failure, not a content rejection, so it returns the distinct,\n // retryable `processing_failed` code AND carries `moderation` (status\n // \"error\") on the result — callers route that to their retryable\n // moderation lane and land the row failed rather than publishing it.\n if (mIn?.status === \"rejected\" || mIn?.status === \"error\") {\n return withInspection({\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n moderation: mIn,\n error:\n mIn.status === \"error\"\n ? {\n code: \"processing_failed\",\n message: \"Moderation check did not complete.\",\n details: { provider: mIn.provider, reason: \"moderation_error\" },\n }\n : {\n code: \"rejected\",\n message: \"Rejected by moderation.\",\n details: { provider: mIn.provider, reasons: mIn.reasons, findings: mIn.findings },\n },\n });\n }\n }\n \n const result = await processMedia(spec, {\n inputPath,\n outputBasePath,\n inputMime: io.input.mime,\n }, { signal, onProgress });\n\n // Output size enforcement (processed outputs)\n if (result.ok && result.outputs?.length) {\n const perFileLimit = spec.maxOutputBytes ?? spec.maxBytes ?? 200 * 1024 * 1024; // 200MB default\n const totalLimit = spec.maxTotalOutputBytes ?? spec.maxOutputBytes ?? spec.maxBytes ?? 400 * 1024 * 1024; // 400MB default\n\n let total = 0;\n for (const o of result.outputs) {\n if (!o.path) continue;\n const s = await stat(o.path);\n const size = Number(s.size ?? 0);\n total += size;\n if (!o.sizeBytes) o.sizeBytes = size;\n if (size > perFileLimit) {\n return withInspection({\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"too_large\",\n message: \"Processed output exceeds allowed size.\",\n details: { key: o.key, sizeBytes: size, limitBytes: perFileLimit },\n },\n });\n }\n if (total > totalLimit) {\n return withInspection({\n ok: false,\n mediaType: mediaTypeFromSpecKind(spec.kind),\n error: {\n code: \"too_large\",\n message: \"Total processed outputs exceed allowed size.\",\n details: { totalBytes: total, limitBytes: totalLimit },\n },\n });\n }\n }\n }\n \n if (!result.ok || !result.outputs?.length) {\n if (mIn) result.moderation = mIn;\n onProgress?.({ phase: \"done\", percent: result.ok ? 1 : 0 });\n return withInspection(result);\n }\n \n // POST moderation (processed outputs) — runs on the LOCAL output paths\n // BEFORE any final persistence, so rejected output never reaches the\n // final store (R2 / emulator). Ordering is load-bearing — see\n // the consuming app's media-serving design docs.\n let mOut: MediaModerationResult | null | undefined;\n if (moderation?.moderateOutput) {\n onProgress?.({ phase: \"moderation_output\", percent: 0 });\n mOut = attachProvider(\n await moderation.moderateOutput({\n spec,\n kind: spec.kind,\n outputs: (result.outputs ?? [])\n .filter((o) => !!o.path)\n .map((o) => ({ key: o.key, localPath: o.path!, mime: o.mime })),\n }),\n moderation.provider\n );\n onProgress?.({ phase: \"moderation_output\", percent: 1 });\n\n // Same fail-closed posture as the input check: block both \"rejected\"\n // and \"error\" before any persistence, so an incomplete check can never\n // publish unscanned output. Ordering is load-bearing — see the\n // consuming app's media-serving design docs.\n if (mOut?.status === \"rejected\" || mOut?.status === \"error\") {\n return withInspection({\n ok: false,\n mediaType: result.mediaType,\n moderation: mOut,\n error:\n mOut.status === \"error\"\n ? {\n code: \"processing_failed\",\n message: \"Moderation check did not complete.\",\n details: { provider: mOut.provider, reason: \"moderation_error\" },\n }\n : {\n code: \"rejected\",\n message: \"Rejected by moderation.\",\n details: { provider: mOut.provider, reasons: mOut.reasons, findings: mOut.findings },\n },\n });\n }\n }\n\n // Persist outputs — only after input AND output moderation both passed.\n onProgress?.({ phase: \"persist_outputs\", percent: 0 });\n const totalOut = result.outputs.length;\n let outIdx = 0;\n for (const out of result.outputs) {\n if (signal?.aborted) {\n return withInspection({ ok: false, mediaType: result.mediaType, error: { code: \"processing_canceled\", message: \"Processing canceled.\" } });\n }\n onProgress?.({ phase: \"persist_outputs\", percent: totalOut ? outIdx / totalOut : 0, detail: { key: out.key } });\n if (!out.path) continue;\n const persisted = await io.output.writeFromFile(out.path, out.key);\n if (persisted.url) out.url = persisted.url;\n if (persisted.path) out.path = persisted.path;\n outIdx += 1;\n }\n onProgress?.({ phase: \"persist_outputs\", percent: 1 });\n\n const merged = mergeModeration(mIn ?? undefined, mOut ?? undefined);\n if (merged) result.moderation = merged;\n\n onProgress?.({ phase: \"done\", percent: 1 });\n \n return withInspection(result);\n } finally {\n await ws.cleanup();\n }\n }\n "]}
@@ -2,6 +2,13 @@ export interface RunCmdResult {
2
2
  code: number;
3
3
  stdout: string;
4
4
  stderr: string;
5
+ /** True when the process was killed by the wall-clock timeout (see runCmd
6
+ * opts.timeoutMs). Callers must treat a timed-out result as INVALID —
7
+ * never parse its partial output as an answer. */
8
+ timedOut?: boolean;
9
+ /** True when a captured stream hit the 1 MiB cap — the output is truncated
10
+ * and must not be parsed as complete (e.g. incomplete probe JSON). */
11
+ truncated?: boolean;
5
12
  }
6
13
  export type FfmpegProgressPhase = "transcode" | "poster";
7
14
  export interface RunFfmpegOptions {
@@ -19,6 +26,13 @@ export interface RunFfmpegOptions {
19
26
  }
20
27
  export declare function runCmd(cmd: string, args: string[], opts?: {
21
28
  cwd?: string;
29
+ /** Hard wall-clock ceiling; on expiry the child is SIGKILLed and the
30
+ * result resolves with `timedOut: true`. A malformed/adversarial input
31
+ * must never be able to wedge a warm instance (canonical-upload-content-
32
+ * classification hardening, 2026-07-31). */
33
+ timeoutMs?: number;
34
+ /** Abort kills the child immediately (same SIGKILL path as the timeout). */
35
+ signal?: AbortSignal;
22
36
  }): Promise<RunCmdResult>;
23
37
  /**
24
38
  * Runs ffmpeg with optional AbortSignal + progress reporting.
@@ -1 +1 @@
1
- {"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,mBAAmB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/F,0CAA0C;IAC1C,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAOD,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAiBlG;AA4CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFxF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CACpD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAC/D,CAcA"}
1
+ {"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;uDAEmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;2EACuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,mBAAmB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/F,0CAA0C;IAC1C,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAOD,wBAAgB,MAAM,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;iDAG6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GACA,OAAO,CAAC,YAAY,CAAC,CAoDvB;AA4CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFxF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CACpD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAC/D,CAcA"}
@@ -7,16 +7,58 @@ export function runCmd(cmd, args, opts) {
7
7
  const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: ["ignore", "pipe", "pipe"] });
8
8
  let stdout = "";
9
9
  let stderr = "";
10
+ let timedOut = false;
11
+ let truncated = false;
12
+ const kill = () => {
13
+ try {
14
+ p.kill("SIGKILL");
15
+ }
16
+ catch {
17
+ /* already dead */
18
+ }
19
+ };
20
+ const timer = opts?.timeoutMs && opts.timeoutMs > 0
21
+ ? setTimeout(() => {
22
+ timedOut = true;
23
+ kill();
24
+ }, opts.timeoutMs)
25
+ : null;
26
+ const onAbort = () => {
27
+ timedOut = true; // treated identically: the result is invalid
28
+ kill();
29
+ };
30
+ if (opts?.signal) {
31
+ if (opts.signal.aborted)
32
+ onAbort();
33
+ else
34
+ opts.signal.addEventListener("abort", onAbort, { once: true });
35
+ }
10
36
  p.stdout.on("data", (d) => {
11
37
  if (stdout.length < MAX_CAPTURE_BYTES)
12
38
  stdout += String(d);
39
+ else
40
+ truncated = true;
13
41
  });
14
42
  p.stderr.on("data", (d) => {
15
43
  if (stderr.length < MAX_CAPTURE_BYTES)
16
44
  stderr += String(d);
45
+ else
46
+ truncated = true;
47
+ });
48
+ p.on("error", (e) => {
49
+ if (timer)
50
+ clearTimeout(timer);
51
+ if (opts?.signal)
52
+ opts.signal.removeEventListener("abort", onAbort);
53
+ reject(e);
54
+ });
55
+ p.on("close", (code) => {
56
+ if (timer)
57
+ clearTimeout(timer);
58
+ if (opts?.signal)
59
+ opts.signal.removeEventListener("abort", onAbort);
60
+ resolve({ code: code ?? 0, stdout, stderr, ...(timedOut ? { timedOut } : {}), ...(truncated ? { truncated } : {}) });
17
61
  });
18
- p.on("error", reject);
19
- p.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr }));
20
62
  });
21
63
  }
22
64
  function abortError() {
@@ -1 +1 @@
1
- {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\n\nexport interface RunCmdResult {\n code: number;\n stdout: string;\n stderr: string;\n}\n\nexport type FfmpegProgressPhase = \"transcode\" | \"poster\";\n\nexport interface RunFfmpegOptions {\n cwd?: string;\n signal?: AbortSignal;\n /** Optional total duration in milliseconds (used to compute percent). */\n durationMs?: number;\n onProgress?: (p: { phase: FfmpegProgressPhase; percent?: number; outTimeMs?: number }) => void;\n /** Which phase to report in callbacks. */\n phase?: FfmpegProgressPhase;\n}\n\nconst MAX_CAPTURE_BYTES = 1024 * 1024; // 1MB per stream\n\nlet ffmpegChecked: boolean | null = null;\nlet ffmpegVersion: string | null = null;\n\nexport function runCmd(cmd: string, args: string[], opts?: { cwd?: string }): Promise<RunCmdResult> {\n return new Promise((resolve, reject) => {\n const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n\n p.stdout.on(\"data\", (d) => {\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += String(d);\n });\n p.stderr.on(\"data\", (d) => {\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += String(d);\n });\n\n p.on(\"error\", reject);\n p.on(\"close\", (code) => resolve({ code: code ?? 0, stdout, stderr }));\n });\n}\n\nfunction abortError(): DOMException {\n return new DOMException(\"Aborted\", \"AbortError\");\n}\n\n// NOTE: ffmpeg prints \"out_time_ms\" but it is microseconds in known builds.\n// We harden parsing and sanity-check values.\nfunction parseOutTimeMicroseconds(value: string): number | undefined {\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) return undefined;\n\n // Discard absurd values (100 years in microseconds).\n const MAX_REASONABLE_US = 100 * 365 * 24 * 60 * 60 * 1_000_000;\n if (n > MAX_REASONABLE_US) return undefined;\n\n return n;\n}\n\nfunction parseOutTimeHmsToMs(value: string): number | undefined {\n // \"HH:MM:SS.micro\" (micro may be omitted)\n const m = String(value).trim().match(/^(\\d+):([0-5]\\d):([0-5]\\d)(?:\\.(\\d{1,6}))?$/);\n if (!m) return undefined;\n\n const hh = Number(m[1]);\n const mm = Number(m[2]);\n const ss = Number(m[3]);\n if (!Number.isFinite(hh) || !Number.isFinite(mm) || !Number.isFinite(ss)) return undefined;\n\n const frac = m[4] ?? \"\";\n const us = frac ? Number((frac + \"000000\").slice(0, 6)) : 0;\n if (!Number.isFinite(us) || us < 0) return undefined;\n\n const totalMs = (hh * 3600 + mm * 60 + ss) * 1000 + Math.floor(us / 1000);\n return totalMs >= 0 ? totalMs : undefined;\n}\n\nfunction clamp01(n: number) {\n if (!Number.isFinite(n)) return undefined;\n if (n < 0) return 0;\n if (n > 1) return 1;\n return n;\n}\n\n/**\n * Runs ffmpeg with optional AbortSignal + progress reporting.\n * If onProgress is provided, ffmpeg is invoked with \"-progress pipe:1\".\n */\nexport function runFfmpeg(args: string[], opts?: RunFfmpegOptions): Promise<RunCmdResult> {\n const phase: FfmpegProgressPhase = opts?.phase ?? \"transcode\";\n const withProgress = !!opts?.onProgress;\n const ffArgs = withProgress ? [\"-progress\", \"pipe:1\", \"-nostats\", ...args] : args;\n\n return new Promise((resolve, reject) => {\n if (opts?.signal?.aborted) return reject(abortError());\n\n const p = spawn(\"ffmpeg\", ffArgs, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let progressBuf = \"\";\n\n // keep progress monotonic\n let lastOutTimeMs: number | undefined;\n\n const onAbort = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {}\n };\n opts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n p.stdout.on(\"data\", (d) => {\n const s = String(d);\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += s;\n\n if (!withProgress) return;\n\n progressBuf += s;\n const lines = progressBuf.split(/\\r?\\n/);\n progressBuf = lines.pop() ?? \"\";\n\n let outTimeMs: number | undefined;\n\n for (const line of lines) {\n const idx = line.indexOf(\"=\");\n if (idx <= 0) continue;\n\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n\n if (k === \"out_time_ms\") {\n const us = parseOutTimeMicroseconds(v);\n if (us !== undefined) outTimeMs = Math.floor(us / 1000);\n } else if (k === \"out_time\" && outTimeMs == null) {\n const ms = parseOutTimeHmsToMs(v);\n if (ms !== undefined) outTimeMs = ms;\n } else if (k === \"progress\" && v === \"end\") {\n opts?.onProgress?.({ phase, percent: 1, outTimeMs: lastOutTimeMs });\n }\n }\n\n if (outTimeMs != null) {\n // monotonic guard\n if (lastOutTimeMs == null || outTimeMs >= lastOutTimeMs) {\n lastOutTimeMs = outTimeMs;\n }\n\n const dur = opts?.durationMs;\n const percent =\n dur && dur > 0 && lastOutTimeMs != null\n ? // clamp to <1 during processing (end event sends 1)\n clamp01(Math.min(0.999, lastOutTimeMs / dur))\n : undefined;\n\n opts?.onProgress?.({ phase, percent, outTimeMs: lastOutTimeMs });\n }\n });\n\n p.stderr.on(\"data\", (d) => {\n const s = String(d);\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += s;\n });\n\n p.on(\"error\", (e) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n\n p.on(\"close\", (code) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n if (opts?.signal?.aborted) return reject(abortError());\n resolve({ code: code ?? 0, stdout, stderr });\n });\n });\n}\n\nexport async function ensureFfmpegAvailable(): Promise<\n { ok: true; version?: string } | { ok: false; error: unknown }\n> {\n if (ffmpegChecked) return { ok: true, version: ffmpegVersion ?? undefined };\n\n try {\n const r = await runCmd(\"ffmpeg\", [\"-version\"]);\n if (r.code === 0) {\n ffmpegChecked = true;\n ffmpegVersion = (r.stdout || r.stderr || \"\").split(/\\r?\\n/)[0] || null;\n return { ok: true, version: ffmpegVersion ?? undefined };\n }\n return { ok: false, error: new Error(\"ffmpeg not available\") };\n } catch (e) {\n return { ok: false, error: e };\n }\n}\n"]}
1
+ {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\n\nexport interface RunCmdResult {\n code: number;\n stdout: string;\n stderr: string;\n /** True when the process was killed by the wall-clock timeout (see runCmd\n * opts.timeoutMs). Callers must treat a timed-out result as INVALID —\n * never parse its partial output as an answer. */\n timedOut?: boolean;\n /** True when a captured stream hit the 1 MiB cap — the output is truncated\n * and must not be parsed as complete (e.g. incomplete probe JSON). */\n truncated?: boolean;\n}\n\nexport type FfmpegProgressPhase = \"transcode\" | \"poster\";\n\nexport interface RunFfmpegOptions {\n cwd?: string;\n signal?: AbortSignal;\n /** Optional total duration in milliseconds (used to compute percent). */\n durationMs?: number;\n onProgress?: (p: { phase: FfmpegProgressPhase; percent?: number; outTimeMs?: number }) => void;\n /** Which phase to report in callbacks. */\n phase?: FfmpegProgressPhase;\n}\n\nconst MAX_CAPTURE_BYTES = 1024 * 1024; // 1MB per stream\n\nlet ffmpegChecked: boolean | null = null;\nlet ffmpegVersion: string | null = null;\n\nexport function runCmd(\n cmd: string,\n args: string[],\n opts?: {\n cwd?: string;\n /** Hard wall-clock ceiling; on expiry the child is SIGKILLed and the\n * result resolves with `timedOut: true`. A malformed/adversarial input\n * must never be able to wedge a warm instance (canonical-upload-content-\n * classification hardening, 2026-07-31). */\n timeoutMs?: number;\n /** Abort kills the child immediately (same SIGKILL path as the timeout). */\n signal?: AbortSignal;\n }\n): Promise<RunCmdResult> {\n return new Promise((resolve, reject) => {\n const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n let truncated = false;\n\n const kill = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {\n /* already dead */\n }\n };\n const timer =\n opts?.timeoutMs && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n kill();\n }, opts.timeoutMs)\n : null;\n const onAbort = () => {\n timedOut = true; // treated identically: the result is invalid\n kill();\n };\n if (opts?.signal) {\n if (opts.signal.aborted) onAbort();\n else opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n p.stdout.on(\"data\", (d) => {\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += String(d);\n else truncated = true;\n });\n p.stderr.on(\"data\", (d) => {\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += String(d);\n else truncated = true;\n });\n\n p.on(\"error\", (e) => {\n if (timer) clearTimeout(timer);\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n p.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", onAbort);\n resolve({ code: code ?? 0, stdout, stderr, ...(timedOut ? { timedOut } : {}), ...(truncated ? { truncated } : {}) });\n });\n });\n}\n\nfunction abortError(): DOMException {\n return new DOMException(\"Aborted\", \"AbortError\");\n}\n\n// NOTE: ffmpeg prints \"out_time_ms\" but it is microseconds in known builds.\n// We harden parsing and sanity-check values.\nfunction parseOutTimeMicroseconds(value: string): number | undefined {\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) return undefined;\n\n // Discard absurd values (100 years in microseconds).\n const MAX_REASONABLE_US = 100 * 365 * 24 * 60 * 60 * 1_000_000;\n if (n > MAX_REASONABLE_US) return undefined;\n\n return n;\n}\n\nfunction parseOutTimeHmsToMs(value: string): number | undefined {\n // \"HH:MM:SS.micro\" (micro may be omitted)\n const m = String(value).trim().match(/^(\\d+):([0-5]\\d):([0-5]\\d)(?:\\.(\\d{1,6}))?$/);\n if (!m) return undefined;\n\n const hh = Number(m[1]);\n const mm = Number(m[2]);\n const ss = Number(m[3]);\n if (!Number.isFinite(hh) || !Number.isFinite(mm) || !Number.isFinite(ss)) return undefined;\n\n const frac = m[4] ?? \"\";\n const us = frac ? Number((frac + \"000000\").slice(0, 6)) : 0;\n if (!Number.isFinite(us) || us < 0) return undefined;\n\n const totalMs = (hh * 3600 + mm * 60 + ss) * 1000 + Math.floor(us / 1000);\n return totalMs >= 0 ? totalMs : undefined;\n}\n\nfunction clamp01(n: number) {\n if (!Number.isFinite(n)) return undefined;\n if (n < 0) return 0;\n if (n > 1) return 1;\n return n;\n}\n\n/**\n * Runs ffmpeg with optional AbortSignal + progress reporting.\n * If onProgress is provided, ffmpeg is invoked with \"-progress pipe:1\".\n */\nexport function runFfmpeg(args: string[], opts?: RunFfmpegOptions): Promise<RunCmdResult> {\n const phase: FfmpegProgressPhase = opts?.phase ?? \"transcode\";\n const withProgress = !!opts?.onProgress;\n const ffArgs = withProgress ? [\"-progress\", \"pipe:1\", \"-nostats\", ...args] : args;\n\n return new Promise((resolve, reject) => {\n if (opts?.signal?.aborted) return reject(abortError());\n\n const p = spawn(\"ffmpeg\", ffArgs, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let progressBuf = \"\";\n\n // keep progress monotonic\n let lastOutTimeMs: number | undefined;\n\n const onAbort = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {}\n };\n opts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n p.stdout.on(\"data\", (d) => {\n const s = String(d);\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += s;\n\n if (!withProgress) return;\n\n progressBuf += s;\n const lines = progressBuf.split(/\\r?\\n/);\n progressBuf = lines.pop() ?? \"\";\n\n let outTimeMs: number | undefined;\n\n for (const line of lines) {\n const idx = line.indexOf(\"=\");\n if (idx <= 0) continue;\n\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n\n if (k === \"out_time_ms\") {\n const us = parseOutTimeMicroseconds(v);\n if (us !== undefined) outTimeMs = Math.floor(us / 1000);\n } else if (k === \"out_time\" && outTimeMs == null) {\n const ms = parseOutTimeHmsToMs(v);\n if (ms !== undefined) outTimeMs = ms;\n } else if (k === \"progress\" && v === \"end\") {\n opts?.onProgress?.({ phase, percent: 1, outTimeMs: lastOutTimeMs });\n }\n }\n\n if (outTimeMs != null) {\n // monotonic guard\n if (lastOutTimeMs == null || outTimeMs >= lastOutTimeMs) {\n lastOutTimeMs = outTimeMs;\n }\n\n const dur = opts?.durationMs;\n const percent =\n dur && dur > 0 && lastOutTimeMs != null\n ? // clamp to <1 during processing (end event sends 1)\n clamp01(Math.min(0.999, lastOutTimeMs / dur))\n : undefined;\n\n opts?.onProgress?.({ phase, percent, outTimeMs: lastOutTimeMs });\n }\n });\n\n p.stderr.on(\"data\", (d) => {\n const s = String(d);\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += s;\n });\n\n p.on(\"error\", (e) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n\n p.on(\"close\", (code) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n if (opts?.signal?.aborted) return reject(abortError());\n resolve({ code: code ?? 0, stdout, stderr });\n });\n });\n}\n\nexport async function ensureFfmpegAvailable(): Promise<\n { ok: true; version?: string } | { ok: false; error: unknown }\n> {\n if (ffmpegChecked) return { ok: true, version: ffmpegVersion ?? undefined };\n\n try {\n const r = await runCmd(\"ffmpeg\", [\"-version\"]);\n if (r.code === 0) {\n ffmpegChecked = true;\n ffmpegVersion = (r.stdout || r.stderr || \"\").split(/\\r?\\n/)[0] || null;\n return { ok: true, version: ffmpegVersion ?? undefined };\n }\n return { ok: false, error: new Error(\"ffmpeg not available\") };\n } catch (e) {\n return { ok: false, error: e };\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/media-processing-core",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",
@@ -31,7 +31,7 @@
31
31
  "prepublishOnly": "npm run clean && npm run build"
32
32
  },
33
33
  "dependencies": {
34
- "@ttt-productions/media-schemas": "^0.14.3",
34
+ "@ttt-productions/media-schemas": "^0.14.5",
35
35
  "aws4fetch": "^1.0.20",
36
36
  "sharp": "^0.35.3"
37
37
  },