@lee-jisoo/n8n-nodes-mediafx 1.6.5 → 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(/\[/g, '\\[')
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
- * Build force_style string for subtitles filter
97
+ * Convert SRT content to ASS format with full styling support
94
98
  */
95
- function buildForceStyle(fontName, fontSize, primaryColor, outlineColor, outlineWidth, backColor, enableBackground, alignment, marginV) {
96
- // BorderStyle: 1=outline+shadow, 3=opaque box, 4=translucent box (limited support)
97
- // For background box, use BorderStyle=3 with BackColour
99
+ function convertSRTtoASS(srtContent, fontName, fontSize, primaryColor, outlineColor, outlineWidth, backColor, enableBackground, alignment, marginV, marginL, marginR) {
100
+ // BorderStyle: 1=outline+shadow, 3=opaque box
98
101
  const borderStyle = enableBackground ? 3 : 1;
99
- const shadow = enableBackground ? 4 : 0; // Add shadow for box effect
100
- const styleParams = [
101
- `FontName=${fontName}`,
102
- `FontSize=${fontSize}`,
103
- `PrimaryColour=${primaryColor}`,
104
- `OutlineColour=${outlineColor}`,
105
- `BackColour=${backColor}`,
106
- `Bold=0`,
107
- `Italic=0`,
108
- `BorderStyle=${borderStyle}`,
109
- `Outline=${enableBackground ? 0 : outlineWidth}`,
110
- `Shadow=${shadow}`,
111
- `Alignment=${alignment}`,
112
- `MarginV=${marginV}`,
113
- ];
114
- return styleParams.join(',');
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';
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,42 @@ 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
- paddingY = (_d = style.paddingY) !== null && _d !== void 0 ? _d : 20;
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. Build FFmpeg command
168
- const escapedPath = escapeSubtitlePath(subtitleFile);
169
- let subtitlesFilter;
188
+ // 4. Prepare subtitle file
189
+ let finalSubtitlePath = subtitleFile;
190
+ let tempAssFile = null;
170
191
  if (isASSFile(subtitleFile)) {
171
- // ASS/SSA file: use directly without force_style
172
- subtitlesFilter = `subtitles='${escapedPath}'`;
192
+ // ASS/SSA file: use directly
193
+ finalSubtitlePath = subtitleFile;
173
194
  }
174
195
  else {
175
- // SRT file: apply force_style
176
- const forceStyle = buildForceStyle(fontName, fontSize, primaryColorASS, outlineColorASS, outlineWidth, backColorASS, enableBackground, effectiveAlignment, marginV);
177
- subtitlesFilter = `subtitles='${escapedPath}':force_style='${forceStyle}'`;
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;
178
203
  }
204
+ // 5. Build FFmpeg command
205
+ const escapedPath = escapeSubtitlePath(finalSubtitlePath);
206
+ const subtitlesFilter = `ass='${escapedPath}'`;
179
207
  const command = ffmpeg(video)
180
208
  .videoFilters([subtitlesFilter])
181
209
  .audioCodec('copy')
@@ -188,5 +216,11 @@ async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
188
216
  await fs.remove(outputPath).catch(() => { });
189
217
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding subtitles to video. FFmpeg error: ${error.message}`, { itemIndex });
190
218
  }
219
+ finally {
220
+ // Clean up temp ASS file
221
+ if (tempAssFile) {
222
+ await fs.remove(tempAssFile).catch(() => { });
223
+ }
224
+ }
191
225
  }
192
226
  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.5",
3
+ "version": "1.6.6",
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": {