import { generateVideo } from './video-generator'; import { renderVideo } from './render'; import { cleanupAssets } from './lib/cleaner'; import { resetInMemoryCache } from './lib/visual-fetcher'; import * as path from 'path'; import * as fs from 'fs'; const INPUT_DIR = path.join(process.cwd(), 'input'); const INPUT_SCRIPTS_FILE = path.join(INPUT_DIR, 'input-scripts.json'); interface VideoJob { id?: string; title: string; script: string; settings?: any; orientation?: 'portrait' | 'landscape'; voice?: string; showText?: boolean; defaultVideo?: string; } function parseArgs() { const args = process.argv.slice(2); const landscape = args.includes('--landscape'); return { landscape }; } function getEnvOrientation(): 'portrait' | 'landscape' { const envOrientation = process.env.VIDEO_ORIENTATION?.toLowerCase(); if (envOrientation === 'landscape' || envOrientation === 'portrait') { return envOrientation; } return 'portrait'; } function sanitizeFilename(name: string): string { return name.replace(/[^a-z0-9]/gi, '_').toLowerCase(); } async function main() { console.log('๐ŸŽฌ Video Generator CLI (Advanced Batch Mode)\n'); const { landscape } = parseArgs(); const envOrientation = getEnvOrientation(); // CLI flag overrides env var const globalOrientation = landscape ? 'landscape' : envOrientation; // console.log(`๐ŸŽฌ Orientation: ${globalOrientation.toUpperCase()}`); // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // STARTUP CLEANUP: Delete old audio, video files, AND CACHE // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // Check for --resume flag to skip cleanup const resume = process.argv.includes('--resume'); if (resume) { console.log('โฉ [STARTUP] Resume mode active: Skipping asset cleanup to preserve existing files.'); } else { console.log('๐Ÿงน [STARTUP] Cleaning old assets to avoid conflicts...'); const videoDir = path.join(process.cwd(), 'public', 'videos'); const audioDir = path.join(process.cwd(), 'public', 'audio'); await cleanupAssets([videoDir, audioDir]); // Clean video cache to ensure fresh high-quality results console.log('๐Ÿงน [STARTUP] Resetting .video-cache.json for fresh run'); resetInMemoryCache(); } let jobs: VideoJob[] = []; // Check for input-scripts.json if (fs.existsSync(INPUT_SCRIPTS_FILE)) { // console.log(`๐Ÿ“„ Found batch jobs file: ${INPUT_SCRIPTS_FILE}`); try { const content = fs.readFileSync(INPUT_SCRIPTS_FILE, 'utf-8'); jobs = JSON.parse(content); // console.log(`๐Ÿ“Š Loaded ${jobs.length} jobs to process.`); } catch (e: any) { // console.error(`โŒ Error parsing input-scripts.json: ${e.message}`); process.exit(1); } } else { // console.error('โŒ Error: No input found!'); // console.error(` Please create: ${INPUT_SCRIPTS_FILE}`); process.exit(1); } // Process Jobs for (let i = 0; i < jobs.length; i++) { const job = jobs[i]; // Determine orientation for this job: // 1. Job specific setting // 2. Global CLI flag // 3. Default to portrait const jobOrientation = job.orientation || globalOrientation; console.log(`\nโ–ถ๏ธ Processing Job ${i + 1}/${jobs.length}: "${job.title}" (${jobOrientation})`); console.log('โ”'.repeat(50)); // Determine output folder: output/{id} (or output/{title} if id missing) const folderName = sanitizeFilename(job.id || job.title || `video_${i + 1}`); const jobOutputDir = path.join(process.cwd(), 'output', folderName); if (!fs.existsSync(jobOutputDir)) { fs.mkdirSync(jobOutputDir, { recursive: true }); } // console.log(` ๐Ÿ“‚ Target: ${jobOutputDir}`); // SEGMENT MODE: Skip generation, just render/stitch const segmentOnly = process.argv.includes('--segment'); if (segmentOnly) { console.log('โฉ [SEGMENT MODE] Skipping generation, jumping to assembly...'); // Check if scene data exists const sceneDataPath = path.join(jobOutputDir, 'scene-data.json'); if (!fs.existsSync(sceneDataPath)) { console.error(`โŒ [SEGMENT MODE] No scene data found in "${jobOutputDir}". Run 'npm run generate' first.`); continue; } try { console.log(` ๐Ÿš€ Starting Render/Assembly...`); await renderVideo(jobOutputDir); console.log(` โœจ Job "${job.title}" Assembled.`); } catch (renderError: any) { console.error(` โŒ Assembly Failed: ${renderError.message}`); console.error(renderError); } continue; // Skip the rest of the loop for this job } // Save Script to file const scriptPath = path.join(jobOutputDir, 'script.txt'); fs.writeFileSync(scriptPath, job.script); // console.log(` ๐Ÿ“ Saved script to: ${scriptPath}`); // Generate try { const result = await generateVideo(job.script, jobOutputDir, { orientation: jobOrientation as 'portrait' | 'landscape', voice: job.voice, title: job.title, showText: job.showText !== false, defaultVideo: job.defaultVideo }); if (result.success) { // console.log(` โœ… Generation Successful for "${job.title}"`); // Render try { // console.log(` ๐Ÿš€ Starting Render...`); await renderVideo(jobOutputDir); console.log(` โœจ Job "${job.title}" Completed.`); } catch (renderError: any) { console.error(` โŒ Render Failed: ${renderError.message}`); } } else { console.error(` โŒ Generation Failed for "${job.title}":`, result.error); } } catch (genError: any) { console.error(` โŒ Critical Error in "${job.title}":`, genError.message); } finally { // Ensure cleanup happens even if generation failed const videoDir = path.join(process.cwd(), 'public', 'videos'); const audioDir = path.join(process.cwd(), 'public', 'audio'); await cleanupAssets([videoDir, audioDir]); } } // console.log('\n๐Ÿ Processing Complete.'); // console.log(`๐Ÿ“‚ Find your videos in the "output" folder.`); } main().catch(() => { }); // main().catch(console.error);