@lee-jisoo/n8n-nodes-mediafx 1.6.4 → 1.6.6
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,28 +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
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
99
|
+
function convertSRTtoASS(srtContent, fontName, fontSize, primaryColor, outlineColor, outlineWidth, backColor, enableBackground, alignment, marginV, marginL, marginR) {
|
|
100
|
+
// BorderStyle: 1=outline+shadow, 3=opaque box
|
|
101
|
+
const borderStyle = enableBackground ? 3 : 1;
|
|
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: 1920
|
|
110
|
+
PlayResY: 1080
|
|
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';
|
|
112
152
|
}
|
|
113
153
|
async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
114
|
-
var _a, _b, _c, _d;
|
|
154
|
+
var _a, _b, _c, _d, _e;
|
|
115
155
|
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
116
156
|
// 1. Get Font
|
|
117
157
|
const allFonts = (0, utils_1.getAvailableFonts)();
|
|
@@ -128,62 +168,42 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
128
168
|
const enableBackground = (_b = style.enableBackground) !== null && _b !== void 0 ? _b : false;
|
|
129
169
|
const backgroundColor = style.backgroundColor || 'black';
|
|
130
170
|
const backgroundOpacity = (_c = style.backgroundOpacity) !== null && _c !== void 0 ? _c : 0.5;
|
|
131
|
-
// 2. Get alignment
|
|
171
|
+
// 2. Get alignment and padding
|
|
132
172
|
const positionType = style.positionType || 'alignment';
|
|
133
173
|
let horizontalAlign = 'center';
|
|
134
174
|
let verticalAlign = 'bottom';
|
|
175
|
+
let paddingX = 20;
|
|
135
176
|
let paddingY = 20;
|
|
136
177
|
if (positionType === 'alignment') {
|
|
137
178
|
horizontalAlign = style.horizontalAlign || 'center';
|
|
138
179
|
verticalAlign = style.verticalAlign || 'bottom';
|
|
139
|
-
|
|
180
|
+
paddingX = (_d = style.paddingX) !== null && _d !== void 0 ? _d : 20;
|
|
181
|
+
paddingY = (_e = style.paddingY) !== null && _e !== void 0 ? _e : 20;
|
|
140
182
|
}
|
|
141
183
|
const alignment = getASSAlignment(horizontalAlign, verticalAlign);
|
|
142
|
-
// Get video height for accurate MarginV calculation
|
|
143
|
-
let videoHeight = 1080; // default fallback
|
|
144
|
-
try {
|
|
145
|
-
const videoInfo = await (0, utils_1.getVideoStreamInfo)(video);
|
|
146
|
-
if (videoInfo && videoInfo.height) {
|
|
147
|
-
videoHeight = videoInfo.height;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch (e) {
|
|
151
|
-
// Use default if ffprobe fails
|
|
152
|
-
console.warn('Could not get video height, using default 1080');
|
|
153
|
-
}
|
|
154
|
-
// Calculate MarginV based on vertical alignment and video height
|
|
155
|
-
// ASS MarginV is the distance from the alignment edge
|
|
156
|
-
// For middle alignment, we calculate the margin to center the subtitle
|
|
157
|
-
let marginV = paddingY;
|
|
158
|
-
if (verticalAlign === 'middle') {
|
|
159
|
-
// For middle alignment (4,5,6), MarginV pushes from bottom
|
|
160
|
-
// To center: marginV = (videoHeight / 2) - fontSize - some_offset
|
|
161
|
-
// Simplified: use ~37% of video height as margin from bottom
|
|
162
|
-
marginV = Math.round(videoHeight * 0.37);
|
|
163
|
-
}
|
|
164
|
-
else if (verticalAlign === 'top') {
|
|
165
|
-
marginV = paddingY;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
// bottom
|
|
169
|
-
marginV = paddingY;
|
|
170
|
-
}
|
|
171
184
|
// 3. Convert colors to ASS format
|
|
172
185
|
const primaryColorASS = colorToASS(fontColor, 1);
|
|
173
186
|
const outlineColorASS = colorToASS(outlineColor, 1);
|
|
174
187
|
const backColorASS = colorToASS(backgroundColor, backgroundOpacity);
|
|
175
|
-
// 4.
|
|
176
|
-
|
|
177
|
-
let
|
|
188
|
+
// 4. Prepare subtitle file
|
|
189
|
+
let finalSubtitlePath = subtitleFile;
|
|
190
|
+
let tempAssFile = null;
|
|
178
191
|
if (isASSFile(subtitleFile)) {
|
|
179
|
-
// ASS/SSA file: use directly
|
|
180
|
-
|
|
192
|
+
// ASS/SSA file: use directly
|
|
193
|
+
finalSubtitlePath = subtitleFile;
|
|
181
194
|
}
|
|
182
195
|
else {
|
|
183
|
-
// SRT file:
|
|
184
|
-
const
|
|
185
|
-
|
|
196
|
+
// SRT file: convert to ASS for full styling support
|
|
197
|
+
const srtContent = await fs.readFile(subtitleFile, 'utf-8');
|
|
198
|
+
const assContent = convertSRTtoASS(srtContent, fontName, fontSize, primaryColorASS, outlineColorASS, outlineWidth, backColorASS, enableBackground, alignment, paddingY, paddingX, paddingX);
|
|
199
|
+
// Create temp ASS file with unique name
|
|
200
|
+
tempAssFile = path.join(os.tmpdir(), `mediafx_sub_${Date.now()}_${Math.random().toString(36).substring(7)}.ass`);
|
|
201
|
+
await fs.writeFile(tempAssFile, assContent, 'utf-8');
|
|
202
|
+
finalSubtitlePath = tempAssFile;
|
|
186
203
|
}
|
|
204
|
+
// 5. Build FFmpeg command
|
|
205
|
+
const escapedPath = escapeSubtitlePath(finalSubtitlePath);
|
|
206
|
+
const subtitlesFilter = `ass='${escapedPath}'`;
|
|
187
207
|
const command = ffmpeg(video)
|
|
188
208
|
.videoFilters([subtitlesFilter])
|
|
189
209
|
.audioCodec('copy')
|
|
@@ -196,5 +216,11 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
196
216
|
await fs.remove(outputPath).catch(() => { });
|
|
197
217
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
198
218
|
}
|
|
219
|
+
finally {
|
|
220
|
+
// Clean up temp ASS file
|
|
221
|
+
if (tempAssFile) {
|
|
222
|
+
await fs.remove(tempAssFile).catch(() => { });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
199
225
|
}
|
|
200
226
|
exports.executeAddSubtitle = executeAddSubtitle;
|
package/package.json
CHANGED