n8n-nodes-sb-render 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.
package/README.md CHANGED
@@ -10,6 +10,8 @@ This is an n8n community node for video rendering with customizable subtitles, b
10
10
  - 🎙️ Narration audio with timing control
11
11
  - 📝 Customizable subtitles with extensive styling options
12
12
  - 🎨 Multiple output formats and quality presets
13
+ - 🖼️ AI-powered parallax effects from static images (Depth Anything V2)
14
+ - 🔀 Video merging with transitions and audio sync
13
15
 
14
16
  Inspired by [n8n-nodes-mediafx](https://github.com/dandacompany/n8n-nodes-mediafx).
15
17
 
@@ -18,6 +20,7 @@ Inspired by [n8n-nodes-mediafx](https://github.com/dandacompany/n8n-nodes-mediaf
18
20
  - [Installation](#installation)
19
21
  - [Prerequisites](#prerequisites)
20
22
  - [Operations](#operations)
23
+ - [Parallax Effect](#parallax-effect)
21
24
  - [Configuration](#configuration)
22
25
  - [Examples](#examples)
23
26
  - [Development](#development)
@@ -99,6 +102,66 @@ Compose a video with optional background music, narration, and subtitles.
99
102
  - ✅ Quality presets (Low, Medium, High, Custom)
100
103
  - ✅ Smart audio merging (handles mixed audio/silent video inputs)
101
104
 
105
+ ### Image → Parallax
106
+
107
+ Create 2.5D parallax video effects from static images using AI depth estimation.
108
+
109
+ **Features:**
110
+ - ✅ AI-powered depth estimation (Depth Anything V2)
111
+ - ✅ True layer separation with inpainting
112
+ - ✅ Direction + Zoom combination effects
113
+ - ✅ Intensity control (subtle, normal, dramatic)
114
+ - ✅ Falls back to Ken Burns effect if AI not available
115
+
116
+ ## Parallax Effect
117
+
118
+ The parallax feature uses **Depth Anything V2** AI model to create professional 2.5D parallax effects from static images.
119
+
120
+ ### How It Works
121
+
122
+ 1. **Depth Estimation**: AI model analyzes the image to create a depth map
123
+ 2. **Layer Separation**: Foreground is extracted based on depth (closer objects)
124
+ 3. **Inpainting**: Background is filled where foreground was removed
125
+ 4. **Animation**: Layers move at different speeds creating parallax illusion
126
+
127
+ ### Parameters
128
+
129
+ | Parameter | Type | Options | Description |
130
+ |-----------|------|---------|-------------|
131
+ | **direction** | String | `left`, `right`, `up`, `down` | Pan direction |
132
+ | **zoom** | String | `none`, `in`, `out` | Zoom effect (combinable with direction) |
133
+ | **intensity** | String | `subtle`, `normal`, `dramatic` | Movement intensity |
134
+ | **duration** | Number | seconds | Video duration |
135
+
136
+ ### Combined Effects
137
+
138
+ You can combine zoom with direction for richer effects:
139
+
140
+ ```json
141
+ {
142
+ "direction": "left",
143
+ "zoom": "in",
144
+ "intensity": "normal",
145
+ "duration": 5
146
+ }
147
+ ```
148
+
149
+ **Effect Combinations:**
150
+ - `left` + `zoomIn`: Pan left while zooming in
151
+ - `right` + `zoomOut`: Pan right while zooming out
152
+ - `up` + `zoomIn`: Pan up with zoom in
153
+ - `zoomIn` only: Pure zoom in effect (set direction to empty)
154
+
155
+ ### Requirements for AI Parallax
156
+
157
+ For AI-powered depth parallax, the following Python packages are required on the server:
158
+
159
+ ```bash
160
+ pip3 install torch transformers pillow opencv-python-headless
161
+ ```
162
+
163
+ If not available, the node automatically falls back to Ken Burns (zoompan) effect.
164
+
102
165
  ## Configuration
103
166
 
104
167
  ### Video Input
@@ -1,9 +1,11 @@
1
1
  export type ParallaxDirection = 'left' | 'right' | 'up' | 'down' | 'zoomIn' | 'zoomOut';
2
2
  export type ParallaxIntensity = 'subtle' | 'normal' | 'dramatic';
3
+ export type ParallaxZoom = 'none' | 'in' | 'out';
3
4
  export interface ParallaxConfig {
4
5
  direction: ParallaxDirection;
5
6
  intensity: ParallaxIntensity;
6
7
  layerCount?: number;
8
+ zoom?: ParallaxZoom;
7
9
  }
8
10
  export declare class ParallaxEngine {
9
11
  private sharpAvailable;
@@ -19,6 +21,7 @@ export declare class ParallaxEngine {
19
21
  /**
20
22
  * Generate parallax video from a single image
21
23
  * Main entry point for creating parallax effect videos
24
+ * Uses AI depth estimation for true parallax, falls back to Ken Burns
22
25
  */
23
26
  generateParallaxVideo(imagePath: string, outputPath: string, config: ParallaxConfig, duration: number, fps?: number, _videoCodec?: string, _quality?: string, _customCRF?: number): Promise<Buffer>;
24
27
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ParallaxEngine.d.ts","sourceRoot":"","sources":["../../../../nodes/SbRender/services/ParallaxEngine.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AACxF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAkdD,qBAAa,cAAc;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAS;IAE5B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IASjC;;OAEG;IACH,wBAAwB,IAAI,OAAO;IAInC;;;OAGG;IACG,qBAAqB,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,GAAG,SAAK,EACR,WAAW,SAAY,EACvB,QAAQ,SAAS,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;IAkDlB;;OAEG;IACG,sBAAsB,CAC1B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,GAAG,SAAK,GACP,OAAO,CAAC,MAAM,EAAE,CAAC;CAKrB;AAGD,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
1
+ {"version":3,"file":"ParallaxEngine.d.ts","sourceRoot":"","sources":["../../../../nodes/SbRender/services/ParallaxEngine.ts"],"names":[],"mappings":"AAoIA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AACxF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AA8KD,qBAAa,cAAc;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAS;IAE5B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IASjC;;OAEG;IACH,wBAAwB,IAAI,OAAO;IAInC;;;;OAIG;IACG,qBAAqB,CACzB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,GAAG,SAAK,EACR,WAAW,SAAY,EACvB,QAAQ,SAAS,EACjB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;IAyElB;;OAEG;IACG,sBAAsB,CAC1B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,GAAG,SAAK,GACP,OAAO,CAAC,MAAM,EAAE,CAAC;CAKrB;AAGD,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
@@ -45,304 +45,181 @@ const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
46
  const ffmpeg_wrapper_1 = require("../utils/ffmpeg-wrapper");
47
47
  const child_process_1 = require("child_process");
48
- // Try to load sharp (optional dependency)
49
- let sharp = null;
50
- async function loadSharp() {
51
- if (sharp !== null)
52
- return true;
53
- try {
54
- sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
55
- console.log('[ParallaxEngine] Sharp loaded successfully');
56
- return true;
57
- }
58
- catch (error) {
59
- console.warn('[ParallaxEngine] Sharp not available:', error.message);
60
- return false;
48
+ // Path to the depth parallax Python script
49
+ // In compiled code, __dirname is dist/nodes/SbRender/services/
50
+ // Script is at project_root/scripts/depth_parallax.py
51
+ const DEPTH_SCRIPT_PATH = path.join(__dirname, '../../../../scripts/depth_parallax.py');
52
+ // Python executable path - prefer mounted venv, fallback to system python3
53
+ const PYTHON_PATHS = ['/opt/venv/bin/python3', 'python3'];
54
+ let pythonPath = null;
55
+ // Check if Python depth estimation is available
56
+ let pythonDepthAvailable = null;
57
+ function findPythonPath() {
58
+ for (const pyPath of PYTHON_PATHS) {
59
+ try {
60
+ (0, child_process_1.execSync)(`${pyPath} -c "import torch; import transformers"`, { stdio: 'pipe' });
61
+ console.log(`[ParallaxEngine] Found working Python at: ${pyPath}`);
62
+ return pyPath;
63
+ }
64
+ catch {
65
+ // Try next path
66
+ }
61
67
  }
68
+ return null;
62
69
  }
63
- /**
64
- * Get motion parameters based on intensity
65
- */
66
- function getIntensityParams(intensity) {
67
- switch (intensity) {
68
- case 'subtle':
69
- return { fgSpeed: 0.04, bgSpeed: 0.015, scaleRange: 0.02 };
70
- case 'normal':
71
- return { fgSpeed: 0.08, bgSpeed: 0.025, scaleRange: 0.04 };
72
- case 'dramatic':
73
- return { fgSpeed: 0.15, bgSpeed: 0.04, scaleRange: 0.08 };
74
- default:
75
- return { fgSpeed: 0.08, bgSpeed: 0.025, scaleRange: 0.04 };
70
+ async function checkPythonDepth() {
71
+ if (pythonDepthAvailable !== null)
72
+ return pythonDepthAvailable;
73
+ pythonPath = findPythonPath();
74
+ if (pythonPath && fs.existsSync(DEPTH_SCRIPT_PATH)) {
75
+ pythonDepthAvailable = true;
76
+ console.log('[ParallaxEngine] Python depth estimation available');
76
77
  }
77
- }
78
- /**
79
- * Create a gradient mask for layer separation
80
- * Returns RGBA buffer where alpha determines visibility
81
- */
82
- function createGradientMask(width, height, type, layerIndex, totalLayers) {
83
- // Create RGBA buffer
84
- const buffer = Buffer.alloc(width * height * 4);
85
- // Calculate layer boundaries
86
- const layerHeight = height / totalLayers;
87
- const layerStart = (totalLayers - 1 - layerIndex) * layerHeight; // Reverse: top layers are background
88
- const layerEnd = layerStart + layerHeight;
89
- // Feather zone for smooth transitions
90
- const featherSize = layerHeight * 0.3;
91
- for (let y = 0; y < height; y++) {
92
- let alpha = 0;
93
- if (y >= layerStart && y < layerEnd) {
94
- // Inside layer
95
- alpha = 255;
96
- // Apply feathering at edges
97
- if (y < layerStart + featherSize) {
98
- alpha = Math.floor(((y - layerStart) / featherSize) * 255);
99
- }
100
- else if (y > layerEnd - featherSize) {
101
- alpha = Math.floor(((layerEnd - y) / featherSize) * 255);
102
- }
103
- }
104
- else if (y < layerStart && y > layerStart - featherSize) {
105
- // Fade in from above
106
- alpha = Math.floor(((layerStart - y) / featherSize) * 128);
107
- }
108
- else if (y >= layerEnd && y < layerEnd + featherSize) {
109
- // Fade out below
110
- alpha = Math.floor(((y - layerEnd) / featherSize) * 128);
111
- }
112
- // For foreground (bottom), extend fully to bottom
113
- if (type === 'foreground' && layerIndex === 0 && y >= layerStart) {
114
- alpha = 255;
78
+ else {
79
+ pythonDepthAvailable = false;
80
+ if (!pythonPath) {
81
+ console.log('[ParallaxEngine] Python depth estimation not available (missing torch/transformers)');
115
82
  }
116
- // For background (top), extend fully to top
117
- if (type === 'background' && layerIndex === totalLayers - 1 && y <= layerEnd) {
118
- alpha = 255;
119
- }
120
- for (let x = 0; x < width; x++) {
121
- const idx = (y * width + x) * 4;
122
- buffer[idx] = 255; // R
123
- buffer[idx + 1] = 255; // G
124
- buffer[idx + 2] = 255; // B
125
- buffer[idx + 3] = alpha; // A
83
+ else {
84
+ console.log('[ParallaxEngine] Depth script not found:', DEPTH_SCRIPT_PATH);
126
85
  }
127
86
  }
128
- return buffer;
87
+ return pythonDepthAvailable;
129
88
  }
130
89
  /**
131
- * Generate true multi-layer parallax with actual layer separation
90
+ * Generate parallax using AI depth estimation (Python + Depth Anything)
91
+ * Creates true layer separation based on depth map
132
92
  */
133
- async function generateLayeredParallax(imagePath, outputPath, config, duration, fps, width, height) {
134
- if (!sharp) {
135
- throw new Error('Sharp not available for parallax');
136
- }
137
- const params = getIntensityParams(config.intensity);
138
- const totalFrames = Math.ceil(duration * fps);
139
- const numLayers = config.layerCount || 3;
140
- const tempDir = path.join(path.dirname(outputPath), `parallax_temp_${Date.now()}`);
141
- console.log(`[ParallaxEngine] Creating ${numLayers}-layer parallax effect`);
142
- console.log(`[ParallaxEngine] Direction: ${config.direction}, Intensity: ${config.intensity}`);
143
- try {
144
- fs.mkdirSync(tempDir, { recursive: true });
145
- // Load and prepare the source image
146
- const sourceBuffer = await sharp(imagePath)
147
- .resize(width, height, { fit: 'cover', position: 'center' })
148
- .ensureAlpha()
149
- .raw()
150
- .toBuffer({ resolveWithObject: true });
151
- const { data: sourceData, info } = sourceBuffer;
152
- const imgWidth = info.width;
153
- const imgHeight = info.height;
154
- console.log(`[ParallaxEngine] Source image: ${imgWidth}x${imgHeight}`);
155
- // Pre-calculate layer masks
156
- const layerMasks = [];
157
- for (let i = 0; i < numLayers; i++) {
158
- const type = i === 0 ? 'foreground' : i === numLayers - 1 ? 'background' : 'middle';
159
- layerMasks.push(createGradientMask(imgWidth, imgHeight, type, i, numLayers));
160
- }
161
- // Generate each frame
162
- console.log(`[ParallaxEngine] Generating ${totalFrames} frames...`);
163
- for (let frameNum = 0; frameNum < totalFrames; frameNum++) {
164
- // Progress from -1 to 1 (centered animation)
165
- const progress = ((frameNum / (totalFrames - 1)) - 0.5) * 2;
166
- // Start with a base canvas (slightly larger for movement headroom)
167
- const canvasWidth = Math.floor(imgWidth * 1.2);
168
- const canvasHeight = Math.floor(imgHeight * 1.2);
169
- // Create composites array for all layers
170
- const composites = [];
171
- // Process each layer (background to foreground)
172
- for (let layerIdx = numLayers - 1; layerIdx >= 0; layerIdx--) {
173
- // Calculate movement for this layer
174
- // Background layers (higher index) move less, foreground layers move more
175
- const depthFactor = layerIdx / (numLayers - 1); // 0 = foreground, 1 = background
176
- const layerSpeed = params.bgSpeed + (params.fgSpeed - params.bgSpeed) * (1 - depthFactor);
177
- let offsetX = 0;
178
- let offsetY = 0;
179
- let scale = 1.0;
180
- switch (config.direction) {
181
- case 'left':
182
- offsetX = Math.round(progress * layerSpeed * imgWidth);
183
- break;
184
- case 'right':
185
- offsetX = Math.round(-progress * layerSpeed * imgWidth);
186
- break;
187
- case 'up':
188
- offsetY = Math.round(progress * layerSpeed * imgHeight);
189
- break;
190
- case 'down':
191
- offsetY = Math.round(-progress * layerSpeed * imgHeight);
192
- break;
193
- case 'zoomIn':
194
- scale = 1 + (progress + 1) * params.scaleRange * (1 - depthFactor * 0.5);
195
- break;
196
- case 'zoomOut':
197
- scale = 1 + (1 - progress) * params.scaleRange * (1 - depthFactor * 0.5);
198
- break;
93
+ async function generateDepthParallax(imagePath, outputPath, config, duration, fps) {
94
+ return new Promise((resolve, reject) => {
95
+ const options = JSON.stringify({
96
+ direction: config.direction,
97
+ intensity: config.intensity,
98
+ duration,
99
+ fps,
100
+ layerCount: config.layerCount || 3,
101
+ zoom: config.zoom || 'none',
102
+ });
103
+ const pyExe = pythonPath || 'python3';
104
+ console.log(`[ParallaxEngine] Running depth estimation: ${pyExe} ${DEPTH_SCRIPT_PATH}`);
105
+ const proc = (0, child_process_1.spawn)(pyExe, [DEPTH_SCRIPT_PATH, imagePath, outputPath, options], {
106
+ stdio: ['pipe', 'pipe', 'pipe'],
107
+ });
108
+ let stdout = '';
109
+ let stderr = '';
110
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
111
+ proc.stderr.on('data', (data) => {
112
+ stderr += data.toString();
113
+ // Log progress messages
114
+ const lines = data.toString().split('\n');
115
+ for (const line of lines) {
116
+ if (line.trim()) {
117
+ console.log(`[DepthParallax] ${line.trim()}`);
199
118
  }
200
- // Apply mask to source image to create this layer
201
- const mask = layerMasks[layerIdx];
202
- const layerData = Buffer.alloc(imgWidth * imgHeight * 4);
203
- for (let i = 0; i < imgWidth * imgHeight; i++) {
204
- const srcIdx = i * 4;
205
- const maskAlpha = mask[srcIdx + 3];
206
- layerData[srcIdx] = sourceData[srcIdx]; // R
207
- layerData[srcIdx + 1] = sourceData[srcIdx + 1]; // G
208
- layerData[srcIdx + 2] = sourceData[srcIdx + 2]; // B
209
- layerData[srcIdx + 3] = maskAlpha; // A from mask
119
+ }
120
+ });
121
+ proc.on('close', (code) => {
122
+ if (code !== 0) {
123
+ console.error('[ParallaxEngine] Depth parallax failed:', stderr);
124
+ reject(new Error(`Depth parallax failed with code ${code}: ${stderr}`));
125
+ return;
126
+ }
127
+ try {
128
+ const result = JSON.parse(stdout);
129
+ if (result.success) {
130
+ console.log('[ParallaxEngine] Depth parallax completed successfully');
131
+ resolve();
210
132
  }
211
- // Create the layer image with transformations
212
- let layerImage = sharp(layerData, {
213
- raw: { width: imgWidth, height: imgHeight, channels: 4 },
214
- });
215
- // Apply scale if needed
216
- if (scale !== 1.0) {
217
- const scaledWidth = Math.round(imgWidth * scale);
218
- const scaledHeight = Math.round(imgHeight * scale);
219
- layerImage = layerImage.resize(scaledWidth, scaledHeight, { fit: 'fill' });
220
- // Extract center portion back to original size
221
- const cropX = Math.floor((scaledWidth - imgWidth) / 2);
222
- const cropY = Math.floor((scaledHeight - imgHeight) / 2);
223
- layerImage = layerImage.extract({
224
- left: Math.max(0, cropX),
225
- top: Math.max(0, cropY),
226
- width: Math.min(imgWidth, scaledWidth),
227
- height: Math.min(imgHeight, scaledHeight),
228
- });
133
+ else {
134
+ reject(new Error(result.error || 'Unknown error'));
229
135
  }
230
- const layerBuffer = await layerImage.ensureAlpha().raw().toBuffer();
231
- // Calculate position on canvas
232
- const baseX = Math.floor((canvasWidth - imgWidth) / 2);
233
- const baseY = Math.floor((canvasHeight - imgHeight) / 2);
234
- composites.push({
235
- input: layerBuffer,
236
- raw: { width: imgWidth, height: imgHeight, channels: 4 },
237
- left: baseX + offsetX,
238
- top: baseY + offsetY,
239
- blend: 'over',
240
- });
241
- }
242
- // Create the final frame by compositing all layers
243
- let frame = sharp({
244
- create: {
245
- width: canvasWidth,
246
- height: canvasHeight,
247
- channels: 4,
248
- background: { r: 0, g: 0, b: 0, alpha: 255 },
249
- },
250
- });
251
- // Add each layer
252
- for (const comp of composites) {
253
- frame = sharp(await frame.png().toBuffer()).composite([{
254
- input: comp.input,
255
- raw: comp.raw,
256
- left: comp.left,
257
- top: comp.top,
258
- blend: comp.blend,
259
- }]);
260
136
  }
261
- // Extract the center portion at original size
262
- const cropX = Math.floor((canvasWidth - imgWidth) / 2);
263
- const cropY = Math.floor((canvasHeight - imgHeight) / 2);
264
- const framePath = path.join(tempDir, `frame_${String(frameNum).padStart(5, '0')}.png`);
265
- await frame
266
- .extract({ left: cropX, top: cropY, width: imgWidth, height: imgHeight })
267
- .png()
268
- .toFile(framePath);
269
- if (frameNum % Math.max(1, Math.floor(totalFrames / 10)) === 0) {
270
- console.log(`[ParallaxEngine] Frame ${frameNum + 1}/${totalFrames}`);
137
+ catch {
138
+ // If no JSON output, assume success if file exists
139
+ if (fs.existsSync(outputPath)) {
140
+ resolve();
141
+ }
142
+ else {
143
+ reject(new Error('Depth parallax failed: no output file'));
144
+ }
271
145
  }
272
- }
273
- // Combine frames into video using FFmpeg
274
- console.log('[ParallaxEngine] Combining frames into video...');
275
- const inputPattern = path.join(tempDir, 'frame_%05d.png');
276
- await new Promise((resolve, reject) => {
277
- const command = (0, ffmpeg_wrapper_1.createCommand)();
278
- command
279
- .input(inputPattern)
280
- .inputOptions([`-framerate ${fps}`])
281
- .outputOptions([
282
- '-c:v libx264',
283
- '-crf 18',
284
- '-preset medium',
285
- '-pix_fmt yuv420p',
286
- '-movflags +faststart',
287
- ])
288
- .format('mp4')
289
- .output(outputPath);
290
- command.on('start', (cmd) => {
291
- console.log(`[ParallaxEngine] FFmpeg: ${cmd}`);
292
- });
293
- command.on('error', (err) => reject(err));
294
- command.on('end', () => resolve());
295
- command.run();
296
146
  });
297
- console.log('[ParallaxEngine] Parallax video created successfully');
147
+ proc.on('error', (err) => {
148
+ console.error('[ParallaxEngine] Failed to spawn Python process:', err);
149
+ reject(err);
150
+ });
151
+ });
152
+ }
153
+ // Try to load sharp (optional dependency)
154
+ let sharp = null;
155
+ async function loadSharp() {
156
+ if (sharp !== null)
157
+ return true;
158
+ try {
159
+ sharp = (await Promise.resolve().then(() => __importStar(require('sharp')))).default;
160
+ console.log('[ParallaxEngine] Sharp loaded successfully');
161
+ return true;
298
162
  }
299
- finally {
300
- // Cleanup temp directory
301
- if (fs.existsSync(tempDir)) {
302
- fs.rmSync(tempDir, { recursive: true, force: true });
303
- }
163
+ catch (error) {
164
+ console.warn('[ParallaxEngine] Sharp not available:', error.message);
165
+ return false;
304
166
  }
305
167
  }
306
168
  /**
307
- * Fallback: Simple Ken Burns effect using FFmpeg zoompan
308
- * Used when sharp is not available
169
+ * Ken Burns effect using zoompan with optimized settings
170
+ * Uses higher zoom and stronger movement for visible parallax
309
171
  */
310
172
  async function generateSimpleKenBurns(imagePath, outputPath, config, duration, fps, width, height) {
311
- const params = getIntensityParams(config.intensity);
312
173
  const totalFrames = Math.ceil(duration * fps);
174
+ // Movement intensity - increased for more visible effect
175
+ const intensityMultiplier = config.intensity === 'subtle' ? 0.7 :
176
+ config.intensity === 'dramatic' ? 1.6 : 1.0;
177
+ // Higher base zoom for smoother subpixel movement
178
+ const baseZoom = 1.3;
179
+ // Stronger zoom change
180
+ const zoomDelta = 0.12 * intensityMultiplier;
181
+ // Stronger pan movement (percentage of available space)
182
+ const panSpeed = 0.15 * intensityMultiplier;
313
183
  let zoomExpr;
314
184
  let xExpr;
315
185
  let yExpr;
316
- const baseZoom = 1.15;
317
- const zoomDelta = params.scaleRange;
186
+ // Smooth easing using sine function for natural movement
187
+ // on = frame number, goes from 0 to totalFrames
188
+ // Using smooth start/end with sine easing
189
+ const progress = `on/${totalFrames}`;
190
+ const easeInOut = `(1-cos(${progress}*PI))/2`;
318
191
  switch (config.direction) {
319
192
  case 'left':
320
- zoomExpr = `${baseZoom}+${zoomDelta}*on/${totalFrames}`;
321
- xExpr = `(iw-ow)/2-${params.fgSpeed}*iw*on/${totalFrames}`;
193
+ // Pan from right to left
194
+ zoomExpr = `${baseZoom}+${zoomDelta}*${progress}`;
195
+ xExpr = `(iw-ow)/2*(1+${panSpeed})-${panSpeed}*(iw-ow)/2*${easeInOut}*2`;
322
196
  yExpr = `(ih-oh)/2`;
323
197
  break;
324
198
  case 'right':
325
- zoomExpr = `${baseZoom}+${zoomDelta}*on/${totalFrames}`;
326
- xExpr = `(iw-ow)/2+${params.fgSpeed}*iw*on/${totalFrames}`;
199
+ // Pan from left to right
200
+ zoomExpr = `${baseZoom}+${zoomDelta}*${progress}`;
201
+ xExpr = `(iw-ow)/2*(1-${panSpeed})+${panSpeed}*(iw-ow)/2*${easeInOut}*2`;
327
202
  yExpr = `(ih-oh)/2`;
328
203
  break;
329
204
  case 'up':
330
- zoomExpr = `${baseZoom}+${zoomDelta}*on/${totalFrames}`;
205
+ // Pan from bottom to top
206
+ zoomExpr = `${baseZoom}+${zoomDelta}*${progress}`;
331
207
  xExpr = `(iw-ow)/2`;
332
- yExpr = `(ih-oh)/2-${params.fgSpeed}*ih*on/${totalFrames}`;
208
+ yExpr = `(ih-oh)/2*(1+${panSpeed})-${panSpeed}*(ih-oh)/2*${easeInOut}*2`;
333
209
  break;
334
210
  case 'down':
335
- zoomExpr = `${baseZoom}+${zoomDelta}*on/${totalFrames}`;
211
+ // Pan from top to bottom
212
+ zoomExpr = `${baseZoom}+${zoomDelta}*${progress}`;
336
213
  xExpr = `(iw-ow)/2`;
337
- yExpr = `(ih-oh)/2+${params.fgSpeed}*ih*on/${totalFrames}`;
214
+ yExpr = `(ih-oh)/2*(1-${panSpeed})+${panSpeed}*(ih-oh)/2*${easeInOut}*2`;
338
215
  break;
339
216
  case 'zoomIn':
340
- zoomExpr = `${baseZoom}+${zoomDelta * 3}*on/${totalFrames}`;
217
+ zoomExpr = `${baseZoom}+${zoomDelta * 2.5}*${easeInOut}`;
341
218
  xExpr = `(iw-ow)/2`;
342
219
  yExpr = `(ih-oh)/2`;
343
220
  break;
344
221
  case 'zoomOut':
345
- zoomExpr = `${baseZoom + zoomDelta * 3}-${zoomDelta * 3}*on/${totalFrames}`;
222
+ zoomExpr = `${baseZoom + zoomDelta * 2.5}-${zoomDelta * 2.5}*${easeInOut}`;
346
223
  xExpr = `(iw-ow)/2`;
347
224
  yExpr = `(ih-oh)/2`;
348
225
  break;
@@ -351,12 +228,13 @@ async function generateSimpleKenBurns(imagePath, outputPath, config, duration, f
351
228
  xExpr = `(iw-ow)/2`;
352
229
  yExpr = `(ih-oh)/2`;
353
230
  }
354
- const zoompanFilter = `zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=${width}x${height}:fps=${fps}`;
231
+ // zoompan filter with smooth interpolation
232
+ const filterChain = `zoompan=z='${zoomExpr}':x='${xExpr}':y='${yExpr}':d=${totalFrames}:s=${width}x${height}:fps=${fps}`;
355
233
  return new Promise((resolve, reject) => {
356
234
  const command = (0, ffmpeg_wrapper_1.createCommand)(imagePath);
357
235
  command
358
236
  .inputOptions(['-loop 1'])
359
- .videoFilters([zoompanFilter])
237
+ .videoFilters([filterChain])
360
238
  .outputOptions([
361
239
  `-t ${duration}`,
362
240
  '-pix_fmt yuv420p',
@@ -443,6 +321,7 @@ class ParallaxEngine {
443
321
  /**
444
322
  * Generate parallax video from a single image
445
323
  * Main entry point for creating parallax effect videos
324
+ * Uses AI depth estimation for true parallax, falls back to Ken Burns
446
325
  */
447
326
  async generateParallaxVideo(imagePath, outputPath, config, duration, fps = 24, _videoCodec = 'libx264', _quality = 'high', _customCRF) {
448
327
  await this.initialize();
@@ -453,15 +332,17 @@ class ParallaxEngine {
453
332
  const width = Math.floor(dimensions.width / 2) * 2; // Ensure even
454
333
  const height = Math.floor(dimensions.height / 2) * 2;
455
334
  console.log(`[ParallaxEngine] Output size: ${width}x${height}`);
335
+ // Check if Python depth estimation is available
336
+ const depthAvailable = await checkPythonDepth();
456
337
  try {
457
- if (this.sharpAvailable) {
458
- // Use true multi-layer parallax
459
- console.log('[ParallaxEngine] Using multi-layer parallax mode');
460
- await generateLayeredParallax(imagePath, outputPath, { ...config, layerCount: config.layerCount || 3 }, duration, fps, width, height);
338
+ if (depthAvailable) {
339
+ // Use AI depth-based parallax for true layer separation
340
+ console.log('[ParallaxEngine] Using AI depth-based parallax (Depth Anything V2)');
341
+ await generateDepthParallax(imagePath, outputPath, config, duration, fps);
461
342
  }
462
343
  else {
463
- // Fallback to simple Ken Burns
464
- console.log('[ParallaxEngine] Using Ken Burns fallback (sharp not available)');
344
+ // Fall back to Ken Burns effect
345
+ console.log('[ParallaxEngine] Using Ken Burns effect (depth estimation not available)');
465
346
  await generateSimpleKenBurns(imagePath, outputPath, config, duration, fps, width, height);
466
347
  }
467
348
  const buffer = fs.readFileSync(outputPath);
@@ -470,6 +351,19 @@ class ParallaxEngine {
470
351
  }
471
352
  catch (error) {
472
353
  console.error('[ParallaxEngine] Error generating parallax:', error);
354
+ // If depth parallax failed, try Ken Burns as fallback
355
+ if (depthAvailable) {
356
+ console.log('[ParallaxEngine] Depth parallax failed, falling back to Ken Burns');
357
+ try {
358
+ await generateSimpleKenBurns(imagePath, outputPath, config, duration, fps, width, height);
359
+ const buffer = fs.readFileSync(outputPath);
360
+ return buffer;
361
+ }
362
+ catch (fallbackError) {
363
+ console.error('[ParallaxEngine] Ken Burns fallback also failed:', fallbackError);
364
+ throw fallbackError;
365
+ }
366
+ }
473
367
  throw error;
474
368
  }
475
369
  }