@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(/\[/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,28 +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
- 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(',');
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
- 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;
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. Build FFmpeg command
176
- const escapedPath = escapeSubtitlePath(subtitleFile);
177
- let subtitlesFilter;
188
+ // 4. Prepare subtitle file
189
+ let finalSubtitlePath = subtitleFile;
190
+ let tempAssFile = null;
178
191
  if (isASSFile(subtitleFile)) {
179
- // ASS/SSA file: use directly without force_style
180
- subtitlesFilter = `subtitles='${escapedPath}'`;
192
+ // ASS/SSA file: use directly
193
+ finalSubtitlePath = subtitleFile;
181
194
  }
182
195
  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}'`;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.4",
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": {