@lee-jisoo/n8n-nodes-mediafx 1.6.0 → 1.6.2
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/dist/fonts/DejaVuSans.ttf +0 -0
- package/dist/fonts/Inter-Regular.ttf +0 -0
- package/dist/fonts/NanumGothic-Regular.ttf +0 -0
- package/dist/fonts/NotoSansKR-Regular.ttf +0 -0
- package/dist/fonts/Pretendard-Regular.otf +0 -0
- package/dist/fonts/Roboto-Regular.ttf +0 -0
- package/dist/nodes/MediaFX/MediaFX.node.d.ts +12 -0
- package/dist/nodes/MediaFX/MediaFX.node.js +559 -0
- package/dist/nodes/MediaFX/mediafx.png +0 -0
- package/dist/nodes/MediaFX/operations/addSubtitle.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/addSubtitle.js +202 -0
- package/dist/nodes/MediaFX/operations/addText.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/addText.js +108 -0
- package/dist/nodes/MediaFX/operations/extractAudio.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/extractAudio.js +57 -0
- package/dist/nodes/MediaFX/operations/imageToVideo.d.ts +5 -0
- package/dist/nodes/MediaFX/operations/imageToVideo.js +65 -0
- package/dist/nodes/MediaFX/operations/index.d.ts +13 -0
- package/dist/nodes/MediaFX/operations/index.js +29 -0
- package/dist/nodes/MediaFX/operations/merge.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/merge.js +121 -0
- package/dist/nodes/MediaFX/operations/mixAudio.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/mixAudio.js +141 -0
- package/dist/nodes/MediaFX/operations/multiVideoTransition.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/multiVideoTransition.js +245 -0
- package/dist/nodes/MediaFX/operations/overlayVideo.d.ts +16 -0
- package/dist/nodes/MediaFX/operations/overlayVideo.js +240 -0
- package/dist/nodes/MediaFX/operations/separateAudio.d.ts +17 -0
- package/dist/nodes/MediaFX/operations/separateAudio.js +78 -0
- package/dist/nodes/MediaFX/operations/singleVideoFade.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/singleVideoFade.js +60 -0
- package/dist/nodes/MediaFX/operations/speed.d.ts +12 -0
- package/dist/nodes/MediaFX/operations/speed.js +110 -0
- package/dist/nodes/MediaFX/operations/stampImage.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/stampImage.js +146 -0
- package/dist/nodes/MediaFX/operations/trim.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/trim.js +49 -0
- package/dist/nodes/MediaFX/properties/audio.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/audio.properties.js +394 -0
- package/dist/nodes/MediaFX/properties/font.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/font.properties.js +186 -0
- package/dist/nodes/MediaFX/properties/image.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/image.properties.js +333 -0
- package/dist/nodes/MediaFX/properties/resources.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/resources.properties.js +34 -0
- package/dist/nodes/MediaFX/properties/subtitle.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/subtitle.properties.js +361 -0
- package/dist/nodes/MediaFX/properties/video.properties.d.ts +2 -0
- package/dist/nodes/MediaFX/properties/video.properties.js +1135 -0
- package/dist/nodes/MediaFX/utils/ffmpegVersion.d.ts +14 -0
- package/dist/nodes/MediaFX/utils/ffmpegVersion.js +97 -0
- package/dist/nodes/MediaFX/utils.d.ts +43 -0
- package/dist/nodes/MediaFX/utils.js +410 -0
- package/package.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.executeAddSubtitle = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const fs = __importStar(require("fs-extra"));
|
|
30
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
31
|
+
const utils_1 = require("../utils");
|
|
32
|
+
/**
|
|
33
|
+
* Escape special characters in file path for FFmpeg subtitles filter.
|
|
34
|
+
*/
|
|
35
|
+
function escapeSubtitlePath(filePath) {
|
|
36
|
+
return filePath
|
|
37
|
+
.replace(/\\/g, '\\\\\\\\')
|
|
38
|
+
.replace(/:/g, '\\:')
|
|
39
|
+
.replace(/\[/g, '\\[')
|
|
40
|
+
.replace(/\]/g, '\\]')
|
|
41
|
+
.replace(/'/g, "\\'");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert color to ASS format (&HAABBGGRR)
|
|
45
|
+
* @param color - CSS color (hex #RRGGBB or named color)
|
|
46
|
+
* @param opacity - Opacity 0-1 (0=transparent, 1=solid)
|
|
47
|
+
*/
|
|
48
|
+
function colorToASS(color, opacity = 1) {
|
|
49
|
+
let r = 'FF', g = 'FF', b = 'FF';
|
|
50
|
+
if (color && color.startsWith('#') && color.length === 7) {
|
|
51
|
+
r = color.substring(1, 3);
|
|
52
|
+
g = color.substring(3, 5);
|
|
53
|
+
b = color.substring(5, 7);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const namedColors = {
|
|
57
|
+
white: 'FFFFFF',
|
|
58
|
+
black: '000000',
|
|
59
|
+
red: 'FF0000',
|
|
60
|
+
green: '00FF00',
|
|
61
|
+
blue: '0000FF',
|
|
62
|
+
yellow: 'FFFF00',
|
|
63
|
+
cyan: '00FFFF',
|
|
64
|
+
magenta: 'FF00FF',
|
|
65
|
+
orange: 'FFA500',
|
|
66
|
+
};
|
|
67
|
+
const hex = namedColors[color.toLowerCase()] || 'FFFFFF';
|
|
68
|
+
r = hex.substring(0, 2);
|
|
69
|
+
g = hex.substring(2, 4);
|
|
70
|
+
b = hex.substring(4, 6);
|
|
71
|
+
}
|
|
72
|
+
// ASS format: &HAABBGGRR (AA = alpha, 00=solid, FF=transparent)
|
|
73
|
+
const alpha = Math.round((1 - opacity) * 255).toString(16).padStart(2, '0').toUpperCase();
|
|
74
|
+
return `&H${alpha}${b}${g}${r}`.toUpperCase();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get ASS alignment number (numpad style)
|
|
78
|
+
*/
|
|
79
|
+
function getASSAlignment(horizontalAlign, verticalAlign) {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
const alignMap = {
|
|
82
|
+
top: { left: 7, center: 8, right: 9 },
|
|
83
|
+
middle: { left: 4, center: 5, right: 6 },
|
|
84
|
+
bottom: { left: 1, center: 2, right: 3 },
|
|
85
|
+
};
|
|
86
|
+
return (_b = (_a = alignMap[verticalAlign]) === null || _a === void 0 ? void 0 : _a[horizontalAlign]) !== null && _b !== void 0 ? _b : 2;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse SRT file and convert to ASS format
|
|
90
|
+
*/
|
|
91
|
+
function convertSRTtoASS(srtContent, fontName, fontSize, primaryColor, outlineColor, outlineWidth, backgroundColor, backgroundOpacity, enableBackground, alignment, marginV) {
|
|
92
|
+
// ASS Header
|
|
93
|
+
const borderStyle = enableBackground ? 4 : 1; // 4=box, 1=outline+shadow
|
|
94
|
+
const backColor = colorToASS(backgroundColor, backgroundOpacity);
|
|
95
|
+
const assHeader = `[Script Info]
|
|
96
|
+
Title: Converted from SRT
|
|
97
|
+
ScriptType: v4.00+
|
|
98
|
+
PlayResX: 1920
|
|
99
|
+
PlayResY: 1080
|
|
100
|
+
WrapStyle: 0
|
|
101
|
+
|
|
102
|
+
[V4+ Styles]
|
|
103
|
+
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
104
|
+
Style: Default,${fontName},${fontSize},${primaryColor},${primaryColor},${outlineColor},${backColor},0,0,0,0,100,100,0,0,${borderStyle},${outlineWidth},0,${alignment},20,20,${marginV},1
|
|
105
|
+
|
|
106
|
+
[Events]
|
|
107
|
+
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
108
|
+
`;
|
|
109
|
+
// Parse SRT content
|
|
110
|
+
const srtBlocks = srtContent.trim().split(/\n\s*\n/);
|
|
111
|
+
const dialogueLines = [];
|
|
112
|
+
for (const block of srtBlocks) {
|
|
113
|
+
const lines = block.trim().split('\n');
|
|
114
|
+
if (lines.length < 3)
|
|
115
|
+
continue;
|
|
116
|
+
// Parse timestamp line (format: 00:00:00,000 --> 00:00:00,000)
|
|
117
|
+
const timestampLine = lines[1];
|
|
118
|
+
const timestampMatch = timestampLine.match(/(\d{2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[,.](\d{3})/);
|
|
119
|
+
if (!timestampMatch)
|
|
120
|
+
continue;
|
|
121
|
+
const startTime = `${timestampMatch[1]}:${timestampMatch[2]}:${timestampMatch[3]}.${timestampMatch[4].substring(0, 2)}`;
|
|
122
|
+
const endTime = `${timestampMatch[5]}:${timestampMatch[6]}:${timestampMatch[7]}.${timestampMatch[8].substring(0, 2)}`;
|
|
123
|
+
// Get text (lines 3+)
|
|
124
|
+
const text = lines.slice(2).join('\\N').replace(/\r/g, '');
|
|
125
|
+
dialogueLines.push(`Dialogue: 0,${startTime},${endTime},Default,,0,0,0,,${text}`);
|
|
126
|
+
}
|
|
127
|
+
return assHeader + dialogueLines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if file is ASS/SSA format
|
|
131
|
+
*/
|
|
132
|
+
function isASSFile(filePath) {
|
|
133
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
134
|
+
return ext === '.ass' || ext === '.ssa';
|
|
135
|
+
}
|
|
136
|
+
async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
137
|
+
var _a, _b, _c, _d;
|
|
138
|
+
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
139
|
+
// 1. Get Font
|
|
140
|
+
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
141
|
+
const fontKey = style.fontKey || 'noto-sans-kr';
|
|
142
|
+
const font = allFonts[fontKey];
|
|
143
|
+
if (!font || !font.path) {
|
|
144
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font key '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
|
|
145
|
+
}
|
|
146
|
+
const fontName = font.name || 'Sans';
|
|
147
|
+
const fontSize = style.size || 48;
|
|
148
|
+
const fontColor = style.color || 'white';
|
|
149
|
+
const outlineWidth = (_a = style.outlineWidth) !== null && _a !== void 0 ? _a : 1;
|
|
150
|
+
const outlineColor = style.outlineColor || 'black';
|
|
151
|
+
const enableBackground = (_b = style.enableBackground) !== null && _b !== void 0 ? _b : false;
|
|
152
|
+
const backgroundColor = style.backgroundColor || 'black';
|
|
153
|
+
const backgroundOpacity = (_c = style.backgroundOpacity) !== null && _c !== void 0 ? _c : 0.5;
|
|
154
|
+
// 2. Get alignment
|
|
155
|
+
const positionType = style.positionType || 'alignment';
|
|
156
|
+
let horizontalAlign = 'center';
|
|
157
|
+
let verticalAlign = 'bottom';
|
|
158
|
+
let marginV = 20;
|
|
159
|
+
if (positionType === 'alignment') {
|
|
160
|
+
horizontalAlign = style.horizontalAlign || 'center';
|
|
161
|
+
verticalAlign = style.verticalAlign || 'bottom';
|
|
162
|
+
marginV = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
|
|
163
|
+
}
|
|
164
|
+
const alignment = getASSAlignment(horizontalAlign, verticalAlign);
|
|
165
|
+
// 3. Prepare subtitle file
|
|
166
|
+
let finalSubtitlePath = subtitleFile;
|
|
167
|
+
let tempAssFile = null;
|
|
168
|
+
if (isASSFile(subtitleFile)) {
|
|
169
|
+
// ASS file: use directly (ignore style options)
|
|
170
|
+
finalSubtitlePath = subtitleFile;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// SRT file: convert to ASS with styles
|
|
174
|
+
const srtContent = await fs.readFile(subtitleFile, 'utf-8');
|
|
175
|
+
const assContent = convertSRTtoASS(srtContent, fontName, fontSize, colorToASS(fontColor, 1), colorToASS(outlineColor, 1), outlineWidth, backgroundColor, backgroundOpacity, enableBackground, alignment, marginV);
|
|
176
|
+
tempAssFile = (0, utils_1.getTempFile)('.ass');
|
|
177
|
+
await fs.writeFile(tempAssFile, assContent, 'utf-8');
|
|
178
|
+
finalSubtitlePath = tempAssFile;
|
|
179
|
+
}
|
|
180
|
+
// 4. Build FFmpeg command
|
|
181
|
+
const escapedPath = escapeSubtitlePath(finalSubtitlePath);
|
|
182
|
+
const subtitlesFilter = `subtitles='${escapedPath}'`;
|
|
183
|
+
const command = ffmpeg(video)
|
|
184
|
+
.videoFilters([subtitlesFilter])
|
|
185
|
+
.audioCodec('copy')
|
|
186
|
+
.save(outputPath);
|
|
187
|
+
try {
|
|
188
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
189
|
+
return outputPath;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await fs.remove(outputPath).catch(() => { });
|
|
193
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
// Clean up temp ASS file
|
|
197
|
+
if (tempAssFile) {
|
|
198
|
+
await fs.remove(tempAssFile).catch(() => { });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.executeAddSubtitle = executeAddSubtitle;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.executeAddText = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const path = __importStar(require("path"));
|
|
29
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
30
|
+
const utils_1 = require("../utils");
|
|
31
|
+
const fs = __importStar(require("fs-extra"));
|
|
32
|
+
function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY) {
|
|
33
|
+
let x;
|
|
34
|
+
let y;
|
|
35
|
+
// Set X position based on horizontal alignment
|
|
36
|
+
switch (horizontalAlign) {
|
|
37
|
+
case 'left':
|
|
38
|
+
x = `${paddingX}`;
|
|
39
|
+
break;
|
|
40
|
+
case 'right':
|
|
41
|
+
x = `w-text_w-${paddingX}`;
|
|
42
|
+
break;
|
|
43
|
+
case 'center':
|
|
44
|
+
default:
|
|
45
|
+
x = '(w-text_w)/2';
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
// Set Y position based on vertical alignment
|
|
49
|
+
switch (verticalAlign) {
|
|
50
|
+
case 'top':
|
|
51
|
+
y = `${paddingY}`;
|
|
52
|
+
break;
|
|
53
|
+
case 'bottom':
|
|
54
|
+
y = `h-th-${paddingY}`;
|
|
55
|
+
break;
|
|
56
|
+
case 'middle':
|
|
57
|
+
default:
|
|
58
|
+
y = '(h-text_h)/2';
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return { x, y };
|
|
62
|
+
}
|
|
63
|
+
async function executeAddText(video, text, options, itemIndex) {
|
|
64
|
+
var _a, _b, _c, _d;
|
|
65
|
+
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
66
|
+
const fontKey = options.fontKey || 'noto-sans-kr';
|
|
67
|
+
const font = allFonts[fontKey];
|
|
68
|
+
if (!font || !font.path) {
|
|
69
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font key '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
|
|
70
|
+
}
|
|
71
|
+
const fontPath = font.path;
|
|
72
|
+
// Set default values for text options
|
|
73
|
+
const fontSize = options.size || 48;
|
|
74
|
+
const fontColor = options.color || 'white';
|
|
75
|
+
const startTime = options.startTime || 0;
|
|
76
|
+
const endTime = options.endTime || 5;
|
|
77
|
+
// Handle position based on position type
|
|
78
|
+
let positionX;
|
|
79
|
+
let positionY;
|
|
80
|
+
const positionType = options.positionType || 'alignment';
|
|
81
|
+
if (positionType === 'alignment') {
|
|
82
|
+
const horizontalAlign = options.horizontalAlign || 'center';
|
|
83
|
+
const verticalAlign = options.verticalAlign || 'bottom';
|
|
84
|
+
const paddingX = (_b = (_a = options.paddingX) !== null && _a !== void 0 ? _a : options.padding) !== null && _b !== void 0 ? _b : 20;
|
|
85
|
+
const paddingY = (_d = (_c = options.paddingY) !== null && _c !== void 0 ? _c : options.padding) !== null && _d !== void 0 ? _d : 20;
|
|
86
|
+
const position = getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY);
|
|
87
|
+
positionX = position.x;
|
|
88
|
+
positionY = position.y;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Custom position
|
|
92
|
+
positionX = options.x || '(w-text_w)/2';
|
|
93
|
+
positionY = options.y || 'h-th-10';
|
|
94
|
+
}
|
|
95
|
+
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
96
|
+
const drawtext = `drawtext=fontfile=${fontPath}:text='${text.replace(/'/g, `''`)}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}:enable='between(t,${startTime},${endTime})'`;
|
|
97
|
+
const command = ffmpeg(video).videoFilters(drawtext).audioCodec('copy').save(outputPath);
|
|
98
|
+
try {
|
|
99
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
100
|
+
return outputPath;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Clean up output file if creation failed
|
|
104
|
+
await fs.remove(outputPath).catch(() => { });
|
|
105
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding text to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.executeAddText = executeAddText;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.executeExtractAudio = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
29
|
+
const utils_1 = require("../utils");
|
|
30
|
+
const fs = __importStar(require("fs-extra"));
|
|
31
|
+
async function executeExtractAudio(input, format, codec, bitrate, itemIndex) {
|
|
32
|
+
const outputPath = (0, utils_1.getTempFile)(`.${format}`);
|
|
33
|
+
let finalCodec = codec;
|
|
34
|
+
// If stream copy ('copy') is selected for a format that requires a specific
|
|
35
|
+
// codec (like mp3), we must override it to prevent an FFmpeg error.
|
|
36
|
+
if (finalCodec === 'copy' && format === 'mp3') {
|
|
37
|
+
// Cannot stream copy a non-mp3 stream (e.g., aac) into an mp3 container.
|
|
38
|
+
// Default to a high-quality mp3 encoder.
|
|
39
|
+
finalCodec = 'libmp3lame';
|
|
40
|
+
}
|
|
41
|
+
const command = ffmpeg(input)
|
|
42
|
+
.output(outputPath)
|
|
43
|
+
.noVideo()
|
|
44
|
+
.audioCodec(finalCodec)
|
|
45
|
+
.audioBitrate(bitrate)
|
|
46
|
+
.outputOptions('-map', '0:a:0'); // Select first audio stream
|
|
47
|
+
try {
|
|
48
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
49
|
+
return outputPath;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// Clean up output file if creation failed
|
|
53
|
+
await fs.remove(outputPath).catch(() => { });
|
|
54
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error extracting audio. Please ensure the source video contains an audio track. FFmpeg error: ${error.message}`, { itemIndex });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.executeExtractAudio = executeExtractAudio;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.executeImageToVideo = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const utils_1 = require("../utils");
|
|
29
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
30
|
+
const fs = __importStar(require("fs-extra"));
|
|
31
|
+
async function executeImageToVideo(imagePath, duration, videoSize, outputFormat, itemIndex) {
|
|
32
|
+
const outputPath = (0, utils_1.getTempFile)(`.${outputFormat}`);
|
|
33
|
+
const command = ffmpeg()
|
|
34
|
+
.input(imagePath)
|
|
35
|
+
.loop(duration)
|
|
36
|
+
// Add a silent audio track for compatibility
|
|
37
|
+
.input('anullsrc')
|
|
38
|
+
.inputFormat('lavfi')
|
|
39
|
+
// Use libx264 codec for broad compatibility
|
|
40
|
+
.videoCodec('libx264')
|
|
41
|
+
// Set pixel format
|
|
42
|
+
.outputOptions('-pix_fmt', 'yuv420p')
|
|
43
|
+
.audioCodec('aac')
|
|
44
|
+
.outputOptions('-shortest'); // End when the shortest input ends.
|
|
45
|
+
if (videoSize.width && videoSize.height) {
|
|
46
|
+
// Many codecs require even dimensions, but we'll let the user decide.
|
|
47
|
+
// For more robustness, we could add validation or rounding logic here.
|
|
48
|
+
command.videoFilters(`scale=${videoSize.width}:${videoSize.height}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Default behavior: ensure even dimensions for compatibility
|
|
52
|
+
command.videoFilters('scale=trunc(iw/2)*2:trunc(ih/2)*2');
|
|
53
|
+
}
|
|
54
|
+
command.save(outputPath);
|
|
55
|
+
try {
|
|
56
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
57
|
+
return outputPath;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Clean up output file if creation failed
|
|
61
|
+
await fs.remove(outputPath).catch(() => { });
|
|
62
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error converting image to video. Please ensure the source image is valid. FFmpeg error: ${error.message}`, { itemIndex });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.executeImageToVideo = executeImageToVideo;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './merge';
|
|
2
|
+
export * from './trim';
|
|
3
|
+
export * from './speed';
|
|
4
|
+
export * from './mixAudio';
|
|
5
|
+
export * from './addSubtitle';
|
|
6
|
+
export * from './addText';
|
|
7
|
+
export * from './extractAudio';
|
|
8
|
+
export * from './separateAudio';
|
|
9
|
+
export * from './multiVideoTransition';
|
|
10
|
+
export * from './singleVideoFade';
|
|
11
|
+
export * from './imageToVideo';
|
|
12
|
+
export * from './stampImage';
|
|
13
|
+
export * from './overlayVideo';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./merge"), exports);
|
|
18
|
+
__exportStar(require("./trim"), exports);
|
|
19
|
+
__exportStar(require("./speed"), exports);
|
|
20
|
+
__exportStar(require("./mixAudio"), exports);
|
|
21
|
+
__exportStar(require("./addSubtitle"), exports);
|
|
22
|
+
__exportStar(require("./addText"), exports);
|
|
23
|
+
__exportStar(require("./extractAudio"), exports);
|
|
24
|
+
__exportStar(require("./separateAudio"), exports);
|
|
25
|
+
__exportStar(require("./multiVideoTransition"), exports);
|
|
26
|
+
__exportStar(require("./singleVideoFade"), exports);
|
|
27
|
+
__exportStar(require("./imageToVideo"), exports);
|
|
28
|
+
__exportStar(require("./stampImage"), exports);
|
|
29
|
+
__exportStar(require("./overlayVideo"), exports);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.executeMerge = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const ffmpeg = require("fluent-ffmpeg");
|
|
29
|
+
const fs = __importStar(require("fs-extra"));
|
|
30
|
+
const utils_1 = require("../utils");
|
|
31
|
+
async function normalizeVideo(inputPath, refInfo) {
|
|
32
|
+
const normalizedPath = (0, utils_1.getTempFile)('.ts');
|
|
33
|
+
const mainCleanup = () => fs.remove(normalizedPath);
|
|
34
|
+
let silentAudioCleanup = null;
|
|
35
|
+
const hasAudio = await (0, utils_1.fileHasAudio)(inputPath);
|
|
36
|
+
const command = ffmpeg(inputPath);
|
|
37
|
+
if (!hasAudio) {
|
|
38
|
+
const duration = await (0, utils_1.getDuration)(inputPath);
|
|
39
|
+
const { filePath: silentAudioPath, cleanup } = await (0, utils_1.createSilentAudio)(duration);
|
|
40
|
+
command.addInput(silentAudioPath);
|
|
41
|
+
silentAudioCleanup = cleanup;
|
|
42
|
+
}
|
|
43
|
+
const targetWidth = refInfo.width;
|
|
44
|
+
const targetHeight = refInfo.height;
|
|
45
|
+
const targetSar = (refInfo.sample_aspect_ratio && refInfo.sample_aspect_ratio !== 'N/A') ? refInfo.sample_aspect_ratio : '1:1';
|
|
46
|
+
const targetFrameRate = refInfo.r_frame_rate || '30';
|
|
47
|
+
const targetPixFmt = 'yuv420p';
|
|
48
|
+
const videoFilter = `[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:-1:-1:color=black,setsar=${targetSar},format=${targetPixFmt},fps=${targetFrameRate},setpts=PTS-STARTPTS[v_out]`;
|
|
49
|
+
const audioFilter = hasAudio
|
|
50
|
+
? `[0:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,asetpts=PTS-STARTPTS[a_out]`
|
|
51
|
+
: `[1:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,asetpts=PTS-STARTPTS[a_out]`;
|
|
52
|
+
command
|
|
53
|
+
.complexFilter([videoFilter, audioFilter])
|
|
54
|
+
.outputOptions(['-map', '[v_out]', '-map', '[a_out]'])
|
|
55
|
+
.videoCodec('libx264')
|
|
56
|
+
.audioCodec('aac')
|
|
57
|
+
.save(normalizedPath);
|
|
58
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
59
|
+
const combinedCleanup = async () => {
|
|
60
|
+
await mainCleanup();
|
|
61
|
+
if (silentAudioCleanup) {
|
|
62
|
+
await silentAudioCleanup();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return { normalizedPath, cleanup: combinedCleanup };
|
|
66
|
+
}
|
|
67
|
+
async function executeMerge(inputs, outputFormat, itemIndex) {
|
|
68
|
+
// Verify FFmpeg is available before proceeding
|
|
69
|
+
try {
|
|
70
|
+
(0, utils_1.verifyFfmpegAvailability)();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `FFmpeg is not available: ${error.message}`, { itemIndex });
|
|
74
|
+
}
|
|
75
|
+
if (inputs.length < 1) {
|
|
76
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Merge operation requires at least one source.', {
|
|
77
|
+
itemIndex,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const intermediateFiles = [];
|
|
81
|
+
let outputPath = null;
|
|
82
|
+
const finalCleanup = async () => {
|
|
83
|
+
for (const file of intermediateFiles) {
|
|
84
|
+
await file.cleanup();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const videoInfos = await Promise.all(inputs.map(utils_1.getVideoStreamInfo));
|
|
89
|
+
const refInfo = videoInfos.find((info) => info !== undefined);
|
|
90
|
+
if (!refInfo || !refInfo.width || !refInfo.height) {
|
|
91
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Could not determine reference video properties for merging. At least one input must be a valid video.', { itemIndex });
|
|
92
|
+
}
|
|
93
|
+
// 1. Normalization Stage
|
|
94
|
+
for (const inputPath of inputs) {
|
|
95
|
+
const { normalizedPath, cleanup } = await normalizeVideo.call(this, inputPath, refInfo);
|
|
96
|
+
intermediateFiles.push({ path: normalizedPath, cleanup });
|
|
97
|
+
}
|
|
98
|
+
// 2. Merging Stage - Using the concat protocol for stability
|
|
99
|
+
outputPath = (0, utils_1.getTempFile)(`.${outputFormat}`);
|
|
100
|
+
const normalizedPaths = intermediateFiles.map((f) => f.path);
|
|
101
|
+
const concatString = `concat:${normalizedPaths.join('|')}`;
|
|
102
|
+
const command = ffmpeg()
|
|
103
|
+
.input(concatString)
|
|
104
|
+
.outputOptions('-c', 'copy')
|
|
105
|
+
.save(outputPath);
|
|
106
|
+
await (0, utils_1.runFfmpeg)(command);
|
|
107
|
+
return outputPath;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Clean up output file if creation failed
|
|
111
|
+
if (outputPath) {
|
|
112
|
+
await fs.remove(outputPath).catch(() => { });
|
|
113
|
+
}
|
|
114
|
+
await finalCleanup();
|
|
115
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error merging videos. Please ensure all source videos are valid. FFmpeg error: ${error.message}`, { itemIndex });
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await finalCleanup();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.executeMerge = executeMerge;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { IDataObject, IExecuteFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare function executeMixAudio(this: IExecuteFunctions, videoPath: string, audioPath: string, videoVolume: number, audioVolume: number, matchLength: 'shortest' | 'longest' | 'first', advancedMixing: IDataObject, itemIndex: number): Promise<string>;
|