simple-ffmpegjs 0.3.1 → 0.3.3

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 CHANGED
@@ -1,10 +1,17 @@
1
- # simple-ffmpeg
1
+ <p align="center">
2
+ <img src="https://7llpl63xkl8jovgt.public.blob.vercel-storage.com/simple-ffmpeg/zENiV5XBIET_cu11ZpOdE.png" alt="simple-ffmpeg" width="100%">
3
+ </p>
2
4
 
3
- [![npm version](https://img.shields.io/npm/v/simple-ffmpegjs.svg)](https://www.npmjs.com/package/simple-ffmpegjs)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/simple-ffmpegjs"><img src="https://img.shields.io/npm/v/simple-ffmpegjs.svg" alt="npm version"></a>
7
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
8
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg" alt="Node.js"></a>
9
+ </p>
6
10
 
7
- A lightweight Node.js library for programmatic video composition using FFmpeg. Designed for data pipelines and automation workflows that need reliable video assembly without the complexity of a full editing suite.
11
+ <p align="center">
12
+ A lightweight Node.js library for programmatic video composition using FFmpeg.<br>
13
+ Define your timeline as a simple array of clips, and the library handles the rest.
14
+ </p>
8
15
 
9
16
  ## Table of Contents
10
17
 
@@ -12,7 +19,8 @@ A lightweight Node.js library for programmatic video composition using FFmpeg. D
12
19
  - [Features](#features)
13
20
  - [Installation](#installation)
14
21
  - [Quick Start](#quick-start)
15
- - [Pre-Validation (for AI Pipelines)](#pre-validation-for-ai-pipelines)
22
+ - [Pre-Validation](#pre-validation)
23
+ - [Schema Export](#schema-export)
16
24
  - [API Reference](#api-reference)
17
25
  - [Constructor](#constructor)
18
26
  - [Methods](#methods)
@@ -24,26 +32,26 @@ A lightweight Node.js library for programmatic video composition using FFmpeg. D
24
32
  - [Cancellation](#cancellation)
25
33
  - [Gap Handling](#gap-handling)
26
34
  - [Examples](#examples)
27
- - [Transitions](#two-clips-with-transition)
28
- - [Text Positioning](#text-positioning-with-offsets)
29
- - [Word-by-Word Animation](#word-by-word-text-animation)
30
- - [Ken Burns Slideshow](#image-slideshow-with-ken-burns)
31
- - [Export Options](#high-quality-export-with-custom-settings)
32
- - [Text Animations](#typewriter-text-effect)
33
- - [Karaoke](#karaoke-text-effect)
34
- - [Subtitles](#import-srtvtt-subtitles)
35
- - [Timeline Behavior](#timeline-behavior)
36
- - [Transition Compensation](#transition-compensation)
37
- - [Auto-Batching](#auto-batching-for-complex-filter-graphs)
35
+ - [Clips & Transitions](#clips--transitions)
36
+ - [Text & Animations](#text--animations)
37
+ - [Karaoke](#karaoke)
38
+ - [Subtitles](#subtitles)
39
+ - [Export Settings](#export-settings)
40
+ - [Real-World Usage Patterns](#real-world-usage-patterns)
41
+ - [Data Pipeline](#data-pipeline-example)
42
+ - [AI Video Pipeline](#ai-video-generation-pipeline-example)
43
+ - [Advanced](#advanced)
44
+ - [Timeline Behavior](#timeline-behavior)
45
+ - [Auto-Batching](#auto-batching)
38
46
  - [Testing](#testing)
39
47
  - [Contributing](#contributing)
40
48
  - [License](#license)
41
49
 
42
50
  ## Why simple-ffmpeg?
43
51
 
44
- With `fluent-ffmpeg` no longer actively maintained, there's a need for a modern, well-supported alternative. simple-ffmpeg fills this gap with a declarative, config-driven API that's particularly well-suited for structured validation with error codes that makes it easy to build feedback loops where AI can generate configs, validate them, and iterate until successful
52
+ FFmpeg is incredibly powerful, but its command-line interface is notoriously difficult to work with programmatically. Composing even a simple two-clip video with a crossfade requires navigating complex filter graphs, input mapping, and stream labeling. simple-ffmpeg abstracts all of that behind a declarative, config-driven API. You describe _what_ your video should look like, and the library figures out _how_ to build the FFmpeg command.
45
53
 
46
- The library handles FFmpeg's complexity internally while exposing a clean interface that both humans and AI can work with effectively.
54
+ The entire timeline is expressed as a plain array of clip objects, making it straightforward to generate configurations from any data source: databases, APIs, templates, or AI models. Structured validation with machine-readable error codes means you can catch problems early and handle them programmatically, whether that's logging a warning, retrying with corrected input, or surfacing feedback to an end user.
47
55
 
48
56
  ## Example Output
49
57
 
@@ -70,6 +78,8 @@ _Click to watch a "Wonders of the World" video created with simple-ffmpeg — co
70
78
  - **Cancellation** — AbortController support for stopping exports
71
79
  - **Gap Handling** — Optional black frame fill for timeline gaps
72
80
  - **Auto-Batching** — Automatically splits complex filter graphs to avoid OS command limits
81
+ - **Schema Export** — Generate a structured description of the clip format for documentation, code generation, or AI context
82
+ - **Pre-Validation** — Validate clip configurations before processing with structured, machine-readable error codes
73
83
  - **TypeScript Ready** — Full type definitions included
74
84
  - **Zero Dependencies** — Only requires FFmpeg on your system
75
85
 
@@ -109,41 +119,50 @@ apt-get install -y ffmpeg fontconfig fonts-dejavu-core
109
119
  ```js
110
120
  import SIMPLEFFMPEG from "simple-ffmpegjs";
111
121
 
112
- const project = new SIMPLEFFMPEG({
113
- width: 1920,
114
- height: 1080,
115
- fps: 30,
116
- });
122
+ // Use a platform preset — or set width/height/fps manually
123
+ const project = new SIMPLEFFMPEG({ preset: "youtube" });
117
124
 
118
125
  await project.load([
119
- { type: "video", url: "./intro.mp4", position: 0, end: 5 },
126
+ // Two video clips with a crossfade transition between them
127
+ { type: "video", url: "./opening-shot.mp4", position: 0, end: 6 },
120
128
  {
121
129
  type: "video",
122
- url: "./main.mp4",
123
- position: 5,
124
- end: 15,
130
+ url: "./highlights.mp4",
131
+ position: 5.5,
132
+ end: 18,
133
+ cutFrom: 3, // start 3s into the source file
125
134
  transition: { type: "fade", duration: 0.5 },
126
135
  },
127
- { type: "music", url: "./bgm.mp3", volume: 0.2 },
136
+
137
+ // Title card with a pop animation
128
138
  {
129
139
  type: "text",
130
- text: "Hello World",
131
- position: 1,
140
+ text: "Summer Highlights 2025",
141
+ position: 0.5,
132
142
  end: 4,
133
- fontColor: "white",
134
- fontSize: 64,
143
+ fontFile: "./fonts/Montserrat-Bold.ttf",
144
+ fontSize: 72,
145
+ fontColor: "#FFFFFF",
146
+ borderColor: "#000000",
147
+ borderWidth: 2,
148
+ xPercent: 0.5,
149
+ yPercent: 0.4,
150
+ animation: { type: "pop", in: 0.3 },
135
151
  },
152
+
153
+ // Background music — loops to fill the whole video
154
+ { type: "music", url: "./chill-beat.mp3", volume: 0.2, loop: true },
136
155
  ]);
137
156
 
138
157
  await project.export({
139
- outputPath: "./output.mp4",
158
+ outputPath: "./summer-highlights.mp4",
140
159
  onProgress: ({ percent }) => console.log(`${percent}% complete`),
141
160
  });
142
161
  ```
143
162
 
144
- ## Pre-Validation (for AI Pipelines)
163
+ ## Pre-Validation
145
164
 
146
- Validate configurations before creating a project—ideal for AI feedback loops where you want to catch errors early and provide structured feedback:
165
+ Validate clip configurations before creating a project. Useful for catching errors early in data pipelines, form-based editors, or any workflow where configurations are generated dynamically:
147
166
 
148
167
  ```js
149
168
  import SIMPLEFFMPEG from "simple-ffmpegjs";
@@ -155,7 +174,7 @@ const clips = [
155
174
 
156
175
  // Validate without creating a project
157
176
  const result = SIMPLEFFMPEG.validate(clips, {
158
- skipFileChecks: true, // Skip file existence checks (useful when files don't exist yet)
177
+ skipFileChecks: true, // Skip file existence checks (useful when files aren't on disk yet)
159
178
  width: 1920, // Project dimensions (for Ken Burns size validation)
160
179
  height: 1080,
161
180
  strictKenBurns: false, // If true, undersized Ken Burns images error instead of warn (default: false)
@@ -190,6 +209,70 @@ if (result.errors.some((e) => e.code === ValidationCodes.TIMELINE_GAP)) {
190
209
  }
191
210
  ```
192
211
 
212
+ ## Schema Export
213
+
214
+ Export a structured, human-readable description of all clip types accepted by `load()`. The output is designed to serve as context for LLMs, documentation generators, code generation tools, or anything that needs to understand the library's clip format.
215
+
216
+ ### Basic Usage
217
+
218
+ ```js
219
+ // Get the full schema (all clip types)
220
+ const schema = SIMPLEFFMPEG.getSchema();
221
+ console.log(schema);
222
+ ```
223
+
224
+ The output is a formatted text document with type definitions, allowed values, usage notes, and examples for each clip type.
225
+
226
+ ### Filtering Modules
227
+
228
+ The schema is broken into modules — one per clip type. You can include or exclude modules to control exactly what appears in the output:
229
+
230
+ ```js
231
+ // Only include video and image clip types
232
+ const schema = SIMPLEFFMPEG.getSchema({ include: ["video", "image"] });
233
+
234
+ // Include everything except text and subtitle
235
+ const schema = SIMPLEFFMPEG.getSchema({ exclude: ["text", "subtitle"] });
236
+
237
+ // See all available module IDs
238
+ SIMPLEFFMPEG.getSchemaModules();
239
+ // ['video', 'audio', 'image', 'text', 'subtitle', 'music']
240
+ ```
241
+
242
+ Available modules:
243
+
244
+ | Module | Covers |
245
+ | ---------- | ----------------------------------------------------------- |
246
+ | `video` | Video clips, transitions, volume, trimming |
247
+ | `audio` | Standalone audio clips |
248
+ | `image` | Image clips, Ken Burns effects |
249
+ | `text` | Text overlays — all modes, animations, positioning, styling |
250
+ | `subtitle` | Subtitle file import (SRT, VTT, ASS, SSA) |
251
+ | `music` | Background music / background audio, looping |
252
+
253
+ ### Custom Instructions
254
+
255
+ Embed your own instructions directly into the schema output. Top-level instructions appear at the beginning, and per-module instructions are placed inside the relevant section — formatted identically to the built-in notes:
256
+
257
+ ```js
258
+ const schema = SIMPLEFFMPEG.getSchema({
259
+ include: ["video", "image", "music"],
260
+ instructions: [
261
+ "You are creating short cooking tutorials for TikTok.",
262
+ "Keep all videos under 30 seconds.",
263
+ ],
264
+ moduleInstructions: {
265
+ video: [
266
+ "Always use fade transitions at 0.5s.",
267
+ "Limit to 5 clips maximum.",
268
+ ],
269
+ music: "Always include background music at volume 0.15.",
270
+ },
271
+ });
272
+ ```
273
+
274
+ Both `instructions` and `moduleInstructions` values accept a `string` or `string[]`. Per-module instructions for excluded modules are silently ignored.
275
+
193
276
  ## API Reference
194
277
 
195
278
  ### Constructor
@@ -215,6 +298,98 @@ Load clip descriptors into the project. Validates the timeline and reads media m
215
298
  await project.load(clips: Clip[]): Promise<void[]>
216
299
  ```
217
300
 
301
+ #### `SIMPLEFFMPEG.getDuration(clips)`
302
+
303
+ Calculate the total visual timeline duration from a clips array. Handles `duration` and auto-sequencing shorthand, and subtracts transition overlaps. Pure function — no file I/O.
304
+
305
+ ```ts
306
+ const clips = [
307
+ { type: "video", url: "./a.mp4", duration: 5 },
308
+ {
309
+ type: "video",
310
+ url: "./b.mp4",
311
+ duration: 10,
312
+ transition: { type: "fade", duration: 0.5 },
313
+ },
314
+ ];
315
+ SIMPLEFFMPEG.getDuration(clips); // 14.5
316
+ ```
317
+
318
+ Useful for computing text overlay timings or background music end times before calling `load()`.
319
+
320
+ **Duration and Auto-Sequencing:**
321
+
322
+ For video, image, and audio clips, you can use shorthand to avoid specifying explicit `position` and `end` values:
323
+
324
+ - **`duration`** — Use instead of `end`. The library computes `end = position + duration`. You cannot specify both `duration` and `end` on the same clip.
325
+ - **Omit `position`** — The clip is placed immediately after the previous clip on its track. Video and image clips share the visual track; audio clips have their own track. The first clip defaults to `position: 0`.
326
+
327
+ These can be combined:
328
+
329
+ ```ts
330
+ // Before: manual position/end for every clip
331
+ await project.load([
332
+ { type: "video", url: "./a.mp4", position: 0, end: 5 },
333
+ { type: "video", url: "./b.mp4", position: 5, end: 10 },
334
+ { type: "video", url: "./c.mp4", position: 10, end: 18, cutFrom: 3 },
335
+ ]);
336
+
337
+ // After: auto-sequencing + duration
338
+ await project.load([
339
+ { type: "video", url: "./a.mp4", duration: 5 },
340
+ { type: "video", url: "./b.mp4", duration: 5 },
341
+ { type: "video", url: "./c.mp4", duration: 8, cutFrom: 3 },
342
+ ]);
343
+ ```
344
+
345
+ You can mix explicit and implicit positioning freely. Clips with explicit `position` are placed there; subsequent auto-sequenced clips follow from the last clip's end:
346
+
347
+ ```ts
348
+ await project.load([
349
+ { type: "video", url: "./a.mp4", duration: 5 }, // position: 0, end: 5
350
+ { type: "video", url: "./b.mp4", position: 10, end: 15 }, // explicit gap
351
+ { type: "video", url: "./c.mp4", duration: 5 }, // position: 15, end: 20
352
+ ]);
353
+ ```
354
+
355
+ Text clips always require an explicit `position` (they're overlays on specific moments). Background music and subtitle clips already have optional `position`/`end` with their own defaults.
356
+
357
+ #### `SIMPLEFFMPEG.probe(filePath)`
358
+
359
+ Probe a media file and return comprehensive metadata using ffprobe. Works with video, audio, and image files.
360
+
361
+ ```ts
362
+ const info = await SIMPLEFFMPEG.probe("./video.mp4");
363
+ // {
364
+ // duration: 30.5, // seconds
365
+ // width: 1920, // pixels
366
+ // height: 1080, // pixels
367
+ // hasVideo: true,
368
+ // hasAudio: true,
369
+ // rotation: 0, // iPhone/mobile rotation
370
+ // videoCodec: "h264",
371
+ // audioCodec: "aac",
372
+ // format: "mov,mp4,m4a,3gp,3g2,mj2",
373
+ // fps: 30,
374
+ // size: 15728640, // bytes
375
+ // bitrate: 4125000, // bits/sec
376
+ // sampleRate: 48000, // Hz
377
+ // channels: 2 // stereo
378
+ // }
379
+ ```
380
+
381
+ Fields that don't apply to the file type are `null` (e.g. `width`/`height`/`videoCodec`/`fps` for audio-only files, `audioCodec`/`sampleRate`/`channels` for video-only files).
382
+
383
+ Throws `MediaNotFoundError` if the file cannot be found or probed.
384
+
385
+ ```ts
386
+ // Audio file
387
+ const audio = await SIMPLEFFMPEG.probe("./music.wav");
388
+ console.log(audio.hasVideo); // false
389
+ console.log(audio.duration); // 180.5
390
+ console.log(audio.sampleRate); // 44100
391
+ ```
392
+
218
393
  #### `project.export(options)`
219
394
 
220
395
  Build and execute the FFmpeg command to render the final video.
@@ -270,8 +445,9 @@ await project.preview(options?: ExportOptions): Promise<{
270
445
  {
271
446
  type: "video";
272
447
  url: string; // File path
273
- position: number; // Timeline start (seconds)
274
- end: number; // Timeline end (seconds)
448
+ position?: number; // Timeline start (seconds). Omit to auto-sequence after previous clip.
449
+ end?: number; // Timeline end (seconds). Use end OR duration, not both.
450
+ duration?: number; // Duration in seconds (alternative to end). end = position + duration.
275
451
  cutFrom?: number; // Source offset (default: 0)
276
452
  volume?: number; // Audio volume (default: 1)
277
453
  transition?: {
@@ -289,8 +465,9 @@ All [xfade transitions](https://trac.ffmpeg.org/wiki/Xfade) are supported.
289
465
  {
290
466
  type: "audio";
291
467
  url: string;
292
- position: number;
293
- end: number;
468
+ position?: number; // Omit to auto-sequence after previous audio clip
469
+ end?: number; // Use end OR duration, not both
470
+ duration?: number; // Duration in seconds (alternative to end)
294
471
  cutFrom?: number;
295
472
  volume?: number;
296
473
  }
@@ -329,8 +506,9 @@ await project.load([
329
506
  {
330
507
  type: "image";
331
508
  url: string;
332
- position: number;
333
- end: number;
509
+ position?: number; // Omit to auto-sequence after previous video/image clip
510
+ end?: number; // Use end OR duration, not both
511
+ duration?: number; // Duration in seconds (alternative to end)
334
512
  kenBurns?: "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "pan-up" | "pan-down";
335
513
  }
336
514
  ```
@@ -341,7 +519,8 @@ await project.load([
341
519
  {
342
520
  type: "text";
343
521
  position: number;
344
- end: number;
522
+ end?: number; // Use end OR duration, not both
523
+ duration?: number; // Duration in seconds (alternative to end)
345
524
 
346
525
  // Content
347
526
  text?: string;
@@ -518,6 +697,7 @@ The `onProgress` callback receives:
518
697
  ```ts
519
698
  {
520
699
  percent?: number; // 0-100
700
+ phase?: string; // "rendering" or "batching"
521
701
  timeProcessed?: number; // Seconds processed
522
702
  frame?: number; // Current frame
523
703
  fps?: number; // Processing speed
@@ -525,6 +705,23 @@ The `onProgress` callback receives:
525
705
  }
526
706
  ```
527
707
 
708
+ The `phase` field indicates what the export is doing:
709
+
710
+ - `"rendering"` — main video export (includes `percent`, `frame`, etc.)
711
+ - `"batching"` — text overlay passes are running (fired once when batching starts)
712
+
713
+ Use `phase` to update your UI when the export hits 100% but still has work to do:
714
+
715
+ ```ts
716
+ onProgress: ({ percent, phase }) => {
717
+ if (phase === "batching") {
718
+ console.log("Applying text overlays...");
719
+ } else {
720
+ console.log(`${percent}%`);
721
+ }
722
+ }
723
+ ```
724
+
528
725
  ### Error Handling
529
726
 
530
727
  The library provides custom error classes for structured error handling:
@@ -597,9 +794,10 @@ await project.load([
597
794
 
598
795
  ## Examples
599
796
 
600
- ### Two Clips with Transition
797
+ ### Clips & Transitions
601
798
 
602
799
  ```ts
800
+ // Two clips with a crossfade
603
801
  await project.load([
604
802
  { type: "video", url: "./a.mp4", position: 0, end: 5 },
605
803
  {
@@ -612,9 +810,24 @@ await project.load([
612
810
  ]);
613
811
  ```
614
812
 
615
- ### Text Positioning with Offsets
813
+ **Image slideshow with Ken Burns effects:**
616
814
 
617
- Text is centered by default. Use `xOffset` and `yOffset` to adjust position relative to any base:
815
+ ```ts
816
+ await project.load([
817
+ { type: "image", url: "./photo1.jpg", duration: 3, kenBurns: "zoom-in" },
818
+ { type: "image", url: "./photo2.jpg", duration: 3, kenBurns: "pan-right" },
819
+ { type: "image", url: "./photo3.jpg", duration: 3, kenBurns: "zoom-out" },
820
+ { type: "music", url: "./music.mp3", volume: 0.3 },
821
+ ]);
822
+ ```
823
+
824
+ When `position` is omitted, clips are placed sequentially — each one starts where the previous one ended. `duration` is an alternative to `end`: the library computes `end = position + duration`. The explicit form (`position: 0, end: 3`) still works identically.
825
+
826
+ > **Note:** Ken Burns effects work best with images at least as large as your output resolution. Smaller images are automatically upscaled (with a validation warning). Use `strictKenBurns: true` in validation options to enforce size requirements instead.
827
+
828
+ ### Text & Animations
829
+
830
+ Text is centered by default. Use `xPercent`/`yPercent` for percentage positioning, `x`/`y` for pixels, or `xOffset`/`yOffset` to nudge from any base:
618
831
 
619
832
  ```ts
620
833
  await project.load([
@@ -640,223 +853,39 @@ await project.load([
640
853
  ]);
641
854
  ```
642
855
 
643
- Offsets work with all positioning methods (`x`/`y` pixels, `xPercent`/`yPercent`, or default center).
644
-
645
- ### Word-by-Word Text Animation
856
+ **Word-by-word replacement:**
646
857
 
647
858
  ```ts
648
- await project.load([
649
- { type: "video", url: "./bg.mp4", position: 0, end: 10 },
650
- {
651
- type: "text",
652
- mode: "word-replace",
653
- text: "One Two Three Four",
654
- position: 2,
655
- end: 6,
656
- wordTimestamps: [2, 3, 4, 5, 6],
657
- animation: { type: "fade-in", in: 0.2 },
658
- fontSize: 72,
659
- fontColor: "white",
660
- },
661
- ]);
662
- ```
663
-
664
- ### Image Slideshow with Ken Burns
665
-
666
- ```ts
667
- await project.load([
668
- {
669
- type: "image",
670
- url: "./photo1.jpg",
671
- position: 0,
672
- end: 3,
673
- kenBurns: "zoom-in",
674
- },
675
- {
676
- type: "image",
677
- url: "./photo2.jpg",
678
- position: 3,
679
- end: 6,
680
- kenBurns: "pan-right",
681
- },
682
- {
683
- type: "image",
684
- url: "./photo3.jpg",
685
- position: 6,
686
- end: 9,
687
- kenBurns: "zoom-out",
688
- },
689
- { type: "music", url: "./music.mp3", volume: 0.3 },
690
- ]);
691
- ```
692
-
693
- > **Note:** Ken Burns effects work best with images at least as large as your output resolution. Smaller images are automatically upscaled (with a validation warning about potential quality loss). Use `strictKenBurns: true` in validation options to enforce size requirements instead.
694
-
695
- ### Export with Progress Tracking
696
-
697
- ```ts
698
- await project.export({
699
- outputPath: "./output.mp4",
700
- onProgress: ({ percent, fps, speed }) => {
701
- process.stdout.write(`\rRendering: ${percent}% (${fps} fps, ${speed}x)`);
702
- },
703
- });
704
- ```
705
-
706
- ### High-Quality Export with Custom Settings
707
-
708
- ```ts
709
- await project.export({
710
- outputPath: "./output.mp4",
711
- videoCodec: "libx265",
712
- crf: 18, // Higher quality
713
- preset: "slow", // Better compression
714
- audioCodec: "libopus",
715
- audioBitrate: "256k",
716
- metadata: {
717
- title: "My Video",
718
- artist: "My Name",
719
- date: "2024",
720
- },
721
- });
722
- ```
723
-
724
- ### Hardware-Accelerated Export (macOS)
725
-
726
- ```ts
727
- await project.export({
728
- outputPath: "./output.mp4",
729
- hwaccel: "videotoolbox",
730
- videoCodec: "h264_videotoolbox",
731
- crf: 23,
732
- });
733
- ```
734
-
735
- ### Two-Pass Encoding for Target File Size
736
-
737
- ```ts
738
- await project.export({
739
- outputPath: "./output.mp4",
740
- twoPass: true,
741
- videoBitrate: "5M", // Target bitrate
742
- preset: "slow",
743
- });
744
- ```
745
-
746
- ### Scale Output Resolution
747
-
748
- ```ts
749
- // Use resolution preset
750
- await project.export({
751
- outputPath: "./output-720p.mp4",
752
- outputResolution: "720p",
753
- });
754
-
755
- // Or specify exact dimensions
756
- await project.export({
757
- outputPath: "./output-custom.mp4",
758
- outputWidth: 1280,
759
- outputHeight: 720,
760
- });
761
- ```
762
-
763
- ### Audio-Only Export
764
-
765
- ```ts
766
- await project.export({
767
- outputPath: "./audio.mp3",
768
- audioOnly: true,
769
- audioCodec: "libmp3lame",
770
- audioBitrate: "320k",
771
- });
772
- ```
773
-
774
- ### Generate Thumbnail
775
-
776
- ```ts
777
- await project.export({
778
- outputPath: "./output.mp4",
779
- thumbnail: {
780
- outputPath: "./thumbnail.jpg",
781
- time: 5, // Capture at 5 seconds
782
- width: 640,
783
- },
784
- });
859
+ {
860
+ type: "text",
861
+ mode: "word-replace",
862
+ text: "One Two Three Four",
863
+ position: 2,
864
+ end: 6,
865
+ wordTimestamps: [2, 3, 4, 5, 6],
866
+ animation: { type: "fade-in", in: 0.2 },
867
+ fontSize: 72,
868
+ fontColor: "white",
869
+ }
785
870
  ```
786
871
 
787
- ### Debug Export Command
872
+ **Typewriter, pulse, and other animations:**
788
873
 
789
874
  ```ts
790
- await project.export({
791
- outputPath: "./output.mp4",
792
- verbose: true, // Log export options
793
- saveCommand: "./ffmpeg-command.txt", // Save command to file
794
- });
795
- ```
875
+ // Typewriter — letters appear one at a time
876
+ { type: "text", text: "Appearing letter by letter...", position: 1, end: 4,
877
+ animation: { type: "typewriter", speed: 15 } }
796
878
 
797
- ### Typewriter Text Effect
879
+ // Pulse rhythmic scaling
880
+ { type: "text", text: "Pulsing...", position: 0.5, end: 4.5,
881
+ animation: { type: "pulse", speed: 2, intensity: 0.2 } }
798
882
 
799
- ```ts
800
- await project.load([
801
- { type: "video", url: "./bg.mp4", position: 0, end: 5 },
802
- {
803
- type: "text",
804
- text: "Appearing letter by letter...",
805
- position: 1,
806
- end: 4,
807
- fontSize: 48,
808
- fontColor: "white",
809
- animation: {
810
- type: "typewriter",
811
- speed: 15, // 15 characters per second
812
- },
813
- },
814
- ]);
883
+ // Also available: fade-in, fade-out, fade-in-out, pop, pop-bounce, scale-in
815
884
  ```
816
885
 
817
- ### Pulsing Text Effect
818
-
819
- ```ts
820
- await project.load([
821
- { type: "video", url: "./bg.mp4", position: 0, end: 5 },
822
- {
823
- type: "text",
824
- text: "Pulsing...",
825
- position: 0.5,
826
- end: 4.5,
827
- fontSize: 52,
828
- fontColor: "cyan",
829
- animation: {
830
- type: "pulse",
831
- speed: 2, // 2 pulses per second
832
- intensity: 0.2, // 20% size variation
833
- },
834
- },
835
- ]);
836
- ```
886
+ ### Karaoke
837
887
 
838
- ### Karaoke Text Effect
839
-
840
- Create word-by-word highlighting like karaoke subtitles:
841
-
842
- ```ts
843
- await project.load([
844
- { type: "video", url: "./music-video.mp4", position: 0, end: 10 },
845
- {
846
- type: "text",
847
- mode: "karaoke",
848
- text: "Never gonna give you up",
849
- position: 2,
850
- end: 6,
851
- fontColor: "#FFFFFF",
852
- highlightColor: "#FFFF00", // Words highlight to yellow
853
- fontSize: 48,
854
- yPercent: 0.85, // Position near bottom
855
- },
856
- ]);
857
- ```
858
-
859
- With precise word timings:
888
+ Word-by-word highlighting with customizable colors. Use `highlightStyle: "instant"` for immediate color changes instead of the default smooth fill:
860
889
 
861
890
  ```ts
862
891
  await project.load([
@@ -877,74 +906,16 @@ await project.load([
877
906
  fontColor: "#FFFFFF",
878
907
  highlightColor: "#00FF00",
879
908
  fontSize: 52,
909
+ yPercent: 0.85,
880
910
  },
881
911
  ]);
882
912
  ```
883
913
 
884
- With instant highlight (words change color immediately instead of gradual fill):
914
+ For simple usage without explicit word timings, just provide `text` and `wordTimestamps` — the library will split on spaces. Multi-line karaoke is supported with `\n` in the text string or `lineBreak: true` in the words array.
885
915
 
886
- ```ts
887
- await project.load([
888
- { type: "video", url: "./music-video.mp4", position: 0, end: 10 },
889
- {
890
- type: "text",
891
- mode: "karaoke",
892
- text: "Each word pops instantly",
893
- position: 1,
894
- end: 5,
895
- fontColor: "#FFFFFF",
896
- highlightColor: "#FF00FF",
897
- highlightStyle: "instant", // Words change color immediately
898
- fontSize: 48,
899
- },
900
- ]);
901
- ```
902
-
903
- Multi-line karaoke (use `\n` for line breaks):
904
-
905
- ```ts
906
- await project.load([
907
- { type: "video", url: "./music-video.mp4", position: 0, end: 10 },
908
- {
909
- type: "text",
910
- mode: "karaoke",
911
- text: "First line of lyrics\nSecond line continues",
912
- position: 0,
913
- end: 6,
914
- fontColor: "#FFFFFF",
915
- highlightColor: "#FFFF00",
916
- fontSize: 36,
917
- yPercent: 0.8,
918
- },
919
- ]);
920
- ```
921
-
922
- Or with explicit line breaks in the words array:
923
-
924
- ```ts
925
- await project.load([
926
- { type: "video", url: "./music-video.mp4", position: 0, end: 10 },
927
- {
928
- type: "text",
929
- mode: "karaoke",
930
- text: "Hello World Goodbye World",
931
- position: 0,
932
- end: 4,
933
- words: [
934
- { text: "Hello", start: 0, end: 1 },
935
- { text: "World", start: 1, end: 2, lineBreak: true }, // Line break after this word
936
- { text: "Goodbye", start: 2, end: 3 },
937
- { text: "World", start: 3, end: 4 },
938
- ],
939
- fontColor: "#FFFFFF",
940
- highlightColor: "#00FF00",
941
- },
942
- ]);
943
- ```
944
-
945
- ### Import SRT/VTT Subtitles
916
+ ### Subtitles
946
917
 
947
- Add existing subtitle files to your video:
918
+ Import external subtitle files (SRT, VTT, ASS/SSA):
948
919
 
949
920
  ```ts
950
921
  await project.load([
@@ -959,72 +930,77 @@ await project.load([
959
930
  ]);
960
931
  ```
961
932
 
962
- With time offset (shift subtitles forward):
933
+ Use `position` to offset all subtitle timestamps forward (e.g., `position: 2.5` delays everything by 2.5s). ASS/SSA files use their own embedded styles — font options are for SRT/VTT imports.
934
+
935
+ ### Export Settings
963
936
 
964
937
  ```ts
965
- await project.load([
966
- { type: "video", url: "./video.mp4", position: 0, end: 60 },
967
- {
968
- type: "subtitle",
969
- url: "./subtitles.srt",
970
- position: 2.5, // Delay subtitles by 2.5 seconds
971
- },
972
- ]);
973
- ```
938
+ // High-quality H.265 with metadata
939
+ await project.export({
940
+ outputPath: "./output.mp4",
941
+ videoCodec: "libx265",
942
+ crf: 18,
943
+ preset: "slow",
944
+ audioCodec: "libopus",
945
+ audioBitrate: "256k",
946
+ metadata: { title: "My Video", artist: "My Name", date: "2025" },
947
+ });
974
948
 
975
- ### Using Platform Presets
949
+ // Hardware-accelerated (macOS)
950
+ await project.export({
951
+ outputPath: "./output.mp4",
952
+ hwaccel: "videotoolbox",
953
+ videoCodec: "h264_videotoolbox",
954
+ });
976
955
 
977
- ```ts
978
- // Create a TikTok-optimized video
979
- const tiktok = new SIMPLEFFMPEG({ preset: "tiktok" });
956
+ // Two-pass encoding for target file size
957
+ await project.export({
958
+ outputPath: "./output.mp4",
959
+ twoPass: true,
960
+ videoBitrate: "5M",
961
+ preset: "slow",
962
+ });
980
963
 
981
- await tiktok.load([
982
- { type: "video", url: "./vertical.mp4", position: 0, end: 15 },
983
- {
984
- type: "text",
985
- text: "Follow for more!",
986
- position: 12,
987
- end: 15,
988
- fontSize: 48,
989
- fontColor: "white",
990
- yPercent: 0.8,
991
- animation: { type: "pop-bounce", in: 0.3 },
992
- },
993
- ]);
964
+ // Scale output resolution
965
+ await project.export({ outputPath: "./720p.mp4", outputResolution: "720p" });
994
966
 
995
- await tiktok.export({
996
- outputPath: "./tiktok-video.mp4",
997
- watermark: {
998
- type: "text",
999
- text: "@myhandle",
1000
- position: "bottom-right",
1001
- opacity: 0.7,
1002
- },
967
+ // Audio-only export
968
+ await project.export({
969
+ outputPath: "./audio.mp3",
970
+ audioOnly: true,
971
+ audioCodec: "libmp3lame",
972
+ audioBitrate: "320k",
973
+ });
974
+
975
+ // Generate thumbnail
976
+ await project.export({
977
+ outputPath: "./output.mp4",
978
+ thumbnail: { outputPath: "./thumb.jpg", time: 5, width: 640 },
979
+ });
980
+
981
+ // Debug — save the FFmpeg command to a file
982
+ await project.export({
983
+ outputPath: "./output.mp4",
984
+ verbose: true,
985
+ saveCommand: "./ffmpeg-command.txt",
1003
986
  });
1004
987
  ```
1005
988
 
1006
- ## Timeline Behavior
989
+ ## Advanced
990
+
991
+ ### Timeline Behavior
1007
992
 
1008
993
  - Clip timing uses `[position, end)` intervals in seconds
1009
994
  - Transitions create overlaps that reduce total duration
1010
995
  - Background music is mixed after video transitions (unaffected by crossfades)
1011
996
 
1012
- ### Transition Compensation
1013
-
1014
- FFmpeg's `xfade` transitions work by **overlapping** clips, which compresses the timeline. For example:
1015
-
1016
- - Clip A: 0-10s
1017
- - Clip B: 10-20s with 1s fade transition
1018
- - **Actual output duration: 19s** (not 20s)
997
+ **Transition Compensation:**
1019
998
 
1020
- With multiple transitions, this compounds—10 clips with 0.5s transitions each would be ~4.5 seconds shorter than the sum of clip durations.
999
+ FFmpeg's `xfade` transitions **overlap** clips, compressing the timeline. A 1s fade between two 10s clips produces 19s of output, not 20s. With multiple transitions this compounds.
1021
1000
 
1022
- **Automatic Compensation (default):**
1023
-
1024
- By default, simple-ffmpeg automatically adjusts text and subtitle timings to compensate for this compression. When you position text at "15s", it appears at the visual 15s mark in the output video, regardless of how many transitions have occurred.
1001
+ By default, simple-ffmpeg automatically adjusts text and subtitle timings to compensate. When you position text at "15s", it appears at the visual 15s mark regardless of how many transitions preceded it:
1025
1002
 
1026
1003
  ```ts
1027
- // Text will appear at the correct visual position even with transitions
1028
1004
  await project.load([
1029
1005
  { type: "video", url: "./a.mp4", position: 0, end: 10 },
1030
1006
  {
@@ -1038,138 +1014,250 @@ await project.load([
1038
1014
  ]);
1039
1015
  ```
1040
1016
 
1041
- **Disabling Compensation:**
1042
-
1043
- If you need raw timeline positioning (e.g., you've pre-calculated offsets yourself):
1044
-
1045
- ```ts
1046
- await project.export({
1047
- outputPath: "./output.mp4",
1048
- compensateTransitions: false, // Use raw timestamps
1049
- });
1050
- ```
1051
-
1052
- ## Auto-Batching for Complex Filter Graphs
1017
+ Disable with `compensateTransitions: false` in export options if you've pre-calculated offsets yourself.
1053
1018
 
1054
- FFmpeg's `filter_complex` has platform-specific length limits (Windows ~32KB, macOS ~1MB, Linux ~2MB). When text animations like typewriter create many filter nodes, the command can exceed these limits.
1019
+ ### Auto-Batching
1055
1020
 
1056
- **simple-ffmpeg automatically handles this:**
1021
+ FFmpeg's `filter_complex` has platform-specific length limits (Windows ~32KB, macOS ~1MB, Linux ~2MB). When text animations create many filter nodes, the command can exceed these limits.
1057
1022
 
1058
- 1. **Auto-detection**: Before running FFmpeg, the library checks if the filter graph exceeds a safe 100KB limit
1059
- 2. **Smart batching**: If too long, text overlays are rendered in multiple passes with intermediate files
1060
- 3. **Optimal batch sizing**: Calculates the ideal number of nodes per pass based on actual filter complexity
1023
+ simple-ffmpeg handles this automatically detecting oversized filter graphs and splitting text overlays into multiple rendering passes with intermediate files. No configuration needed.
1061
1024
 
1062
- This happens transparently—you don't need to configure anything. For very complex projects, you can tune it manually:
1025
+ For very complex projects, you can tune it:
1063
1026
 
1064
1027
  ```js
1065
1028
  await project.export({
1066
- outputPath: "./output.mp4",
1067
- // Lower this if you have many complex text animations
1068
1029
  textMaxNodesPerPass: 30, // default: 75
1069
- // Intermediate encoding settings (used between passes)
1070
1030
  intermediateVideoCodec: "libx264", // default
1071
1031
  intermediateCrf: 18, // default (high quality)
1072
1032
  intermediatePreset: "veryfast", // default (fast encoding)
1073
1033
  });
1074
1034
  ```
1075
1035
 
1076
- **When batching activates:**
1077
-
1078
- - Typewriter animations with long text (creates one filter node per character)
1079
- - Many simultaneous text overlays
1080
- - Complex animation combinations
1081
-
1082
- With `verbose: true`, you'll see when auto-batching kicks in:
1083
-
1084
- ```
1085
- simple-ffmpeg: Auto-batching text (filter too long: 150000 > 100000). Using 35 nodes per pass.
1086
- ```
1036
+ Batching activates for typewriter animations with long text, many simultaneous text overlays, or complex animation combinations. With `verbose: true`, you'll see when it kicks in.
1087
1037
 
1088
1038
  ## Real-World Usage Patterns
1089
1039
 
1090
1040
  ### Data Pipeline Example
1091
1041
 
1092
- Generate videos programmatically from structured data (JSON, database, API, CMS):
1042
+ Generate videos programmatically from structured data database records, API responses, CMS content, etc. This example creates property tour videos from real estate listings:
1093
1043
 
1094
1044
  ```js
1095
- const SIMPLEFFMPEG = require("simple-ffmpegjs");
1045
+ import SIMPLEFFMPEG from "simple-ffmpegjs";
1096
1046
 
1097
- // Your data source - could be database records, API response, etc.
1098
- const quotes = [
1099
- {
1100
- text: "The only way to do great work is to love what you do.",
1101
- author: "Steve Jobs",
1102
- },
1103
- { text: "Move fast and break things.", author: "Mark Zuckerberg" },
1104
- ];
1047
+ const listings = await db.getActiveListings(); // your data source
1048
+
1049
+ async function generateListingVideo(listing, outputPath) {
1050
+ const photos = listing.photos; // ['kitchen.jpg', 'living-room.jpg', ...]
1051
+ const slideDuration = 4;
1052
+
1053
+ // Build an image slideshow from listing photos (auto-sequenced with crossfades)
1054
+ const transitionDuration = 0.5;
1055
+ const photoClips = photos.map((photo, i) => ({
1056
+ type: "image",
1057
+ url: photo,
1058
+ duration: slideDuration,
1059
+ kenBurns: i % 2 === 0 ? "zoom-in" : "pan-right",
1060
+ ...(i > 0 && {
1061
+ transition: { type: "fade", duration: transitionDuration },
1062
+ }),
1063
+ }));
1064
+
1065
+ const totalDuration = SIMPLEFFMPEG.getDuration(photoClips);
1105
1066
 
1106
- async function generateQuoteVideo(quote, outputPath) {
1107
1067
  const clips = [
1108
- { type: "video", url: "./backgrounds/default.mp4", position: 0, end: 5 },
1068
+ ...photoClips,
1069
+ // Price banner
1109
1070
  {
1110
1071
  type: "text",
1111
- text: `"${quote.text}"`,
1072
+ text: listing.price,
1112
1073
  position: 0.5,
1113
- end: 4,
1114
- fontSize: 42,
1074
+ end: totalDuration - 0.5,
1075
+ fontSize: 36,
1115
1076
  fontColor: "#FFFFFF",
1116
- yPercent: 0.4,
1117
- animation: { type: "fade-in", in: 0.3 },
1077
+ backgroundColor: "#000000",
1078
+ backgroundOpacity: 0.6,
1079
+ padding: 12,
1080
+ xPercent: 0.5,
1081
+ yPercent: 0.1,
1118
1082
  },
1083
+ // Address at the bottom
1119
1084
  {
1120
1085
  type: "text",
1121
- text: `— ${quote.author}`,
1122
- position: 1.5,
1123
- end: 4.5,
1086
+ text: listing.address,
1087
+ position: 0.5,
1088
+ end: totalDuration - 0.5,
1124
1089
  fontSize: 28,
1125
- fontColor: "#CCCCCC",
1126
- yPercent: 0.6,
1127
- animation: { type: "fade-in", in: 0.3 },
1090
+ fontColor: "#FFFFFF",
1091
+ borderColor: "#000000",
1092
+ borderWidth: 2,
1093
+ xPercent: 0.5,
1094
+ yPercent: 0.9,
1128
1095
  },
1096
+ { type: "music", url: "./assets/ambient.mp3", volume: 0.15, loop: true },
1129
1097
  ];
1130
1098
 
1131
- const project = new SIMPLEFFMPEG({ preset: "tiktok" });
1099
+ const project = new SIMPLEFFMPEG({ preset: "instagram-reel" });
1132
1100
  await project.load(clips);
1133
1101
  return project.export({ outputPath });
1134
1102
  }
1135
1103
 
1136
- // Batch process all quotes
1137
- for (const [i, quote] of quotes.entries()) {
1138
- await generateQuoteVideo(quote, `./output/quote-${i + 1}.mp4`);
1104
+ // Batch generate videos for all listings
1105
+ for (const listing of listings) {
1106
+ await generateListingVideo(listing, `./output/${listing.id}.mp4`);
1139
1107
  }
1140
1108
  ```
1141
1109
 
1142
- ### AI Generation with Validation Loop
1110
+ ### AI Video Generation Pipeline Example
1143
1111
 
1144
- The structured validation with error codes makes it easy to build AI feedback loops:
1112
+ Combine schema export, validation, and structured error codes to build a complete AI-driven video generation pipeline. The schema gives the model the exact specification it needs, and the validation loop lets it self-correct until the output is valid.
1145
1113
 
1146
1114
  ```js
1147
- const SIMPLEFFMPEG = require("simple-ffmpegjs");
1148
-
1149
- async function generateVideoWithAI(prompt) {
1150
- let config = await ai.generateVideoConfig(prompt);
1151
- let result = SIMPLEFFMPEG.validate(config, { skipFileChecks: true });
1152
- let retries = 0;
1153
-
1154
- // Let AI fix its own mistakes
1155
- while (!result.valid && retries < 3) {
1156
- // Feed structured errors back to AI for correction
1157
- config = await ai.fixConfig(config, result.errors);
1158
- result = SIMPLEFFMPEG.validate(config, { skipFileChecks: true });
1159
- retries++;
1115
+ import SIMPLEFFMPEG from "simple-ffmpegjs";
1116
+
1117
+ // 1. Build the schema context for the AI
1118
+ // Only expose the clip types you want the AI to work with.
1119
+ // Developer-level config (codecs, resolution, etc.) stays out of the schema.
1120
+
1121
+ const schema = SIMPLEFFMPEG.getSchema({
1122
+ include: ["video", "image", "text", "music"],
1123
+ instructions: [
1124
+ "You are composing a short-form video for TikTok.",
1125
+ "Keep total duration under 30 seconds.",
1126
+ "Return ONLY valid JSON an array of clip objects.",
1127
+ ],
1128
+ moduleInstructions: {
1129
+ video: "Use fade transitions between clips. Keep each clip 3-6 seconds.",
1130
+ text: [
1131
+ "Add a title in the first 2 seconds with fontSize 72.",
1132
+ "Use white text with a black border for readability.",
1133
+ ],
1134
+ music: "Always include looping background music at volume 0.15.",
1135
+ },
1136
+ });
1137
+
1138
+ // 2. Send the schema + prompt to your LLM
1139
+
1140
+ async function askAI(systemPrompt, userPrompt) {
1141
+ // Replace with your LLM provider (OpenAI, Anthropic, etc.)
1142
+ const response = await llm.chat({
1143
+ messages: [
1144
+ { role: "system", content: systemPrompt },
1145
+ { role: "user", content: userPrompt },
1146
+ ],
1147
+ });
1148
+ return JSON.parse(response.content);
1149
+ }
1150
+
1151
+ // 3. Generate → Validate → Retry loop
1152
+
1153
+ async function generateVideo(userPrompt, media) {
1154
+ // Build the system prompt with schema + available media and their details.
1155
+ // Descriptions and durations help the AI make good creative decisions —
1156
+ // ordering clips logically, setting accurate position/end times, etc.
1157
+ const mediaList = media
1158
+ .map((m) => ` - ${m.file} (${m.duration}s) — ${m.description}`)
1159
+ .join("\n");
1160
+
1161
+ const systemPrompt = [
1162
+ "You are a video editor. Given the user's request and the available media,",
1163
+ "produce a clips array that follows this schema:\n",
1164
+ schema,
1165
+ "\nAvailable media (use these exact file paths):",
1166
+ mediaList,
1167
+ ].join("\n");
1168
+
1169
+ const knownPaths = media.map((m) => m.file);
1170
+
1171
+ // First attempt
1172
+ let clips = await askAI(systemPrompt, userPrompt);
1173
+ let result = SIMPLEFFMPEG.validate(clips, { skipFileChecks: true });
1174
+ let attempts = 1;
1175
+
1176
+ // Self-correction loop: feed structured errors back to the AI
1177
+ while (!result.valid && attempts < 3) {
1178
+ const errorFeedback = result.errors
1179
+ .map((e) => `[${e.code}] ${e.path}: ${e.message}`)
1180
+ .join("\n");
1181
+
1182
+ clips = await askAI(
1183
+ systemPrompt,
1184
+ [
1185
+ `Your previous output had validation errors:\n${errorFeedback}`,
1186
+ `\nOriginal request: ${userPrompt}`,
1187
+ "\nPlease fix the errors and return the corrected clips array.",
1188
+ ].join("\n")
1189
+ );
1190
+
1191
+ result = SIMPLEFFMPEG.validate(clips, { skipFileChecks: true });
1192
+ attempts++;
1160
1193
  }
1161
1194
 
1162
1195
  if (!result.valid) {
1163
- throw new Error("AI failed to generate valid config");
1196
+ throw new Error(
1197
+ `Failed to generate valid config after ${attempts} attempts:\n` +
1198
+ SIMPLEFFMPEG.formatValidationResult(result)
1199
+ );
1200
+ }
1201
+
1202
+ // 4. Verify the AI only used known media paths
1203
+ // The structural loop (skipFileChecks: true) can't catch hallucinated paths.
1204
+ // You could also put this inside the retry loop to let the AI self-correct
1205
+ // bad paths — just append the unknown paths to the error feedback string.
1206
+
1207
+ const usedPaths = clips.filter((c) => c.url).map((c) => c.url);
1208
+ const unknownPaths = usedPaths.filter((p) => !knownPaths.includes(p));
1209
+ if (unknownPaths.length > 0) {
1210
+ throw new Error(`AI used unknown media paths: ${unknownPaths.join(", ")}`);
1164
1211
  }
1165
1212
 
1166
- const project = new SIMPLEFFMPEG({ width: 1080, height: 1920 });
1167
- await project.load(config);
1168
- return project.export({ outputPath: "./output.mp4" });
1213
+ // 5. Build and export
1214
+ // load() will also throw MediaNotFoundError if any file is missing on disk.
1215
+
1216
+ const project = new SIMPLEFFMPEG({ preset: "tiktok" });
1217
+ await project.load(clips);
1218
+
1219
+ return project.export({
1220
+ outputPath: "./output.mp4",
1221
+ onProgress: ({ percent }) => console.log(`Rendering: ${percent}%`),
1222
+ });
1169
1223
  }
1224
+
1225
+ // Usage
1226
+
1227
+ await generateVideo("Make a hype travel montage with upbeat text overlays", [
1228
+ {
1229
+ file: "clips/beach-drone.mp4",
1230
+ duration: 4,
1231
+ description:
1232
+ "Aerial drone shot of a tropical beach with people playing volleyball",
1233
+ },
1234
+ {
1235
+ file: "clips/city-timelapse.mp4",
1236
+ duration: 8,
1237
+ description: "Timelapse of a city skyline transitioning from day to night",
1238
+ },
1239
+ {
1240
+ file: "clips/sunset.mp4",
1241
+ duration: 6,
1242
+ description: "Golden hour sunset over the ocean with gentle waves",
1243
+ },
1244
+ {
1245
+ file: "music/upbeat-track.mp3",
1246
+ duration: 120,
1247
+ description:
1248
+ "Upbeat electronic track with a strong beat, good for montages",
1249
+ },
1250
+ ]);
1170
1251
  ```
1171
1252
 
1172
- Each validation error includes a `code` (e.g., `INVALID_TIMELINE`, `MISSING_REQUIRED`) and `path` (e.g., `clips[2].position`) for precise AI feedback.
1253
+ The key parts of this pattern:
1254
+
1255
+ 1. **`getSchema()`** gives the AI a precise specification of what it can produce, with only the clip types you've chosen to expose.
1256
+ 2. **`instructions` / `moduleInstructions`** embed your creative constraints directly into the spec — the AI treats them the same as built-in rules.
1257
+ 3. **Media descriptions** with durations and content details give the AI enough context to make good creative decisions — ordering clips logically, setting accurate timings, and choosing the right media for each part of the video.
1258
+ 4. **`validate()`** with `skipFileChecks: true` checks structural correctness in the retry loop — types, timelines, required fields — without touching the filesystem.
1259
+ 5. **The retry loop** lets the AI self-correct. Most validation failures resolve in one retry.
1260
+ 6. **The path guard** catches hallucinated file paths before `load()` hits the filesystem. You can optionally move this check inside the retry loop to let the AI self-correct bad paths. `load()` itself will also throw `MediaNotFoundError` if a file is missing on disk.
1173
1261
 
1174
1262
  ## Testing
1175
1263