@lee-jisoo/n8n-nodes-mediafx 1.6.5 → 1.6.7
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.
|
@@ -27,18 +27,18 @@ exports.executeAddSubtitle = void 0;
|
|
|
27
27
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
28
|
const path = __importStar(require("path"));
|
|
29
29
|
const fs = __importStar(require("fs-extra"));
|
|
30
|
+
const os = __importStar(require("os"));
|
|
30
31
|
const ffmpeg = require("fluent-ffmpeg");
|
|
31
32
|
const utils_1 = require("../utils");
|
|
32
33
|
/**
|
|
33
34
|
* Escape special characters in file path for FFmpeg subtitles filter.
|
|
34
35
|
*/
|
|
35
36
|
function escapeSubtitlePath(filePath) {
|
|
37
|
+
// For Windows paths and special characters
|
|
36
38
|
return filePath
|
|
37
|
-
.replace(/\\/g, '
|
|
39
|
+
.replace(/\\/g, '/')
|
|
38
40
|
.replace(/:/g, '\\:')
|
|
39
|
-
.replace(
|
|
40
|
-
.replace(/\]/g, '\\]')
|
|
41
|
-
.replace(/'/g, "\\'");
|
|
41
|
+
.replace(/'/g, "'\\''");
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Convert color to ASS format (&HAABBGGRR)
|
|
@@ -67,11 +67,15 @@ function colorToASS(color, opacity = 1) {
|
|
|
67
67
|
g = hex.substring(2, 4);
|
|
68
68
|
b = hex.substring(4, 6);
|
|
69
69
|
}
|
|
70
|
+
// ASS format: &HAABBGGRR (AA = alpha, 00=solid, FF=transparent)
|
|
70
71
|
const alpha = Math.round((1 - opacity) * 255).toString(16).padStart(2, '0').toUpperCase();
|
|
71
72
|
return `&H${alpha}${b}${g}${r}`.toUpperCase();
|
|
72
73
|
}
|
|
73
74
|
/**
|
|
74
75
|
* Get ASS alignment number (numpad style)
|
|
76
|
+
* 7=top-left, 8=top-center, 9=top-right
|
|
77
|
+
* 4=middle-left, 5=middle-center, 6=middle-right
|
|
78
|
+
* 1=bottom-left, 2=bottom-center, 3=bottom-right
|
|
75
79
|
*/
|
|
76
80
|
function getASSAlignment(horizontalAlign, verticalAlign) {
|
|
77
81
|
var _a, _b;
|
|
@@ -90,31 +94,64 @@ function isASSFile(filePath) {
|
|
|
90
94
|
return ext === '.ass' || ext === '.ssa';
|
|
91
95
|
}
|
|
92
96
|
/**
|
|
93
|
-
*
|
|
97
|
+
* Convert SRT content to ASS format with full styling support
|
|
94
98
|
*/
|
|
95
|
-
function
|
|
96
|
-
// BorderStyle: 1=outline+shadow, 3=opaque box
|
|
97
|
-
// For background box, use BorderStyle=3 with BackColour
|
|
99
|
+
function convertSRTtoASS(srtContent, fontName, fontSize, primaryColor, outlineColor, outlineWidth, backColor, enableBackground, alignment, marginV, marginL, marginR, videoWidth, videoHeight) {
|
|
100
|
+
// BorderStyle: 1=outline+shadow, 3=opaque box
|
|
98
101
|
const borderStyle = enableBackground ? 3 : 1;
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
const outline = enableBackground ? 0 : outlineWidth;
|
|
103
|
+
const shadow = enableBackground ? 2 : 0;
|
|
104
|
+
const header = `[Script Info]
|
|
105
|
+
Title: Generated by MediaFX
|
|
106
|
+
ScriptType: v4.00+
|
|
107
|
+
WrapStyle: 0
|
|
108
|
+
ScaledBorderAndShadow: yes
|
|
109
|
+
PlayResX: ${videoWidth}
|
|
110
|
+
PlayResY: ${videoHeight}
|
|
111
|
+
|
|
112
|
+
[V4+ Styles]
|
|
113
|
+
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
114
|
+
Style: Default,${fontName},${fontSize},${primaryColor},${primaryColor},${outlineColor},${backColor},0,0,0,0,100,100,0,0,${borderStyle},${outline},${shadow},${alignment},${marginL},${marginR},${marginV},1
|
|
115
|
+
|
|
116
|
+
[Events]
|
|
117
|
+
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
118
|
+
`;
|
|
119
|
+
// Parse SRT blocks
|
|
120
|
+
const blocks = srtContent.trim().split(/\r?\n\r?\n/);
|
|
121
|
+
const dialogues = [];
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
const lines = block.trim().split(/\r?\n/);
|
|
124
|
+
if (lines.length < 2)
|
|
125
|
+
continue;
|
|
126
|
+
// Find timestamp line (may be first or second line)
|
|
127
|
+
let timestampLine = '';
|
|
128
|
+
let textStartIndex = 0;
|
|
129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
130
|
+
if (lines[i].includes('-->')) {
|
|
131
|
+
timestampLine = lines[i];
|
|
132
|
+
textStartIndex = i + 1;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!timestampLine)
|
|
137
|
+
continue;
|
|
138
|
+
// Parse timestamps: 00:00:01,000 --> 00:00:04,000
|
|
139
|
+
const match = timestampLine.match(/(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})/);
|
|
140
|
+
if (!match)
|
|
141
|
+
continue;
|
|
142
|
+
// Convert to ASS time format: H:MM:SS.cc
|
|
143
|
+
const startTime = `${parseInt(match[1])}:${match[2]}:${match[3]}.${match[4].substring(0, 2)}`;
|
|
144
|
+
const endTime = `${parseInt(match[5])}:${match[6]}:${match[7]}.${match[8].substring(0, 2)}`;
|
|
145
|
+
// Get subtitle text (remaining lines)
|
|
146
|
+
const text = lines.slice(textStartIndex).join('\\N');
|
|
147
|
+
if (text.trim()) {
|
|
148
|
+
dialogues.push(`Dialogue: 0,${startTime},${endTime},Default,,0,0,0,,${text}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return header + dialogues.join('\n') + '\n';
|
|
115
152
|
}
|
|
116
153
|
async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
117
|
-
var _a, _b, _c, _d;
|
|
154
|
+
var _a, _b, _c, _d, _e;
|
|
118
155
|
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
119
156
|
// 1. Get Font
|
|
120
157
|
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
@@ -131,51 +168,55 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
131
168
|
const enableBackground = (_b = style.enableBackground) !== null && _b !== void 0 ? _b : false;
|
|
132
169
|
const backgroundColor = style.backgroundColor || 'black';
|
|
133
170
|
const backgroundOpacity = (_c = style.backgroundOpacity) !== null && _c !== void 0 ? _c : 0.5;
|
|
134
|
-
// 2. Get alignment
|
|
171
|
+
// 2. Get alignment and padding
|
|
135
172
|
const positionType = style.positionType || 'alignment';
|
|
136
173
|
let horizontalAlign = 'center';
|
|
137
174
|
let verticalAlign = 'bottom';
|
|
175
|
+
let paddingX = 20;
|
|
138
176
|
let paddingY = 20;
|
|
139
177
|
if (positionType === 'alignment') {
|
|
140
178
|
horizontalAlign = style.horizontalAlign || 'center';
|
|
141
179
|
verticalAlign = style.verticalAlign || 'bottom';
|
|
142
|
-
|
|
180
|
+
paddingX = (_d = style.paddingX) !== null && _d !== void 0 ? _d : 20;
|
|
181
|
+
paddingY = (_e = style.paddingY) !== null && _e !== void 0 ? _e : 20;
|
|
143
182
|
}
|
|
144
183
|
const alignment = getASSAlignment(horizontalAlign, verticalAlign);
|
|
145
|
-
// Calculate MarginV based on vertical alignment
|
|
146
|
-
// For SRT files, middle alignment (4,5,6) doesn't work reliably with force_style
|
|
147
|
-
// So we use bottom alignment (2) with large MarginV to simulate middle position
|
|
148
|
-
let marginV = paddingY;
|
|
149
|
-
let effectiveAlignment = alignment;
|
|
150
|
-
if (verticalAlign === 'middle') {
|
|
151
|
-
// Use bottom-center alignment but push up with MarginV
|
|
152
|
-
// For middle position, we want subtitle at ~45% from bottom
|
|
153
|
-
effectiveAlignment = horizontalAlign === 'left' ? 1 : horizontalAlign === 'right' ? 3 : 2;
|
|
154
|
-
marginV = 350; // Works for most video heights (720p-1080p)
|
|
155
|
-
}
|
|
156
|
-
else if (verticalAlign === 'top') {
|
|
157
|
-
marginV = paddingY;
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
// bottom
|
|
161
|
-
marginV = paddingY;
|
|
162
|
-
}
|
|
163
184
|
// 3. Convert colors to ASS format
|
|
164
185
|
const primaryColorASS = colorToASS(fontColor, 1);
|
|
165
186
|
const outlineColorASS = colorToASS(outlineColor, 1);
|
|
166
187
|
const backColorASS = colorToASS(backgroundColor, backgroundOpacity);
|
|
167
|
-
// 4.
|
|
168
|
-
|
|
169
|
-
let
|
|
188
|
+
// 4. Get video dimensions
|
|
189
|
+
let videoWidth = 1080;
|
|
190
|
+
let videoHeight = 1920;
|
|
191
|
+
try {
|
|
192
|
+
const videoInfo = await (0, utils_1.getVideoStreamInfo)(video);
|
|
193
|
+
if (videoInfo && videoInfo.width && videoInfo.height) {
|
|
194
|
+
videoWidth = videoInfo.width;
|
|
195
|
+
videoHeight = videoInfo.height;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
// Use default if ffprobe fails
|
|
200
|
+
}
|
|
201
|
+
// 5. Prepare subtitle file
|
|
202
|
+
let finalSubtitlePath = subtitleFile;
|
|
203
|
+
let tempAssFile = null;
|
|
170
204
|
if (isASSFile(subtitleFile)) {
|
|
171
|
-
// ASS/SSA file: use directly
|
|
172
|
-
|
|
205
|
+
// ASS/SSA file: use directly
|
|
206
|
+
finalSubtitlePath = subtitleFile;
|
|
173
207
|
}
|
|
174
208
|
else {
|
|
175
|
-
// SRT file:
|
|
176
|
-
const
|
|
177
|
-
|
|
209
|
+
// SRT file: convert to ASS for full styling support
|
|
210
|
+
const srtContent = await fs.readFile(subtitleFile, 'utf-8');
|
|
211
|
+
const assContent = convertSRTtoASS(srtContent, fontName, fontSize, primaryColorASS, outlineColorASS, outlineWidth, backColorASS, enableBackground, alignment, paddingY, paddingX, paddingX, videoWidth, videoHeight);
|
|
212
|
+
// Create temp ASS file with unique name
|
|
213
|
+
tempAssFile = path.join(os.tmpdir(), `mediafx_sub_${Date.now()}_${Math.random().toString(36).substring(7)}.ass`);
|
|
214
|
+
await fs.writeFile(tempAssFile, assContent, 'utf-8');
|
|
215
|
+
finalSubtitlePath = tempAssFile;
|
|
178
216
|
}
|
|
217
|
+
// 5. Build FFmpeg command
|
|
218
|
+
const escapedPath = escapeSubtitlePath(finalSubtitlePath);
|
|
219
|
+
const subtitlesFilter = `ass='${escapedPath}'`;
|
|
179
220
|
const command = ffmpeg(video)
|
|
180
221
|
.videoFilters([subtitlesFilter])
|
|
181
222
|
.audioCodec('copy')
|
|
@@ -188,5 +229,11 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
188
229
|
await fs.remove(outputPath).catch(() => { });
|
|
189
230
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
190
231
|
}
|
|
232
|
+
finally {
|
|
233
|
+
// Clean up temp ASS file
|
|
234
|
+
if (tempAssFile) {
|
|
235
|
+
await fs.remove(tempAssFile).catch(() => { });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
191
238
|
}
|
|
192
239
|
exports.executeAddSubtitle = executeAddSubtitle;
|
package/package.json
CHANGED