@lee-jisoo/n8n-nodes-mediafx 1.6.2 → 1.6.4
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));
|
|
@@ -155,31 +132,58 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
155
132
|
const positionType = style.positionType || 'alignment';
|
|
156
133
|
let horizontalAlign = 'center';
|
|
157
134
|
let verticalAlign = 'bottom';
|
|
158
|
-
let
|
|
135
|
+
let paddingY = 20;
|
|
159
136
|
if (positionType === 'alignment') {
|
|
160
137
|
horizontalAlign = style.horizontalAlign || 'center';
|
|
161
138
|
verticalAlign = style.verticalAlign || 'bottom';
|
|
162
|
-
|
|
139
|
+
paddingY = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
|
|
163
140
|
}
|
|
164
141
|
const alignment = getASSAlignment(horizontalAlign, verticalAlign);
|
|
165
|
-
//
|
|
166
|
-
let
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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;
|
|
171
166
|
}
|
|
172
167
|
else {
|
|
173
|
-
//
|
|
174
|
-
|
|
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;
|
|
168
|
+
// bottom
|
|
169
|
+
marginV = paddingY;
|
|
179
170
|
}
|
|
171
|
+
// 3. Convert colors to ASS format
|
|
172
|
+
const primaryColorASS = colorToASS(fontColor, 1);
|
|
173
|
+
const outlineColorASS = colorToASS(outlineColor, 1);
|
|
174
|
+
const backColorASS = colorToASS(backgroundColor, backgroundOpacity);
|
|
180
175
|
// 4. Build FFmpeg command
|
|
181
|
-
const escapedPath = escapeSubtitlePath(
|
|
182
|
-
|
|
176
|
+
const escapedPath = escapeSubtitlePath(subtitleFile);
|
|
177
|
+
let subtitlesFilter;
|
|
178
|
+
if (isASSFile(subtitleFile)) {
|
|
179
|
+
// ASS/SSA file: use directly without force_style
|
|
180
|
+
subtitlesFilter = `subtitles='${escapedPath}'`;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// SRT file: apply force_style
|
|
184
|
+
const forceStyle = buildForceStyle(fontName, fontSize, primaryColorASS, outlineColorASS, outlineWidth, backColorASS, enableBackground, alignment, marginV);
|
|
185
|
+
subtitlesFilter = `subtitles='${escapedPath}':force_style='${forceStyle}'`;
|
|
186
|
+
}
|
|
183
187
|
const command = ffmpeg(video)
|
|
184
188
|
.videoFilters([subtitlesFilter])
|
|
185
189
|
.audioCodec('copy')
|
|
@@ -192,11 +196,5 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
|
|
|
192
196
|
await fs.remove(outputPath).catch(() => { });
|
|
193
197
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
|
|
194
198
|
}
|
|
195
|
-
finally {
|
|
196
|
-
// Clean up temp ASS file
|
|
197
|
-
if (tempAssFile) {
|
|
198
|
-
await fs.remove(tempAssFile).catch(() => { });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
199
|
}
|
|
202
200
|
exports.executeAddSubtitle = executeAddSubtitle;
|
package/package.json
CHANGED