#!/usr/bin/env node
const https = require('https');
const fs = require('fs').promises;
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', 'config', '.env') });
class AffogatoVideoGenerator {
constructor() {
this.name = "Affogato Video Generator";
this.apiKey = process.env.AFFOGATO_API_KEY;
this.baseUrl = 'api.rendernet.ai';
this.outputPath = path.join(__dirname, '..', 'data', 'FINAL_DELIVERY', 'generated_assets');
}
async executeTask(input) {
console.log(`🎬 ${this.name} executing task...`);
const { sceneSpecs, characterId, audioFile, outputDirectory } = input;
if (outputDirectory) {
this.outputPath = outputDirectory;
}
await this.ensureOutputDirectory();
try {
// Generate scene images using existing Director character
const sceneImages = await this.generateSceneImages(sceneSpecs, characterId);
// Create lipsync videos for singing scenes
const lipsyncVideos = await this.createLipsyncVideos(sceneImages, audioFile);
// Download all generated assets
await this.downloadAssets(sceneImages, lipsyncVideos);
return {
success: true,
sceneImages,
lipsyncVideos,
outputPath: this.outputPath,
message: `Generated ${sceneImages.length} scenes and ${lipsyncVideos.length} lipsync videos`
};
} catch (error) {
console.error(`❌ ${this.name} failed:`, error.message);
return {
success: false,
error: error.message,
fallbackInstructions: this.createFallbackInstructions(sceneSpecs)
};
}
}
async generateSceneImages(sceneSpecs, characterId) {
console.log('🎨 Generating scene images with existing Director character...');
const sceneImages = [];
for (const scene of sceneSpecs) {
console.log(` 🎬 Scene ${scene.id}: ${scene.title}`);
try {
const imageData = {
positive: `Professional creative director, ${scene.description}, cinematic music video quality, 4K photorealistic`,
character_id: characterId,
mode: "strong",
aspect_ratio: "16:9",
quality: "Plus",
cfg_scale: 8,
steps: 25
};
const response = await this.makeApiRequest('/pub/v1/generations', imageData);
sceneImages.push({
id: scene.id,
title: scene.title,
image_url: response.media?.[0]?.url,
generation_id: response.generation_id,
status: 'generated',
filename: `scene_${scene.id.toString().padStart(2, '0')}.jpg`
});
console.log(` ✅ Scene ${scene.id} generated`);
await this.delay(5000); // Respect rate limits
} catch (error) {
console.log(` ⚠️ Scene ${scene.id} failed: ${error.message}`);
sceneImages.push({
id: scene.id,
title: scene.title,
status: 'failed',
error: error.message
});
}
}
return sceneImages;
}
async createLipsyncVideos(sceneImages, audioFile) {
console.log('🎭 Creating lipsync videos for singing scenes...');
const lipsyncVideos = [];
// Scenes that need lipsync (singing scenes)
const lipsyncSceneIds = [2, 3, 4, 5, 7, 8, 9, 11, 12];
const timings = {
2: { start: 15, end: 30 },
3: { start: 30, end: 45 },
4: { start: 45, end: 60 },
5: { start: 60, end: 75 },
7: { start: 90, end: 105 },
8: { start: 105, end: 120 },
9: { start: 120, end: 135 },
11: { start: 150, end: 195 },
12: { start: 195, end: 225 }
};
for (const sceneId of lipsyncSceneIds) {
const sceneImage = sceneImages.find(s => s.id === sceneId);
if (!sceneImage || sceneImage.status !== 'generated') {
console.log(` ⚠️ Scene ${sceneId} image not available, skipping lipsync`);
continue;
}
console.log(` 🎭 Scene ${sceneId} lipsync video...`);
try {
const lipsyncData = {
positive: `lipsync speaking video, professional director`,
image_url: sceneImage.image_url,
narrator: {
audio_file: audioFile,
start_time: timings[sceneId].start,
end_time: timings[sceneId].end
},
video_anyone: true,
quality: "Plus",
aspect_ratio: "16:9"
};
const response = await this.makeApiRequest('/pub/v1/generations', lipsyncData);
lipsyncVideos.push({
sceneId,
video_url: response.media?.[0]?.url,
generation_id: response.generation_id,
status: 'generated',
filename: `scene_${sceneId.toString().padStart(2, '0')}_lipsync.mp4`,
timing: timings[sceneId]
});
console.log(` ✅ Scene ${sceneId} lipsync created`);
await this.delay(8000); // Longer delay for video processing
} catch (error) {
console.log(` ⚠️ Scene ${sceneId} lipsync failed: ${error.message}`);
lipsyncVideos.push({
sceneId,
status: 'failed',
error: error.message
});
}
}
return lipsyncVideos;
}
async downloadAssets(sceneImages, lipsyncVideos) {
console.log('📥 Downloading generated assets...');
// Download scene images
for (const scene of sceneImages.filter(s => s.status === 'generated' && s.image_url)) {
try {
const filepath = path.join(this.outputPath, scene.filename);
await this.downloadFile(scene.image_url, filepath);
console.log(` ✅ Downloaded ${scene.filename}`);
} catch (error) {
console.log(` ⚠️ Failed to download ${scene.filename}: ${error.message}`);
}
}
// Download lipsync videos
for (const video of lipsyncVideos.filter(v => v.status === 'generated' && v.video_url)) {
try {
const filepath = path.join(this.outputPath, video.filename);
await this.downloadFile(video.video_url, filepath);
console.log(` ✅ Downloaded ${video.filename}`);
} catch (error) {
console.log(` ⚠️ Failed to download ${video.filename}: ${error.message}`);
}
}
}
async makeApiRequest(endpoint, data) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify(data);
const options = {
hostname: this.baseUrl,
path: endpoint,
method: 'POST',
headers: {
'X-API-KEY': this.apiKey,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => responseData += chunk);
res.on('end', () => {
try {
const parsedData = JSON.parse(responseData);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsedData.data || parsedData);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${parsedData.message || parsedData.error || responseData}`));
}
} catch (error) {
reject(new Error(`Parse error: ${error.message}. Response: ${responseData}`));
}
});
});
req.on('error', (error) => reject(new Error(`Request error: ${error.message}`)));
req.write(postData);
req.end();
});
}
async downloadFile(url, outputPath) {
return new Promise((resolve, reject) => {
const file = require('fs').createWriteStream(outputPath);
const request = url.startsWith('https:') ? https : require('http');
request.get(url, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve(outputPath);
});
file.on('error', (error) => {
require('fs').unlink(outputPath, () => {});
reject(error);
});
}).on('error', reject);
});
}
async ensureOutputDirectory() {
await fs.mkdir(this.outputPath, { recursive: true });
}
async delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
createFallbackInstructions(sceneSpecs) {
return `If API generation fails, use the manual generation guide with these exact prompts:\n${sceneSpecs.map(s => `Scene ${s.id}: ${s.description}`).join('\n')}`;
}
}
module.exports = AffogatoVideoGenerator;