@ttt-productions/media-processing-core 0.8.17 → 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 (90) hide show
  1. package/dist/audio/audio-processor.d.ts.map +1 -1
  2. package/dist/audio/audio-processor.js +1 -2
  3. package/dist/audio/audio-processor.js.map +1 -1
  4. package/dist/audio/probe.d.ts.map +1 -1
  5. package/dist/audio/probe.js +0 -2
  6. package/dist/audio/probe.js.map +1 -1
  7. package/dist/duration/media-duration.d.ts +0 -11
  8. package/dist/duration/media-duration.d.ts.map +1 -1
  9. package/dist/duration/media-duration.js +1 -28
  10. package/dist/duration/media-duration.js.map +1 -1
  11. package/dist/image/formats.d.ts +0 -11
  12. package/dist/image/formats.d.ts.map +1 -1
  13. package/dist/image/formats.js +0 -10
  14. package/dist/image/formats.js.map +1 -1
  15. package/dist/image/image-processor.d.ts.map +1 -1
  16. package/dist/image/image-processor.js +1 -20
  17. package/dist/image/image-processor.js.map +1 -1
  18. package/dist/image/resize.d.ts +0 -14
  19. package/dist/image/resize.d.ts.map +1 -1
  20. package/dist/image/resize.js +0 -15
  21. package/dist/image/resize.js.map +1 -1
  22. package/dist/inspection/inspect-media.d.ts +0 -7
  23. package/dist/inspection/inspect-media.d.ts.map +1 -1
  24. package/dist/inspection/inspect-media.js +4 -64
  25. package/dist/inspection/inspect-media.js.map +1 -1
  26. package/dist/io/fs.d.ts.map +1 -1
  27. package/dist/io/fs.js +0 -1
  28. package/dist/io/fs.js.map +1 -1
  29. package/dist/io/local-input.d.ts +0 -20
  30. package/dist/io/local-input.d.ts.map +1 -1
  31. package/dist/io/local-input.js +0 -20
  32. package/dist/io/local-input.js.map +1 -1
  33. package/dist/io/types.d.ts +0 -3
  34. package/dist/io/types.d.ts.map +1 -1
  35. package/dist/io/types.js.map +1 -1
  36. package/dist/moderation/merge.d.ts.map +1 -1
  37. package/dist/moderation/merge.js +0 -1
  38. package/dist/moderation/merge.js.map +1 -1
  39. package/dist/moderation/types.d.ts +0 -2
  40. package/dist/moderation/types.d.ts.map +1 -1
  41. package/dist/moderation/types.js.map +1 -1
  42. package/dist/run-pipeline.d.ts +0 -17
  43. package/dist/run-pipeline.d.ts.map +1 -1
  44. package/dist/run-pipeline.js +2 -27
  45. package/dist/run-pipeline.js.map +1 -1
  46. package/dist/server/download-token.d.ts +0 -6
  47. package/dist/server/download-token.d.ts.map +1 -1
  48. package/dist/server/download-token.js +0 -10
  49. package/dist/server/download-token.js.map +1 -1
  50. package/dist/server/firebase-media-io.d.ts +1 -11
  51. package/dist/server/firebase-media-io.d.ts.map +1 -1
  52. package/dist/server/firebase-media-io.js +0 -5
  53. package/dist/server/firebase-media-io.js.map +1 -1
  54. package/dist/server/gcs-errors.d.ts +0 -8
  55. package/dist/server/gcs-errors.d.ts.map +1 -1
  56. package/dist/server/gcs-errors.js +0 -8
  57. package/dist/server/gcs-errors.js.map +1 -1
  58. package/dist/server/pending-media.d.ts +1 -32
  59. package/dist/server/pending-media.d.ts.map +1 -1
  60. package/dist/server/pending-media.js +1 -24
  61. package/dist/server/pending-media.js.map +1 -1
  62. package/dist/server/r2-retry.d.ts +0 -48
  63. package/dist/server/r2-retry.d.ts.map +1 -1
  64. package/dist/server/r2-retry.js +4 -90
  65. package/dist/server/r2-retry.js.map +1 -1
  66. package/dist/server/staged-object.d.ts +0 -44
  67. package/dist/server/staged-object.d.ts.map +1 -1
  68. package/dist/server/staged-object.js +0 -44
  69. package/dist/server/staged-object.js.map +1 -1
  70. package/dist/server/storage-ops.d.ts +2 -62
  71. package/dist/server/storage-ops.d.ts.map +1 -1
  72. package/dist/server/storage-ops.js +9 -88
  73. package/dist/server/storage-ops.js.map +1 -1
  74. package/dist/types.d.ts +0 -2
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js.map +1 -1
  77. package/dist/utils/safe-path.d.ts.map +1 -1
  78. package/dist/utils/safe-path.js +1 -2
  79. package/dist/utils/safe-path.js.map +1 -1
  80. package/dist/video/ffmpeg.d.ts +0 -22
  81. package/dist/video/ffmpeg.d.ts.map +1 -1
  82. package/dist/video/ffmpeg.js +6 -21
  83. package/dist/video/ffmpeg.js.map +1 -1
  84. package/dist/video/probe.d.ts.map +1 -1
  85. package/dist/video/probe.js +0 -1
  86. package/dist/video/probe.js.map +1 -1
  87. package/dist/video/video-processor.d.ts.map +1 -1
  88. package/dist/video/video-processor.js +2 -15
  89. package/dist/video/video-processor.js.map +1 -1
  90. package/package.json +2 -2
@@ -1 +1 @@
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,CA0JhC"}
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"}
@@ -23,7 +23,7 @@ function acceptAllowsAudio(spec) {
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));
@@ -100,7 +100,6 @@ export async function processAudio(spec, ctx, opts) {
100
100
  },
101
101
  };
102
102
  }
103
- // Normalize to AAC in m4a (small + widely supported)
104
103
  const outPath = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, 'm4a');
105
104
  tempFiles.push(outPath);
106
105
  const args = ['-y', '-i', ctx.inputPath, '-vn', '-c:a', 'aac', '-b:a', '128k', outPath];
@@ -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 {\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; // 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 {\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 // 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 = ['-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
+ {"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 +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,CAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAarB"}
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,8 +1,6 @@
1
1
  import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';
2
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
4
  const r = await runCmd('ffprobe', ['-v', 'error', inputPath], {
7
5
  timeoutMs: DEFAULT_PROBE_TIMEOUT_MS,
8
6
  signal: opts?.signal,
@@ -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,CAC9B,SAAiB,EACjB,IAA6B;IAE7B,2EAA2E;IAC3E,2EAA2E;IAC3E,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 // 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], {\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
+ {"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
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). */
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
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,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;;;iDAGiD;AACjD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GAAG,OAAO,EACvB,cAAc,EAAE,MAAM,GACrB,oBAAoB,CAStB"}
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
1
  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). */
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+$/;
@@ -94,14 +73,12 @@ 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;
@@ -127,10 +104,6 @@ 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
109
  code: 'unsupported_format',
@@ -1 +1 @@
1
- {"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../../src/duration/media-duration.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,+EAA+E;AAC/E,sCAAsC;AACtC,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,+EAA+E;AAC/E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,6EAA6E;AAC7E,yEAAyE;AACzE,2EAA2E;AAC3E,wEAAwE;AACxE,uBAAuB;AACvB,8EAA8E;AAC9E,0EAA0E;AAC1E,qBAAqB;AAGrB,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C;+EAC+E;AAC/E,MAAM,CAAC,MAAM,mCAAmC,GAAG,OAAO,CAAC;AAE3D,uEAAuE;AACvE,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAClC,MAAM,YAAY,GAAG,OAAO,CAAC;AAE7B,SAAS,OAAO,CAAC,GAAuB;IACtC,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9D,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,CAAC,OAAO,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,EAAE,GAAG,CAAC;YAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC;AAWD,MAAM,UAAU,sBAAsB;IACpC,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0C,CAAC;IAChE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,OAAO;QACL,GAAG,CAAC,IAAI;YACN,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACzB,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;gBAC5C,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;oBAAE,OAAO;gBACnE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;gBACjF,IAAI,KAAK,KAAK,SAAS;oBAAE,OAAO;gBAChC,MAAM,GAAG,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC3E,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC9B,IAAI,IAAI,EAAE,CAAC;oBACT,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK;wBAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;oBAC3C,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG;wBAAE,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;gBACrC,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,IAAI,mBAAmB,EAAE,CAAC;oBAC7C,QAAQ,GAAG,IAAI,CAAC;gBAClB,CAAC;qBAAM,CAAC;oBACN,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;iBAAM,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBACrC,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;oBAAE,OAAO;gBACnE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,KAAK,GAAG,CAAC;gBAChE,IAAI,IAAI,KAAK,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,QAAQ,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YACvF,CAAC;QACH,CAAC;QACD,SAAS;YACP,IAAI,QAAQ;gBAAE,OAAO,SAAS,CAAC;YAC/B,IAAI,KAAK,GAAG,QAAQ,CAAC;YACrB,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;YACpB,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;gBAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC9B,IAAI,CAAC,IAAI;oBAAE,SAAS;gBACpB,IAAI,IAAI,CAAC,KAAK,GAAG,KAAK;oBAAE,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;gBAC3C,IAAI,IAAI,CAAC,GAAG,GAAG,GAAG;oBAAE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;YACrC,CAAC;YACD,wEAAwE;YACxE,0DAA0D;YAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;YAC3E,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3D,CAAC;KACF,CAAC;AACJ,CAAC;AAOD,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAiB,EACjB,IAA6B;IAE7B,MAAM,IAAI,GAAG,sBAAsB,EAAE,CAAC;IACtC,IAAI,CAAC,CAAC;IACN,IAAI,CAAC;QACH,CAAC,GAAG,MAAM,MAAM,CACd,SAAS,EACT;YACE,IAAI;YACJ,OAAO;YACP,eAAe;YACf,6GAA6G;YAC7G,KAAK;YACL,SAAS;YACT,SAAS;SACV,EACD;YACE,SAAS,EAAE,IAAI,EAAE,SAAS,IAAI,mCAAmC;YACjE,MAAM,EAAE,IAAI,EAAE,MAAM;YACpB,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;SACvC,CACF,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAChE,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;AAC1B,CAAC;AAOD;;;iDAGiD;AACjD,MAAM,UAAU,qBAAqB,CACnC,IAAuB,EACvB,cAAsB;IAEtB,OAAO;QACL,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EACL,IAAI,KAAK,OAAO;YACd,CAAC,CAAC,uCAAuC;YACzC,CAAC,CAAC,uCAAuC;QAC7C,OAAO,EAAE,EAAE,cAAc,EAAE;KAC5B,CAAC;AACJ,CAAC","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(\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\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(\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
+ {"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
1
  import type { Sharp } from 'sharp';
2
- /** Output image formats the processor can emit. */
3
2
  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
- */
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"}
@@ -1,7 +1,3 @@
1
- /**
2
- * Map a requested format string (case-insensitive) to its sharp format id and
3
- * file extension. Unknown or missing formats fall back to jpeg.
4
- */
5
1
  export function pickFormat(fmt) {
6
2
  switch ((fmt ?? 'jpeg').toLowerCase()) {
7
3
  case 'png':
@@ -16,7 +12,6 @@ export function pickFormat(fmt) {
16
12
  return { ext: 'jpg', format: 'jpeg' };
17
13
  }
18
14
  }
19
- /** Map an image format string to its MIME type. */
20
15
  export function mimeForImageFormat(fmt) {
21
16
  if (!fmt)
22
17
  return undefined;
@@ -33,11 +28,6 @@ export function mimeForImageFormat(fmt) {
33
28
  return 'image/avif';
34
29
  return `image/${f}`;
35
30
  }
36
- /**
37
- * Apply the chosen output format (and optional quality) to a sharp pipeline.
38
- * png is lossless so quality is ignored; jpeg/webp/avif honor quality when set,
39
- * otherwise sharp's per-format defaults apply.
40
- */
41
31
  export function applyImageFormat(pipeline, format, quality) {
42
32
  const q = typeof quality === 'number' ? quality : undefined;
43
33
  switch (format) {
@@ -1 +1 @@
1
- {"version":3,"file":"formats.js","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,GAAY;IACrC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACtC,KAAK,KAAK;YACR,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACvC,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACzC,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACzC,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1C,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,YAAY,CAAC;IACrC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IACpC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,OAAO,SAAS,CAAC,EAAE,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAe,EACf,MAAyB,EACzB,OAAgB;IAEhB,MAAM,CAAC,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,KAAK;YACR,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC;QACxB,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvD,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvD,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACzD,CAAC;AACH,CAAC","sourcesContent":["// shared image format / quality presets\nimport type { Sharp } from 'sharp';\n\n/** Output image formats the processor can emit. */\nexport type ImageOutputFormat = 'jpeg' | 'png' | 'webp' | 'avif';\n\n/**\n * Map a requested format string (case-insensitive) to its sharp format id and\n * file extension. Unknown or missing formats fall back to jpeg.\n */\nexport function pickFormat(fmt?: string): { ext: string; format: ImageOutputFormat } {\n switch ((fmt ?? 'jpeg').toLowerCase()) {\n case 'png':\n return { ext: 'png', format: 'png' };\n case 'webp':\n return { ext: 'webp', format: 'webp' };\n case 'avif':\n return { ext: 'avif', format: 'avif' };\n case 'jpg':\n case 'jpeg':\n default:\n return { ext: 'jpg', format: 'jpeg' };\n }\n}\n\n/** Map an image format string to its MIME type. */\nexport function mimeForImageFormat(fmt?: string): string | undefined {\n if (!fmt) return undefined;\n const f = fmt.toLowerCase();\n if (f === 'jpg') return 'image/jpeg';\n if (f === 'jpeg') return 'image/jpeg';\n if (f === 'png') return 'image/png';\n if (f === 'webp') return 'image/webp';\n if (f === 'avif') return 'image/avif';\n return `image/${f}`;\n}\n\n/**\n * Apply the chosen output format (and optional quality) to a sharp pipeline.\n * png is lossless so quality is ignored; jpeg/webp/avif honor quality when set,\n * otherwise sharp's per-format defaults apply.\n */\nexport function applyImageFormat(\n pipeline: Sharp,\n format: ImageOutputFormat,\n quality?: number,\n): Sharp {\n const q = typeof quality === 'number' ? quality : undefined;\n switch (format) {\n case 'png':\n return pipeline.png();\n case 'webp':\n return pipeline.webp(q ? { quality: q } : undefined);\n case 'avif':\n return pipeline.avif(q ? { quality: q } : undefined);\n case 'jpeg':\n default:\n return pipeline.jpeg(q ? { quality: q } : undefined);\n }\n}\n"]}
1
+ {"version":3,"file":"formats.js","sourceRoot":"","sources":["../../src/image/formats.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,UAAU,CAAC,GAAY;IACrC,QAAQ,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACtC,KAAK,KAAK;YACR,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACvC,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACzC,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QACzC,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IAC5B,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,YAAY,CAAC;IACrC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IACpC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,YAAY,CAAC;IACtC,OAAO,SAAS,CAAC,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,QAAe,EACf,MAAyB,EACzB,OAAgB;IAEhB,MAAM,CAAC,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,KAAK;YACR,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC;QACxB,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvD,KAAK,MAAM;YACT,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvD,KAAK,MAAM,CAAC;QACZ;YACE,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACzD,CAAC;AACH,CAAC","sourcesContent":["import type { Sharp } from 'sharp';\n\nexport type ImageOutputFormat = 'jpeg' | 'png' | 'webp' | 'avif';\n\nexport function pickFormat(fmt?: string): { ext: string; format: ImageOutputFormat } {\n switch ((fmt ?? 'jpeg').toLowerCase()) {\n case 'png':\n return { ext: 'png', format: 'png' };\n case 'webp':\n return { ext: 'webp', format: 'webp' };\n case 'avif':\n return { ext: 'avif', format: 'avif' };\n case 'jpg':\n case 'jpeg':\n default:\n return { ext: 'jpg', format: 'jpeg' };\n }\n}\n\nexport function mimeForImageFormat(fmt?: string): string | undefined {\n if (!fmt) return undefined;\n const f = fmt.toLowerCase();\n if (f === 'jpg') return 'image/jpeg';\n if (f === 'jpeg') return 'image/jpeg';\n if (f === 'png') return 'image/png';\n if (f === 'webp') return 'image/webp';\n if (f === 'avif') return 'image/avif';\n return `image/${f}`;\n}\n\nexport function applyImageFormat(\n pipeline: Sharp,\n format: ImageOutputFormat,\n quality?: number,\n): Sharp {\n const q = typeof quality === 'number' ? quality : undefined;\n switch (format) {\n case 'png':\n return pipeline.png();\n case 'webp':\n return pipeline.webp(q ? { quality: q } : undefined);\n case 'avif':\n return pipeline.avif(q ? { quality: q } : undefined);\n case 'jpeg':\n default:\n return pipeline.jpeg(q ? { quality: q } : undefined);\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"image-processor.d.ts","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA8LvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CA4GhC"}
1
+ {"version":3,"file":"image-processor.d.ts","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,gCAAgC,CAAC;AAMxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA4KvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CA2GhC"}
@@ -23,7 +23,7 @@ function acceptAllowsImage(spec) {
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));
@@ -39,14 +39,6 @@ function aspect(width, height) {
39
39
  function aspectClose(a, b, tolerance) {
40
40
  return Math.abs(a - b) <= tolerance;
41
41
  }
42
- /**
43
- * Build a "normalized base" pipeline that enforces spec-level uniformity:
44
- * - if imageCrop exists -> always crop to outputWidth/outputHeight
45
- * - else if requiredWidth/requiredHeight exist -> crop/resize to those
46
- * - else if requiredAspectRatio exists -> center-crop to that aspect (no scaling unless needed later)
47
- *
48
- * If the source violates constraints and we cannot/should not auto-fix, returns a rejection error.
49
- */
50
42
  async function buildBase(spec, inputPath) {
51
43
  const input = sharp(inputPath, { failOn: 'none' });
52
44
  const meta = await input.metadata();
@@ -94,8 +86,6 @@ async function buildBase(spec, inputPath) {
94
86
  const w = meta.width ?? undefined;
95
87
  const h = meta.height ?? undefined;
96
88
  if (!w || !h) {
97
- // Can't validate/crop reliably without dimensions.
98
- // We'll still proceed unless strict dims/aspect are required without crop/auto.
99
89
  if (spec.requiredWidth || spec.requiredHeight || spec.requiredAspectRatio) {
100
90
  if (!spec.allowAutoFormat && !spec.imageCrop) {
101
91
  return {
@@ -109,24 +99,19 @@ async function buildBase(spec, inputPath) {
109
99
  }
110
100
  }
111
101
  }
112
- // Start from raw input each time; callers can clone as needed.
113
102
  let base = sharp(inputPath);
114
- // Keep metadata only if explicitly requested.
115
103
  if (spec.image?.stripMetadata === false) {
116
104
  base = base.withMetadata({ orientation: meta.orientation });
117
105
  }
118
- // 1) Explicit crop spec (most strict)
119
106
  const crop = spec.imageCrop;
120
107
  if (crop) {
121
108
  base = resizeCover(base, crop.outputWidth, crop.outputHeight);
122
109
  return { ok: true, base, inputMeta: meta };
123
110
  }
124
- // 2) Exact required size -> auto crop/resize only if allowAutoFormat OR the request is inherently "uniform"
125
111
  const reqW = spec.requiredWidth;
126
112
  const reqH = spec.requiredHeight;
127
113
  if (reqW && reqH) {
128
114
  const canFix = spec.allowAutoFormat === true;
129
- // If not allowed to auto-fix, require exact match
130
115
  if (!canFix && w && h && (w !== reqW || h !== reqH)) {
131
116
  return {
132
117
  ok: false,
@@ -137,11 +122,9 @@ async function buildBase(spec, inputPath) {
137
122
  },
138
123
  };
139
124
  }
140
- // Auto-fix path (or exact already): enforce normalized base
141
125
  base = resizeCover(base, reqW, reqH);
142
126
  return { ok: true, base, inputMeta: meta };
143
127
  }
144
- // 3) Aspect ratio requirement (no exact size)
145
128
  const reqAspect = spec.requiredAspectRatio;
146
129
  if (reqAspect && w && h) {
147
130
  const a = aspect(w, h);
@@ -157,7 +140,6 @@ async function buildBase(spec, inputPath) {
157
140
  },
158
141
  };
159
142
  }
160
- // Auto center-crop to required aspect ratio (no forced scale yet).
161
143
  base = base.extract(computeAspectCropBox(w, h, reqAspect));
162
144
  }
163
145
  }
@@ -222,7 +204,6 @@ export async function processImage(spec, ctx, opts) {
222
204
  height: info.height,
223
205
  });
224
206
  }
225
- // success: keep outputs
226
207
  tempFiles.length = 0;
227
208
  return {
228
209
  ok: true,
@@ -1 +1 @@
1
- {"version":3,"file":"image-processor.js","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingError,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport sharp from 'sharp';\nimport type { Sharp, Metadata } from 'sharp';\nimport { mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { applyImageFormat, mimeForImageFormat, pickFormat } from './formats.js';\nimport {\n computeAspectCropBox,\n resizeCover,\n resizeInside,\n toSharpPosition,\n type Gravity,\n} from './resize.js';\nimport { IMAGE_DEFAULT_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 acceptAllowsImage(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('image');\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\nfunction aspect(width?: number, height?: number): number | undefined {\n if (!width || !height) return undefined;\n return width / height;\n}\n\nfunction aspectClose(a: number, b: number, tolerance: number): boolean {\n return Math.abs(a - b) <= tolerance;\n}\n\n/**\n * Build a \"normalized base\" pipeline that enforces spec-level uniformity:\n * - if imageCrop exists -> always crop to outputWidth/outputHeight\n * - else if requiredWidth/requiredHeight exist -> crop/resize to those\n * - else if requiredAspectRatio exists -> center-crop to that aspect (no scaling unless needed later)\n *\n * If the source violates constraints and we cannot/should not auto-fix, returns a rejection error.\n */\nasync function buildBase(\n spec: MediaProcessingSpec,\n inputPath: string,\n): Promise<\n { ok: true; base: Sharp; inputMeta: Metadata } | { ok: false; error: MediaProcessingError }\n> {\n const input = sharp(inputPath, { failOn: 'none' });\n const meta = await input.metadata();\n\n if (!meta.width || !meta.height || meta.width <= 0 || meta.height <= 0) {\n return {\n ok: false,\n error: {\n code: 'processing_failed',\n message: 'Unable to read valid image dimensions. File may be corrupt.',\n details: { width: meta.width, height: meta.height, format: meta.format, path: inputPath },\n },\n };\n }\n\n if (!meta.format) {\n return {\n ok: false,\n error: {\n code: 'unsupported_format',\n message: 'Unable to determine image format. File may be corrupt.',\n details: { path: inputPath },\n },\n };\n }\n\n if (!acceptAllowsImage(spec)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow image uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n const detectedMime = mimeForImageFormat(meta.format);\n if (!acceptAllowsMime(spec, detectedMime)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Image mime type is not allowed.',\n details: { detectedMime, accept: spec.accept },\n },\n };\n }\n\n const w = meta.width ?? undefined;\n const h = meta.height ?? undefined;\n\n if (!w || !h) {\n // Can't validate/crop reliably without dimensions.\n // We'll still proceed unless strict dims/aspect are required without crop/auto.\n if (spec.requiredWidth || spec.requiredHeight || spec.requiredAspectRatio) {\n if (!spec.allowAutoFormat && !spec.imageCrop) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Unable to read image dimensions for validation.',\n details: { width: w, height: h },\n },\n };\n }\n }\n }\n\n // Start from raw input each time; callers can clone as needed.\n let base = sharp(inputPath);\n\n // Keep metadata only if explicitly requested.\n if (spec.image?.stripMetadata === false) {\n base = base.withMetadata({ orientation: meta.orientation });\n }\n\n // 1) Explicit crop spec (most strict)\n const crop = spec.imageCrop;\n if (crop) {\n base = resizeCover(base, crop.outputWidth, crop.outputHeight);\n return { ok: true, base, inputMeta: meta };\n }\n\n // 2) Exact required size -> auto crop/resize only if allowAutoFormat OR the request is inherently \"uniform\"\n const reqW = spec.requiredWidth;\n const reqH = spec.requiredHeight;\n if (reqW && reqH) {\n const canFix = spec.allowAutoFormat === true;\n // If not allowed to auto-fix, require exact match\n if (!canFix && w && h && (w !== reqW || h !== reqH)) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Image dimensions do not match the required format.',\n details: { requiredWidth: reqW, requiredHeight: reqH, width: w, height: h },\n },\n };\n }\n\n // Auto-fix path (or exact already): enforce normalized base\n base = resizeCover(base, reqW, reqH);\n\n return { ok: true, base, inputMeta: meta };\n }\n\n // 3) Aspect ratio requirement (no exact size)\n const reqAspect = spec.requiredAspectRatio;\n if (reqAspect && w && h) {\n const a = aspect(w, h);\n const tol = spec.aspectRatioTolerance ?? 0.02;\n if (a && !aspectClose(a, reqAspect, tol)) {\n if (!spec.allowAutoFormat) {\n return {\n ok: false,\n error: {\n code: 'aspect_ratio_mismatch',\n message: 'Image aspect ratio does not match the required format.',\n details: { requiredAspectRatio: reqAspect, aspectRatio: a, width: w, height: h },\n },\n };\n }\n\n // Auto center-crop to required aspect ratio (no forced scale yet).\n base = base.extract(computeAspectCropBox(w, h, reqAspect));\n }\n }\n\n return { ok: true, base, inputMeta: meta };\n}\n\nexport async function processImage(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: 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: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const variants = spec.image?.variants?.length\n ? spec.image.variants\n : [{ key: IMAGE_DEFAULT_OUTPUT_KEY }];\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const baseRes = await buildBase(spec, ctx.inputPath);\n if (!baseRes.ok) {\n return { ok: false, mediaType: 'image', error: baseRes.error };\n }\n\n const { base, inputMeta } = baseRes;\n\n const outputs: MediaOutput[] = [];\n const totalVariants = variants.length || 1;\n let idx = 0;\n\n for (const v of variants) {\n idx += 1;\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n opts?.onProgress?.({\n phase: 'process',\n percent: (idx - 1) / totalVariants,\n detail: { step: 'image_variant', key: v.key },\n });\n\n const { ext, format } = pickFormat(v.format);\n const outPath = outputPathFor(ctx.outputBasePath, v.key, ext);\n tempFiles.push(outPath);\n\n let p = base.clone();\n\n if (v.crop) {\n p = resizeCover(\n p,\n v.crop.width,\n v.crop.height,\n toSharpPosition((v.crop.gravity as Gravity) ?? 'center'),\n );\n } else if (v.maxWidth || v.maxHeight) {\n p = resizeInside(p, v.maxWidth, v.maxHeight);\n }\n\n p = applyImageFormat(p, format, v.quality);\n\n const info = await p.toFile(outPath);\n\n outputs.push({\n key: v.key,\n url: `file://${outPath}`,\n path: outPath,\n mime: mimeForImageFormat(info.format),\n sizeBytes: info.size,\n width: info.width,\n height: info.height,\n });\n }\n\n // success: keep outputs\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'image',\n outputs,\n meta: {\n mime: mimeForImageFormat(inputMeta.format),\n width: inputMeta.width,\n height: inputMeta.height,\n },\n };\n } catch (e: any) {\n return {\n ok: false,\n mediaType: 'image',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Image 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":"image-processor.js","sourceRoot":"","sources":["../../src/image/image-processor.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n MediaOutput,\n MediaProcessingError,\n MediaProcessingResult,\n MediaProcessingSpec,\n} from '@ttt-productions/media-schemas';\nimport sharp from 'sharp';\nimport type { Sharp, Metadata } from 'sharp';\nimport { mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport { safeOutputPathFor } from '../utils/safe-path.js';\nimport type { ProcessMediaOptions } from '../types.js';\nimport { applyImageFormat, mimeForImageFormat, pickFormat } from './formats.js';\nimport {\n computeAspectCropBox,\n resizeCover,\n resizeInside,\n toSharpPosition,\n type Gravity,\n} from './resize.js';\nimport { IMAGE_DEFAULT_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 acceptAllowsImage(spec: MediaProcessingSpec): boolean {\n const kinds = spec.accept?.kinds?.filter(Boolean) ?? [];\n return kinds.length === 0 ? true : kinds.includes('image');\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\nfunction aspect(width?: number, height?: number): number | undefined {\n if (!width || !height) return undefined;\n return width / height;\n}\n\nfunction aspectClose(a: number, b: number, tolerance: number): boolean {\n return Math.abs(a - b) <= tolerance;\n}\n\nasync function buildBase(\n spec: MediaProcessingSpec,\n inputPath: string,\n): Promise<\n { ok: true; base: Sharp; inputMeta: Metadata } | { ok: false; error: MediaProcessingError }\n> {\n const input = sharp(inputPath, { failOn: 'none' });\n const meta = await input.metadata();\n\n if (!meta.width || !meta.height || meta.width <= 0 || meta.height <= 0) {\n return {\n ok: false,\n error: {\n code: 'processing_failed',\n message: 'Unable to read valid image dimensions. File may be corrupt.',\n details: { width: meta.width, height: meta.height, format: meta.format, path: inputPath },\n },\n };\n }\n\n if (!meta.format) {\n return {\n ok: false,\n error: {\n code: 'unsupported_format',\n message: 'Unable to determine image format. File may be corrupt.',\n details: { path: inputPath },\n },\n };\n }\n\n if (!acceptAllowsImage(spec)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Spec does not allow image uploads.',\n details: { accept: spec.accept },\n },\n };\n }\n\n const detectedMime = mimeForImageFormat(meta.format);\n if (!acceptAllowsMime(spec, detectedMime)) {\n return {\n ok: false,\n error: {\n code: 'invalid_mime',\n message: 'Image mime type is not allowed.',\n details: { detectedMime, accept: spec.accept },\n },\n };\n }\n\n const w = meta.width ?? undefined;\n const h = meta.height ?? undefined;\n\n if (!w || !h) {\n if (spec.requiredWidth || spec.requiredHeight || spec.requiredAspectRatio) {\n if (!spec.allowAutoFormat && !spec.imageCrop) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Unable to read image dimensions for validation.',\n details: { width: w, height: h },\n },\n };\n }\n }\n }\n\n let base = sharp(inputPath);\n\n if (spec.image?.stripMetadata === false) {\n base = base.withMetadata({ orientation: meta.orientation });\n }\n\n const crop = spec.imageCrop;\n if (crop) {\n base = resizeCover(base, crop.outputWidth, crop.outputHeight);\n return { ok: true, base, inputMeta: meta };\n }\n\n const reqW = spec.requiredWidth;\n const reqH = spec.requiredHeight;\n if (reqW && reqH) {\n const canFix = spec.allowAutoFormat === true;\n if (!canFix && w && h && (w !== reqW || h !== reqH)) {\n return {\n ok: false,\n error: {\n code: 'dimensions_mismatch',\n message: 'Image dimensions do not match the required format.',\n details: { requiredWidth: reqW, requiredHeight: reqH, width: w, height: h },\n },\n };\n }\n\n base = resizeCover(base, reqW, reqH);\n\n return { ok: true, base, inputMeta: meta };\n }\n\n const reqAspect = spec.requiredAspectRatio;\n if (reqAspect && w && h) {\n const a = aspect(w, h);\n const tol = spec.aspectRatioTolerance ?? 0.02;\n if (a && !aspectClose(a, reqAspect, tol)) {\n if (!spec.allowAutoFormat) {\n return {\n ok: false,\n error: {\n code: 'aspect_ratio_mismatch',\n message: 'Image aspect ratio does not match the required format.',\n details: { requiredAspectRatio: reqAspect, aspectRatio: a, width: w, height: h },\n },\n };\n }\n\n base = base.extract(computeAspectCropBox(w, h, reqAspect));\n }\n }\n\n return { ok: true, base, inputMeta: meta };\n}\n\nexport async function processImage(\n spec: MediaProcessingSpec,\n ctx: {\n inputPath: string;\n outputBasePath: 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: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n const variants = spec.image?.variants?.length\n ? spec.image.variants\n : [{ key: IMAGE_DEFAULT_OUTPUT_KEY }];\n\n const outDir = path.dirname(ctx.outputBasePath);\n await mkdir(outDir, { recursive: true });\n\n const baseRes = await buildBase(spec, ctx.inputPath);\n if (!baseRes.ok) {\n return { ok: false, mediaType: 'image', error: baseRes.error };\n }\n\n const { base, inputMeta } = baseRes;\n\n const outputs: MediaOutput[] = [];\n const totalVariants = variants.length || 1;\n let idx = 0;\n\n for (const v of variants) {\n idx += 1;\n\n if (opts?.signal?.aborted) {\n return {\n ok: false,\n mediaType: 'image',\n error: { code: 'processing_canceled', message: 'Processing canceled.' },\n };\n }\n\n opts?.onProgress?.({\n phase: 'process',\n percent: (idx - 1) / totalVariants,\n detail: { step: 'image_variant', key: v.key },\n });\n\n const { ext, format } = pickFormat(v.format);\n const outPath = outputPathFor(ctx.outputBasePath, v.key, ext);\n tempFiles.push(outPath);\n\n let p = base.clone();\n\n if (v.crop) {\n p = resizeCover(\n p,\n v.crop.width,\n v.crop.height,\n toSharpPosition((v.crop.gravity as Gravity) ?? 'center'),\n );\n } else if (v.maxWidth || v.maxHeight) {\n p = resizeInside(p, v.maxWidth, v.maxHeight);\n }\n\n p = applyImageFormat(p, format, v.quality);\n\n const info = await p.toFile(outPath);\n\n outputs.push({\n key: v.key,\n url: `file://${outPath}`,\n path: outPath,\n mime: mimeForImageFormat(info.format),\n sizeBytes: info.size,\n width: info.width,\n height: info.height,\n });\n }\n\n tempFiles.length = 0;\n\n return {\n ok: true,\n mediaType: 'image',\n outputs,\n meta: {\n mime: mimeForImageFormat(inputMeta.format),\n width: inputMeta.width,\n height: inputMeta.height,\n },\n };\n } catch (e: any) {\n return {\n ok: false,\n mediaType: 'image',\n error: {\n code: 'processing_failed',\n message: e?.message ? String(e.message) : 'Image 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,22 +1,8 @@
1
1
  import type { Sharp, Gravity as SharpGravity } from 'sharp';
2
- /** Logical crop/resize gravity, mapped to sharp positions by toSharpPosition. */
3
2
  export type Gravity = 'center' | 'top' | 'bottom' | 'left' | 'right';
4
- /** Map a logical gravity to a sharp position string. Defaults to centre. */
5
3
  export declare function toSharpPosition(g?: Gravity): SharpGravity;
6
- /**
7
- * Cover-resize to an exact width/height (crop to fill), never enlarging beyond
8
- * the source. Used for explicit crops and required-size normalization.
9
- */
10
4
  export declare function resizeCover(pipeline: Sharp, width: number, height: number, position?: SharpGravity): Sharp;
11
- /**
12
- * Fit-inside resize bounded by maxWidth/maxHeight (preserves aspect ratio),
13
- * never enlarging beyond the source. Either bound may be omitted.
14
- */
15
5
  export declare function resizeInside(pipeline: Sharp, maxWidth?: number, maxHeight?: number): Sharp;
16
- /**
17
- * Compute a centered crop box that brings (width x height) to targetAspect,
18
- * shrinking only the longer dimension. Returns sharp .extract() coordinates.
19
- */
20
6
  export declare function computeAspectCropBox(width: number, height: number, targetAspect: number): {
21
7
  left: number;
22
8
  top: number;
@@ -1 +1 @@
1
- {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,OAAO,CAAC;AAE5D,iFAAiF;AACjF,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAErE,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAczD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,KAAK,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,YAAuB,GAChC,KAAK,CAMP;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAK1F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAiB9D"}
1
+ {"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,OAAO,CAAC;AAE5D,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAErE,wBAAgB,eAAe,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAczD;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,KAAK,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,YAAuB,GAChC,KAAK,CAMP;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAK1F;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAe9D"}
@@ -1,4 +1,3 @@
1
- /** Map a logical gravity to a sharp position string. Defaults to centre. */
2
1
  export function toSharpPosition(g) {
3
2
  switch (g) {
4
3
  case 'top':
@@ -14,10 +13,6 @@ export function toSharpPosition(g) {
14
13
  return 'centre';
15
14
  }
16
15
  }
17
- /**
18
- * Cover-resize to an exact width/height (crop to fill), never enlarging beyond
19
- * the source. Used for explicit crops and required-size normalization.
20
- */
21
16
  export function resizeCover(pipeline, width, height, position = 'centre') {
22
17
  return pipeline.resize(width, height, {
23
18
  fit: 'cover',
@@ -25,30 +20,20 @@ export function resizeCover(pipeline, width, height, position = 'centre') {
25
20
  withoutEnlargement: true,
26
21
  });
27
22
  }
28
- /**
29
- * Fit-inside resize bounded by maxWidth/maxHeight (preserves aspect ratio),
30
- * never enlarging beyond the source. Either bound may be omitted.
31
- */
32
23
  export function resizeInside(pipeline, maxWidth, maxHeight) {
33
24
  return pipeline.resize(maxWidth ?? null, maxHeight ?? null, {
34
25
  fit: 'inside',
35
26
  withoutEnlargement: true,
36
27
  });
37
28
  }
38
- /**
39
- * Compute a centered crop box that brings (width x height) to targetAspect,
40
- * shrinking only the longer dimension. Returns sharp .extract() coordinates.
41
- */
42
29
  export function computeAspectCropBox(width, height, targetAspect) {
43
30
  const a = width / height;
44
31
  let cropW = width;
45
32
  let cropH = height;
46
33
  if (a > targetAspect) {
47
- // too wide -> reduce width
48
34
  cropW = Math.max(1, Math.floor(height * targetAspect));
49
35
  }
50
36
  else {
51
- // too tall -> reduce height
52
37
  cropH = Math.max(1, Math.floor(width / targetAspect));
53
38
  }
54
39
  const left = Math.max(0, Math.floor((width - cropW) / 2));
@@ -1 +1 @@
1
- {"version":3,"file":"resize.js","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AAMA,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,CAAW;IACzC,QAAQ,CAAC,EAAE,CAAC;QACV,KAAK,KAAK;YACR,OAAO,OAAO,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,MAAM,CAAC;QAChB,KAAK,QAAQ,CAAC;QACd;YACE,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,QAAe,EACf,KAAa,EACb,MAAc,EACd,WAAyB,QAAQ;IAEjC,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE;QACpC,GAAG,EAAE,OAAO;QACZ,QAAQ;QACR,kBAAkB,EAAE,IAAI;KACzB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,QAAe,EAAE,QAAiB,EAAE,SAAkB;IACjF,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE;QAC1D,GAAG,EAAE,QAAQ;QACb,kBAAkB,EAAE,IAAI;KACzB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAa,EACb,MAAc,EACd,YAAoB;IAEpB,MAAM,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC;IACzB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,KAAK,GAAG,MAAM,CAAC;IAEnB,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC;QACrB,2BAA2B;QAC3B,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,4BAA4B;QAC5B,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAE1D,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACpD,CAAC","sourcesContent":["// sharp resize helpers extracted from the image-processor\nimport type { Sharp, Gravity as SharpGravity } from 'sharp';\n\n/** Logical crop/resize gravity, mapped to sharp positions by toSharpPosition. */\nexport type Gravity = 'center' | 'top' | 'bottom' | 'left' | 'right';\n\n/** Map a logical gravity to a sharp position string. Defaults to centre. */\nexport function toSharpPosition(g?: Gravity): SharpGravity {\n switch (g) {\n case 'top':\n return 'north';\n case 'bottom':\n return 'south';\n case 'left':\n return 'west';\n case 'right':\n return 'east';\n case 'center':\n default:\n return 'centre';\n }\n}\n\n/**\n * Cover-resize to an exact width/height (crop to fill), never enlarging beyond\n * the source. Used for explicit crops and required-size normalization.\n */\nexport function resizeCover(\n pipeline: Sharp,\n width: number,\n height: number,\n position: SharpGravity = 'centre',\n): Sharp {\n return pipeline.resize(width, height, {\n fit: 'cover',\n position,\n withoutEnlargement: true,\n });\n}\n\n/**\n * Fit-inside resize bounded by maxWidth/maxHeight (preserves aspect ratio),\n * never enlarging beyond the source. Either bound may be omitted.\n */\nexport function resizeInside(pipeline: Sharp, maxWidth?: number, maxHeight?: number): Sharp {\n return pipeline.resize(maxWidth ?? null, maxHeight ?? null, {\n fit: 'inside',\n withoutEnlargement: true,\n });\n}\n\n/**\n * Compute a centered crop box that brings (width x height) to targetAspect,\n * shrinking only the longer dimension. Returns sharp .extract() coordinates.\n */\nexport function computeAspectCropBox(\n width: number,\n height: number,\n targetAspect: number,\n): { left: number; top: number; width: number; height: number } {\n const a = width / height;\n let cropW = width;\n let cropH = height;\n\n if (a > targetAspect) {\n // too wide -> reduce width\n cropW = Math.max(1, Math.floor(height * targetAspect));\n } else {\n // too tall -> reduce height\n cropH = Math.max(1, Math.floor(width / targetAspect));\n }\n\n const left = Math.max(0, Math.floor((width - cropW) / 2));\n const top = Math.max(0, Math.floor((height - cropH) / 2));\n\n return { left, top, width: cropW, height: cropH };\n}\n"]}
1
+ {"version":3,"file":"resize.js","sourceRoot":"","sources":["../../src/image/resize.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,eAAe,CAAC,CAAW;IACzC,QAAQ,CAAC,EAAE,CAAC;QACV,KAAK,KAAK;YACR,OAAO,OAAO,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,MAAM,CAAC;QAChB,KAAK,QAAQ,CAAC;QACd;YACE,OAAO,QAAQ,CAAC;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,QAAe,EACf,KAAa,EACb,MAAc,EACd,WAAyB,QAAQ;IAEjC,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE;QACpC,GAAG,EAAE,OAAO;QACZ,QAAQ;QACR,kBAAkB,EAAE,IAAI;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAe,EAAE,QAAiB,EAAE,SAAkB;IACjF,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,EAAE,SAAS,IAAI,IAAI,EAAE;QAC1D,GAAG,EAAE,QAAQ;QACb,kBAAkB,EAAE,IAAI;KACzB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,KAAa,EACb,MAAc,EACd,YAAoB;IAEpB,MAAM,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC;IACzB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,IAAI,KAAK,GAAG,MAAM,CAAC;IAEnB,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC;QACrB,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC;IACzD,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAE1D,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACpD,CAAC","sourcesContent":["import type { Sharp, Gravity as SharpGravity } from 'sharp';\n\nexport type Gravity = 'center' | 'top' | 'bottom' | 'left' | 'right';\n\nexport function toSharpPosition(g?: Gravity): SharpGravity {\n switch (g) {\n case 'top':\n return 'north';\n case 'bottom':\n return 'south';\n case 'left':\n return 'west';\n case 'right':\n return 'east';\n case 'center':\n default:\n return 'centre';\n }\n}\n\nexport function resizeCover(\n pipeline: Sharp,\n width: number,\n height: number,\n position: SharpGravity = 'centre',\n): Sharp {\n return pipeline.resize(width, height, {\n fit: 'cover',\n position,\n withoutEnlargement: true,\n });\n}\n\nexport function resizeInside(pipeline: Sharp, maxWidth?: number, maxHeight?: number): Sharp {\n return pipeline.resize(maxWidth ?? null, maxHeight ?? null, {\n fit: 'inside',\n withoutEnlargement: true,\n });\n}\n\nexport function computeAspectCropBox(\n width: number,\n height: number,\n targetAspect: number,\n): { left: number; top: number; width: number; height: number } {\n const a = width / height;\n let cropW = width;\n let cropH = height;\n\n if (a > targetAspect) {\n cropW = Math.max(1, Math.floor(height * targetAspect));\n } else {\n cropH = Math.max(1, Math.floor(width / targetAspect));\n }\n\n const left = Math.max(0, Math.floor((width - cropW) / 2));\n const top = Math.max(0, Math.floor((height - cropH) / 2));\n\n return { left, top, width: cropW, height: cropH };\n}\n"]}