simple-ffmpegjs 0.3.5 → 0.4.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 +221 -55
- package/package.json +1 -1
- package/src/core/gaps.js +4 -4
- package/src/core/resolve.js +1 -1
- package/src/core/validation.js +509 -43
- package/src/ffmpeg/audio_builder.js +3 -1
- package/src/ffmpeg/bgm_builder.js +3 -1
- package/src/ffmpeg/effect_builder.js +138 -0
- package/src/ffmpeg/video_builder.js +220 -75
- package/src/ffmpeg/watermark_builder.js +13 -0
- package/src/lib/gradient.js +257 -0
- package/src/loaders.js +55 -2
- package/src/schema/formatter.js +2 -0
- package/src/schema/index.js +4 -0
- package/src/schema/modules/color.js +54 -0
- package/src/schema/modules/effect.js +77 -0
- package/src/schema/modules/image.js +24 -6
- package/src/simpleffmpeg.js +72 -21
- package/types/index.d.mts +104 -12
- package/types/index.d.ts +115 -12
package/src/simpleffmpeg.js
CHANGED
|
@@ -7,6 +7,7 @@ const Loaders = require("./loaders");
|
|
|
7
7
|
const { buildVideoFilter } = require("./ffmpeg/video_builder");
|
|
8
8
|
const { buildAudioForVideoClips } = require("./ffmpeg/audio_builder");
|
|
9
9
|
const { buildBackgroundMusicMix } = require("./ffmpeg/bgm_builder");
|
|
10
|
+
const { buildEffectFilters } = require("./ffmpeg/effect_builder");
|
|
10
11
|
const {
|
|
11
12
|
getClipAudioString,
|
|
12
13
|
hasProblematicChars,
|
|
@@ -55,7 +56,6 @@ class SIMPLEFFMPEG {
|
|
|
55
56
|
* @param {number} options.fps - Frames per second (default: 30)
|
|
56
57
|
* @param {string} options.preset - Platform preset ('tiktok', 'youtube', 'instagram-post', etc.)
|
|
57
58
|
* @param {string} options.validationMode - Validation behavior: 'warn' or 'strict' (default: 'warn')
|
|
58
|
-
* @param {string} options.fillGaps - Gap handling: 'none' or 'black' (default: 'none')
|
|
59
59
|
*
|
|
60
60
|
* @example
|
|
61
61
|
* const project = new SIMPLEFFMPEG({ preset: 'tiktok' });
|
|
@@ -64,8 +64,7 @@ class SIMPLEFFMPEG {
|
|
|
64
64
|
* const project = new SIMPLEFFMPEG({
|
|
65
65
|
* width: 1920,
|
|
66
66
|
* height: 1080,
|
|
67
|
-
* fps: 30
|
|
68
|
-
* fillGaps: 'black'
|
|
67
|
+
* fps: 30
|
|
69
68
|
* });
|
|
70
69
|
*/
|
|
71
70
|
constructor(options = {}) {
|
|
@@ -87,12 +86,12 @@ class SIMPLEFFMPEG {
|
|
|
87
86
|
width: options.width || presetConfig.width || C.DEFAULT_WIDTH,
|
|
88
87
|
height: options.height || presetConfig.height || C.DEFAULT_HEIGHT,
|
|
89
88
|
validationMode: options.validationMode || C.DEFAULT_VALIDATION_MODE,
|
|
90
|
-
fillGaps: options.fillGaps || "none", // 'none' | 'black'
|
|
91
89
|
preset: options.preset || null,
|
|
92
90
|
};
|
|
93
91
|
this.videoOrAudioClips = [];
|
|
94
92
|
this.textClips = [];
|
|
95
93
|
this.subtitleClips = [];
|
|
94
|
+
this.effectClips = [];
|
|
96
95
|
this.filesToClean = [];
|
|
97
96
|
this._isLoading = false; // Guard against concurrent load() calls
|
|
98
97
|
this._isExporting = false; // Guard against concurrent export() calls
|
|
@@ -105,9 +104,15 @@ class SIMPLEFFMPEG {
|
|
|
105
104
|
*/
|
|
106
105
|
_getInputStreams() {
|
|
107
106
|
return this.videoOrAudioClips
|
|
107
|
+
.filter((clip) => {
|
|
108
|
+
// Flat color clips use the color= filter source — no file input needed
|
|
109
|
+
if (clip.type === "color" && clip._isFlatColor) return false;
|
|
110
|
+
return true;
|
|
111
|
+
})
|
|
108
112
|
.map((clip) => {
|
|
109
113
|
const escapedUrl = escapeFilePath(clip.url);
|
|
110
|
-
|
|
114
|
+
// Gradient color clips and image clips are looped images
|
|
115
|
+
if (clip.type === "image" || (clip.type === "color" && !clip._isFlatColor)) {
|
|
111
116
|
const duration = Math.max(0, clip.end - clip.position || 0);
|
|
112
117
|
return `-loop 1 -t ${duration} -i "${escapedUrl}"`;
|
|
113
118
|
}
|
|
@@ -188,7 +193,7 @@ class SIMPLEFFMPEG {
|
|
|
188
193
|
* Load clips into the project for processing
|
|
189
194
|
*
|
|
190
195
|
* @param {Array} clipObjs - Array of clip configuration objects
|
|
191
|
-
* @param {string} clipObjs[].type - Clip type: 'video', 'audio', 'image', 'text', 'music', 'backgroundAudio', 'subtitle'
|
|
196
|
+
* @param {string} clipObjs[].type - Clip type: 'video', 'audio', 'image', 'color', 'text', 'effect', 'music', 'backgroundAudio', 'subtitle'
|
|
192
197
|
* @param {string} clipObjs[].url - Media file path (required for video, audio, image, music, subtitle)
|
|
193
198
|
* @param {number} clipObjs[].position - Start time on timeline in seconds
|
|
194
199
|
* @param {number} clipObjs[].end - End time on timeline in seconds
|
|
@@ -223,7 +228,6 @@ class SIMPLEFFMPEG {
|
|
|
223
228
|
|
|
224
229
|
// Merge resolution errors into validation
|
|
225
230
|
const result = validateConfig(resolved.clips, {
|
|
226
|
-
fillGaps: this.options.fillGaps,
|
|
227
231
|
width: this.options.width,
|
|
228
232
|
height: this.options.height,
|
|
229
233
|
});
|
|
@@ -255,12 +259,16 @@ class SIMPLEFFMPEG {
|
|
|
255
259
|
if (clipObj.type === "video" || clipObj.type === "audio") {
|
|
256
260
|
clipObj.volume = clipObj.volume != null ? clipObj.volume : 1;
|
|
257
261
|
clipObj.cutFrom = clipObj.cutFrom || 0;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
}
|
|
263
|
+
// Normalize transitions for all visual clip types
|
|
264
|
+
if (
|
|
265
|
+
(clipObj.type === "video" || clipObj.type === "image" || clipObj.type === "color") &&
|
|
266
|
+
clipObj.transition
|
|
267
|
+
) {
|
|
268
|
+
clipObj.transition = {
|
|
269
|
+
type: clipObj.transition.type || clipObj.transition,
|
|
270
|
+
duration: clipObj.transition.duration || 0.5,
|
|
271
|
+
};
|
|
264
272
|
}
|
|
265
273
|
if (clipObj.type === "video") {
|
|
266
274
|
return Loaders.loadVideo(this, clipObj);
|
|
@@ -271,9 +279,15 @@ class SIMPLEFFMPEG {
|
|
|
271
279
|
if (clipObj.type === "text") {
|
|
272
280
|
return Loaders.loadText(this, clipObj);
|
|
273
281
|
}
|
|
282
|
+
if (clipObj.type === "effect") {
|
|
283
|
+
return Loaders.loadEffect(this, clipObj);
|
|
284
|
+
}
|
|
274
285
|
if (clipObj.type === "image") {
|
|
275
286
|
return Loaders.loadImage(this, clipObj);
|
|
276
287
|
}
|
|
288
|
+
if (clipObj.type === "color") {
|
|
289
|
+
return Loaders.loadColor(this, clipObj);
|
|
290
|
+
}
|
|
277
291
|
if (clipObj.type === "music" || clipObj.type === "backgroundAudio") {
|
|
278
292
|
return Loaders.loadBackgroundAudio(this, clipObj);
|
|
279
293
|
}
|
|
@@ -381,8 +395,22 @@ class SIMPLEFFMPEG {
|
|
|
381
395
|
})
|
|
382
396
|
);
|
|
383
397
|
|
|
398
|
+
// Build a mapping from clip to its FFmpeg input stream index.
|
|
399
|
+
// Flat color clips use the color= filter source and do not have file inputs,
|
|
400
|
+
// so they are skipped by _getInputStreams(). All other clips' indices must
|
|
401
|
+
// account for this offset.
|
|
402
|
+
this._inputIndexMap = new Map();
|
|
403
|
+
let _inputIdx = 0;
|
|
404
|
+
for (const clip of this.videoOrAudioClips) {
|
|
405
|
+
if (clip.type === "color" && clip._isFlatColor) {
|
|
406
|
+
continue; // No file input for flat color clips
|
|
407
|
+
}
|
|
408
|
+
this._inputIndexMap.set(clip, _inputIdx);
|
|
409
|
+
_inputIdx++;
|
|
410
|
+
}
|
|
411
|
+
|
|
384
412
|
const videoClips = this.videoOrAudioClips.filter(
|
|
385
|
-
(clip) => clip.type === "video" || clip.type === "image"
|
|
413
|
+
(clip) => clip.type === "video" || clip.type === "image" || clip.type === "color"
|
|
386
414
|
);
|
|
387
415
|
const audioClips = this.videoOrAudioClips.filter(
|
|
388
416
|
(clip) => clip.type === "audio"
|
|
@@ -397,7 +425,7 @@ class SIMPLEFFMPEG {
|
|
|
397
425
|
let hasVideo = false;
|
|
398
426
|
let hasAudio = false;
|
|
399
427
|
|
|
400
|
-
|
|
428
|
+
let totalVideoDuration = (() => {
|
|
401
429
|
if (videoClips.length === 0) return 0;
|
|
402
430
|
const baseSum = videoClips.reduce(
|
|
403
431
|
(acc, c) => acc + Math.max(0, (c.end || 0) - (c.position || 0)),
|
|
@@ -425,7 +453,8 @@ class SIMPLEFFMPEG {
|
|
|
425
453
|
)
|
|
426
454
|
.map((c) => (typeof c.end === "number" ? c.end : 0));
|
|
427
455
|
const bgOrAudioEnd = audioEnds.length > 0 ? Math.max(...audioEnds) : 0;
|
|
428
|
-
|
|
456
|
+
|
|
457
|
+
let finalVisualEnd =
|
|
429
458
|
videoClips.length > 0
|
|
430
459
|
? Math.max(...videoClips.map((c) => c.end))
|
|
431
460
|
: Math.max(textEnd, bgOrAudioEnd);
|
|
@@ -436,6 +465,27 @@ class SIMPLEFFMPEG {
|
|
|
436
465
|
filterComplex += vres.filter;
|
|
437
466
|
finalVideoLabel = vres.finalVideoLabel;
|
|
438
467
|
hasVideo = vres.hasVideo;
|
|
468
|
+
|
|
469
|
+
// Use the actual video output length for finalVisualEnd so that
|
|
470
|
+
// audio trim and BGM duration match the real video stream length,
|
|
471
|
+
// rather than an original-timeline position that may differ due to
|
|
472
|
+
// transition compression.
|
|
473
|
+
if (typeof vres.videoDuration === "number" && vres.videoDuration > 0) {
|
|
474
|
+
totalVideoDuration = vres.videoDuration;
|
|
475
|
+
}
|
|
476
|
+
if (
|
|
477
|
+
typeof vres.videoDuration === "number" &&
|
|
478
|
+
vres.videoDuration > finalVisualEnd
|
|
479
|
+
) {
|
|
480
|
+
finalVisualEnd = vres.videoDuration;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Overlay effects (adjustment layer clips) on the composed video output.
|
|
485
|
+
if (this.effectClips.length > 0 && hasVideo && finalVideoLabel) {
|
|
486
|
+
const effectRes = buildEffectFilters(this.effectClips, finalVideoLabel);
|
|
487
|
+
filterComplex += effectRes.filter;
|
|
488
|
+
finalVideoLabel = effectRes.finalVideoLabel || finalVideoLabel;
|
|
439
489
|
}
|
|
440
490
|
|
|
441
491
|
// Audio for video clips (aligned amix)
|
|
@@ -461,7 +511,9 @@ class SIMPLEFFMPEG {
|
|
|
461
511
|
let audioString = "";
|
|
462
512
|
let audioConcatInputs = [];
|
|
463
513
|
audioClips.forEach((clip) => {
|
|
464
|
-
const inputIndex = this.
|
|
514
|
+
const inputIndex = this._inputIndexMap
|
|
515
|
+
? this._inputIndexMap.get(clip)
|
|
516
|
+
: this.videoOrAudioClips.indexOf(clip);
|
|
465
517
|
const { audioStringPart, audioConcatInput } = getClipAudioString(
|
|
466
518
|
clip,
|
|
467
519
|
inputIndex
|
|
@@ -1119,7 +1171,6 @@ class SIMPLEFFMPEG {
|
|
|
1119
1171
|
* @param {Array} clips - Array of clip objects to validate
|
|
1120
1172
|
* @param {Object} options - Validation options
|
|
1121
1173
|
* @param {boolean} options.skipFileChecks - Skip file existence checks (useful for AI)
|
|
1122
|
-
* @param {string} options.fillGaps - Gap handling ('none' | 'black') - affects gap validation
|
|
1123
1174
|
* @returns {Object} Validation result { valid, errors, warnings }
|
|
1124
1175
|
*
|
|
1125
1176
|
* @example
|
|
@@ -1166,9 +1217,9 @@ class SIMPLEFFMPEG {
|
|
|
1166
1217
|
// Resolve shorthand (duration → end, auto-sequencing)
|
|
1167
1218
|
const { clips: resolved } = resolveClips(clips);
|
|
1168
1219
|
|
|
1169
|
-
// Filter to visual clips (video + image)
|
|
1220
|
+
// Filter to visual clips (video + image + color)
|
|
1170
1221
|
const visual = resolved.filter(
|
|
1171
|
-
(c) => c.type === "video" || c.type === "image"
|
|
1222
|
+
(c) => c.type === "video" || c.type === "image" || c.type === "color"
|
|
1172
1223
|
);
|
|
1173
1224
|
|
|
1174
1225
|
if (visual.length === 0) return 0;
|
|
@@ -1379,7 +1430,7 @@ class SIMPLEFFMPEG {
|
|
|
1379
1430
|
* Get the list of available schema module IDs.
|
|
1380
1431
|
* Use these IDs with getSchema({ include: [...] }) or getSchema({ exclude: [...] }).
|
|
1381
1432
|
*
|
|
1382
|
-
* @returns {string[]} Array of module IDs: ['video', 'audio', 'image', 'text', 'subtitle', 'music']
|
|
1433
|
+
* @returns {string[]} Array of module IDs: ['video', 'audio', 'image', 'color', 'effect', 'text', 'subtitle', 'music']
|
|
1383
1434
|
*/
|
|
1384
1435
|
static getSchemaModules() {
|
|
1385
1436
|
return getSchemaModules();
|
package/types/index.d.mts
CHANGED
|
@@ -51,7 +51,9 @@ declare namespace SIMPLEFFMPEG {
|
|
|
51
51
|
| "music"
|
|
52
52
|
| "backgroundAudio"
|
|
53
53
|
| "image"
|
|
54
|
-
| "subtitle"
|
|
54
|
+
| "subtitle"
|
|
55
|
+
| "color"
|
|
56
|
+
| "effect";
|
|
55
57
|
|
|
56
58
|
interface BaseClip {
|
|
57
59
|
type: ClipType;
|
|
@@ -88,16 +90,37 @@ declare namespace SIMPLEFFMPEG {
|
|
|
88
90
|
loop?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
type KenBurnsEffect =
|
|
94
|
+
| "zoom-in"
|
|
95
|
+
| "zoom-out"
|
|
96
|
+
| "pan-left"
|
|
97
|
+
| "pan-right"
|
|
98
|
+
| "pan-up"
|
|
99
|
+
| "pan-down"
|
|
100
|
+
| "smart"
|
|
101
|
+
| "custom";
|
|
102
|
+
|
|
103
|
+
type KenBurnsAnchor = "top" | "bottom" | "left" | "right";
|
|
104
|
+
type KenBurnsEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
105
|
+
|
|
106
|
+
interface KenBurnsSpec {
|
|
107
|
+
type?: KenBurnsEffect;
|
|
108
|
+
startZoom?: number;
|
|
109
|
+
endZoom?: number;
|
|
110
|
+
startX?: number;
|
|
111
|
+
startY?: number;
|
|
112
|
+
endX?: number;
|
|
113
|
+
endY?: number;
|
|
114
|
+
anchor?: KenBurnsAnchor;
|
|
115
|
+
easing?: KenBurnsEasing;
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
interface ImageClip extends BaseClip {
|
|
92
119
|
type: "image";
|
|
93
120
|
url: string;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| "pan-left"
|
|
98
|
-
| "pan-right"
|
|
99
|
-
| "pan-up"
|
|
100
|
-
| "pan-down";
|
|
121
|
+
width?: number;
|
|
122
|
+
height?: number;
|
|
123
|
+
kenBurns?: KenBurnsEffect | KenBurnsSpec;
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
type TextMode = "static" | "word-replace" | "word-sequential" | "karaoke";
|
|
@@ -199,11 +222,82 @@ declare namespace SIMPLEFFMPEG {
|
|
|
199
222
|
opacity?: number;
|
|
200
223
|
}
|
|
201
224
|
|
|
225
|
+
/** Gradient specification for color clips */
|
|
226
|
+
interface GradientSpec {
|
|
227
|
+
type: "linear-gradient" | "radial-gradient";
|
|
228
|
+
/** Array of color strings (at least 2). Evenly distributed across the gradient. */
|
|
229
|
+
colors: string[];
|
|
230
|
+
/** For linear gradients: "vertical" (default), "horizontal", or angle in degrees */
|
|
231
|
+
direction?: "vertical" | "horizontal" | number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Color clip — solid color or gradient for filling gaps, transitions, etc. */
|
|
235
|
+
interface ColorClip {
|
|
236
|
+
type: "color";
|
|
237
|
+
/** Flat color string (e.g. "black", "#FF0000") or gradient specification */
|
|
238
|
+
color: string | GradientSpec;
|
|
239
|
+
/** Start time on timeline in seconds. Omit to auto-sequence after previous visual clip. */
|
|
240
|
+
position?: number;
|
|
241
|
+
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
242
|
+
end?: number;
|
|
243
|
+
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
244
|
+
duration?: number;
|
|
245
|
+
/** Transition effect from the previous visual clip */
|
|
246
|
+
transition?: { type: string; duration: number };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type EffectName = "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust";
|
|
250
|
+
type EffectEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
251
|
+
|
|
252
|
+
interface EffectParamsBase {
|
|
253
|
+
amount?: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface VignetteEffectParams extends EffectParamsBase {
|
|
257
|
+
angle?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface FilmGrainEffectParams extends EffectParamsBase {
|
|
261
|
+
temporal?: boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
interface GaussianBlurEffectParams extends EffectParamsBase {
|
|
265
|
+
sigma?: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface ColorAdjustEffectParams extends EffectParamsBase {
|
|
269
|
+
brightness?: number;
|
|
270
|
+
contrast?: number;
|
|
271
|
+
saturation?: number;
|
|
272
|
+
gamma?: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type EffectParams =
|
|
276
|
+
| VignetteEffectParams
|
|
277
|
+
| FilmGrainEffectParams
|
|
278
|
+
| GaussianBlurEffectParams
|
|
279
|
+
| ColorAdjustEffectParams;
|
|
280
|
+
|
|
281
|
+
/** Effect clip — timed overlay adjustment layer over composed video */
|
|
282
|
+
interface EffectClip {
|
|
283
|
+
type: "effect";
|
|
284
|
+
effect: EffectName;
|
|
285
|
+
position: number;
|
|
286
|
+
end?: number;
|
|
287
|
+
duration?: number;
|
|
288
|
+
fadeIn?: number;
|
|
289
|
+
fadeOut?: number;
|
|
290
|
+
easing?: EffectEasing;
|
|
291
|
+
params: EffectParams;
|
|
292
|
+
}
|
|
293
|
+
|
|
202
294
|
type Clip =
|
|
203
295
|
| VideoClip
|
|
204
296
|
| AudioClip
|
|
205
297
|
| BackgroundMusicClip
|
|
206
298
|
| ImageClip
|
|
299
|
+
| ColorClip
|
|
300
|
+
| EffectClip
|
|
207
301
|
| TextClip
|
|
208
302
|
| SubtitleClip;
|
|
209
303
|
|
|
@@ -276,8 +370,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
276
370
|
interface ValidateOptions {
|
|
277
371
|
/** Skip file existence checks (useful for AI generating configs before files exist) */
|
|
278
372
|
skipFileChecks?: boolean;
|
|
279
|
-
/** Gap handling mode - affects timeline gap validation */
|
|
280
|
-
fillGaps?: "none" | "black";
|
|
281
373
|
/** Project width - used to validate Ken Burns images are large enough */
|
|
282
374
|
width?: number;
|
|
283
375
|
/** Project height - used to validate Ken Burns images are large enough */
|
|
@@ -297,8 +389,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
297
389
|
height?: number;
|
|
298
390
|
/** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
|
|
299
391
|
validationMode?: "warn" | "strict";
|
|
300
|
-
/** How to handle visual gaps: 'none' throws error, 'black' fills with black frames (default: 'none') */
|
|
301
|
-
fillGaps?: "none" | "black";
|
|
302
392
|
}
|
|
303
393
|
|
|
304
394
|
/** Log entry passed to onLog callback */
|
|
@@ -539,6 +629,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
539
629
|
| "video"
|
|
540
630
|
| "audio"
|
|
541
631
|
| "image"
|
|
632
|
+
| "color"
|
|
633
|
+
| "effect"
|
|
542
634
|
| "text"
|
|
543
635
|
| "subtitle"
|
|
544
636
|
| "music";
|
package/types/index.d.ts
CHANGED
|
@@ -51,7 +51,9 @@ declare namespace SIMPLEFFMPEG {
|
|
|
51
51
|
| "music"
|
|
52
52
|
| "backgroundAudio"
|
|
53
53
|
| "image"
|
|
54
|
-
| "subtitle"
|
|
54
|
+
| "subtitle"
|
|
55
|
+
| "color"
|
|
56
|
+
| "effect";
|
|
55
57
|
|
|
56
58
|
interface BaseClip {
|
|
57
59
|
type: ClipType;
|
|
@@ -88,16 +90,37 @@ declare namespace SIMPLEFFMPEG {
|
|
|
88
90
|
loop?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
type KenBurnsEffect =
|
|
94
|
+
| "zoom-in"
|
|
95
|
+
| "zoom-out"
|
|
96
|
+
| "pan-left"
|
|
97
|
+
| "pan-right"
|
|
98
|
+
| "pan-up"
|
|
99
|
+
| "pan-down"
|
|
100
|
+
| "smart"
|
|
101
|
+
| "custom";
|
|
102
|
+
|
|
103
|
+
type KenBurnsAnchor = "top" | "bottom" | "left" | "right";
|
|
104
|
+
type KenBurnsEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
105
|
+
|
|
106
|
+
interface KenBurnsSpec {
|
|
107
|
+
type?: KenBurnsEffect;
|
|
108
|
+
startZoom?: number;
|
|
109
|
+
endZoom?: number;
|
|
110
|
+
startX?: number;
|
|
111
|
+
startY?: number;
|
|
112
|
+
endX?: number;
|
|
113
|
+
endY?: number;
|
|
114
|
+
anchor?: KenBurnsAnchor;
|
|
115
|
+
easing?: KenBurnsEasing;
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
interface ImageClip extends BaseClip {
|
|
92
119
|
type: "image";
|
|
93
120
|
url: string;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| "pan-left"
|
|
98
|
-
| "pan-right"
|
|
99
|
-
| "pan-up"
|
|
100
|
-
| "pan-down";
|
|
121
|
+
width?: number;
|
|
122
|
+
height?: number;
|
|
123
|
+
kenBurns?: KenBurnsEffect | KenBurnsSpec;
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
type TextMode = "static" | "word-replace" | "word-sequential" | "karaoke";
|
|
@@ -199,11 +222,93 @@ declare namespace SIMPLEFFMPEG {
|
|
|
199
222
|
opacity?: number;
|
|
200
223
|
}
|
|
201
224
|
|
|
225
|
+
/** Gradient specification for color clips */
|
|
226
|
+
interface GradientSpec {
|
|
227
|
+
type: "linear-gradient" | "radial-gradient";
|
|
228
|
+
/** Array of color strings (at least 2). Evenly distributed across the gradient. */
|
|
229
|
+
colors: string[];
|
|
230
|
+
/** For linear gradients: "vertical" (default), "horizontal", or angle in degrees */
|
|
231
|
+
direction?: "vertical" | "horizontal" | number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Color clip — solid color or gradient for filling gaps, transitions, etc. */
|
|
235
|
+
interface ColorClip {
|
|
236
|
+
type: "color";
|
|
237
|
+
/** Flat color string (e.g. "black", "#FF0000") or gradient specification */
|
|
238
|
+
color: string | GradientSpec;
|
|
239
|
+
/** Start time on timeline in seconds. Omit to auto-sequence after previous visual clip. */
|
|
240
|
+
position?: number;
|
|
241
|
+
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
242
|
+
end?: number;
|
|
243
|
+
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
244
|
+
duration?: number;
|
|
245
|
+
/** Transition effect from the previous visual clip */
|
|
246
|
+
transition?: { type: string; duration: number };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
type EffectName = "vignette" | "filmGrain" | "gaussianBlur" | "colorAdjust";
|
|
250
|
+
type EffectEasing = "linear" | "ease-in" | "ease-out" | "ease-in-out";
|
|
251
|
+
|
|
252
|
+
interface EffectParamsBase {
|
|
253
|
+
/** Base blend amount from 0 to 1 (default: 1) */
|
|
254
|
+
amount?: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface VignetteEffectParams extends EffectParamsBase {
|
|
258
|
+
/** Vignette angle in radians (default: PI/5) */
|
|
259
|
+
angle?: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface FilmGrainEffectParams extends EffectParamsBase {
|
|
263
|
+
/** Temporal grain changes every frame (default: true) */
|
|
264
|
+
temporal?: boolean;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface GaussianBlurEffectParams extends EffectParamsBase {
|
|
268
|
+
/** Gaussian blur sigma (default derived from amount) */
|
|
269
|
+
sigma?: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface ColorAdjustEffectParams extends EffectParamsBase {
|
|
273
|
+
brightness?: number;
|
|
274
|
+
contrast?: number;
|
|
275
|
+
saturation?: number;
|
|
276
|
+
gamma?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
type EffectParams =
|
|
280
|
+
| VignetteEffectParams
|
|
281
|
+
| FilmGrainEffectParams
|
|
282
|
+
| GaussianBlurEffectParams
|
|
283
|
+
| ColorAdjustEffectParams;
|
|
284
|
+
|
|
285
|
+
/** Effect clip — timed overlay adjustment layer over composed video */
|
|
286
|
+
interface EffectClip {
|
|
287
|
+
type: "effect";
|
|
288
|
+
effect: EffectName;
|
|
289
|
+
/** Start time on timeline in seconds. Required for effect clips. */
|
|
290
|
+
position: number;
|
|
291
|
+
/** End time on timeline in seconds. Mutually exclusive with duration. */
|
|
292
|
+
end?: number;
|
|
293
|
+
/** Duration in seconds (alternative to end). end = position + duration. */
|
|
294
|
+
duration?: number;
|
|
295
|
+
/** Ramp-in duration in seconds */
|
|
296
|
+
fadeIn?: number;
|
|
297
|
+
/** Ramp-out duration in seconds */
|
|
298
|
+
fadeOut?: number;
|
|
299
|
+
/** Envelope easing for fade ramps */
|
|
300
|
+
easing?: EffectEasing;
|
|
301
|
+
/** Effect-specific params */
|
|
302
|
+
params: EffectParams;
|
|
303
|
+
}
|
|
304
|
+
|
|
202
305
|
type Clip =
|
|
203
306
|
| VideoClip
|
|
204
307
|
| AudioClip
|
|
205
308
|
| BackgroundMusicClip
|
|
206
309
|
| ImageClip
|
|
310
|
+
| ColorClip
|
|
311
|
+
| EffectClip
|
|
207
312
|
| TextClip
|
|
208
313
|
| SubtitleClip;
|
|
209
314
|
|
|
@@ -276,8 +381,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
276
381
|
interface ValidateOptions {
|
|
277
382
|
/** Skip file existence checks (useful for AI generating configs before files exist) */
|
|
278
383
|
skipFileChecks?: boolean;
|
|
279
|
-
/** Gap handling mode - affects timeline gap validation */
|
|
280
|
-
fillGaps?: "none" | "black";
|
|
281
384
|
/** Project width - used to validate Ken Burns images are large enough */
|
|
282
385
|
width?: number;
|
|
283
386
|
/** Project height - used to validate Ken Burns images are large enough */
|
|
@@ -297,8 +400,6 @@ declare namespace SIMPLEFFMPEG {
|
|
|
297
400
|
height?: number;
|
|
298
401
|
/** Validation mode: 'warn' logs warnings, 'strict' throws on warnings (default: 'warn') */
|
|
299
402
|
validationMode?: "warn" | "strict";
|
|
300
|
-
/** How to handle visual gaps: 'none' throws error, 'black' fills with black frames (default: 'none') */
|
|
301
|
-
fillGaps?: "none" | "black";
|
|
302
403
|
}
|
|
303
404
|
|
|
304
405
|
/** Log entry passed to onLog callback */
|
|
@@ -636,6 +737,8 @@ declare namespace SIMPLEFFMPEG {
|
|
|
636
737
|
| "video"
|
|
637
738
|
| "audio"
|
|
638
739
|
| "image"
|
|
740
|
+
| "color"
|
|
741
|
+
| "effect"
|
|
639
742
|
| "text"
|
|
640
743
|
| "subtitle"
|
|
641
744
|
| "music";
|