n8n-nodes-sb-render 1.1.4 → 1.1.5

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 sb-render contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 sb-render contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,442 +1,442 @@
1
- # n8n-nodes-sb-render
2
-
3
- [![npm version](https://badge.fury.io/js/n8n-nodes-sb-render.svg)](https://badge.fury.io/js/n8n-nodes-sb-render)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
-
6
- This is an n8n community node for video rendering with customizable subtitles, background music (BGM), and narration using FFmpeg.
7
-
8
- **sb-render** allows you to automate video composition workflows in n8n, combining video files with:
9
- - 🎵 Background music with volume control and fade effects
10
- - 🎙️ Narration audio with timing control
11
- - 📝 Customizable subtitles with extensive styling options
12
- - 🎨 Multiple output formats and quality presets
13
-
14
- Inspired by [n8n-nodes-mediafx](https://github.com/dandacompany/n8n-nodes-mediafx).
15
-
16
- ## Table of Contents
17
-
18
- - [Installation](#installation)
19
- - [Prerequisites](#prerequisites)
20
- - [Operations](#operations)
21
- - [Configuration](#configuration)
22
- - [Examples](#examples)
23
- - [Development](#development)
24
- - [Troubleshooting](#troubleshooting)
25
- - [License](#license)
26
-
27
- ## Installation
28
-
29
- ### Community Nodes (Recommended)
30
-
31
- 1. Go to **Settings > Community Nodes** in n8n
32
- 2. Select **Install**
33
- 3. Enter `n8n-nodes-sb-render`
34
- 4. Agree to the risks and install
35
-
36
- ### Manual Installation
37
-
38
- ```bash
39
- npm install n8n-nodes-sb-render
40
- ```
41
-
42
- For n8n self-hosted installations:
43
-
44
- ```bash
45
- cd ~/.n8n
46
- npm install n8n-nodes-sb-render
47
- # Restart n8n
48
- ```
49
-
50
- ## Prerequisites
51
-
52
- - **n8n version**: 1.0.0 or higher
53
- - **Node.js**: 16.0.0 or higher
54
- - **FFmpeg**: Automatically installed via `@ffmpeg-installer/ffmpeg`
55
-
56
- ## Operations
57
-
58
- ### Video → Render
59
-
60
- Compose a video with optional background music, narration, and subtitles.
61
-
62
- **Features:**
63
- - ✅ Multiple input sources (URL or binary data)
64
- - ✅ Audio mixing with independent volume controls
65
- - ✅ Fade in/out effects for BGM
66
- - ✅ Customizable subtitle positioning and styling
67
- - ✅ Multiple output formats (MP4, MOV, WebM)
68
- - ✅ Quality presets (Low, Medium, High, Custom)
69
-
70
- ## Configuration
71
-
72
- ### Video Input
73
-
74
- | Parameter | Type | Required | Description |
75
- |-----------|------|----------|-------------|
76
- | **Video Source** | Options | Yes | `url` or `binary` |
77
- | **Video URL** | String | If URL | URL of the video file |
78
- | **Video Binary Property** | String | If Binary | Name of binary property containing video |
79
-
80
- ### Background Music (BGM)
81
-
82
- | Parameter | Type | Default | Description |
83
- |-----------|------|---------|-------------|
84
- | **Enable BGM** | Boolean | false | Add background music |
85
- | **BGM Source** | Options | - | `url` or `binary` |
86
- | **BGM URL** | String | - | URL of BGM file |
87
- | **BGM Volume** | Number | 30 | Volume 0-100 |
88
- | **BGM Fade In** | Number | 2 | Fade-in duration (seconds) |
89
- | **BGM Fade Out** | Number | 2 | Fade-out duration (seconds) |
90
-
91
- ### Narration
92
-
93
- | Parameter | Type | Default | Description |
94
- |-----------|------|---------|-------------|
95
- | **Enable Narration** | Boolean | false | Add narration audio |
96
- | **Narration Source** | Options | - | `url` or `binary` |
97
- | **Narration URL** | String | - | URL of narration file |
98
- | **Narration Volume** | Number | 80 | Volume 0-100 |
99
- | **Narration Delay** | Number | 0 | Delay before start (seconds) |
100
-
101
- ### Subtitles
102
-
103
- | Parameter | Type | Default | Description |
104
- |-----------|------|---------|-------------|
105
- | **Enable Subtitles** | Boolean | false | Add subtitles |
106
- | **Text** | String | - | Subtitle text content |
107
- | **Start Time** | Number | - | Start time in seconds |
108
- | **End Time** | Number | - | End time in seconds |
109
- | **Position** | Options | bottom | `top`, `middle`, `bottom`, `custom` |
110
- | **Font Size** | Number | 48 | Text size |
111
- | **Font Color** | Color | #FFFFFF | Text color (hex) |
112
- | **Font Family** | String | Arial | Font name |
113
- | **Alignment** | Options | center | `left`, `center`, `right` |
114
- | **Background Color** | Color | #000000 | Background color (hex) |
115
- | **Background Opacity** | Number | 80 | Opacity 0-100 |
116
- | **Border Color** | Color | #000000 | Border color (hex) |
117
- | **Border Width** | Number | 2 | Border width (pixels) |
118
-
119
- ### Output Options
120
-
121
- | Parameter | Type | Default | Description |
122
- |-----------|------|---------|-------------|
123
- | **Output Format** | Options | mp4 | `mp4`, `mov`, `webm` |
124
- | **Video Codec** | Options | libx264 | `libx264`, `libx265`, `vp9` |
125
- | **Quality** | Options | high | `low`, `medium`, `high`, `custom` |
126
- | **Custom CRF** | Number | 18 | CRF value 0-51 (if custom quality) |
127
- | **Output Binary Property** | String | data | Property name for output |
128
-
129
- ## Examples
130
-
131
- ### Example 1: Simple Video with Subtitles
132
-
133
- ```json
134
- {
135
- "videoSource": "url",
136
- "videoUrl": "https://example.com/video.mp4",
137
- "enableSubtitles": true,
138
- "subtitles": {
139
- "subtitle": [
140
- {
141
- "text": "Welcome to our video!",
142
- "startTime": 0,
143
- "endTime": 5,
144
- "position": "bottom",
145
- "fontSize": 48,
146
- "fontColor": "#FFFFFF",
147
- "alignment": "center"
148
- }
149
- ]
150
- },
151
- "outputFormat": "mp4",
152
- "quality": "high"
153
- }
154
- ```
155
-
156
- ### Example 2: Video with BGM and Narration
157
-
158
- ```json
159
- {
160
- "videoSource": "url",
161
- "videoUrl": "https://example.com/video.mp4",
162
- "enableBGM": true,
163
- "bgmSource": "url",
164
- "bgmUrl": "https://example.com/music.mp3",
165
- "bgmVolume": 20,
166
- "bgmFadeIn": 3,
167
- "bgmFadeOut": 3,
168
- "enableNarration": true,
169
- "narrationSource": "url",
170
- "narrationUrl": "https://example.com/narration.mp3",
171
- "narrationVolume": 90,
172
- "narrationDelay": 2,
173
- "outputFormat": "mp4"
174
- }
175
- ```
176
-
177
- ### Example 3: Custom Positioned Subtitles
178
-
179
- ```json
180
- {
181
- "videoSource": "url",
182
- "videoUrl": "https://example.com/video.mp4",
183
- "enableSubtitles": true,
184
- "subtitles": {
185
- "subtitle": [
186
- {
187
- "text": "Top-left subtitle",
188
- "startTime": 0,
189
- "endTime": 5,
190
- "position": "custom",
191
- "customX": 100,
192
- "customY": 100,
193
- "fontSize": 36,
194
- "fontColor": "#FFFF00",
195
- "alignment": "left",
196
- "backgroundColor": "#000000",
197
- "backgroundOpacity": 70,
198
- "borderColor": "#FFFFFF",
199
- "borderWidth": 3
200
- }
201
- ]
202
- }
203
- }
204
- ```
205
-
206
- ### Example 4: Workflow Integration
207
-
208
- **Scenario**: Download video from URL, add BGM and subtitles, upload to cloud storage
209
-
210
- ```
211
- HTTP Request → SB Render → Google Drive
212
- ```
213
-
214
- 1. **HTTP Request**: Download video file
215
- 2. **SB Render**: Add BGM and subtitles
216
- 3. **Google Drive**: Upload rendered video
217
-
218
- ## Development
219
-
220
- ### Build
221
-
222
- ```bash
223
- npm install
224
- npm run build
225
- ```
226
-
227
- ### Development Mode
228
-
229
- ```bash
230
- npm run dev
231
- ```
232
-
233
- ### Linting
234
-
235
- ```bash
236
- npm run lint
237
- npm run lintfix
238
- ```
239
-
240
- ### Project Structure
241
-
242
- ```
243
- sb-render/
244
- ├── nodes/
245
- │ └── SbRender/
246
- │ ├── SbRender.node.ts # Main node implementation
247
- │ ├── SbRender.node.json # Node metadata
248
- │ ├── services/
249
- │ │ ├── FileManager.ts # File handling
250
- │ │ ├── AudioMixer.ts # Audio composition
251
- │ │ ├── SubtitleEngine.ts # Subtitle generation
252
- │ │ └── VideoComposer.ts # Video rendering
253
- │ ├── interfaces/
254
- │ │ └── index.ts # TypeScript types
255
- │ └── utils/
256
- │ ├── ffmpeg.ts # FFmpeg helpers
257
- │ └── validation.ts # Input validation
258
- ├── fonts/ # Custom fonts
259
- ├── package.json
260
- ├── tsconfig.json
261
- └── README.md
262
- ```
263
-
264
- ## Troubleshooting
265
-
266
- ### FFmpeg Not Found
267
-
268
- The node automatically installs FFmpeg via `@ffmpeg-installer/ffmpeg`. If you encounter issues:
269
-
270
- ```bash
271
- npm install @ffmpeg-installer/ffmpeg --force
272
- ```
273
-
274
- ### File Download Errors
275
-
276
- **Issue**: Video/audio URLs fail to download
277
-
278
- **Solutions**:
279
- - Verify URL accessibility
280
- - Check for authentication requirements
281
- - Ensure sufficient disk space
282
- - Try using binary data input instead
283
-
284
- ### Subtitle Not Appearing
285
-
286
- **Issue**: Subtitles don't show in output video
287
-
288
- **Solutions**:
289
- - Verify timing (start/end times within video duration)
290
- - Check subtitle position and size
291
- - Ensure font is available on system
292
- - Try different output format (MP4 recommended)
293
-
294
- ### Memory Issues
295
-
296
- **Issue**: Process crashes with large video files
297
-
298
- **Solutions**:
299
- - Reduce video resolution before processing
300
- - Use lower quality preset
301
- - Process videos in smaller batches
302
- - Increase Node.js memory limit:
303
-
304
- ```bash
305
- export NODE_OPTIONS="--max-old-space-size=4096"
306
- ```
307
-
308
- ### Audio Sync Issues
309
-
310
- **Issue**: Audio out of sync with video
311
-
312
- **Solutions**:
313
- - Ensure all audio files are in compatible formats
314
- - Check narration delay settings
315
- - Verify video frame rate compatibility
316
- - Try re-encoding source files
317
-
318
- ## Technical Details
319
-
320
- ### FFmpeg Commands
321
-
322
- The node uses **fluent-ffmpeg** to construct FFmpeg commands:
323
-
324
- **Audio Mixing**:
325
- ```bash
326
- -filter_complex "[0:a]volume=1.0[original];[1:a]volume=0.3,afade=t=in:st=0:d=2[bgm];[original][bgm]amix=inputs=2[mixed]"
327
- ```
328
-
329
- **Subtitle Overlay**:
330
- ```bash
331
- -vf "ass=subtitles.ass"
332
- ```
333
-
334
- **Complete Command**:
335
- ```bash
336
- ffmpeg -i video.mp4 -i bgm.mp3 -filter_complex "..." -vf "ass=..." -c:v libx264 -crf 18 output.mp4
337
- ```
338
-
339
- ### Subtitle Formats
340
-
341
- - **SRT**: Simple subtitle format (limited styling)
342
- - **ASS**: Advanced SubStation Alpha (full styling support) ← **Used by default**
343
-
344
- ASS format provides:
345
- - Custom fonts and colors
346
- - Precise positioning
347
- - Background colors and opacity
348
- - Border/outline effects
349
- - Multiple alignment options
350
-
351
- ## Performance
352
-
353
- ### Resource Usage
354
-
355
- | Video Length | Memory | Processing Time |
356
- |--------------|--------|-----------------|
357
- | 1 minute | ~200MB | ~30 seconds |
358
- | 5 minutes | ~500MB | ~2 minutes |
359
- | 10 minutes | ~1GB | ~5 minutes |
360
-
361
- *Times measured on standard VPS (2 CPU, 4GB RAM)*
362
-
363
- ### Optimization Tips
364
-
365
- 1. **Use appropriate quality**: High quality for final output, medium for testing
366
- 2. **Compress BGM**: Use lower bitrate audio files (128-192 kbps)
367
- 3. **Batch processing**: Process multiple videos in parallel workflows
368
- 4. **Cache audio files**: Reuse same BGM/narration across multiple videos
369
-
370
- ## Compatibility
371
-
372
- ### Supported Video Formats
373
-
374
- **Input**: MP4, MOV, WebM, AVI, MKV
375
- **Output**: MP4, MOV, WebM
376
-
377
- ### Supported Audio Formats
378
-
379
- **Input**: MP3, WAV, AAC, OGG, M4A
380
- **Output**: AAC (default)
381
-
382
- ### Codec Compatibility
383
-
384
- | Codec | Quality | Speed | Browser Support |
385
- |-------|---------|-------|-----------------|
386
- | H.264 (libx264) | Good | Fast | Excellent |
387
- | H.265 (libx265) | Better | Slower | Limited |
388
- | VP9 | Good | Slow | Good (WebM) |
389
-
390
- ## Contributing
391
-
392
- Contributions are welcome! Please:
393
-
394
- 1. Fork the repository
395
- 2. Create a feature branch
396
- 3. Make your changes
397
- 4. Add tests if applicable
398
- 5. Submit a pull request
399
-
400
- ### Development Guidelines
401
-
402
- - Follow existing code style (ESLint configured)
403
- - Add TypeScript types for new features
404
- - Update documentation for new parameters
405
- - Test with various video formats
406
-
407
- ## Roadmap
408
-
409
- ### v1.1 (Planned)
410
-
411
- - [ ] Multiple video layers
412
- - [ ] Video transitions
413
- - [ ] Logo/watermark overlay
414
- - [ ] Preset subtitle templates
415
- - [ ] Batch processing mode
416
-
417
- ### v2.0 (Future)
418
-
419
- - [ ] GPU acceleration (NVENC, VideoToolbox)
420
- - [ ] Cloud storage integration
421
- - [ ] Progress callbacks
422
- - [ ] Advanced video effects
423
-
424
- ## Credits
425
-
426
- - Inspired by [n8n-nodes-mediafx](https://github.com/dandacompany/n8n-nodes-mediafx)
427
- - Uses [FFmpeg](https://ffmpeg.org/) for video processing
428
- - Built with [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg)
429
-
430
- ## License
431
-
432
- [MIT](LICENSE)
433
-
434
- ## Support
435
-
436
- - **Issues**: [GitHub Issues](https://github.com/choisb87/sb-render/issues)
437
- - **Documentation**: [GitHub Wiki](https://github.com/choisb87/sb-render/wiki)
438
- - **n8n Community**: [n8n Community Forum](https://community.n8n.io/)
439
-
440
- ---
441
-
442
- **Made with ❤️ for the n8n community**
1
+ # n8n-nodes-sb-render
2
+
3
+ [![npm version](https://badge.fury.io/js/n8n-nodes-sb-render.svg)](https://badge.fury.io/js/n8n-nodes-sb-render)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ This is an n8n community node for video rendering with customizable subtitles, background music (BGM), and narration using FFmpeg.
7
+
8
+ **sb-render** allows you to automate video composition workflows in n8n, combining video files with:
9
+ - 🎵 Background music with volume control and fade effects
10
+ - 🎙️ Narration audio with timing control
11
+ - 📝 Customizable subtitles with extensive styling options
12
+ - 🎨 Multiple output formats and quality presets
13
+
14
+ Inspired by [n8n-nodes-mediafx](https://github.com/dandacompany/n8n-nodes-mediafx).
15
+
16
+ ## Table of Contents
17
+
18
+ - [Installation](#installation)
19
+ - [Prerequisites](#prerequisites)
20
+ - [Operations](#operations)
21
+ - [Configuration](#configuration)
22
+ - [Examples](#examples)
23
+ - [Development](#development)
24
+ - [Troubleshooting](#troubleshooting)
25
+ - [License](#license)
26
+
27
+ ## Installation
28
+
29
+ ### Community Nodes (Recommended)
30
+
31
+ 1. Go to **Settings > Community Nodes** in n8n
32
+ 2. Select **Install**
33
+ 3. Enter `n8n-nodes-sb-render`
34
+ 4. Agree to the risks and install
35
+
36
+ ### Manual Installation
37
+
38
+ ```bash
39
+ npm install n8n-nodes-sb-render
40
+ ```
41
+
42
+ For n8n self-hosted installations:
43
+
44
+ ```bash
45
+ cd ~/.n8n
46
+ npm install n8n-nodes-sb-render
47
+ # Restart n8n
48
+ ```
49
+
50
+ ## Prerequisites
51
+
52
+ - **n8n version**: 1.0.0 or higher
53
+ - **Node.js**: 16.0.0 or higher
54
+ - **FFmpeg**: Automatically installed via `@ffmpeg-installer/ffmpeg`
55
+
56
+ ## Operations
57
+
58
+ ### Video → Render
59
+
60
+ Compose a video with optional background music, narration, and subtitles.
61
+
62
+ **Features:**
63
+ - ✅ Multiple input sources (URL or binary data)
64
+ - ✅ Audio mixing with independent volume controls
65
+ - ✅ Fade in/out effects for BGM
66
+ - ✅ Customizable subtitle positioning and styling
67
+ - ✅ Multiple output formats (MP4, MOV, WebM)
68
+ - ✅ Quality presets (Low, Medium, High, Custom)
69
+
70
+ ## Configuration
71
+
72
+ ### Video Input
73
+
74
+ | Parameter | Type | Required | Description |
75
+ |-----------|------|----------|-------------|
76
+ | **Video Source** | Options | Yes | `url` or `binary` |
77
+ | **Video URL** | String | If URL | URL of the video file |
78
+ | **Video Binary Property** | String | If Binary | Name of binary property containing video |
79
+
80
+ ### Background Music (BGM)
81
+
82
+ | Parameter | Type | Default | Description |
83
+ |-----------|------|---------|-------------|
84
+ | **Enable BGM** | Boolean | false | Add background music |
85
+ | **BGM Source** | Options | - | `url` or `binary` |
86
+ | **BGM URL** | String | - | URL of BGM file |
87
+ | **BGM Volume** | Number | 30 | Volume 0-100 |
88
+ | **BGM Fade In** | Number | 2 | Fade-in duration (seconds) |
89
+ | **BGM Fade Out** | Number | 2 | Fade-out duration (seconds) |
90
+
91
+ ### Narration
92
+
93
+ | Parameter | Type | Default | Description |
94
+ |-----------|------|---------|-------------|
95
+ | **Enable Narration** | Boolean | false | Add narration audio |
96
+ | **Narration Source** | Options | - | `url` or `binary` |
97
+ | **Narration URL** | String | - | URL of narration file |
98
+ | **Narration Volume** | Number | 80 | Volume 0-100 |
99
+ | **Narration Delay** | Number | 0 | Delay before start (seconds) |
100
+
101
+ ### Subtitles
102
+
103
+ | Parameter | Type | Default | Description |
104
+ |-----------|------|---------|-------------|
105
+ | **Enable Subtitles** | Boolean | false | Add subtitles |
106
+ | **Text** | String | - | Subtitle text content |
107
+ | **Start Time** | Number | - | Start time in seconds |
108
+ | **End Time** | Number | - | End time in seconds |
109
+ | **Position** | Options | bottom | `top`, `middle`, `bottom`, `custom` |
110
+ | **Font Size** | Number | 48 | Text size |
111
+ | **Font Color** | Color | #FFFFFF | Text color (hex) |
112
+ | **Font Family** | String | Arial | Font name |
113
+ | **Alignment** | Options | center | `left`, `center`, `right` |
114
+ | **Background Color** | Color | #000000 | Background color (hex) |
115
+ | **Background Opacity** | Number | 80 | Opacity 0-100 |
116
+ | **Border Color** | Color | #000000 | Border color (hex) |
117
+ | **Border Width** | Number | 2 | Border width (pixels) |
118
+
119
+ ### Output Options
120
+
121
+ | Parameter | Type | Default | Description |
122
+ |-----------|------|---------|-------------|
123
+ | **Output Format** | Options | mp4 | `mp4`, `mov`, `webm` |
124
+ | **Video Codec** | Options | libx264 | `libx264`, `libx265`, `vp9` |
125
+ | **Quality** | Options | high | `low`, `medium`, `high`, `custom` |
126
+ | **Custom CRF** | Number | 18 | CRF value 0-51 (if custom quality) |
127
+ | **Output Binary Property** | String | data | Property name for output |
128
+
129
+ ## Examples
130
+
131
+ ### Example 1: Simple Video with Subtitles
132
+
133
+ ```json
134
+ {
135
+ "videoSource": "url",
136
+ "videoUrl": "https://example.com/video.mp4",
137
+ "enableSubtitles": true,
138
+ "subtitles": {
139
+ "subtitle": [
140
+ {
141
+ "text": "Welcome to our video!",
142
+ "startTime": 0,
143
+ "endTime": 5,
144
+ "position": "bottom",
145
+ "fontSize": 48,
146
+ "fontColor": "#FFFFFF",
147
+ "alignment": "center"
148
+ }
149
+ ]
150
+ },
151
+ "outputFormat": "mp4",
152
+ "quality": "high"
153
+ }
154
+ ```
155
+
156
+ ### Example 2: Video with BGM and Narration
157
+
158
+ ```json
159
+ {
160
+ "videoSource": "url",
161
+ "videoUrl": "https://example.com/video.mp4",
162
+ "enableBGM": true,
163
+ "bgmSource": "url",
164
+ "bgmUrl": "https://example.com/music.mp3",
165
+ "bgmVolume": 20,
166
+ "bgmFadeIn": 3,
167
+ "bgmFadeOut": 3,
168
+ "enableNarration": true,
169
+ "narrationSource": "url",
170
+ "narrationUrl": "https://example.com/narration.mp3",
171
+ "narrationVolume": 90,
172
+ "narrationDelay": 2,
173
+ "outputFormat": "mp4"
174
+ }
175
+ ```
176
+
177
+ ### Example 3: Custom Positioned Subtitles
178
+
179
+ ```json
180
+ {
181
+ "videoSource": "url",
182
+ "videoUrl": "https://example.com/video.mp4",
183
+ "enableSubtitles": true,
184
+ "subtitles": {
185
+ "subtitle": [
186
+ {
187
+ "text": "Top-left subtitle",
188
+ "startTime": 0,
189
+ "endTime": 5,
190
+ "position": "custom",
191
+ "customX": 100,
192
+ "customY": 100,
193
+ "fontSize": 36,
194
+ "fontColor": "#FFFF00",
195
+ "alignment": "left",
196
+ "backgroundColor": "#000000",
197
+ "backgroundOpacity": 70,
198
+ "borderColor": "#FFFFFF",
199
+ "borderWidth": 3
200
+ }
201
+ ]
202
+ }
203
+ }
204
+ ```
205
+
206
+ ### Example 4: Workflow Integration
207
+
208
+ **Scenario**: Download video from URL, add BGM and subtitles, upload to cloud storage
209
+
210
+ ```
211
+ HTTP Request → SB Render → Google Drive
212
+ ```
213
+
214
+ 1. **HTTP Request**: Download video file
215
+ 2. **SB Render**: Add BGM and subtitles
216
+ 3. **Google Drive**: Upload rendered video
217
+
218
+ ## Development
219
+
220
+ ### Build
221
+
222
+ ```bash
223
+ npm install
224
+ npm run build
225
+ ```
226
+
227
+ ### Development Mode
228
+
229
+ ```bash
230
+ npm run dev
231
+ ```
232
+
233
+ ### Linting
234
+
235
+ ```bash
236
+ npm run lint
237
+ npm run lintfix
238
+ ```
239
+
240
+ ### Project Structure
241
+
242
+ ```
243
+ sb-render/
244
+ ├── nodes/
245
+ │ └── SbRender/
246
+ │ ├── SbRender.node.ts # Main node implementation
247
+ │ ├── SbRender.node.json # Node metadata
248
+ │ ├── services/
249
+ │ │ ├── FileManager.ts # File handling
250
+ │ │ ├── AudioMixer.ts # Audio composition
251
+ │ │ ├── SubtitleEngine.ts # Subtitle generation
252
+ │ │ └── VideoComposer.ts # Video rendering
253
+ │ ├── interfaces/
254
+ │ │ └── index.ts # TypeScript types
255
+ │ └── utils/
256
+ │ ├── ffmpeg.ts # FFmpeg helpers
257
+ │ └── validation.ts # Input validation
258
+ ├── fonts/ # Custom fonts
259
+ ├── package.json
260
+ ├── tsconfig.json
261
+ └── README.md
262
+ ```
263
+
264
+ ## Troubleshooting
265
+
266
+ ### FFmpeg Not Found
267
+
268
+ The node automatically installs FFmpeg via `@ffmpeg-installer/ffmpeg`. If you encounter issues:
269
+
270
+ ```bash
271
+ npm install @ffmpeg-installer/ffmpeg --force
272
+ ```
273
+
274
+ ### File Download Errors
275
+
276
+ **Issue**: Video/audio URLs fail to download
277
+
278
+ **Solutions**:
279
+ - Verify URL accessibility
280
+ - Check for authentication requirements
281
+ - Ensure sufficient disk space
282
+ - Try using binary data input instead
283
+
284
+ ### Subtitle Not Appearing
285
+
286
+ **Issue**: Subtitles don't show in output video
287
+
288
+ **Solutions**:
289
+ - Verify timing (start/end times within video duration)
290
+ - Check subtitle position and size
291
+ - Ensure font is available on system
292
+ - Try different output format (MP4 recommended)
293
+
294
+ ### Memory Issues
295
+
296
+ **Issue**: Process crashes with large video files
297
+
298
+ **Solutions**:
299
+ - Reduce video resolution before processing
300
+ - Use lower quality preset
301
+ - Process videos in smaller batches
302
+ - Increase Node.js memory limit:
303
+
304
+ ```bash
305
+ export NODE_OPTIONS="--max-old-space-size=4096"
306
+ ```
307
+
308
+ ### Audio Sync Issues
309
+
310
+ **Issue**: Audio out of sync with video
311
+
312
+ **Solutions**:
313
+ - Ensure all audio files are in compatible formats
314
+ - Check narration delay settings
315
+ - Verify video frame rate compatibility
316
+ - Try re-encoding source files
317
+
318
+ ## Technical Details
319
+
320
+ ### FFmpeg Commands
321
+
322
+ The node uses **fluent-ffmpeg** to construct FFmpeg commands:
323
+
324
+ **Audio Mixing**:
325
+ ```bash
326
+ -filter_complex "[0:a]volume=1.0[original];[1:a]volume=0.3,afade=t=in:st=0:d=2[bgm];[original][bgm]amix=inputs=2[mixed]"
327
+ ```
328
+
329
+ **Subtitle Overlay**:
330
+ ```bash
331
+ -vf "ass=subtitles.ass"
332
+ ```
333
+
334
+ **Complete Command**:
335
+ ```bash
336
+ ffmpeg -i video.mp4 -i bgm.mp3 -filter_complex "..." -vf "ass=..." -c:v libx264 -crf 18 output.mp4
337
+ ```
338
+
339
+ ### Subtitle Formats
340
+
341
+ - **SRT**: Simple subtitle format (limited styling)
342
+ - **ASS**: Advanced SubStation Alpha (full styling support) ← **Used by default**
343
+
344
+ ASS format provides:
345
+ - Custom fonts and colors
346
+ - Precise positioning
347
+ - Background colors and opacity
348
+ - Border/outline effects
349
+ - Multiple alignment options
350
+
351
+ ## Performance
352
+
353
+ ### Resource Usage
354
+
355
+ | Video Length | Memory | Processing Time |
356
+ |--------------|--------|-----------------|
357
+ | 1 minute | ~200MB | ~30 seconds |
358
+ | 5 minutes | ~500MB | ~2 minutes |
359
+ | 10 minutes | ~1GB | ~5 minutes |
360
+
361
+ *Times measured on standard VPS (2 CPU, 4GB RAM)*
362
+
363
+ ### Optimization Tips
364
+
365
+ 1. **Use appropriate quality**: High quality for final output, medium for testing
366
+ 2. **Compress BGM**: Use lower bitrate audio files (128-192 kbps)
367
+ 3. **Batch processing**: Process multiple videos in parallel workflows
368
+ 4. **Cache audio files**: Reuse same BGM/narration across multiple videos
369
+
370
+ ## Compatibility
371
+
372
+ ### Supported Video Formats
373
+
374
+ **Input**: MP4, MOV, WebM, AVI, MKV
375
+ **Output**: MP4, MOV, WebM
376
+
377
+ ### Supported Audio Formats
378
+
379
+ **Input**: MP3, WAV, AAC, OGG, M4A
380
+ **Output**: AAC (default)
381
+
382
+ ### Codec Compatibility
383
+
384
+ | Codec | Quality | Speed | Browser Support |
385
+ |-------|---------|-------|-----------------|
386
+ | H.264 (libx264) | Good | Fast | Excellent |
387
+ | H.265 (libx265) | Better | Slower | Limited |
388
+ | VP9 | Good | Slow | Good (WebM) |
389
+
390
+ ## Contributing
391
+
392
+ Contributions are welcome! Please:
393
+
394
+ 1. Fork the repository
395
+ 2. Create a feature branch
396
+ 3. Make your changes
397
+ 4. Add tests if applicable
398
+ 5. Submit a pull request
399
+
400
+ ### Development Guidelines
401
+
402
+ - Follow existing code style (ESLint configured)
403
+ - Add TypeScript types for new features
404
+ - Update documentation for new parameters
405
+ - Test with various video formats
406
+
407
+ ## Roadmap
408
+
409
+ ### v1.1 (Planned)
410
+
411
+ - [ ] Multiple video layers
412
+ - [ ] Video transitions
413
+ - [ ] Logo/watermark overlay
414
+ - [ ] Preset subtitle templates
415
+ - [ ] Batch processing mode
416
+
417
+ ### v2.0 (Future)
418
+
419
+ - [ ] GPU acceleration (NVENC, VideoToolbox)
420
+ - [ ] Cloud storage integration
421
+ - [ ] Progress callbacks
422
+ - [ ] Advanced video effects
423
+
424
+ ## Credits
425
+
426
+ - Inspired by [n8n-nodes-mediafx](https://github.com/dandacompany/n8n-nodes-mediafx)
427
+ - Uses [FFmpeg](https://ffmpeg.org/) for video processing
428
+ - Built with [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg)
429
+
430
+ ## License
431
+
432
+ [MIT](LICENSE)
433
+
434
+ ## Support
435
+
436
+ - **Issues**: [GitHub Issues](https://github.com/choisb87/sb-render/issues)
437
+ - **Documentation**: [GitHub Wiki](https://github.com/choisb87/sb-render/wiki)
438
+ - **n8n Community**: [n8n Community Forum](https://community.n8n.io/)
439
+
440
+ ---
441
+
442
+ **Made with ❤️ for the n8n community**
@@ -1,7 +1,7 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
- <rect width="100" height="100" fill="#4A90E2" rx="15"/>
3
- <path d="M30 35 L30 65 L60 50 Z" fill="white"/>
4
- <rect x="65" y="60" width="15" height="8" fill="white" rx="2"/>
5
- <rect x="65" y="45" width="15" height="8" fill="white" rx="2"/>
6
- <rect x="65" y="30" width="15" height="8" fill="white" rx="2"/>
7
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <rect width="100" height="100" fill="#4A90E2" rx="15"/>
3
+ <path d="M30 35 L30 65 L60 50 Z" fill="white"/>
4
+ <rect x="65" y="60" width="15" height="8" fill="white" rx="2"/>
5
+ <rect x="65" y="45" width="15" height="8" fill="white" rx="2"/>
6
+ <rect x="65" y="30" width="15" height="8" fill="white" rx="2"/>
7
+ </svg>
@@ -65,12 +65,12 @@ class SubtitleEngine {
65
65
  * Generate ASS file header
66
66
  */
67
67
  generateASSHeader(videoWidth, videoHeight) {
68
- return `[Script Info]
69
- Title: sb-render subtitles
70
- ScriptType: v4.00+
71
- WrapStyle: 0
72
- PlayResX: ${videoWidth}
73
- PlayResY: ${videoHeight}
68
+ return `[Script Info]
69
+ Title: sb-render subtitles
70
+ ScriptType: v4.00+
71
+ WrapStyle: 0
72
+ PlayResX: ${videoWidth}
73
+ PlayResY: ${videoHeight}
74
74
  ScaledBorderAndShadow: yes`;
75
75
  }
76
76
  /**
@@ -86,7 +86,7 @@ ScaledBorderAndShadow: yes`;
86
86
  * Generate ASS styles section
87
87
  */
88
88
  generateASSStyles(subtitles) {
89
- const stylesHeader = `[V4+ Styles]
89
+ const stylesHeader = `[V4+ Styles]
90
90
  Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding`;
91
91
  const uniqueStyles = new Map();
92
92
  const fontName = this.getFontName();
@@ -145,7 +145,7 @@ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour,
145
145
  * Generate ASS events section
146
146
  */
147
147
  generateASSEvents(subtitles, videoWidth, videoHeight) {
148
- const eventsHeader = `[Events]
148
+ const eventsHeader = `[Events]
149
149
  Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text`;
150
150
  const eventLines = [];
151
151
  subtitles.forEach((subtitle, index) => {
@@ -263,7 +263,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text`
263
263
  // Default configuration for SRT-based subtitles
264
264
  const config = {
265
265
  position: 'bottom',
266
- fontSize: 70,
266
+ fontSize: 90,
267
267
  fontColor: '#FFFFFF',
268
268
  fontFamily: 'NanumGothic',
269
269
  alignment: 'center',
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
- // Entry point for n8n community node
2
- // This file is generated by the TypeScript compiler
3
- module.exports = require('./dist/nodes/SbRender/SbRender.node');
1
+ // Entry point for n8n community node
2
+ // This file is generated by the TypeScript compiler
3
+ module.exports = require('./dist/nodes/SbRender/SbRender.node');
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
- {
2
- "name": "n8n-nodes-sb-render",
3
- "version": "1.1.4",
4
- "description": "n8n community node for video rendering with customizable subtitles, BGM, and narration",
5
- "keywords": [
6
- "n8n-community-node-package",
7
- "n8n",
8
- "video",
9
- "ffmpeg",
10
- "subtitle",
11
- "rendering",
12
- "video-composition"
13
- ],
14
- "license": "MIT",
15
- "homepage": "https://github.com/choisb87/sb-render",
16
- "author": {
17
- "name": "choisb87",
18
- "email": "choisb87@gmail.com"
19
- },
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/choisb87/sb-render.git"
23
- },
24
- "main": "index.js",
25
- "scripts": {
26
- "build": "tsc && gulp build:icons",
27
- "dev": "tsc --watch",
28
- "format": "prettier nodes --write",
29
- "lint": "eslint nodes --ext .ts",
30
- "lintfix": "eslint nodes --ext .ts --fix",
31
- "prepublishOnly": "npm run build && npm run lint",
32
- "postinstall": "node scripts/fix-ffprobe-permissions.js"
33
- },
34
- "files": [
35
- "dist",
36
- "fonts",
37
- "scripts"
38
- ],
39
- "n8n": {
40
- "n8nNodesApiVersion": 1,
41
- "credentials": [],
42
- "nodes": [
43
- "dist/nodes/SbRender/SbRender.node.js"
44
- ]
45
- },
46
- "devDependencies": {
47
- "@types/fluent-ffmpeg": "^2.1.21",
48
- "@types/node": "^18.16.0",
49
- "@types/node-fetch": "^2.6.13",
50
- "@typescript-eslint/eslint-plugin": "^5.59.0",
51
- "@typescript-eslint/parser": "^5.59.0",
52
- "eslint": "^8.40.0",
53
- "eslint-plugin-n8n-nodes-base": "^1.11.0",
54
- "gulp": "^4.0.2",
55
- "n8n-workflow": "^1.0.0",
56
- "prettier": "^2.8.8",
57
- "ts-node": "^10.9.2",
58
- "typescript": "^5.0.4"
59
- },
60
- "dependencies": {
61
- "@ffmpeg-installer/ffmpeg": "^1.1.0",
62
- "@ffprobe-installer/ffprobe": "^2.1.2",
63
- "fluent-ffmpeg": "^2.1.2",
64
- "node-fetch": "^2.6.12",
65
- "tmp-promise": "^3.0.3"
66
- },
67
- "peerDependencies": {
68
- "n8n-workflow": "*"
69
- },
70
- "engines": {
71
- "node": ">=16.0.0"
72
- }
73
- }
1
+ {
2
+ "name": "n8n-nodes-sb-render",
3
+ "version": "1.1.5",
4
+ "description": "n8n community node for video rendering with customizable subtitles, BGM, and narration",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "n8n",
8
+ "video",
9
+ "ffmpeg",
10
+ "subtitle",
11
+ "rendering",
12
+ "video-composition"
13
+ ],
14
+ "license": "MIT",
15
+ "homepage": "https://github.com/choisb87/sb-render",
16
+ "author": {
17
+ "name": "choisb87",
18
+ "email": "choisb87@gmail.com"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/choisb87/sb-render.git"
23
+ },
24
+ "main": "index.js",
25
+ "scripts": {
26
+ "build": "tsc && gulp build:icons",
27
+ "dev": "tsc --watch",
28
+ "format": "prettier nodes --write",
29
+ "lint": "eslint nodes --ext .ts",
30
+ "lintfix": "eslint nodes --ext .ts --fix",
31
+ "prepublishOnly": "npm run build && npm run lint",
32
+ "postinstall": "node scripts/fix-ffprobe-permissions.js"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "fonts",
37
+ "scripts"
38
+ ],
39
+ "n8n": {
40
+ "n8nNodesApiVersion": 1,
41
+ "credentials": [],
42
+ "nodes": [
43
+ "dist/nodes/SbRender/SbRender.node.js"
44
+ ]
45
+ },
46
+ "devDependencies": {
47
+ "@types/fluent-ffmpeg": "^2.1.21",
48
+ "@types/node": "^18.16.0",
49
+ "@types/node-fetch": "^2.6.13",
50
+ "@typescript-eslint/eslint-plugin": "^5.59.0",
51
+ "@typescript-eslint/parser": "^5.59.0",
52
+ "eslint": "^8.40.0",
53
+ "eslint-plugin-n8n-nodes-base": "^1.11.0",
54
+ "gulp": "^4.0.2",
55
+ "n8n-workflow": "^1.0.0",
56
+ "prettier": "^2.8.8",
57
+ "ts-node": "^10.9.2",
58
+ "typescript": "^5.0.4"
59
+ },
60
+ "dependencies": {
61
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
62
+ "@ffprobe-installer/ffprobe": "^2.1.2",
63
+ "fluent-ffmpeg": "^2.1.2",
64
+ "node-fetch": "^2.6.12",
65
+ "tmp-promise": "^3.0.3"
66
+ },
67
+ "peerDependencies": {
68
+ "n8n-workflow": "*"
69
+ },
70
+ "engines": {
71
+ "node": ">=16.0.0"
72
+ }
73
+ }
@@ -1,52 +1,52 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Automatically set execute permissions on ffprobe binary after npm install
5
- * This is required for @ffprobe-installer package to work correctly
6
- */
7
-
8
- const { chmodSync, existsSync } = require('fs');
9
- const { resolve, join } = require('path');
10
-
11
- // Try multiple possible paths where ffprobe might be installed
12
- const possiblePaths = [
13
- // Direct dependency path
14
- resolve(__dirname, '../node_modules/@ffprobe-installer/linux-x64/ffprobe'),
15
- // Nested dependency path (when installed as part of another package)
16
- resolve(__dirname, '../node_modules/@ffprobe-installer/ffprobe/node_modules/@ffprobe-installer/linux-x64/ffprobe'),
17
- // Platform-agnostic approach - check all platform binaries
18
- resolve(__dirname, '../node_modules/@ffprobe-installer/darwin-x64/ffprobe'),
19
- resolve(__dirname, '../node_modules/@ffprobe-installer/darwin-arm64/ffprobe'),
20
- resolve(__dirname, '../node_modules/@ffprobe-installer/win32-x64/ffprobe.exe'),
21
- ];
22
-
23
- let successCount = 0;
24
- let errorCount = 0;
25
-
26
- console.log('🔧 Fixing ffprobe permissions...');
27
-
28
- for (const ffprobePath of possiblePaths) {
29
- if (existsSync(ffprobePath)) {
30
- try {
31
- // Set execute permissions (755 = rwxr-xr-x)
32
- chmodSync(ffprobePath, 0o755);
33
- console.log(` ✅ Set permissions: ${ffprobePath}`);
34
- successCount++;
35
- } catch (error) {
36
- console.warn(` ⚠️ Could not set permissions for ${ffprobePath}:`, error.message);
37
- errorCount++;
38
- }
39
- }
40
- }
41
-
42
- if (successCount === 0 && errorCount === 0) {
43
- console.warn('⚠️ No ffprobe binaries found. This is normal if @ffprobe-installer is not yet installed.');
44
- console.warn(' If you encounter "EACCES" errors when running SB Render, try:');
45
- console.warn(' chmod +x node_modules/@ffprobe-installer/linux-x64/ffprobe');
46
- } else if (successCount > 0) {
47
- console.log(`✅ Successfully set permissions on ${successCount} ffprobe binary(ies)`);
48
- } else {
49
- console.error('❌ Failed to set permissions on any ffprobe binaries');
50
- console.error(' You may need to manually run:');
51
- console.error(' chmod +x node_modules/@ffprobe-installer/linux-x64/ffprobe');
52
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Automatically set execute permissions on ffprobe binary after npm install
5
+ * This is required for @ffprobe-installer package to work correctly
6
+ */
7
+
8
+ const { chmodSync, existsSync } = require('fs');
9
+ const { resolve, join } = require('path');
10
+
11
+ // Try multiple possible paths where ffprobe might be installed
12
+ const possiblePaths = [
13
+ // Direct dependency path
14
+ resolve(__dirname, '../node_modules/@ffprobe-installer/linux-x64/ffprobe'),
15
+ // Nested dependency path (when installed as part of another package)
16
+ resolve(__dirname, '../node_modules/@ffprobe-installer/ffprobe/node_modules/@ffprobe-installer/linux-x64/ffprobe'),
17
+ // Platform-agnostic approach - check all platform binaries
18
+ resolve(__dirname, '../node_modules/@ffprobe-installer/darwin-x64/ffprobe'),
19
+ resolve(__dirname, '../node_modules/@ffprobe-installer/darwin-arm64/ffprobe'),
20
+ resolve(__dirname, '../node_modules/@ffprobe-installer/win32-x64/ffprobe.exe'),
21
+ ];
22
+
23
+ let successCount = 0;
24
+ let errorCount = 0;
25
+
26
+ console.log('🔧 Fixing ffprobe permissions...');
27
+
28
+ for (const ffprobePath of possiblePaths) {
29
+ if (existsSync(ffprobePath)) {
30
+ try {
31
+ // Set execute permissions (755 = rwxr-xr-x)
32
+ chmodSync(ffprobePath, 0o755);
33
+ console.log(` ✅ Set permissions: ${ffprobePath}`);
34
+ successCount++;
35
+ } catch (error) {
36
+ console.warn(` ⚠️ Could not set permissions for ${ffprobePath}:`, error.message);
37
+ errorCount++;
38
+ }
39
+ }
40
+ }
41
+
42
+ if (successCount === 0 && errorCount === 0) {
43
+ console.warn('⚠️ No ffprobe binaries found. This is normal if @ffprobe-installer is not yet installed.');
44
+ console.warn(' If you encounter "EACCES" errors when running SB Render, try:');
45
+ console.warn(' chmod +x node_modules/@ffprobe-installer/linux-x64/ffprobe');
46
+ } else if (successCount > 0) {
47
+ console.log(`✅ Successfully set permissions on ${successCount} ffprobe binary(ies)`);
48
+ } else {
49
+ console.error('❌ Failed to set permissions on any ffprobe binaries');
50
+ console.error(' You may need to manually run:');
51
+ console.error(' chmod +x node_modules/@ffprobe-installer/linux-x64/ffprobe');
52
+ }