import FormData from 'form-data';
import fs from 'fs';
import path from 'path';
import https from 'https';
import http from 'http';
export interface AffogatoUploadResponse {
id: string;
filename: string;
url?: string;
}
export interface AffogatoCharacterResponse {
character_id: string;
asset_id: string;
name: string;
description: string;
created_at: string;
}
export interface AffogatoGenerationResponse {
data: {
credits_remaining: number;
generation_id: string;
media: Array<{
id: string;
url: string;
dim: { height: number; width: number };
model: string;
status: string;
style?: string;
type: string;
}>;
result: string;
};
err: any;
}
export interface AffogatoNarratorConfig {
audio_file: string;
start_time: number;
end_time: number;
}
export class AffogatoClient {
private apiKey: string;
private baseUrl: string = 'api.rendernet.ai';
private outputPath: string = './generated_assets';
private maxRetries: number = 3;
private baseDelay: number = 1000;
constructor(apiKey: string) {
if (!apiKey) {
throw new Error('Affogato API key is required');
}
this.apiKey = apiKey;
}
async makeApiRequest(endpoint: string, data?: any, method: string = 'POST', retryCount: number = 0): Promise<any> {
const postData = data ? JSON.stringify(data) : '';
console.log(`🔗 Affogato API Request: ${method} https://${this.baseUrl}${endpoint} (attempt ${retryCount + 1}/${this.maxRetries + 1})`);
if (retryCount > 0) {
console.log(` ⏰ Retry attempt after ${this.calculateDelay(retryCount - 1)}ms delay`);
}
try {
const result = await this._executeRequest(endpoint, postData, method);
return result;
} catch (error: any) {
if (retryCount < this.maxRetries && this.isRetryableError(error)) {
const delay = this.calculateDelay(retryCount);
console.log(`⚠️ Request failed, retrying in ${delay}ms...`);
await this.delay(delay);
return this.makeApiRequest(endpoint, data, method, retryCount + 1);
}
throw error;
}
}
private async _executeRequest(endpoint: string, postData: string, method: string): Promise<any> {
return new Promise((resolve, reject) => {
const options = {
hostname: this.baseUrl,
path: endpoint,
method: method,
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 && res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsedData.data || parsedData);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${parsedData.message || parsedData.error || responseData}`));
}
} catch (error: any) {
reject(new Error(`Parse error: ${error.message}. Response: ${responseData}`));
}
});
});
req.on('error', (error) => reject(new Error(`Request error: ${error.message}`)));
if (postData) {
req.write(postData);
}
req.end();
});
}
private calculateDelay(retryCount: number): number {
return Math.min(this.baseDelay * Math.pow(2, retryCount), 30000);
}
private isRetryableError(error: any): boolean {
return error.message.includes('timeout') ||
error.message.includes('ECONNRESET') ||
error.message.includes('502') ||
error.message.includes('503');
}
private async delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async uploadAsset(filePath: string): Promise<AffogatoUploadResponse> {
return new Promise((resolve, reject) => {
const formData = new FormData();
const fileName = path.basename(filePath);
formData.append('file', fs.createReadStream(filePath), {
filename: fileName,
contentType: this.getMimeType(fileName)
});
const options = {
hostname: this.baseUrl,
path: '/pub/v1/assets/upload',
method: 'POST',
headers: {
'X-API-KEY': this.apiKey,
...formData.getHeaders()
}
};
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 && res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsedData.data || parsedData);
} else {
reject(new Error(`Upload failed: ${res.statusCode} ${parsedData.message || responseData}`));
}
} catch (error: any) {
reject(new Error(`Upload parse error: ${error.message}`));
}
});
});
req.on('error', (error) => reject(error));
formData.pipe(req);
});
}
async createCharacter(
assetId: string,
name: string,
description: string = '',
style: string = 'realistic'
): Promise<AffogatoCharacterResponse> {
const characterData = {
name,
description,
style,
asset_id: assetId
};
console.log(`🎭 Creating FaceLock character: ${name}`);
const response = await this.makeApiRequest('/pub/v1/characters', characterData);
return {
character_id: response.character_id || response.id,
asset_id: assetId,
name,
description,
created_at: new Date().toISOString()
};
}
async getCharacter(characterId: string): Promise<AffogatoCharacterResponse> {
const response = await this.makeApiRequest(`/pub/v1/characters/${characterId}`, null, 'GET');
return response;
}
async listCharacters(): Promise<AffogatoCharacterResponse[]> {
const response = await this.makeApiRequest('/pub/v1/characters', null, 'GET');
return Array.isArray(response) ? response : [response];
}
// Generate scene images with FaceLock character consistency (following guidance notes)
async generateSceneImage(
characterId: string,
scenePrompt: string,
aspectRatio: string = '16:9',
mode: string = 'strong'
): Promise<AffogatoGenerationResponse> {
console.log(`🎬 Generating scene with FaceLock character: ${characterId}`);
const imageData = {
positive: `${scenePrompt}, cinematic quality, 4K photorealistic`,
character_id: characterId,
mode: mode, // 'strong' for FaceLock consistency
aspect_ratio: aspectRatio,
quality: 'Plus',
cfg_scale: 8,
steps: 25
};
return await this.makeApiRequest('/pub/v1/generations', imageData);
}
// Generate lipsync video with narrator feature (following guidance notes)
async generateLipsyncVideo(
imageUrl: string,
narratorConfig: AffogatoNarratorConfig,
prompt: string = 'lipsync speaking video, professional',
aspectRatio: string = '16:9'
): Promise<AffogatoGenerationResponse> {
console.log(`🎭 Generating lipsync video with narrator feature`);
const lipsyncData = {
positive: prompt,
image_url: imageUrl,
narrator: {
audio_file: narratorConfig.audio_file,
start_time: narratorConfig.start_time,
end_time: narratorConfig.end_time
},
video_anyone: true, // Enable video generation
quality: 'Plus',
aspect_ratio: aspectRatio
};
return await this.makeApiRequest('/pub/v1/generations', lipsyncData);
}
// Generate video from image (following guidance notes)
async generateVideoFromImage(
imageUrl: string,
prompt: string,
duration: number = 5,
aspectRatio: string = '16:9'
): Promise<AffogatoGenerationResponse> {
console.log(`🎥 Converting image to video with Video Agent`);
const videoData = {
positive: prompt,
image_url: imageUrl,
video_anyone: true,
duration: duration,
quality: 'Plus',
aspect_ratio: aspectRatio
};
return await this.makeApiRequest('/pub/v1/generations', videoData);
}
// Get generation status
async getGenerationStatus(generationId: string): Promise<any> {
return await this.makeApiRequest(`/pub/v1/generations/${generationId}`, null, 'GET');
}
// Download generated media
async downloadMedia(url: string, outputPath: string): Promise<string> {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(outputPath);
const request = url.startsWith('https:') ? https : http;
request.get(url, (response) => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve(outputPath);
});
file.on('error', (error: any) => {
fs.unlink(outputPath, () => {});
reject(error);
});
}).on('error', reject);
});
}
private getMimeType(filename: string): string {
const ext = path.extname(filename).toLowerCase();
const mimeTypes: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.mp4': 'video/mp4',
'.mov': 'video/quicktime',
'.avi': 'video/x-msvideo',
'.mp3': 'audio/mpeg',
'.wav': 'audio/wav',
'.m4a': 'audio/mp4'
};
return mimeTypes[ext] || 'application/octet-stream';
}
}