@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 marginV = 20;
135
+ let paddingY = 20;
159
136
  if (positionType === 'alignment') {
160
137
  horizontalAlign = style.horizontalAlign || 'center';
161
138
  verticalAlign = style.verticalAlign || 'bottom';
162
- marginV = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
139
+ paddingY = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
163
140
  }
164
141
  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;
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
- // 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;
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(finalSubtitlePath);
182
- const subtitlesFilter = `subtitles='${escapedPath}'`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "N8N custom nodes for video editing and media processing (Enhanced fork with Speed control and Subtitle fixes)",
5
5
  "license": "MIT",
6
6
  "author": {