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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/audio/audio-processor.d.ts +2 -2
  2. package/dist/audio/audio-processor.d.ts.map +1 -1
  3. package/dist/audio/audio-processor.js +63 -50
  4. package/dist/audio/audio-processor.js.map +1 -1
  5. package/dist/audio/probe.d.ts +1 -1
  6. package/dist/audio/probe.d.ts.map +1 -1
  7. package/dist/audio/probe.js +6 -5
  8. package/dist/audio/probe.js.map +1 -1
  9. package/dist/duration/media-duration.d.ts +2 -13
  10. package/dist/duration/media-duration.d.ts.map +1 -1
  11. package/dist/duration/media-duration.js +24 -49
  12. package/dist/duration/media-duration.js.map +1 -1
  13. package/dist/image/formats.d.ts +2 -13
  14. package/dist/image/formats.d.ts.map +1 -1
  15. package/dist/image/formats.js +25 -35
  16. package/dist/image/formats.js.map +1 -1
  17. package/dist/image/image-processor.d.ts +2 -2
  18. package/dist/image/image-processor.d.ts.map +1 -1
  19. package/dist/image/image-processor.js +48 -57
  20. package/dist/image/image-processor.js.map +1 -1
  21. package/dist/image/resize.d.ts +2 -16
  22. package/dist/image/resize.d.ts.map +1 -1
  23. package/dist/image/resize.js +13 -28
  24. package/dist/image/resize.js.map +1 -1
  25. package/dist/index.d.ts +19 -16
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +18 -16
  28. package/dist/index.js.map +1 -1
  29. package/dist/inspection/inspect-media.d.ts +13 -20
  30. package/dist/inspection/inspect-media.d.ts.map +1 -1
  31. package/dist/inspection/inspect-media.js +197 -201
  32. package/dist/inspection/inspect-media.js.map +1 -1
  33. package/dist/io/fs.d.ts +1 -1
  34. package/dist/io/fs.d.ts.map +1 -1
  35. package/dist/io/fs.js +4 -5
  36. package/dist/io/fs.js.map +1 -1
  37. package/dist/io/local-input.d.ts +1 -21
  38. package/dist/io/local-input.d.ts.map +1 -1
  39. package/dist/io/local-input.js +15 -35
  40. package/dist/io/local-input.js.map +1 -1
  41. package/dist/io/types.d.ts +0 -3
  42. package/dist/io/types.d.ts.map +1 -1
  43. package/dist/io/types.js.map +1 -1
  44. package/dist/moderation/merge.d.ts +1 -1
  45. package/dist/moderation/merge.d.ts.map +1 -1
  46. package/dist/moderation/merge.js +1 -2
  47. package/dist/moderation/merge.js.map +1 -1
  48. package/dist/moderation/types.d.ts +3 -5
  49. package/dist/moderation/types.d.ts.map +1 -1
  50. package/dist/moderation/types.js.map +1 -1
  51. package/dist/process-media.d.ts +2 -3
  52. package/dist/process-media.d.ts.map +1 -1
  53. package/dist/process-media.js +9 -9
  54. package/dist/process-media.js.map +1 -1
  55. package/dist/processing/errors.d.ts.map +1 -1
  56. package/dist/processing/errors.js +1 -1
  57. package/dist/processing/errors.js.map +1 -1
  58. package/dist/processing/result.d.ts +2 -2
  59. package/dist/processing/result.js +1 -1
  60. package/dist/processing/result.js.map +1 -1
  61. package/dist/run-pipeline.d.ts +5 -22
  62. package/dist/run-pipeline.d.ts.map +1 -1
  63. package/dist/run-pipeline.js +72 -81
  64. package/dist/run-pipeline.js.map +1 -1
  65. package/dist/server/download-token.d.ts +0 -6
  66. package/dist/server/download-token.d.ts.map +1 -1
  67. package/dist/server/download-token.js +3 -13
  68. package/dist/server/download-token.js.map +1 -1
  69. package/dist/server/firebase-media-io.d.ts +3 -13
  70. package/dist/server/firebase-media-io.d.ts.map +1 -1
  71. package/dist/server/firebase-media-io.js +1 -6
  72. package/dist/server/firebase-media-io.js.map +1 -1
  73. package/dist/server/gcs-errors.d.ts +0 -8
  74. package/dist/server/gcs-errors.d.ts.map +1 -1
  75. package/dist/server/gcs-errors.js +2 -10
  76. package/dist/server/gcs-errors.js.map +1 -1
  77. package/dist/server/index.d.ts +7 -6
  78. package/dist/server/index.d.ts.map +1 -1
  79. package/dist/server/index.js +7 -6
  80. package/dist/server/index.js.map +1 -1
  81. package/dist/server/pending-media.d.ts +3 -34
  82. package/dist/server/pending-media.d.ts.map +1 -1
  83. package/dist/server/pending-media.js +13 -30
  84. package/dist/server/pending-media.js.map +1 -1
  85. package/dist/server/r2-retry.d.ts +4 -52
  86. package/dist/server/r2-retry.d.ts.map +1 -1
  87. package/dist/server/r2-retry.js +49 -134
  88. package/dist/server/r2-retry.js.map +1 -1
  89. package/dist/server/staged-object.d.ts +2 -46
  90. package/dist/server/staged-object.d.ts.map +1 -1
  91. package/dist/server/staged-object.js +4 -48
  92. package/dist/server/staged-object.js.map +1 -1
  93. package/dist/server/storage-ops.d.ts +5 -67
  94. package/dist/server/storage-ops.d.ts.map +1 -1
  95. package/dist/server/storage-ops.js +110 -185
  96. package/dist/server/storage-ops.js.map +1 -1
  97. package/dist/types.d.ts +2 -4
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/utils/log.d.ts.map +1 -1
  101. package/dist/utils/log.js +3 -3
  102. package/dist/utils/log.js.map +1 -1
  103. package/dist/utils/paths.d.ts.map +1 -1
  104. package/dist/utils/paths.js +1 -4
  105. package/dist/utils/paths.js.map +1 -1
  106. package/dist/utils/safe-path.d.ts.map +1 -1
  107. package/dist/utils/safe-path.js +8 -9
  108. package/dist/utils/safe-path.js.map +1 -1
  109. package/dist/validation/validate-duration.d.ts.map +1 -1
  110. package/dist/validation/validate-duration.js.map +1 -1
  111. package/dist/validation/validate-mime.d.ts.map +1 -1
  112. package/dist/validation/validate-mime.js.map +1 -1
  113. package/dist/validation/validate-size.d.ts.map +1 -1
  114. package/dist/validation/validate-size.js.map +1 -1
  115. package/dist/video/ffmpeg.d.ts +1 -23
  116. package/dist/video/ffmpeg.d.ts.map +1 -1
  117. package/dist/video/ffmpeg.js +57 -64
  118. package/dist/video/ffmpeg.js.map +1 -1
  119. package/dist/video/probe.d.ts +1 -1
  120. package/dist/video/probe.d.ts.map +1 -1
  121. package/dist/video/probe.js +10 -8
  122. package/dist/video/probe.js.map +1 -1
  123. package/dist/video/video-processor.d.ts +2 -2
  124. package/dist/video/video-processor.d.ts.map +1 -1
  125. package/dist/video/video-processor.js +125 -96
  126. package/dist/video/video-processor.js.map +1 -1
  127. package/dist/workspace/temp.js +4 -4
  128. package/dist/workspace/temp.js.map +1 -1
  129. package/package.json +2 -2
  130. package/dist/audio/index.d.ts +0 -3
  131. package/dist/audio/index.d.ts.map +0 -1
  132. package/dist/audio/index.js +0 -3
  133. package/dist/audio/index.js.map +0 -1
  134. package/dist/image/index.d.ts +0 -4
  135. package/dist/image/index.d.ts.map +0 -1
  136. package/dist/image/index.js +0 -4
  137. package/dist/image/index.js.map +0 -1
  138. package/dist/video/index.d.ts +0 -4
  139. package/dist/video/index.d.ts.map +0 -1
  140. package/dist/video/index.js +0 -4
  141. package/dist/video/index.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import type { MediaProcessingResult, MediaProcessingSpec } from "@ttt-productions/media-schemas";
2
- import type { ProcessMediaOptions } from "../types.js";
1
+ import type { MediaProcessingResult, MediaProcessingSpec } from '@ttt-productions/media-schemas';
2
+ import type { ProcessMediaOptions } from '../types.js';
3
3
  export declare function processAudio(spec: MediaProcessingSpec, ctx: {
4
4
  inputPath: string;
5
5
  outputBasePath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"audio-processor.d.ts","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAO9G,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA4BvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CA4IhC"}
1
+ {"version":3,"file":"audio-processor.d.ts","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAOxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA4BvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CAyJhC"}
@@ -1,29 +1,29 @@
1
- import { mkdir, stat } from "node:fs/promises";
2
- import path from "node:path";
3
- import { safeOutputPathFor } from "../utils/safe-path.js";
4
- import { ensureFfmpegAvailable, runFfmpeg } from "../video/ffmpeg.js";
5
- import { probeAudio } from "./probe.js";
6
- import { unreadableLengthError } from "../duration/media-duration.js";
7
- import { TIMED_MEDIA_MAIN_OUTPUT_KEY } from "@ttt-productions/media-schemas";
1
+ import { mkdir, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { safeOutputPathFor } from '../utils/safe-path.js';
4
+ import { ensureFfmpegAvailable, runFfmpeg } from '../video/ffmpeg.js';
5
+ import { probeAudio } from './probe.js';
6
+ import { unreadableLengthError } from '../duration/media-duration.js';
7
+ import { TIMED_MEDIA_MAIN_OUTPUT_KEY } from '@ttt-productions/media-schemas';
8
8
  function matchMime(accepted, actual) {
9
9
  const a = accepted.trim().toLowerCase();
10
10
  const m = actual.trim().toLowerCase();
11
11
  if (!a)
12
12
  return true;
13
- if (a === "*/*")
13
+ if (a === '*/*')
14
14
  return true;
15
- if (a.endsWith("/*"))
15
+ if (a.endsWith('/*'))
16
16
  return m.startsWith(a.slice(0, -1));
17
17
  return a === m;
18
18
  }
19
19
  function acceptAllowsAudio(spec) {
20
20
  const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];
21
- return kinds.length === 0 ? true : kinds.includes("audio");
21
+ return kinds.length === 0 ? true : kinds.includes('audio');
22
22
  }
23
23
  function acceptAllowsMime(spec, actualMime) {
24
24
  const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];
25
25
  if (mimes.length === 0)
26
- return true; // empty => accept anything
26
+ return true;
27
27
  if (!actualMime)
28
28
  return false;
29
29
  return mimes.some((a) => matchMime(a, actualMime));
@@ -35,84 +35,93 @@ export async function processAudio(spec, ctx, opts) {
35
35
  const tempFiles = [];
36
36
  try {
37
37
  if (opts?.signal?.aborted) {
38
- return { ok: false, mediaType: "audio", error: { code: "processing_canceled", message: "Processing canceled." } };
38
+ return {
39
+ ok: false,
40
+ mediaType: 'audio',
41
+ error: { code: 'processing_canceled', message: 'Processing canceled.' },
42
+ };
39
43
  }
40
44
  const ff = await ensureFfmpegAvailable();
41
45
  if (!ff.ok) {
42
46
  return {
43
47
  ok: false,
44
- mediaType: "audio",
48
+ mediaType: 'audio',
45
49
  error: {
46
- code: "processing_failed",
47
- message: "ffmpeg is not available.",
48
- details: { error: String(ff.error ?? "unknown") },
50
+ code: 'processing_failed',
51
+ message: 'ffmpeg is not available.',
52
+ details: { error: String(ff.error ?? 'unknown') },
49
53
  },
50
54
  };
51
55
  }
52
56
  if (!acceptAllowsAudio(spec)) {
53
57
  return {
54
58
  ok: false,
55
- mediaType: "audio",
56
- error: { code: "invalid_mime", message: "Spec does not allow audio uploads.", details: { accept: spec.accept } },
59
+ mediaType: 'audio',
60
+ error: {
61
+ code: 'invalid_mime',
62
+ message: 'Spec does not allow audio uploads.',
63
+ details: { accept: spec.accept },
64
+ },
57
65
  };
58
66
  }
59
67
  if (!acceptAllowsMime(spec, ctx.inputMime)) {
60
68
  return {
61
69
  ok: false,
62
- mediaType: "audio",
63
- error: { code: "invalid_mime", message: "Audio mime type is not allowed.", details: { inputMime: ctx.inputMime, accept: spec.accept } },
70
+ mediaType: 'audio',
71
+ error: {
72
+ code: 'invalid_mime',
73
+ message: 'Audio mime type is not allowed.',
74
+ details: { inputMime: ctx.inputMime, accept: spec.accept },
75
+ },
64
76
  };
65
77
  }
66
78
  const outDir = path.dirname(ctx.outputBasePath);
67
79
  await mkdir(outDir, { recursive: true });
68
80
  const probe = await probeAudio(ctx.inputPath, { signal: opts?.signal });
69
81
  if (opts?.signal?.aborted) {
70
- return { ok: false, mediaType: "audio", error: { code: "processing_canceled", message: "Processing canceled." } };
82
+ return {
83
+ ok: false,
84
+ mediaType: 'audio',
85
+ error: { code: 'processing_canceled', message: 'Processing canceled.' },
86
+ };
71
87
  }
72
88
  const maxDur = spec.maxDurationSec ?? spec.audio?.maxDurationSec;
73
89
  if (maxDur && probe.durationSec === undefined) {
74
- return { ok: false, mediaType: "audio", error: unreadableLengthError("audio", maxDur) };
90
+ return { ok: false, mediaType: 'audio', error: unreadableLengthError('audio', maxDur) };
75
91
  }
76
92
  if (maxDur && probe.durationSec !== undefined && probe.durationSec > maxDur) {
77
93
  return {
78
94
  ok: false,
79
- mediaType: "audio",
95
+ mediaType: 'audio',
80
96
  error: {
81
- code: "too_long",
82
- message: "Audio is too long.",
97
+ code: 'too_long',
98
+ message: 'Audio is too long.',
83
99
  details: { durationSec: probe.durationSec, maxDurationSec: maxDur },
84
100
  },
85
101
  };
86
102
  }
87
- // Normalize to AAC in m4a (small + widely supported)
88
- const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, "m4a");
103
+ const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, 'm4a');
89
104
  tempFiles.push(outPath);
90
- const args = [
91
- "-y",
92
- "-i",
93
- ctx.inputPath,
94
- "-vn",
95
- "-c:a",
96
- "aac",
97
- "-b:a",
98
- "128k",
99
- outPath,
100
- ];
105
+ const args = ['-y', '-i', ctx.inputPath, '-vn', '-c:a', 'aac', '-b:a', '128k', outPath];
101
106
  const r = await runFfmpeg(args, {
102
107
  signal: opts?.signal,
103
108
  durationMs: probe.durationSec ? probe.durationSec * 1000 : undefined,
104
109
  onProgress: (p) => {
105
- opts?.onProgress?.({ phase: "process", percent: p.percent, detail: { step: "audio_transcode", outTimeMs: p.outTimeMs } });
110
+ opts?.onProgress?.({
111
+ phase: 'process',
112
+ percent: p.percent,
113
+ detail: { step: 'audio_transcode', outTimeMs: p.outTimeMs },
114
+ });
106
115
  },
107
- phase: "transcode",
116
+ phase: 'transcode',
108
117
  });
109
118
  if (r.code !== 0) {
110
119
  return {
111
120
  ok: false,
112
- mediaType: "audio",
121
+ mediaType: 'audio',
113
122
  error: {
114
- code: "processing_failed",
115
- message: "ffmpeg audio transcode failed.",
123
+ code: 'processing_failed',
124
+ message: 'ffmpeg audio transcode failed.',
116
125
  details: { stderr: r.stderr, stdout: r.stdout },
117
126
  },
118
127
  };
@@ -123,7 +132,7 @@ export async function processAudio(spec, ctx, opts) {
123
132
  key: TIMED_MEDIA_MAIN_OUTPUT_KEY,
124
133
  url: `file://${outPath}`,
125
134
  path: outPath,
126
- mime: "audio/mp4",
135
+ mime: 'audio/mp4',
127
136
  sizeBytes: s.size,
128
137
  durationSec: probe.durationSec,
129
138
  },
@@ -131,7 +140,7 @@ export async function processAudio(spec, ctx, opts) {
131
140
  tempFiles.length = 0;
132
141
  return {
133
142
  ok: true,
134
- mediaType: "audio",
143
+ mediaType: 'audio',
135
144
  outputs,
136
145
  meta: {
137
146
  mime: ctx.inputMime,
@@ -141,21 +150,25 @@ export async function processAudio(spec, ctx, opts) {
141
150
  }
142
151
  catch (e) {
143
152
  if (opts?.signal?.aborted) {
144
- return { ok: false, mediaType: "audio", error: { code: "processing_canceled", message: "Processing canceled." } };
153
+ return {
154
+ ok: false,
155
+ mediaType: 'audio',
156
+ error: { code: 'processing_canceled', message: 'Processing canceled.' },
157
+ };
145
158
  }
146
159
  return {
147
160
  ok: false,
148
- mediaType: "audio",
161
+ mediaType: 'audio',
149
162
  error: {
150
- code: "processing_failed",
151
- message: e?.message ? String(e.message) : "Audio processing failed",
163
+ code: 'processing_failed',
164
+ message: e?.message ? String(e.message) : 'Audio processing failed',
152
165
  details: { name: e?.name },
153
166
  },
154
167
  };
155
168
  }
156
169
  finally {
157
170
  if (tempFiles.length) {
158
- const { rm } = await import("node:fs/promises");
171
+ const { rm } = await import('node:fs/promises');
159
172
  await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => { })));
160
173
  }
161
174
  }
@@ -1 +1 @@
1
- {"version":3,"file":"audio-processor.js","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MediaOutput, MediaProcessingResult, MediaProcessingSpec } from \"@ttt-productions/media-schemas\";\nimport { mkdir, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { safeOutputPathFor } from \"../utils/safe-path.js\";\nimport { ensureFfmpegAvailable, runFfmpeg } from \"../video/ffmpeg.js\";\nimport { probeAudio } from \"./probe.js\";\nimport { unreadableLengthError } from \"../duration/media-duration.js\";\nimport type { ProcessMediaOptions } from \"../types.js\";\nimport { TIMED_MEDIA_MAIN_OUTPUT_KEY } from \"@ttt-productions/media-schemas\";\n\nfunction matchMime(accepted: string, actual: string): boolean {\n const a = accepted.trim().toLowerCase();\n const m = actual.trim().toLowerCase();\n if (!a) return true;\n if (a === \"*/*\") return true;\n if (a.endsWith(\"/*\")) return m.startsWith(a.slice(0, -1));\n return a === m;\n}\n\nfunction acceptAllowsAudio(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes(\"audio\");\n}\n\nfunction acceptAllowsMime(spec: MediaProcessingSpec, actualMime?: string): boolean {\n const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];\n if (mimes.length === 0) return true; // empty => accept anything\n if (!actualMime) return false;\n return mimes.some((a) => matchMime(a, actualMime));\n}\n\nfunction outputPathFor(base: string, key: string, ext: string): string {\n return safeOutputPathFor(base, key, ext);\n}\n\nexport async function processAudio(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: string;\n inputMime?: string;\n },\n opts?: ProcessMediaOptions\n): Promise<MediaProcessingResult> {\n const tempFiles: string[] = [];\n\n try {\n if (opts?.signal?.aborted) {\n return { ok: false, mediaType: \"audio\", error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n const ff = await ensureFfmpegAvailable();\n if (!ff.ok) {\n return {\n ok: false,\n mediaType: \"audio\",\n error: {\n code: \"processing_failed\",\n message: \"ffmpeg is not available.\",\n details: { error: String((ff as any).error ?? \"unknown\") },\n },\n };\n }\n if (!acceptAllowsAudio(spec)) {\n return {\n ok: false,\n mediaType: \"audio\",\n error: { code: \"invalid_mime\", message: \"Spec does not allow audio uploads.\", details: { accept: spec.accept } },\n };\n }\n\n if (!acceptAllowsMime(spec, ctx.inputMime)) {\n return {\n ok: false,\n mediaType: \"audio\",\n error: { code: \"invalid_mime\", message: \"Audio mime type is not allowed.\", details: { inputMime: ctx.inputMime, accept: spec.accept } },\n };\n }\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const probe = await probeAudio(ctx.inputPath, { signal: opts?.signal });\n\n if (opts?.signal?.aborted) {\n return { ok: false, mediaType: \"audio\", error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n\n const maxDur = spec.maxDurationSec ?? spec.audio?.maxDurationSec;\n if (maxDur && probe.durationSec === undefined) {\n return { ok: false, mediaType: \"audio\", error: unreadableLengthError(\"audio\", maxDur) };\n }\n if (maxDur && probe.durationSec !== undefined && probe.durationSec > maxDur) {\n return {\n ok: false,\n mediaType: \"audio\",\n error: {\n code: \"too_long\",\n message: \"Audio is too long.\",\n details: { durationSec: probe.durationSec, maxDurationSec: maxDur },\n },\n };\n }\n\n // Normalize to AAC in m4a (small + widely supported)\n const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, \"m4a\");\n\n tempFiles.push(outPath);\n\n const args = [\n \"-y\",\n \"-i\",\n ctx.inputPath,\n \"-vn\",\n \"-c:a\",\n \"aac\",\n \"-b:a\",\n \"128k\",\n outPath,\n ];\n\n const r = await runFfmpeg(args, {\n signal: opts?.signal,\n durationMs: probe.durationSec ? probe.durationSec * 1000 : undefined,\n onProgress: (p) => {\n opts?.onProgress?.({ phase: \"process\", percent: p.percent, detail: { step: \"audio_transcode\", outTimeMs: p.outTimeMs } });\n },\n phase: \"transcode\",\n });\n if (r.code !== 0) {\n return {\n ok: false,\n mediaType: \"audio\",\n error: {\n code: \"processing_failed\",\n message: \"ffmpeg audio transcode failed.\",\n details: { stderr: r.stderr, stdout: r.stdout },\n },\n };\n }\n\n const s = await stat(outPath);\n\n const outputs: MediaOutput[] = [\n {\n key: TIMED_MEDIA_MAIN_OUTPUT_KEY,\n url: `file://${outPath}`,\n path: outPath,\n mime: \"audio/mp4\",\n sizeBytes: s.size,\n durationSec: probe.durationSec,\n },\n ];\n\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: \"audio\",\n outputs,\n meta: {\n mime: ctx.inputMime,\n durationSec: probe.durationSec,\n },\n };\n } catch (e: any) {\n if (opts?.signal?.aborted) {\n return { ok: false, mediaType: \"audio\", error: { code: \"processing_canceled\", message: \"Processing canceled.\" } };\n }\n return {\n ok: false,\n mediaType: \"audio\",\n error: {\n code: \"processing_failed\",\n message: e?.message ? String(e.message) : \"Audio processing failed\",\n details: { name: e?.name },\n },\n };\n } finally {\n if (tempFiles.length) {\n const { rm } = await import(\"node:fs/promises\");\n await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => {})));\n }\n }\n}\n"]}
1
+ {"version":3,"file":"audio-processor.js","sourceRoot":"","sources":["../../src/audio/audio-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport { mkdir, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport { ensureFfmpegAvailable, runFfmpeg } from '../video/ffmpeg.js';\nimport { probeAudio } from './probe.js';\nimport { unreadableLengthError } from '../duration/media-duration.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { TIMED_MEDIA_MAIN_OUTPUT_KEY } from '@ttt-productions/media-schemas';\n\nfunction matchMime(accepted: string, actual: string): boolean {\n const a = accepted.trim().toLowerCase();\n const m = actual.trim().toLowerCase();\n if (!a) return true;\n if (a === '*/*') return true;\n if (a.endsWith('/*')) return m.startsWith(a.slice(0, -1));\n return a === m;\n}\n\nfunction acceptAllowsAudio(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('audio');\n}\n\nfunction acceptAllowsMime(spec: MediaProcessingSpec, actualMime?: string): boolean {\n const mimes = spec.accept?.mimes?.filter(Boolean) ?? [];\n if (mimes.length === 0) return true;\n if (!actualMime) return false;\n return mimes.some((a) => matchMime(a, actualMime));\n}\n\nfunction outputPathFor(base: string, key: string, ext: string): string {\n return safeOutputPathFor(base, key, ext);\n}\n\nexport async function processAudio(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: string;\n inputMime?: string;\n },\n opts?: ProcessMediaOptions,\n): Promise<MediaProcessingResult> {\n const tempFiles: string[] = [];\n\n try {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n const ff = await ensureFfmpegAvailable();\n if (!ff.ok) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: 'ffmpeg is not available.',\n details: { error: String((ff as any).error ?? 'unknown') },\n },\n };\n }\n if (!acceptAllowsAudio(spec)) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow audio uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n if (!acceptAllowsMime(spec, ctx.inputMime)) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'invalid_mime',\n message: 'Audio mime type is not allowed.',\n details: { inputMime: ctx.inputMime, accept: spec.accept },\n },\n };\n }\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const probe = await probeAudio(ctx.inputPath, { signal: opts?.signal });\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const maxDur = spec.maxDurationSec ?? spec.audio?.maxDurationSec;\n if (maxDur && probe.durationSec === undefined) {\n return { ok: false, mediaType: 'audio', error: unreadableLengthError('audio', maxDur) };\n }\n if (maxDur && probe.durationSec !== undefined && probe.durationSec > maxDur) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'too_long',\n message: 'Audio is too long.',\n details: { durationSec: probe.durationSec, maxDurationSec: maxDur },\n },\n };\n }\n\n const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, 'm4a');\n\n tempFiles.push(outPath);\n\n const args = ['-y', '-i', ctx.inputPath, '-vn', '-c:a', 'aac', '-b:a', '128k', outPath];\n\n const r = await runFfmpeg(args, {\n signal: opts?.signal,\n durationMs: probe.durationSec ? probe.durationSec * 1000 : undefined,\n onProgress: (p) => {\n opts?.onProgress?.({\n phase: 'process',\n percent: p.percent,\n detail: { step: 'audio_transcode', outTimeMs: p.outTimeMs },\n });\n },\n phase: 'transcode',\n });\n if (r.code !== 0) {\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: 'ffmpeg audio transcode failed.',\n details: { stderr: r.stderr, stdout: r.stdout },\n },\n };\n }\n\n const s = await stat(outPath);\n\n const outputs: MediaOutput[] = [\n {\n key: TIMED_MEDIA_MAIN_OUTPUT_KEY,\n url: `file://${outPath}`,\n path: outPath,\n mime: 'audio/mp4',\n sizeBytes: s.size,\n durationSec: probe.durationSec,\n },\n ];\n\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'audio',\n outputs,\n meta: {\n mime: ctx.inputMime,\n durationSec: probe.durationSec,\n },\n };\n } catch (e: any) {\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'audio',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n return {\n ok: false,\n mediaType: 'audio',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Audio processing failed',\n details: { name: e?.name },\n },\n };\n } finally {\n if (tempFiles.length) {\n const { rm } = await import('node:fs/promises');\n await Promise.all(tempFiles.map((f) => rm(f, { force: true }).catch(() => {})));\n }\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type MeasureDurationOptions } from "../duration/media-duration.js";
1
+ import { type MeasureDurationOptions } from '../duration/media-duration.js';
2
2
  export interface AudioProbe {
3
3
  durationSec?: number;
4
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEhG,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtG"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEhG,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAWrB"}
@@ -1,9 +1,10 @@
1
- import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from "../video/ffmpeg.js";
2
- import { measureDurationSec } from "../duration/media-duration.js";
1
+ import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';
2
+ import { measureDurationSec } from '../duration/media-duration.js';
3
3
  export async function probeAudio(inputPath, opts) {
4
- // ffprobe opening the file is the readability check: a file it cannot open
5
- // throws here rather than reaching the length rule as merely unmeasurable.
6
- const r = await runCmd("ffprobe", ["-v", "error", inputPath], { timeoutMs: DEFAULT_PROBE_TIMEOUT_MS, signal: opts?.signal });
4
+ const r = await runCmd('ffprobe', ['-v', 'error', inputPath], {
5
+ timeoutMs: DEFAULT_PROBE_TIMEOUT_MS,
6
+ signal: opts?.signal,
7
+ });
7
8
  if (r.timedOut || r.code !== 0) {
8
9
  throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);
9
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAA+B,MAAM,+BAA+B,CAAC;AAMhG,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAiB,EAAE,IAA6B;IAC/E,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAE7H,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from \"../video/ffmpeg.js\";\nimport { measureDurationSec, type MeasureDurationOptions } from \"../duration/media-duration.js\";\n\nexport interface AudioProbe {\n durationSec?: number;\n}\n\nexport async function probeAudio(inputPath: string, opts?: MeasureDurationOptions): Promise<AudioProbe> {\n // ffprobe opening the file is the readability check: a file it cannot open\n // throws here rather than reaching the length rule as merely unmeasurable.\n const r = await runCmd(\"ffprobe\", [\"-v\", \"error\", inputPath], { timeoutMs: DEFAULT_PROBE_TIMEOUT_MS, signal: opts?.signal });\n\n if (r.timedOut || r.code !== 0) {\n throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);\n }\n\n return { durationSec: await measureDurationSec(inputPath, opts) };\n}\n"]}
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/audio/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAA+B,MAAM,+BAA+B,CAAC;AAMhG,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,SAAiB,EACjB,IAA6B;IAE7B,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE;QAC5D,SAAS,EAAE,wBAAwB;QACnC,MAAM,EAAE,IAAI,EAAE,MAAM;KACrB,CAAC,CAAC;IAEH,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;AACpE,CAAC","sourcesContent":["import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';\nimport { measureDurationSec, type MeasureDurationOptions } from '../duration/media-duration.js';\n\nexport interface AudioProbe {\n durationSec?: number;\n}\n\nexport async function probeAudio(\n inputPath: string,\n opts?: MeasureDurationOptions,\n): Promise<AudioProbe> {\n const r = await runCmd('ffprobe', ['-v', 'error', inputPath], {\n timeoutMs: DEFAULT_PROBE_TIMEOUT_MS,\n signal: opts?.signal,\n });\n\n if (r.timedOut || r.code !== 0) {\n throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);\n }\n\n return { durationSec: await measureDurationSec(inputPath, opts) };\n}\n"]}
@@ -1,13 +1,7 @@
1
- import type { MediaProcessingError } from "@ttt-productions/media-schemas";
2
- /** Wall-clock ceiling for the measuring pass; it bounds an adversarial input,
3
- * not a real upload (a 920 MB, 30-minute WebM measures in about a second). */
1
+ import type { MediaProcessingError } from '@ttt-productions/media-schemas';
4
2
  export declare const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120000;
5
3
  export interface PacketLengthFold {
6
- /** One line of ffprobe's `-of compact` packet and stream listing. */
7
4
  add(line: string): void;
8
- /** Seconds from the earliest packet start (never before zero) to the latest
9
- * packet end across the timed (audio, and non-cover-art video) streams, or
10
- * `undefined`. */
11
5
  lengthSec(): number | undefined;
12
6
  }
13
7
  export declare function createPacketLengthFold(): PacketLengthFold;
@@ -15,12 +9,7 @@ export interface MeasureDurationOptions {
15
9
  timeoutMs?: number;
16
10
  signal?: AbortSignal;
17
11
  }
18
- /** The file's length in seconds, or `undefined` when it cannot be measured. */
19
12
  export declare function measureDurationSec(localPath: string, opts?: MeasureDurationOptions): Promise<number | undefined>;
20
13
  export type MeasureDurationFn = (localPath: string, opts?: MeasureDurationOptions) => Promise<number | undefined>;
21
- /** The refusal for a file with no length under a spec that caps length: a
22
- * length that cannot be measured cannot be shown to be within the cap. The
23
- * code is `unsupported_format`, never `too_long` — the file is not known to
24
- * be too long, and its copy must not say so. */
25
- export declare function unreadableLengthError(kind: "video" | "audio", maxDurationSec: number): MediaProcessingError;
14
+ export declare function unreadableLengthError(kind: 'video' | 'audio', maxDurationSec: number): MediaProcessingError;
26
15
  //# sourceMappingURL=media-duration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"media-duration.d.ts","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAG3E;+EAC+E;AAC/E,eAAO,MAAM,mCAAmC,SAAU,CAAC;AAwB3D,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;uBAEmB;IACnB,SAAS,IAAI,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAgDzD;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,+EAA+E;AAC/E,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0BtH;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,sBAAsB,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAElH;;;iDAGiD;AACjD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,oBAAoB,CAM3G"}
1
+ {"version":3,"file":"media-duration.d.ts","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAG3E,eAAO,MAAM,mCAAmC,SAAU,CAAC;AAuB3D,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,IAAI,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CA+CzD;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0B7B;AAED,MAAM,MAAM,iBAAiB,GAAG,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,KAC1B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjC,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GAAG,OAAO,EACvB,cAAc,EAAE,MAAM,GACrB,oBAAoB,CAStB"}
@@ -1,26 +1,5 @@
1
- // The ONE rule for a timed media file's length (ENG-002). The inspection's
2
- // `durationSec` and both processors' too-long checks read it here, so they can
3
- // never disagree about the same file.
4
- //
5
- // 1. The length is always MEASURED from the packets themselves: ffprobe
6
- // lists every packet's timestamp and duration (demux only, no decode),
7
- // and the length is the span from the earliest packet start (never before
8
- // zero) to the latest packet end across the audio and video streams. A
9
- // container header's declared length (`format.duration`) is never used:
10
- // a header can claim a short length for a long file, and Chromium's
11
- // MediaRecorder writes WebM with none. ffmpeg's `-progress` output time
12
- // is not used either: it trails the true end by the B-frame reorder
13
- // delay. The listing is read one line at a time and folded into a few
14
- // numbers per stream, so a file of any packet count is measured in
15
- // bounded memory.
16
- // 2. A measure that fails, times out, is aborted, is cut short, or finds no
17
- // timed packet is NO length. Every caller treats that as unknown and
18
- // fails closed.
19
- import { runCmd } from "../video/ffmpeg.js";
20
- /** Wall-clock ceiling for the measuring pass; it bounds an adversarial input,
21
- * not a real upload (a 920 MB, 30-minute WebM measures in about a second). */
1
+ import { runCmd } from '../video/ffmpeg.js';
22
2
  export const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120_000;
23
- /** More streams than this is an adversarial file, and is no length. */
24
3
  const MAX_TRACKED_STREAMS = 256;
25
4
  const SECONDS = /^-?\d+(\.\d+)?$/;
26
5
  const STREAM_INDEX = /^\d+$/;
@@ -31,10 +10,10 @@ function seconds(raw) {
31
10
  return Number.isFinite(n) ? n : undefined;
32
11
  }
33
12
  function fields(line) {
34
- const [section = "", ...pairs] = line.split("|");
13
+ const [section = '', ...pairs] = line.split('|');
35
14
  const values = new Map();
36
15
  for (const pair of pairs) {
37
- const eq = pair.indexOf("=");
16
+ const eq = pair.indexOf('=');
38
17
  if (eq > 0)
39
18
  values.set(pair.slice(0, eq), pair.slice(eq + 1).trim());
40
19
  }
@@ -47,15 +26,15 @@ export function createPacketLengthFold() {
47
26
  return {
48
27
  add(line) {
49
28
  const { section, values } = fields(line);
50
- if (section === "packet") {
51
- const rawIndex = values.get("stream_index");
29
+ if (section === 'packet') {
30
+ const rawIndex = values.get('stream_index');
52
31
  if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex))
53
32
  return;
54
33
  const index = Number(rawIndex);
55
- const start = seconds(values.get("pts_time")) ?? seconds(values.get("dts_time"));
34
+ const start = seconds(values.get('pts_time')) ?? seconds(values.get('dts_time'));
56
35
  if (start === undefined)
57
36
  return;
58
- const end = start + Math.max(0, seconds(values.get("duration_time")) ?? 0);
37
+ const end = start + Math.max(0, seconds(values.get('duration_time')) ?? 0);
59
38
  const span = spans.get(index);
60
39
  if (span) {
61
40
  if (start < span.start)
@@ -70,13 +49,13 @@ export function createPacketLengthFold() {
70
49
  spans.set(index, { start, end });
71
50
  }
72
51
  }
73
- else if (section === "stream") {
74
- const rawIndex = values.get("index");
52
+ else if (section === 'stream') {
53
+ const rawIndex = values.get('index');
75
54
  if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex))
76
55
  return;
77
- const type = values.get("codec_type");
78
- const coverArt = values.get("disposition:attached_pic") === "1";
79
- if (type === "audio" || (type === "video" && !coverArt))
56
+ const type = values.get('codec_type');
57
+ const coverArt = values.get('disposition:attached_pic') === '1';
58
+ if (type === 'audio' || (type === 'video' && !coverArt))
80
59
  timed.add(Number(rawIndex));
81
60
  }
82
61
  },
@@ -94,25 +73,23 @@ export function createPacketLengthFold() {
94
73
  if (span.end > end)
95
74
  end = span.end;
96
75
  }
97
- // Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip)
98
- // that a player discards, so they add no playable length.
76
+ // Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip) a player discards.
99
77
  const sec = Math.round((end - Math.max(0, start)) * 1_000_000) / 1_000_000;
100
78
  return Number.isFinite(sec) && sec > 0 ? sec : undefined;
101
79
  },
102
80
  };
103
81
  }
104
- /** The file's length in seconds, or `undefined` when it cannot be measured. */
105
82
  export async function measureDurationSec(localPath, opts) {
106
83
  const fold = createPacketLengthFold();
107
84
  let r;
108
85
  try {
109
- r = await runCmd("ffprobe", [
110
- "-v",
111
- "error",
112
- "-show_entries",
113
- "stream=index,codec_type:stream_disposition=attached_pic:packet=stream_index,pts_time,dts_time,duration_time",
114
- "-of",
115
- "compact",
86
+ r = await runCmd('ffprobe', [
87
+ '-v',
88
+ 'error',
89
+ '-show_entries',
90
+ 'stream=index,codec_type:stream_disposition=attached_pic:packet=stream_index,pts_time,dts_time,duration_time',
91
+ '-of',
92
+ 'compact',
116
93
  localPath,
117
94
  ], {
118
95
  timeoutMs: opts?.timeoutMs ?? DEFAULT_DURATION_MEASURE_TIMEOUT_MS,
@@ -127,14 +104,12 @@ export async function measureDurationSec(localPath, opts) {
127
104
  return undefined;
128
105
  return fold.lengthSec();
129
106
  }
130
- /** The refusal for a file with no length under a spec that caps length: a
131
- * length that cannot be measured cannot be shown to be within the cap. The
132
- * code is `unsupported_format`, never `too_long` — the file is not known to
133
- * be too long, and its copy must not say so. */
134
107
  export function unreadableLengthError(kind, maxDurationSec) {
135
108
  return {
136
- code: "unsupported_format",
137
- message: kind === "video" ? "The video's length could not be read." : "The audio's length could not be read.",
109
+ code: 'unsupported_format',
110
+ message: kind === 'video'
111
+ ? "The video's length could not be read."
112
+ : "The audio's length could not be read.",
138
113
  details: { maxDurationSec },
139
114
  };
140
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"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","sourcesContent":["// The ONE rule for a timed media file's length (ENG-002). The inspection's\n// `durationSec` and both processors' too-long checks read it here, so they can\n// never disagree about the same file.\n//\n// 1. The length is always MEASURED from the packets themselves: ffprobe\n// lists every packet's timestamp and duration (demux only, no decode),\n// and the length is the span from the earliest packet start (never before\n// zero) to the latest packet end across the audio and video streams. A\n// container header's declared length (`format.duration`) is never used:\n// a header can claim a short length for a long file, and Chromium's\n// MediaRecorder writes WebM with none. ffmpeg's `-progress` output time\n// is not used either: it trails the true end by the B-frame reorder\n// delay. The listing is read one line at a time and folded into a few\n// numbers per stream, so a file of any packet count is measured in\n// bounded memory.\n// 2. A measure that fails, times out, is aborted, is cut short, or finds no\n// timed packet is NO length. Every caller treats that as unknown and\n// fails closed.\n\nimport type { MediaProcessingError } from \"@ttt-productions/media-schemas\";\nimport { runCmd } from \"../video/ffmpeg.js\";\n\n/** Wall-clock ceiling for the measuring pass; it bounds an adversarial input,\n * not a real upload (a 920 MB, 30-minute WebM measures in about a second). */\nexport const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120_000;\n\n/** More streams than this is an adversarial file, and is no length. */\nconst MAX_TRACKED_STREAMS = 256;\n\nconst SECONDS = /^-?\\d+(\\.\\d+)?$/;\nconst STREAM_INDEX = /^\\d+$/;\n\nfunction seconds(raw: string | undefined): number | undefined {\n if (raw === undefined || !SECONDS.test(raw)) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\nfunction fields(line: string): { section: string; values: Map<string, string> } {\n const [section = \"\", ...pairs] = line.split(\"|\");\n const values = new Map<string, string>();\n for (const pair of pairs) {\n const eq = pair.indexOf(\"=\");\n if (eq > 0) values.set(pair.slice(0, eq), pair.slice(eq + 1).trim());\n }\n return { section, values };\n}\n\nexport interface PacketLengthFold {\n /** One line of ffprobe's `-of compact` packet and stream listing. */\n add(line: string): void;\n /** Seconds from the earliest packet start (never before zero) to the latest\n * packet end across the timed (audio, and non-cover-art video) streams, or\n * `undefined`. */\n lengthSec(): number | undefined;\n}\n\nexport function createPacketLengthFold(): PacketLengthFold {\n const spans = new Map<number, { start: number; end: number }>();\n const timed = new Set<number>();\n let overflow = false;\n\n return {\n add(line) {\n const { section, values } = fields(line);\n if (section === \"packet\") {\n const rawIndex = values.get(\"stream_index\");\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const index = Number(rawIndex);\n const start = seconds(values.get(\"pts_time\")) ?? seconds(values.get(\"dts_time\"));\n if (start === undefined) return;\n const end = start + Math.max(0, seconds(values.get(\"duration_time\")) ?? 0);\n const span = spans.get(index);\n if (span) {\n if (start < span.start) span.start = start;\n if (end > span.end) span.end = end;\n } else if (spans.size >= MAX_TRACKED_STREAMS) {\n overflow = true;\n } else {\n spans.set(index, { start, end });\n }\n } else if (section === \"stream\") {\n const rawIndex = values.get(\"index\");\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const type = values.get(\"codec_type\");\n const coverArt = values.get(\"disposition:attached_pic\") === \"1\";\n if (type === \"audio\" || (type === \"video\" && !coverArt)) timed.add(Number(rawIndex));\n }\n },\n lengthSec() {\n if (overflow) return undefined;\n let start = Infinity;\n let end = -Infinity;\n for (const index of timed) {\n const span = spans.get(index);\n if (!span) continue;\n if (span.start < start) start = span.start;\n if (span.end > end) end = span.end;\n }\n // Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip)\n // that a player discards, so they add no playable length.\n const sec = Math.round((end - Math.max(0, start)) * 1_000_000) / 1_000_000;\n return Number.isFinite(sec) && sec > 0 ? sec : undefined;\n },\n };\n}\n\nexport interface MeasureDurationOptions {\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\n/** The file's length in seconds, or `undefined` when it cannot be measured. */\nexport async function measureDurationSec(localPath: string, opts?: MeasureDurationOptions): Promise<number | undefined> {\n const fold = createPacketLengthFold();\n let r;\n try {\n r = await runCmd(\n \"ffprobe\",\n [\n \"-v\",\n \"error\",\n \"-show_entries\",\n \"stream=index,codec_type:stream_disposition=attached_pic:packet=stream_index,pts_time,dts_time,duration_time\",\n \"-of\",\n \"compact\",\n localPath,\n ],\n {\n timeoutMs: opts?.timeoutMs ?? DEFAULT_DURATION_MEASURE_TIMEOUT_MS,\n signal: opts?.signal,\n onStdoutLine: (line) => fold.add(line),\n },\n );\n } catch {\n return undefined;\n }\n if (r.timedOut || r.truncated || r.code !== 0) return undefined;\n return fold.lengthSec();\n}\n\nexport type MeasureDurationFn = (localPath: string, opts?: MeasureDurationOptions) => Promise<number | undefined>;\n\n/** The refusal for a file with no length under a spec that caps length: a\n * length that cannot be measured cannot be shown to be within the cap. The\n * code is `unsupported_format`, never `too_long` — the file is not known to\n * be too long, and its copy must not say so. */\nexport function unreadableLengthError(kind: \"video\" | \"audio\", maxDurationSec: number): MediaProcessingError {\n return {\n code: \"unsupported_format\",\n message: kind === \"video\" ? \"The video's length could not be read.\" : \"The audio's length could not be read.\",\n details: { maxDurationSec },\n };\n}\n"]}
1
+ {"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MediaProcessingError } from '@ttt-productions/media-schemas';\nimport { runCmd } from '../video/ffmpeg.js';\n\nexport const DEFAULT_DURATION_MEASURE_TIMEOUT_MS = 120_000;\n\nconst MAX_TRACKED_STREAMS = 256;\n\nconst SECONDS = /^-?\\d+(\\.\\d+)?$/;\nconst STREAM_INDEX = /^\\d+$/;\n\nfunction seconds(raw: string | undefined): number | undefined {\n if (raw === undefined || !SECONDS.test(raw)) return undefined;\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n}\n\nfunction fields(line: string): { section: string; values: Map<string, string> } {\n const [section = '', ...pairs] = line.split('|');\n const values = new Map<string, string>();\n for (const pair of pairs) {\n const eq = pair.indexOf('=');\n if (eq > 0) values.set(pair.slice(0, eq), pair.slice(eq + 1).trim());\n }\n return { section, values };\n}\n\nexport interface PacketLengthFold {\n add(line: string): void;\n lengthSec(): number | undefined;\n}\n\nexport function createPacketLengthFold(): PacketLengthFold {\n const spans = new Map<number, { start: number; end: number }>();\n const timed = new Set<number>();\n let overflow = false;\n\n return {\n add(line) {\n const { section, values } = fields(line);\n if (section === 'packet') {\n const rawIndex = values.get('stream_index');\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const index = Number(rawIndex);\n const start = seconds(values.get('pts_time')) ?? seconds(values.get('dts_time'));\n if (start === undefined) return;\n const end = start + Math.max(0, seconds(values.get('duration_time')) ?? 0);\n const span = spans.get(index);\n if (span) {\n if (start < span.start) span.start = start;\n if (end > span.end) span.end = end;\n } else if (spans.size >= MAX_TRACKED_STREAMS) {\n overflow = true;\n } else {\n spans.set(index, { start, end });\n }\n } else if (section === 'stream') {\n const rawIndex = values.get('index');\n if (rawIndex === undefined || !STREAM_INDEX.test(rawIndex)) return;\n const type = values.get('codec_type');\n const coverArt = values.get('disposition:attached_pic') === '1';\n if (type === 'audio' || (type === 'video' && !coverArt)) timed.add(Number(rawIndex));\n }\n },\n lengthSec() {\n if (overflow) return undefined;\n let start = Infinity;\n let end = -Infinity;\n for (const index of timed) {\n const span = spans.get(index);\n if (!span) continue;\n if (span.start < start) start = span.start;\n if (span.end > end) end = span.end;\n }\n // Packets before zero are encoder pre-roll (AAC priming, Opus pre-skip) a player discards.\n const sec = Math.round((end - Math.max(0, start)) * 1_000_000) / 1_000_000;\n return Number.isFinite(sec) && sec > 0 ? sec : undefined;\n },\n };\n}\n\nexport interface MeasureDurationOptions {\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\nexport async function measureDurationSec(\n localPath: string,\n opts?: MeasureDurationOptions,\n): Promise<number | undefined> {\n const fold = createPacketLengthFold();\n let r;\n try {\n r = await runCmd(\n 'ffprobe',\n [\n '-v',\n 'error',\n '-show_entries',\n 'stream=index,codec_type:stream_disposition=attached_pic:packet=stream_index,pts_time,dts_time,duration_time',\n '-of',\n 'compact',\n localPath,\n ],\n {\n timeoutMs: opts?.timeoutMs ?? DEFAULT_DURATION_MEASURE_TIMEOUT_MS,\n signal: opts?.signal,\n onStdoutLine: (line) => fold.add(line),\n },\n );\n } catch {\n return undefined;\n }\n if (r.timedOut || r.truncated || r.code !== 0) return undefined;\n return fold.lengthSec();\n}\n\nexport type MeasureDurationFn = (\n localPath: string,\n opts?: MeasureDurationOptions,\n) => Promise<number | undefined>;\n\nexport function unreadableLengthError(\n kind: 'video' | 'audio',\n maxDurationSec: number,\n): MediaProcessingError {\n return {\n code: 'unsupported_format',\n message:\n kind === 'video'\n ? \"The video's length could not be read.\"\n : \"The audio's length could not be read.\",\n details: { maxDurationSec },\n };\n}\n"]}
@@ -1,20 +1,9 @@
1
- import type { Sharp } from "sharp";
2
- /** Output image formats the processor can emit. */
3
- export type ImageOutputFormat = "jpeg" | "png" | "webp" | "avif";
4
- /**
5
- * Map a requested format string (case-insensitive) to its sharp format id and
6
- * file extension. Unknown or missing formats fall back to jpeg.
7
- */
1
+ import type { Sharp } from 'sharp';
2
+ export type ImageOutputFormat = 'jpeg' | 'png' | 'webp' | 'avif';
8
3
  export declare function pickFormat(fmt?: string): {
9
4
  ext: string;
10
5
  format: ImageOutputFormat;
11
6
  };
12
- /** Map an image format string to its MIME type. */
13
7
  export declare function mimeForImageFormat(fmt?: string): string | undefined;
14
- /**
15
- * Apply the chosen output format (and optional quality) to a sharp pipeline.
16
- * png is lossless so quality is ignored; jpeg/webp/avif honor quality when set,
17
- * otherwise sharp's per-format defaults apply.
18
- */
19
8
  export declare function applyImageFormat(pipeline: Sharp, format: ImageOutputFormat, quality?: number): Sharp;
20
9
  //# sourceMappingURL=formats.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,mDAAmD;AACnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAanF;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASnE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,KAAK,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,KAAK,CAaP"}
1
+ {"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE,wBAAgB,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAanF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASnE;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,KAAK,EACf,MAAM,EAAE,iBAAiB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,KAAK,CAaP"}