@hadialmarzooq/agent-media-ffmpeg 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @hadialmarzooq/agent-media-ffmpeg
2
+
3
+ Safe FFmpeg execution, progress reporting, and verified media workflows for software agents.
4
+
5
+ ## What it does
6
+
7
+ The FFmpeg backend for [Agent Media](https://github.com/HadiAlMarzooq/agent-media). Compiles semantic Media IR plans into deterministic FFmpeg invocations, executes them with progress reporting, and inspects outputs for verification.
8
+
9
+ ### Five high-level workflows
10
+
11
+ All workflows share the same contract: **inspect → plan → serialize → execute → verify**. Each returns `{ source, plan, serializedPlan, output, verification }`.
12
+
13
+ ```ts
14
+ import {
15
+ makeVertical,
16
+ optimizeForWeb,
17
+ normalize,
18
+ extractAudio,
19
+ extractFrame,
20
+ } from '@hadialmarzooq/agent-media-ffmpeg';
21
+
22
+ // 9:16 vertical, H.264/yuv420p, faststart, size-constrained
23
+ const vertical = await makeVertical({
24
+ input: 'demo.mp4',
25
+ output: 'vertical.mp4',
26
+ maxSizeMB: 25,
27
+ onProgress: ({ phase, percent }) => console.error(`${phase}: ${percent}%`),
28
+ });
29
+
30
+ // Web-optimized: balanced quality, H.264, faststart
31
+ const web = await optimizeForWeb({
32
+ input: 'demo.mp4',
33
+ output: 'web.mp4',
34
+ maxSizeMB: 10,
35
+ });
36
+
37
+ // Normalized high-compatibility copy
38
+ const norm = await normalize({ input: 'demo.mp4', output: 'normalized.mp4' });
39
+
40
+ // Extract audio
41
+ const audio = await extractAudio({ input: 'demo.mp4', output: 'audio.m4a' });
42
+
43
+ // Extract a still frame
44
+ const frame = await extractFrame({ input: 'demo.mp4', output: 'frame.jpg', atSeconds: 2 });
45
+ ```
46
+
47
+ ### Explicit plan and replay
48
+
49
+ ```ts
50
+ import { inspectMedia, executePlan, getCapabilities } from '@hadialmarzooq/agent-media-ffmpeg';
51
+ import { planMedia, serializePlan, parsePlan, verifyMedia } from '@hadialmarzooq/agent-media-core';
52
+
53
+ const source = await inspectMedia('demo.mp4');
54
+ const plan = planMedia({
55
+ source,
56
+ capabilities: await getCapabilities(),
57
+ goals: { aspectRatio: '9:16', compatibility: 'high', maxSizeMB: 25 },
58
+ });
59
+
60
+ // Save and replay
61
+ const json = serializePlan(plan);
62
+ const replayed = parsePlan(json);
63
+ const result = await executePlan(replayed, { output: 'vertical.mp4' });
64
+ const output = await inspectMedia(result.output);
65
+ const report = verifyMedia(output, replayed.expectations);
66
+ ```
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ npm install @hadialmarzooq/agent-media-core @hadialmarzooq/agent-media-ffmpeg
72
+ ```
73
+
74
+ Prerequisites: Node.js 22+, `ffmpeg` and `ffprobe` on `PATH`.
75
+
76
+ ## Safety
77
+
78
+ - Rejects source overwrite, output collisions, and directory escape
79
+ - Cancellation and timeout controls with partial cleanup
80
+ - Concatenation preflight rejects incompatible streams before execution
81
+ - Progress is monotonic and isolated — UI callbacks can't change execution semantics
82
+
83
+ ## Documentation
84
+
85
+ - [Full docs](https://github.com/HadiAlMarzooq/agent-media/tree/main/docs)
86
+ - [Workflows](https://github.com/HadiAlMarzooq/agent-media/blob/main/docs/workflows.md)
87
+ - [API reference](https://github.com/HadiAlMarzooq/agent-media/blob/main/docs/api.md)
88
+ - [Reliability](https://github.com/HadiAlMarzooq/agent-media/blob/main/docs/reliability.md)
89
+
90
+ ## License
91
+
92
+ MIT
package/dist/index.d.ts CHANGED
@@ -80,6 +80,9 @@ interface ExtractFrameOptions extends WorkflowOptions {
80
80
  atSeconds?: number;
81
81
  format?: 'jpg' | 'png';
82
82
  }
83
+ interface ConcatenateOptions extends WorkflowOptions {
84
+ inputs: string[];
85
+ }
83
86
  interface WorkflowResult {
84
87
  source: MediaMetadata;
85
88
  plan: MediaPlan;
@@ -110,5 +113,9 @@ declare function extractAudio(options: ExtractAudioOptions): Promise<WorkflowRes
110
113
  * Inspect, plan, execute, and verify a still frame extraction from a video source.
111
114
  */
112
115
  declare function extractFrame(options: ExtractFrameOptions): Promise<WorkflowResult>;
116
+ /**
117
+ * Inspect, plan, execute, and verify concatenation of multiple media sources.
118
+ */
119
+ declare function concatenate(options: ConcatenateOptions): Promise<WorkflowResult>;
113
120
 
114
- export { type CompiledOperation, type ExecuteOptions, type ExecutionResult, type ExtractAudioOptions, type ExtractFrameOptions, type FfmpegOptions, type MakeVerticalOptions, type MediaProgress, type MediaProgressPhase, type NormalizeOptions, type OptimizeForWebOptions, type ProgressCallback, type WorkflowOptions, type WorkflowResult, compilePlan, executePlan, extensionForPlan, extractAudio, extractFrame, getCapabilities, inspectMedia, makeVertical, normalize, optimizeForWeb };
121
+ export { type CompiledOperation, type ConcatenateOptions, type ExecuteOptions, type ExecutionResult, type ExtractAudioOptions, type ExtractFrameOptions, type FfmpegOptions, type MakeVerticalOptions, type MediaProgress, type MediaProgressPhase, type NormalizeOptions, type OptimizeForWebOptions, type ProgressCallback, type WorkflowOptions, type WorkflowResult, compilePlan, concatenate, executePlan, extensionForPlan, extractAudio, extractFrame, getCapabilities, inspectMedia, makeVertical, normalize, optimizeForWeb };
package/dist/index.js CHANGED
@@ -254,12 +254,12 @@ function extensionForPlan(plan) {
254
254
 
255
255
  // src/executor.ts
256
256
  import { access, constants, rm } from "fs/promises";
257
- import { dirname, relative, resolve as resolve2 } from "path";
257
+ import { dirname, extname as extname2, relative, resolve as resolve2 } from "path";
258
258
  import { MediaError as MediaError4, validatePlan } from "@hadialmarzooq/agent-media-core";
259
259
 
260
260
  // src/inspect.ts
261
261
  import { stat } from "fs/promises";
262
- import { basename, resolve } from "path";
262
+ import { resolve } from "path";
263
263
  import { MediaError as MediaError3 } from "@hadialmarzooq/agent-media-core";
264
264
  async function inspectMedia(input, options = {}) {
265
265
  const path = resolve(input);
@@ -269,7 +269,7 @@ async function inspectMedia(input, options = {}) {
269
269
  } catch {
270
270
  throw new MediaError3({
271
271
  code: "UNSUPPORTED_INPUT",
272
- message: `The input file does not exist: ${basename(input)}.`,
272
+ message: `The input file does not exist: ${path}.`,
273
273
  context: { input: path },
274
274
  suggestedActions: ["Check the source path and permissions."]
275
275
  });
@@ -442,6 +442,14 @@ function parseSpeed(value) {
442
442
  async function executePlan(planInput, options) {
443
443
  const plan = validatePlan(planInput);
444
444
  const output = resolve2(options.output);
445
+ const release = await acquireOutputLock(output);
446
+ try {
447
+ return await executePlanInternal(plan, options, output);
448
+ } finally {
449
+ releaseOutputLock(output, release);
450
+ }
451
+ }
452
+ async function executePlanInternal(plan, options, output) {
445
453
  if (output === resolve2(plan.source.path)) {
446
454
  throw new MediaError4({
447
455
  code: "PATH_NOT_ALLOWED",
@@ -466,6 +474,25 @@ async function executePlan(planInput, options) {
466
474
  suggestedActions: ["Choose a different output path or explicitly enable overwrite."]
467
475
  });
468
476
  }
477
+ const outputDir = dirname(output);
478
+ if (!await exists(outputDir)) {
479
+ throw new MediaError4({
480
+ code: "OUTPUT_DIR_MISSING",
481
+ message: "The output directory does not exist.",
482
+ context: { output, directory: outputDir },
483
+ suggestedActions: ["Create the output directory before execution."]
484
+ });
485
+ }
486
+ const expectedExt = extensionForPlan(plan);
487
+ const actualExt = extname2(output);
488
+ if (expectedExt && actualExt && expectedExt !== actualExt) {
489
+ throw new MediaError4({
490
+ code: "OUTPUT_EXTENSION_MISMATCH",
491
+ message: `The output extension "${actualExt}" does not match the plan's expected "${expectedExt}".`,
492
+ context: { output, expectedExtension: expectedExt, actualExtension: actualExt },
493
+ suggestedActions: [`Use a "${expectedExt}" output extension, or adjust the plan.`]
494
+ });
495
+ }
469
496
  const sourceMetadata = options.sourceMetadata ?? await inspectMedia(plan.source.path, {
470
497
  ...options.ffprobePath === void 0 ? {} : { ffprobePath: options.ffprobePath },
471
498
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
@@ -551,10 +578,10 @@ function executionDuration(plan, source) {
551
578
  return source.durationSeconds === void 0 ? void 0 : Math.max(0, source.durationSeconds - trim.startSeconds);
552
579
  }
553
580
  async function preflightConcatenation(plan, source, options) {
554
- const concatenate = plan.steps.find((step) => step.operation === "concatenate");
555
- if (concatenate?.operation !== "concatenate") return;
581
+ const concatenate2 = plan.steps.find((step) => step.operation === "concatenate");
582
+ if (concatenate2?.operation !== "concatenate") return;
556
583
  const metadata = await Promise.all(
557
- concatenate.inputs.map(async (input, index) => {
584
+ concatenate2.inputs.map(async (input, index) => {
558
585
  if (index === 0) return source;
559
586
  return inspectMedia(input, {
560
587
  ...options.ffprobePath === void 0 ? {} : { ffprobePath: options.ffprobePath },
@@ -572,7 +599,7 @@ async function preflightConcatenation(plan, source, options) {
572
599
  code: "UNSUPPORTED_INPUT",
573
600
  message: "Concatenation inputs have incompatible stream layouts.",
574
601
  context: {
575
- input: concatenate.inputs[index],
602
+ input: concatenate2.inputs[index],
576
603
  inputIndex: index,
577
604
  incompatibleFields
578
605
  },
@@ -630,6 +657,24 @@ function isWithin(path, directory) {
630
657
  const pathRelative = relative(directory, path);
631
658
  return pathRelative === "" || !pathRelative.startsWith("..") && !pathRelative.includes("..\\");
632
659
  }
660
+ var outputLocks = /* @__PURE__ */ new Map();
661
+ async function acquireOutputLock(output) {
662
+ const prev = outputLocks.get(output) ?? Promise.resolve();
663
+ let release = () => void 0;
664
+ const next = new Promise((resolve3) => {
665
+ release = resolve3;
666
+ });
667
+ outputLocks.set(
668
+ output,
669
+ prev.then(() => next)
670
+ );
671
+ await prev;
672
+ return release;
673
+ }
674
+ function releaseOutputLock(output, release) {
675
+ release();
676
+ outputLocks.delete(output);
677
+ }
633
678
 
634
679
  // src/workflows.ts
635
680
  import { MediaError as MediaError5, planMedia, serializePlan, verifyMedia } from "@hadialmarzooq/agent-media-core";
@@ -689,6 +734,13 @@ async function extractFrame(options) {
689
734
  });
690
735
  return executeAndVerify(options, source, plan, "Frame extraction is verified and ready.");
691
736
  }
737
+ async function concatenate(options) {
738
+ const source = await inspectPhase(options, "concatenation");
739
+ const plan = await planningPhase(options, "concatenation", source, {
740
+ concatenate: options.inputs
741
+ });
742
+ return executeAndVerify(options, source, plan, "Concatenation is verified and ready.");
743
+ }
692
744
  function verticalDimensions(width, height) {
693
745
  if (width === void 0 !== (height === void 0)) {
694
746
  throw new MediaError5({
@@ -762,6 +814,7 @@ function emit(onProgress, phase, percent, message) {
762
814
  }
763
815
  export {
764
816
  compilePlan,
817
+ concatenate,
765
818
  executePlan,
766
819
  extensionForPlan,
767
820
  extractAudio,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadialmarzooq/agent-media-ffmpeg",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Safe FFmpeg execution, progress, and verified media workflows for software agents.",
5
5
  "keywords": [
6
6
  "media",
@@ -32,13 +32,15 @@
32
32
  }
33
33
  },
34
34
  "files": [
35
- "dist"
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
36
38
  ],
37
39
  "publishConfig": {
38
40
  "access": "public"
39
41
  },
40
42
  "dependencies": {
41
- "@hadialmarzooq/agent-media-core": "0.1.0"
43
+ "@hadialmarzooq/agent-media-core": "0.2.0"
42
44
  },
43
45
  "scripts": {
44
46
  "build": "tsup src/index.ts --format esm --dts",