import { bundle } from '@remotion/bundler'; import { renderMedia, selectComposition, renderStill } from '@remotion/renderer'; import * as path from 'path'; import * as fs from 'fs'; import { execSync } from 'child_process'; import { cleanupAssets } from './lib/cleaner'; import { logError, logInfo, logWarn, resolveProjectPath, writeProgress } from './runtime'; import { createPipelineWorkspace } from './pipeline-workspace'; const console = { log: (...args: unknown[]) => logInfo(...args), warn: (...args: unknown[]) => logWarn(...args), error: (...args: unknown[]) => logError(...args), }; console.log('\n๐ŸŽฅ [RENDER] Module loaded (Segmented Mode)'); console.log(`๐ŸŽฅ [RENDER] Working directory: ${process.cwd()}`); interface Scene { sceneNumber: number; duration: number; visualDescription: string; voiceoverText: string; searchKeywords: string[]; visual?: { type: 'image' | 'video'; url: string; width: number; height: number; localPath?: string; videoDuration?: number; videoTrimAfterFrames?: number; } | null; audioPath?: string; } interface SceneData { scenes: Scene[]; totalDuration: number; style: string; orientation?: 'portrait' | 'landscape'; title?: string; showText?: boolean; assetNamespace?: string; } /** * Segmented Video Renderer * Renders video scene-by-scene for memory efficiency and crash recovery */ export const renderVideo = async (outputDir: string = resolveProjectPath('output')) => { const totalStartTime = Date.now(); console.log('\n'); console.log('โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ ๐ŸŽฅ SEGMENTED REMOTION RENDER PIPELINE โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); console.log(`\n๐ŸŽฅ [RENDER] Start time: ${new Date().toISOString()}`); let bundleLocation: string | undefined; let assetWorkspaceDir: string | undefined; let renderCompleted = false; const segmentsDir = path.join(outputDir, 'segments'); try { // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 1: LOAD SCENE DATA // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 1: LOADING SCENE DATA โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); const sceneDataPath = path.join(outputDir, 'scene-data.json'); if (!fs.existsSync(sceneDataPath)) { throw new Error(`Scene data file not found: ${sceneDataPath}`); } const fileContent = fs.readFileSync(sceneDataPath, 'utf8'); const sceneData: SceneData = JSON.parse(fileContent); assetWorkspaceDir = sceneData.assetNamespace ? resolveProjectPath('public', sceneData.assetNamespace) : createPipelineWorkspace(outputDir).workspaceDir; console.log(`๐Ÿ“‹ [RENDER] Loaded ${sceneData.scenes.length} scenes`); console.log(`๐Ÿ“‹ [RENDER] Total duration: ${sceneData.totalDuration}s`); const fps = 30; const isLandscape = sceneData.orientation === 'landscape'; const width = isLandscape ? 1920 : 1080; const height = isLandscape ? 1080 : 1350; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 2: BUNDLE REMOTION PROJECT (ONCE) // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 2: BUNDLING PROJECT (ONCE) โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); const bundleStart = Date.now(); const entryPoint = resolveProjectPath('remotion', 'index.ts'); if (!fs.existsSync(entryPoint)) { throw new Error(`Entry point not found: ${entryPoint}`); } console.log('๐Ÿ“ฆ [RENDER] Bundling with Webpack...'); bundleLocation = await bundle({ entryPoint, }); console.log(`โœ… [RENDER] Bundle complete in ${Date.now() - bundleStart}ms`); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 3: CREATE SEGMENTS DIRECTORY // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 3: PREPARING SEGMENTS DIR โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); if (!fs.existsSync(segmentsDir)) { fs.mkdirSync(segmentsDir, { recursive: true }); } // Check for existing segments (resume capability) const existingSegments = fs.readdirSync(segmentsDir) .filter(f => f.startsWith('segment_') && f.endsWith('.mp4')); if (existingSegments.length > 0) { console.log(`๐Ÿ“‚ [RENDER] Found ${existingSegments.length} existing segments (resume mode)`); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 4: RENDER THUMBNAIL (from first scene) // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 4: RENDERING THUMBNAIL โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); const thumbnailLocation = path.join(outputDir, 'thumbnail.jpg'); if (!fs.existsSync(thumbnailLocation)) { const firstScene = sceneData.scenes[0]; const thumbnailComposition = await selectComposition({ serveUrl: bundleLocation, id: 'SingleScene', inputProps: { scene: firstScene, isFirstScene: true, isLastScene: false, showText: sceneData.showText !== false, }, }); thumbnailComposition.width = width; thumbnailComposition.height = height; thumbnailComposition.durationInFrames = Math.round(firstScene.duration * fps); await renderStill({ composition: thumbnailComposition, serveUrl: bundleLocation, output: thumbnailLocation, frame: Math.min(30, Math.floor(thumbnailComposition.durationInFrames / 2)), inputProps: { scene: firstScene, isFirstScene: true, isLastScene: false, showText: sceneData.showText !== false, }, }); console.log(`โœ… [RENDER] Thumbnail saved: ${thumbnailLocation}`); } else { console.log(`โญ๏ธ [RENDER] Thumbnail already exists, skipping`); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 5: RENDER EACH SCENE AS SEGMENT // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 5: RENDERING SCENE SEGMENTS โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); const segments: string[] = []; let renderedCount = 0; let skippedCount = 0; for (let i = 0; i < sceneData.scenes.length; i++) { const scene = sceneData.scenes[i]; const segmentFilename = `segment_${String(i + 1).padStart(3, '0')}.mp4`; const segmentPath = path.join(segmentsDir, segmentFilename); // RESUME CAPABILITY: Skip if segment already exists if (fs.existsSync(segmentPath)) { const stats = fs.statSync(segmentPath); if (stats.size > 10000) { // At least 10KB console.log(`โญ๏ธ Scene ${i + 1}/${sceneData.scenes.length} - Already rendered, skipping`); segments.push(segmentPath); skippedCount++; continue; } } const sceneStart = Date.now(); const isFirstScene = i === 0; const isLastScene = i === sceneData.scenes.length - 1; const sceneDurationFrames = Math.round(scene.duration * fps); console.log(`\n๐ŸŽฌ Scene ${i + 1}/${sceneData.scenes.length}: "${scene.voiceoverText.substring(0, 40)}..."`); console.log(` Duration: ${scene.duration}s (${sceneDurationFrames} frames)`); try { // SAFETY CHECK: Ensure visual asset exists if (scene.visual && scene.visual.localPath) { const absVisualPath = resolveProjectPath('public', scene.visual.localPath); if (!fs.existsSync(absVisualPath)) { console.warn(`\n โš ๏ธ [WARNING] Visual asset missing: ${scene.visual.localPath}`); console.warn(` โš ๏ธ Switching to fallback background for this scene.`); // Remove visual object entirely to force gradient fallback in component scene.visual = null; } } // SAFETY CHECK: Ensure audio asset exists if (scene.audioPath) { // Check if it's an absolute path or relative // The scene-data.json usually has absolute paths for audio let absAudioPath = scene.audioPath; if (!path.isAbsolute(absAudioPath)) { absAudioPath = resolveProjectPath('public', scene.audioPath); } if (!fs.existsSync(absAudioPath)) { console.warn(`\n โš ๏ธ [WARNING] Audio asset missing: ${path.basename(scene.audioPath)}`); console.warn(` โš ๏ธ Process will continue without audio for this scene.`); // Remove audioPath to prevent 404 in Remotion scene.audioPath = undefined; } } // Select composition for this scene const composition = await selectComposition({ serveUrl: bundleLocation, id: 'SingleScene', inputProps: { scene, isFirstScene, isLastScene, showText: sceneData.showText !== false, }, }); // Override dimensions for this scene composition.width = width; composition.height = height; composition.durationInFrames = sceneDurationFrames; // Render the segment await renderMedia({ composition, serveUrl: bundleLocation, codec: 'h264', outputLocation: segmentPath, inputProps: { scene, isFirstScene, isLastScene, showText: sceneData.showText !== false, }, crf: 18, timeoutInMilliseconds: 300000, // 5 min per scene max concurrency: 1, chromiumOptions: { disableWebSecurity: true, }, onProgress: ({ progress }) => { const percent = Math.round(progress * 100); writeProgress(`\r โณ Progress: ${percent}%`); } }); const sceneTime = Date.now() - sceneStart; const stats = fs.statSync(segmentPath); console.log(`\n โœ… Saved: ${segmentFilename} (${(stats.size / 1024 / 1024).toFixed(2)} MB) in ${(sceneTime / 1000).toFixed(1)}s`); segments.push(segmentPath); renderedCount++; } catch (sceneError: any) { console.error(`\n โŒ Scene ${i + 1} failed: ${sceneError.message}`); console.error(` ๐Ÿ’ก Re-run to retry from this scene`); throw sceneError; // Stop and allow resume } } console.log(`\n๐Ÿ“Š [RENDER] Rendered: ${renderedCount}, Skipped: ${skippedCount}, Total: ${segments.length}`); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 6: CONCATENATE ALL SEGMENTS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 6: CONCATENATING SEGMENTS โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); const videoTitle = sceneData.title || 'video'; const safeFilename = videoTitle.replace(/[<>:"/\\|?*]/g, '').trim(); const finalOutput = path.join(outputDir, `${safeFilename}.mp4`); const missingSegments = segments.filter((segmentPath) => !fs.existsSync(segmentPath)); if (missingSegments.length > 0) { throw new Error(`Missing segment files before concat: ${missingSegments.join(', ')}`); } // Create FFmpeg concat list const concatListPath = path.join(segmentsDir, 'segments.txt'); const concatList = segments .map((segmentPath) => `file '${segmentPath.replace(/\\/g, '/')}'`) .join('\n'); fs.writeFileSync(concatListPath, concatList); console.log(`๐Ÿ”— [RENDER] Concatenating ${segments.length} segments...`); // Run FFmpeg concat (lossless copy) // Detect FFmpeg path - try to use ffmpeg-static let ffmpegPath = 'ffmpeg'; try { const ffmpegStatic = require('ffmpeg-static'); if (ffmpegStatic) ffmpegPath = ffmpegStatic; } catch (e) { console.log('โš ๏ธ [RENDER] Could not resolve ffmpeg-static, falling back to global command'); } console.log(` ๐Ÿ› ๏ธ Using FFmpeg: ${ffmpegPath}`); const ffmpegCmd = `"${ffmpegPath}" -y -f concat -safe 0 -i "${concatListPath}" -c copy "${finalOutput}"`; try { execSync(ffmpegCmd, { stdio: 'pipe' }); } catch (ffmpegError: any) { // Try with re-encoding if concat copy fails console.log('โš ๏ธ [RENDER] Lossless concat failed, trying with re-encode...'); const ffmpegReencodeCmd = `"${ffmpegPath}" -y -f concat -safe 0 -i "${concatListPath}" -c:v libx264 -crf 18 -c:a aac "${finalOutput}"`; try { execSync(ffmpegReencodeCmd, { stdio: 'pipe' }); } catch (reencodeError: any) { console.error(`โŒ [RENDER] FFmpeg failed: ${reencodeError.message}`); throw reencodeError; } } // Get final file info const finalStats = fs.statSync(finalOutput); const finalSizeMB = (finalStats.size / 1024 / 1024).toFixed(2); console.log(`โœ… [RENDER] Final video: ${finalOutput}`); console.log(`๐Ÿ“Š [RENDER] File size: ${finalSizeMB} MB`); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STEP 7: CLEANUP SEGMENTS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ STEP 7: CLEANING UP โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); // Delete segment files for (const segment of segments) { try { fs.unlinkSync(segment); } catch (e) { // Ignore cleanup errors } } // Delete concat list try { fs.unlinkSync(concatListPath); fs.rmdirSync(segmentsDir); } catch (e) { // Ignore cleanup errors } console.log(`๐Ÿงน [RENDER] Cleaned up ${segments.length} segment files`); renderCompleted = true; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // COMPLETE // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const totalTime = Date.now() - totalStartTime; console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ ๐ŸŽ‰ RENDER COMPLETE! โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); console.log(`\n๐Ÿ“Š [SUMMARY]`); console.log(` Total scenes: ${sceneData.scenes.length}`); console.log(` Total time: ${(totalTime / 1000 / 60).toFixed(1)} minutes`); console.log(` Output: ${finalOutput}`); console.log(` Size: ${finalSizeMB} MB`); console.log(` Duration: ${sceneData.totalDuration}s`); console.log('\n'); } catch (err: any) { const totalTime = Date.now() - totalStartTime; console.log('\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—'); console.log('โ•‘ โŒ RENDER FAILED! โ•‘'); console.log('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); const errorMessage = err.message || String(err); console.error(`\nโŒ [RENDER] Error: ${errorMessage}`); // Check segment progress if (fs.existsSync(segmentsDir)) { const completedSegments = fs.readdirSync(segmentsDir) .filter(f => f.startsWith('segment_') && f.endsWith('.mp4')); console.log(`\n๐Ÿ’พ [RECOVERY] ${completedSegments.length} segments saved to disk`); console.log(`๐Ÿ’ก [RECOVERY] Run again to resume from last completed segment`); } console.error(`\nโŒ [RENDER] Stack trace:\n${err.stack}`); console.log(`\nโฑ๏ธ Failed after: ${(totalTime / 1000).toFixed(1)}s`); throw err; } finally { // Cleanup assets regardless of success/failure await runCleanup(bundleLocation, renderCompleted ? assetWorkspaceDir : undefined); } }; const runCleanup = async (bundleLocation?: string, assetWorkspaceDir?: string) => { const dirsToClean: string[] = []; if (assetWorkspaceDir) { dirsToClean.push(assetWorkspaceDir); } if (bundleLocation) { dirsToClean.push(bundleLocation); } if (dirsToClean.length > 0) { await cleanupAssets(dirsToClean); } } if (require.main === module) { renderVideo(); }