ffmpeg-framecraft 1.0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025, the ffmpeg-framecraft contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,467 @@
1
+ # ffmpeg-framecraft
2
+
3
+ A Node.js FFmpeg wrapper for video processing: crop to 9:16, slice by time, add transitions between clips, subtitles, background music, and audio extraction. Suited for vertical/short-form output (e.g. YouTube Shorts, TikTok, Reels) and general pipelines.
4
+
5
+ ---
6
+
7
+ ## Table of contents
8
+
9
+ - [Prerequisites](#prerequisites)
10
+ - [Installation](#installation)
11
+ - [Quick start](#quick-start)
12
+ - [Features](#features)
13
+ - [Crop to 9:16](#1-crop-to-916)
14
+ - [Slice by timestamp](#2-slice-by-timestamp)
15
+ - [Multiple slices with transitions](#3-multiple-slices-with-transitions)
16
+ - [Subtitles](#4-subtitles)
17
+ - [Background music](#5-background-music)
18
+ - [Extract thumbnail](#6-extract-thumbnail)
19
+ - [Extract audio only](#7-extract-audio-only)
20
+ - [Pipelines (compose)](#pipelines-compose)
21
+ - [Platform presets](#platform-presets)
22
+ - [Transition presets](#transition-presets)
23
+ - [Progress reporting](#progress-reporting)
24
+ - [API reference](#api-reference)
25
+ - [Advanced: filter builders](#advanced-filter-builders)
26
+ - [License](#license)
27
+
28
+ ---
29
+
30
+ ## Prerequisites
31
+
32
+ **FFmpeg** and **ffprobe** must be installed:
33
+
34
+ | Platform | Command |
35
+ |-----------|--------|
36
+ | macOS | `brew install ffmpeg` |
37
+ | Ubuntu/Debian | `sudo apt install ffmpeg` |
38
+ | Windows | [FFmpeg download](https://ffmpeg.org/download.html) |
39
+
40
+ If the binaries are not in `PATH`, set:
41
+
42
+ - `FFMPEG_PATH` — path to `ffmpeg`
43
+ - `FFPROBE_PATH` — path to `ffprobe`
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ cd shared/video_tool
51
+ npm install
52
+ ```
53
+
54
+ From another package in the repo:
55
+
56
+ ```javascript
57
+ const { FramecraftEngine } = require('../shared/video_tool');
58
+ ```
59
+
60
+ Or when installed as a dependency:
61
+
62
+ ```javascript
63
+ const { FramecraftEngine } = require('ffmpeg-framecraft');
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick start
69
+
70
+ ```javascript
71
+ const { FramecraftEngine } = require('ffmpeg-framecraft');
72
+
73
+ const engine = new FramecraftEngine();
74
+
75
+ // Crop a landscape video to vertical 9:16
76
+ await engine.cropTo916('input.mp4', 'shorts.mp4');
77
+
78
+ // Extract one segment
79
+ await engine.slice('input.mp4', 'clip.mp4', { start: 10, end: 30 });
80
+
81
+ // Combine several segments with a fade between them
82
+ await engine.slicesWithTransitions('input.mp4', 'highlight.mp4', {
83
+ slices: [
84
+ { start: 0, end: 10 },
85
+ { start: 45, end: 55 },
86
+ { start: 120, end: 130 },
87
+ ],
88
+ transition: 'fade',
89
+ });
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Features
95
+
96
+ ### 1. Crop to 9:16
97
+
98
+ Converts horizontal video to vertical (9:16). Uses a centered crop then scales to **720×1280** (baseline-friendly). Suited for Shorts/Reels/TikTok and similar formats.
99
+
100
+ | Parameter | Type | Description |
101
+ |---------------|--------|-------------|
102
+ | `inputPath` | string | Source video path |
103
+ | `outputPath` | string | Output path (e.g. `.mp4`) |
104
+ | `opts.onProgress` | function | `(progress) => {}`; `progress.percent` is 0–100 |
105
+
106
+ **Example**
107
+
108
+ ```javascript
109
+ await engine.cropTo916('landscape.mp4', 'vertical.mp4');
110
+
111
+ await engine.cropTo916('landscape.mp4', 'vertical.mp4', {
112
+ onProgress: (p) => console.log(`${p.percent?.toFixed(1) ?? '-'}%`),
113
+ });
114
+ ```
115
+
116
+ ---
117
+
118
+ ### 2. Slice by timestamp
119
+
120
+ Extracts a single segment from the video. Start/end can be **seconds** or **time strings** (`"MM:SS"`, `"HH:MM:SS"`).
121
+
122
+ | Parameter | Type | Description |
123
+ |-------------|--------|-------------|
124
+ | `inputPath` | string | Source video |
125
+ | `outputPath`| string | Output path |
126
+ | `range` | object | `{ start, end }` in seconds or time string |
127
+ | `opts.onProgress` | function | Optional progress callback |
128
+
129
+ **Example**
130
+
131
+ ```javascript
132
+ // Seconds
133
+ await engine.slice('input.mp4', 'clip.mp4', { start: 10, end: 30 });
134
+
135
+ // Time strings
136
+ await engine.slice('input.mp4', 'clip.mp4', { start: '0:10', end: '1:30' });
137
+ await engine.slice('input.mp4', 'clip.mp4', { start: '1:00:00', end: '1:05:00' });
138
+ ```
139
+
140
+ ---
141
+
142
+ ### 3. Multiple slices with transitions
143
+
144
+ Trims several time ranges from **one** video and concatenates them with a transition (xfade + acrossfade). Progress is based on **output** duration.
145
+
146
+ | Parameter | Type | Description |
147
+ |---------------|--------|-------------|
148
+ | `inputPath` | string | Source video |
149
+ | `outputPath` | string | Output path |
150
+ | `options.slices` | array | `[{ start, end }, ...]`; times in seconds or `"MM:SS"` |
151
+ | `options.transition` | string \| object | Preset name (e.g. `'fade'`) or `{ type, duration }` (seconds) |
152
+ | `options.preset` | string \| object | Platform preset name or object; sets default transition |
153
+ | `opts.onProgress` | function | Optional progress callback |
154
+
155
+ **Per-slice transition:** Any slice (except the first) can override the transition:
156
+
157
+ ```javascript
158
+ { start: 10, end: 20, transition: 'wipeleft' } // only this boundary uses wipeleft
159
+ ```
160
+
161
+ **Example — same transition for all boundaries**
162
+
163
+ ```javascript
164
+ await engine.slicesWithTransitions('input.mp4', 'highlight.mp4', {
165
+ slices: [
166
+ { start: 0, end: 8 },
167
+ { start: 20, end: 28 },
168
+ { start: 60, end: 68 },
169
+ ],
170
+ transition: 'fade',
171
+ });
172
+ ```
173
+
174
+ **Example — custom duration**
175
+
176
+ ```javascript
177
+ await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
178
+ slices: [{ start: 0, end: 5 }, { start: 10, end: 15 }],
179
+ transition: { type: 'dissolve', duration: 1.5 },
180
+ });
181
+ ```
182
+
183
+ **Example — platform preset (transition from preset)**
184
+
185
+ ```javascript
186
+ await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
187
+ slices: [{ start: 0, end: 10 }, { start: 30, end: 40 }],
188
+ preset: 'youtubeShort', // uses preset's transition + duration
189
+ });
190
+ ```
191
+
192
+ **Example — per-slice transitions**
193
+
194
+ ```javascript
195
+ await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
196
+ slices: [
197
+ { start: 0, end: 9 },
198
+ { start: 38, end: 62, transition: 'fadewhite', duration: 2 },
199
+ { start: 172, end: 176.4, transition: 'dissolve', duration: 1 },
200
+ ],
201
+ transition: 'fade', // default for boundaries that don't specify one
202
+ });
203
+ ```
204
+
205
+ Slices are clamped to the file duration; invalid or zero-length ranges throw.
206
+
207
+ ---
208
+
209
+ ### 4. Subtitles
210
+
211
+ Burns an **SRT** file into the video. Optional ASS-style options for font/size/colour (useful for AI-generated captions).
212
+
213
+ | Parameter | Type | Description |
214
+ |---------------|--------|-------------|
215
+ | `inputPath` | string | Source video |
216
+ | `outputPath` | string | Output path |
217
+ | `srtPath` | string | Path to `.srt` file |
218
+ | `opts` | object | `onProgress` + optional style: `fontName`, `fontSize`, `primaryColour`, `outlineColour`, `backColour`, `outline`, `shadow` |
219
+
220
+ **Example**
221
+
222
+ ```javascript
223
+ await engine.addSubtitles('input.mp4', 'with_subs.mp4', 'captions.srt');
224
+
225
+ await engine.addSubtitles('input.mp4', 'with_subs.mp4', 'captions.srt', {
226
+ fontSize: 28,
227
+ primaryColour: '&Hffffff',
228
+ outline: 2,
229
+ onProgress: (p) => console.log(p.percent?.toFixed(1) + '%'),
230
+ });
231
+ ```
232
+
233
+ ---
234
+
235
+ ### 5. Background music
236
+
237
+ Mixes the video’s audio with a second audio file (e.g. music). If the video has no audio, only the music track is used.
238
+
239
+ | Parameter | Type | Description |
240
+ |---------------|--------|-------------|
241
+ | `inputPath` | string | Source video |
242
+ | `outputPath` | string | Output path |
243
+ | `musicPath` | string | Path to music/audio (e.g. `.mp3`, `.m4a`) |
244
+ | `opts.onProgress` | function | Optional progress callback |
245
+
246
+ **Example**
247
+
248
+ ```javascript
249
+ await engine.addBackgroundMusic('talk.mp4', 'with_music.mp4', 'background.mp3');
250
+ ```
251
+
252
+ ---
253
+
254
+ ### 6. Extract thumbnail
255
+
256
+ Exports a single frame as an image. Format is taken from the file extension (`.jpg`, `.png`).
257
+
258
+ | Parameter | Type | Description |
259
+ |---------------|--------|-------------|
260
+ | `inputPath` | string | Source video |
261
+ | `outputPath` | string | Output image path (e.g. `frame.jpg`, `poster.png`) |
262
+ | `time` | number \| string | Time in seconds or `"MM:SS"` / `"HH:MM:SS"` (default: 0) |
263
+
264
+ **Example**
265
+
266
+ ```javascript
267
+ await engine.extractThumbnail('input.mp4', 'frame.jpg', 5);
268
+ await engine.extractThumbnail('input.mp4', 'poster.png', '0:01:30');
269
+ ```
270
+
271
+ ---
272
+
273
+ ### 7. Extract audio only
274
+
275
+ Re-encodes audio to **AAC** (`.m4a`) or **MP3** (`.mp3`) based on the output extension. No video stream.
276
+
277
+ | Parameter | Type | Description |
278
+ |---------------|--------|-------------|
279
+ | `inputPath` | string | Source video |
280
+ | `outputPath` | string | Output path (`.m4a` or `.mp3`) |
281
+ | `opts.bitrate`| number | kbps (default: 256 for AAC, 320 for MP3) |
282
+ | `opts.sampleRate` | number | Hz (default: 48000) |
283
+ | `opts.onProgress` | function | Optional progress callback |
284
+
285
+ **Example**
286
+
287
+ ```javascript
288
+ await engine.extractAudioOnly('video.mp4', 'audio.m4a');
289
+ await engine.extractAudioOnly('video.mp4', 'audio.mp3');
290
+ await engine.extractAudioOnly('video.mp4', 'audio.mp3', { bitrate: 256 });
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Pipelines (compose)
296
+
297
+ Run several operations in sequence with a single call. Intermediate files are written to the same directory as the final output and **deleted** after the pipeline finishes.
298
+
299
+ **Preset:** one named operation.
300
+
301
+ ```javascript
302
+ await engine.compose('input.mp4', 'output.mp4', 'shorts');
303
+ // Same as: cropTo916(input, output)
304
+ ```
305
+
306
+ **Pipeline:** array of steps. Each step has an `op` and step-specific options.
307
+
308
+ | Step `op` | Required options | Description |
309
+ |-----------|------------------|-------------|
310
+ | `crop916` | — | Crop to 9:16 |
311
+ | `slice` | `start`, `end` | Extract segment |
312
+ | `subtitles` | `srtPath` | Burn SRT |
313
+ | `music` | `musicPath` | Add background music |
314
+ | `slicesWithTransitions` | `slices` | Multiple slices + transitions; optional `transition`, `preset` |
315
+ | `audioOnly` | `outputPath` | Extract audio to given path (no video output file from compose) |
316
+
317
+ **Example — crop then slice**
318
+
319
+ ```javascript
320
+ await engine.compose('input.mp4', 'output.mp4', [
321
+ { op: 'crop916' },
322
+ { op: 'slice', start: 0, end: 60 },
323
+ ]);
324
+ ```
325
+
326
+ **Example — slice then extract audio only**
327
+
328
+ ```javascript
329
+ await engine.compose('input.mp4', 'output.mp4', [
330
+ { op: 'slice', start: 10, end: 20 },
331
+ { op: 'audioOnly', outputPath: path.join(__dirname, 'data', 'clip_audio.mp3') },
332
+ ], { onProgress: (p) => console.log(p.percent?.toFixed(1) + '%') });
333
+ // Only clip_audio.mp3 is kept; intermediate slice file is removed.
334
+ ```
335
+
336
+ **Example — full pipeline**
337
+
338
+ ```javascript
339
+ await engine.compose('input.mp4', 'output.mp4', [
340
+ { op: 'slicesWithTransitions', slices: [{ start: 0, end: 10 }, { start: 30, end: 40 }], preset: 'youtubeShort' },
341
+ { op: 'subtitles', srtPath: 'subs.srt' },
342
+ { op: 'music', musicPath: 'bgm.mp3' },
343
+ ], { onProgress: (p) => console.log(p.percent?.toFixed(1) + '%') });
344
+ ```
345
+
346
+ ---
347
+
348
+ ## Platform presets
349
+
350
+ Presets define aspect ratio, resolution, transition, and codec hints. Use **preset name** (string) or the **object** from the package.
351
+
352
+ | Preset | Name | Notes |
353
+ |----------------------|------------------|--------|
354
+ | YouTube Shorts | `youtubeShort` | 9:16, 1080×1920, fade, subtitles/watermark flags |
355
+ | TikTok | `tiktok` | 9:16, 1080×1920, fade |
356
+ | Instagram Reels | `instagramReels` | 9:16, 1080×1920, dissolve |
357
+ | Shorts (720p) | `shorts` | 9:16, 720×1280, baseline profile |
358
+
359
+ **Example**
360
+
361
+ ```javascript
362
+ const { youtubeShortPreset, getPreset } = require('ffmpeg-framecraft');
363
+
364
+ console.log(youtubeShortPreset.transition); // 'fade'
365
+ console.log(getPreset('tiktok').aspectRatio); // '9:16'
366
+
367
+ await engine.slicesWithTransitions('input.mp4', 'out.mp4', {
368
+ slices: [...],
369
+ preset: 'youtubeShort',
370
+ });
371
+ ```
372
+
373
+ ---
374
+
375
+ ## Transition presets
376
+
377
+ Used by `slicesWithTransitions`. You can pass a **preset name** or **`{ type, duration }`** (duration in seconds).
378
+
379
+ **Available names:**
380
+ `fade`, `fadeLong`, `wipeleft`, `wiperight`, `wipeup`, `wipedown`, `slideleft`, `slideright`, `slideup`, `slidedown`, `circleopen`, `circleclose`, `rectcrop`, `distance`, `fadeblack`, `fadewhite`, `radial`, `dissolve`, `pixelize`, `zoomin`, `zoomout`.
381
+
382
+ **Example**
383
+
384
+ ```javascript
385
+ const { TRANSITION_PRESETS, getTransition } = require('ffmpeg-framecraft');
386
+
387
+ // Preset name
388
+ transition: 'dissolve'
389
+
390
+ // Custom type + duration
391
+ transition: { type: 'fade', duration: 1 }
392
+
393
+ // Resolve preset to object
394
+ getTransition('fadeLong'); // { type: 'fade', duration: 1 }
395
+ ```
396
+
397
+ Transition duration is automatically clamped so it does not exceed the length of either segment at that boundary.
398
+
399
+ ---
400
+
401
+ ## Progress reporting
402
+
403
+ All operations that take `opts.onProgress` report:
404
+
405
+ - **`percent`** — 0–100, based on **output** duration when known (slice, slicesWithTransitions, cropTo916, addBackgroundMusic). Otherwise from FFmpeg input duration.
406
+ - **`timemark`** — current output time (e.g. `"0:00:12.50"`).
407
+ - **`frames`**, **`currentFps`**, **`currentKbps`** when available.
408
+
409
+ On completion, the executor emits a final `onProgress({ percent: 100 })` so the UI can show 100% before “Compose completed”.
410
+
411
+ ---
412
+
413
+ ## API reference
414
+
415
+ | Method | Description |
416
+ |--------|-------------|
417
+ | `cropTo916(inputPath, outputPath, opts?)` | Crop to 720×1280 vertical (9:16). |
418
+ | `slice(inputPath, outputPath, { start, end }, opts?)` | Extract one segment; start/end in seconds or time string. |
419
+ | `addSubtitles(inputPath, outputPath, srtPath, opts?)` | Burn SRT; optional ASS style options. |
420
+ | `extractThumbnail(inputPath, outputPath, time?)` | Single frame to image (.jpg / .png). |
421
+ | `addBackgroundMusic(inputPath, outputPath, musicPath, opts?)` | Mix video audio with music. |
422
+ | `extractAudioOnly(inputPath, outputPath, opts?)` | Audio only → .m4a (AAC) or .mp3. |
423
+ | `slicesWithTransitions(inputPath, outputPath, { slices, transition?, preset? }, opts?)` | Multiple slices joined with xfade/acrossfade. |
424
+ | `compose(inputPath, outputPath, pipeline?, opts?)` | Run preset `'shorts'` or pipeline array. |
425
+
426
+ **Common `opts`:** `onProgress: (progress) => {}`.
427
+
428
+ ---
429
+
430
+ ## Advanced: filter builders
431
+
432
+ For tests or custom FFmpeg graphs you can use the low-level filter helpers.
433
+
434
+ ```javascript
435
+ const {
436
+ cropTo916Filter,
437
+ subtitleFilter,
438
+ amixFilter,
439
+ buildSlicesWithTransitionsFilter,
440
+ SHORTS_WIDTH,
441
+ SHORTS_HEIGHT,
442
+ } = require('ffmpeg-framecraft');
443
+
444
+ // Build -vf style filter strings (no execution)
445
+ cropTo916Filter(1920, 1080);
446
+ // => "crop=607:1080:656:0,scale=720:1280"
447
+
448
+ subtitleFilter('/path/to/subs.srt', { fontSize: 24 });
449
+ // => "subtitles='...':force_style='FontSize=24'"
450
+
451
+ amixFilter(true);
452
+ // => "[0:a][1:a]amix=inputs=2:duration=shortest[aout]"
453
+
454
+ // Slices + transitions: pass normalized slices and hasAudio
455
+ const slices = [
456
+ { startSeconds: 0, endSeconds: 10, transition: null },
457
+ { startSeconds: 20, endSeconds: 30, transition: { type: 'fade', duration: 0.5 } },
458
+ ];
459
+ const { filterComplex, mapVideo, mapAudio } = buildSlicesWithTransitionsFilter(slices, true);
460
+ // Use filterComplex with -filter_complex and mapVideo/mapAudio with -map
461
+ ```
462
+
463
+ ---
464
+
465
+ ## License
466
+
467
+ ISC
package/index.js ADDED
@@ -0,0 +1,29 @@
1
+ const { FramecraftEngine } = require('./src/engine');
2
+ const { cropTo916Filter, subtitleFilter, amixFilter, buildSlicesWithTransitionsFilter, SHORTS_WIDTH, SHORTS_HEIGHT } = require('./src/filters');
3
+ const { PRESETS: TRANSITION_PRESETS, getTransition } = require('./src/presets/transitions');
4
+ const {
5
+ youtubeShortPreset,
6
+ tiktokPreset,
7
+ instagramReelsPreset,
8
+ shortsPresetConfig,
9
+ PLATFORM_PRESETS,
10
+ getPreset,
11
+ } = require('./src/presets/presets');
12
+
13
+ module.exports = {
14
+ FramecraftEngine,
15
+ cropTo916Filter,
16
+ subtitleFilter,
17
+ amixFilter,
18
+ buildSlicesWithTransitionsFilter,
19
+ SHORTS_WIDTH,
20
+ SHORTS_HEIGHT,
21
+ TRANSITION_PRESETS,
22
+ getTransition,
23
+ youtubeShortPreset,
24
+ tiktokPreset,
25
+ instagramReelsPreset,
26
+ shortsPresetConfig,
27
+ PLATFORM_PRESETS,
28
+ getPreset,
29
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "ffmpeg-framecraft",
3
+ "version": "1.0.0",
4
+ "description": "FFmpeg-based video processing: crop, slice, transitions, subtitles, audio extraction",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node --test test/"
8
+ },
9
+ "files": [
10
+ "LICENSE",
11
+ "README.md",
12
+ "index.js",
13
+ "src/"
14
+ ],
15
+ "keywords": [
16
+ "ffmpeg",
17
+ "video",
18
+ "crop",
19
+ "slice",
20
+ "transitions",
21
+ "subtitles",
22
+ "9:16",
23
+ "shorts",
24
+ "reels",
25
+ "tiktok"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "homepage": "https://github.com/dxmari/ffmpeg-framecraft",
31
+ "bugs": {
32
+ "url": "https://github.com/dxmari/ffmpeg-framecraft/issues"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/dxmari/ffmpeg-framecraft.git"
37
+ },
38
+ "author": "Mariselvam",
39
+ "license": "ISC",
40
+ "dependencies": {
41
+ "fluent-ffmpeg": "^2.1.3"
42
+ }
43
+ }