@lee-jisoo/n8n-nodes-mediafx 1.6.2 → 1.6.3
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.
|
@@ -42,8 +42,6 @@ function escapeSubtitlePath(filePath) {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
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
45
|
*/
|
|
48
46
|
function colorToASS(color, opacity = 1) {
|
|
49
47
|
let r = 'FF', g = 'FF', b = 'FF';
|
|
@@ -64,12 +62,11 @@ function colorToASS(color, opacity = 1) {
|
|
|
64
62
|
magenta: 'FF00FF',
|
|
65
63
|
orange: 'FFA500',
|
|
66
64
|
};
|
|
67
|
-
const hex = namedColors[color.toLowerCase()] || 'FFFFFF';
|
|
65
|
+
const hex = namedColors[(color || 'white').toLowerCase()] || 'FFFFFF';
|
|
68
66
|
r = hex.substring(0, 2);
|
|
69
67
|
g = hex.substring(2, 4);
|
|
70
68
|
b = hex.substring(4, 6);
|
|
71
69
|
}
|
|
72
|
-
// ASS format: &HAABBGGRR (AA = alpha, 00=solid, FF=transparent)
|
|
73
70
|
const alpha = Math.round((1 - opacity) * 255).toString(16).padStart(2, '0').toUpperCase();
|
|
74
71
|
return `&H${alpha}${b}${g}${r}`.toUpperCase();
|
|
75
72
|
}
|
|
@@ -85,47 +82,6 @@ function getASSAlignment(horizontalAlign, verticalAlign) {
|
|
|
85
82
|
};
|
|
86
83
|
return (_b = (_a = alignMap[verticalAlign]) === null || _a === void 0 ? void 0 : _a[horizontalAlign]) !== null && _b !== void 0 ? _b : 2;
|
|
87
84
|
}
|
|
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
85
|
/**
|
|
130
86
|
* Check if file is ASS/SSA format
|
|
131
87
|
*/
|
|
@@ -133,6 +89,27 @@ function isASSFile(filePath) {
|
|
|
133
89
|
const ext = path.extname(filePath).toLowerCase();
|
|
134
90
|
return ext === '.ass' || ext === '.ssa';
|
|
135
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Build force_style string for subtitles filter
|
|
94
|
+
*/
|
|
95
|
+
function buildForceStyle(fontName, fontSize, primaryColor, outlineColor, outlineWidth, backColor, enableBackground, alignment, marginV) {
|
|
96
|
+
const borderStyle = enableBackground ? 4 : 1;
|
|
97
|
+
const styleParams = [
|
|
98
|
+
`FontName=${fontName}`,
|
|
99
|
+
`FontSize=${fontSize}`,
|
|
100
|
+
`PrimaryColour=${primaryColor}`,
|
|
101
|
+
`OutlineColour=${outlineColor}`,
|
|
102
|
+
`BackColour=${backColor}`,
|
|
103
|
+
`Bold=0`,
|
|
104
|
+
`Italic=0`,
|
|
105
|
+
`BorderStyle=${borderStyle}`,
|
|
106
|
+
`Outline=${outlineWidth}`,
|
|
107
|
+
`Shadow=0`,
|
|
108
|
+
`Alignment=${alignment}`,
|
|
109
|
+
`MarginV=${marginV}`,
|
|
110
|
+
];
|
|
111
|
+
return styleParams.join(',');
|
|
112
|
+
}
|
|
136
113
|
async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
137
114
|
var _a, _b, _c, _d;
|
|
138
115
|
const outputPath = (0, utils_1.getTempFile)(path.extname(video));
|
|
@@ -162,24 +139,22 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
162
139
|
marginV = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
|
|
163
140
|
}
|
|
164
141
|
const alignment = getASSAlignment(horizontalAlign, verticalAlign);
|
|
165
|
-
// 3.
|
|
166
|
-
|
|
167
|
-
|
|
142
|
+
// 3. Convert colors to ASS format
|
|
143
|
+
const primaryColorASS = colorToASS(fontColor, 1);
|
|
144
|
+
const outlineColorASS = colorToASS(outlineColor, 1);
|
|
145
|
+
const backColorASS = colorToASS(backgroundColor, backgroundOpacity);
|
|
146
|
+
// 4. Build FFmpeg command
|
|
147
|
+
const escapedPath = escapeSubtitlePath(subtitleFile);
|
|
148
|
+
let subtitlesFilter;
|
|
168
149
|
if (isASSFile(subtitleFile)) {
|
|
169
|
-
// ASS file: use directly
|
|
170
|
-
|
|
150
|
+
// ASS/SSA file: use directly without force_style
|
|
151
|
+
subtitlesFilter = `subtitles='${escapedPath}'`;
|
|
171
152
|
}
|
|
172
153
|
else {
|
|
173
|
-
// SRT file:
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
tempAssFile = (0, utils_1.getTempFile)('.ass');
|
|
177
|
-
await fs.writeFile(tempAssFile, assContent, 'utf-8');
|
|
178
|
-
finalSubtitlePath = tempAssFile;
|
|
154
|
+
// SRT file: apply force_style
|
|
155
|
+
const forceStyle = buildForceStyle(fontName, fontSize, primaryColorASS, outlineColorASS, outlineWidth, backColorASS, enableBackground, alignment, marginV);
|
|
156
|
+
subtitlesFilter = `subtitles='${escapedPath}':force_style='${forceStyle}'`;
|
|
179
157
|
}
|
|
180
|
-
// 4. Build FFmpeg command
|
|
181
|
-
const escapedPath = escapeSubtitlePath(finalSubtitlePath);
|
|
182
|
-
const subtitlesFilter = `subtitles='${escapedPath}'`;
|
|
183
158
|
const command = ffmpeg(video)
|
|
184
159
|
.videoFilters([subtitlesFilter])
|
|
185
160
|
.audioCodec('copy')
|
|
@@ -192,11 +167,5 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
192
167
|
await fs.remove(outputPath).catch(() => { });
|
|
193
168
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
194
169
|
}
|
|
195
|
-
finally {
|
|
196
|
-
// Clean up temp ASS file
|
|
197
|
-
if (tempAssFile) {
|
|
198
|
-
await fs.remove(tempAssFile).catch(() => { });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
170
|
}
|
|
202
171
|
exports.executeAddSubtitle = executeAddSubtitle;
|
package/package.json
CHANGED