Skip to main content
Glama
affogato-client.jsβ€’24.9 kB
import { Client } from '@notionhq/client'; import { CharacterConsistencyManager } from './character-consistency.js'; import https from 'https'; import http from 'http'; import fs from 'fs'; import path from 'path'; import FormData from 'form-data'; export class AffogatoClient { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = 'api.rendernet.ai'; this.outputPath = './generated_assets'; this.consistencyManager = new CharacterConsistencyManager(); this.currentCharacterTraits = null; // Retry configuration this.maxRetries = 3; this.baseDelay = 1000; // 1 second this.maxDelay = 30000; // 30 seconds // Authentication state tracking this.authFailureCount = 0; this.lastAuthFailure = null; } async makeApiRequest(endpoint, data, method = 'POST', retryCount = 0) { const postData = data ? JSON.stringify(data) : ''; // Log the request for debugging console.log(`πŸ”— 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); // Reset auth failure count on successful request if (this.authFailureCount > 0) { console.log(`βœ… Authentication recovered after ${this.authFailureCount} failures`); this.authFailureCount = 0; this.lastAuthFailure = null; } return result; } catch (error) { // Handle authentication failures specifically if (this.isAuthenticationError(error)) { this.authFailureCount++; this.lastAuthFailure = new Date(); console.error(`πŸ”‘ Authentication failure #${this.authFailureCount}: ${error.message}`); // If too many auth failures, suggest credential refresh if (this.authFailureCount >= 3) { const errorMsg = `Authentication failed ${this.authFailureCount} times. ` + `API key may be invalid or expired. Please check: ` + `https://app.rendernet.ai/account to regenerate your API key.`; throw new Error(errorMsg); } } // Retry logic for retryable errors if (retryCount < this.maxRetries && this.isRetryableError(error)) { const delay = this.calculateDelay(retryCount); console.log(`⏳ Retrying in ${delay}ms due to: ${error.message}`); await this.delay(delay); return this.makeApiRequest(endpoint, data, method, retryCount + 1); } // Add detailed error context const enhancedError = new Error( `API request failed after ${retryCount + 1} attempts: ${error.message}\n` + `Endpoint: ${method} https://${this.baseUrl}${endpoint}\n` + `Auth failures: ${this.authFailureCount}\n` + `Last auth failure: ${this.lastAuthFailure || 'None'}` ); enhancedError.originalError = error; enhancedError.retryCount = retryCount; enhancedError.authFailureCount = this.authFailureCount; throw enhancedError; } } async _executeRequest(endpoint, postData, method) { return new Promise((resolve, reject) => { const options = { hostname: this.baseUrl, path: endpoint, method: method, headers: { 'X-API-KEY': this.apiKey, 'Content-Type': 'application/json', 'User-Agent': 'AffogatoClient/1.0.0', ...(postData && { 'Content-Length': Buffer.byteLength(postData) }) }, timeout: 30000 // 30 second timeout }; const req = https.request(options, (res) => { let responseData = ''; res.on('data', (chunk) => responseData += chunk); res.on('end', () => { try { const parsedData = JSON.parse(responseData); // Log response details for debugging console.log(` πŸ“¨ Response: ${res.statusCode} (${responseData.length} bytes)`); if (res.statusCode >= 200 && res.statusCode < 300) { resolve(parsedData.data || parsedData); } else { // Enhanced error message with response details const errorMsg = parsedData.err?.message || parsedData.message || parsedData.error || `HTTP ${res.statusCode}`; const errorCode = parsedData.err?.code || 'UNKNOWN'; const error = new Error(`${errorMsg} (${errorCode})`); error.statusCode = res.statusCode; error.errorCode = errorCode; error.responseData = parsedData; reject(error); } } catch (parseError) { const error = new Error(`Parse error: ${parseError.message}. Response: ${responseData.slice(0, 500)}`); error.parseError = true; error.rawResponse = responseData; reject(error); } }); }); req.on('error', (error) => { const networkError = new Error(`Network error: ${error.message}`); networkError.networkError = true; networkError.originalError = error; reject(networkError); }); req.on('timeout', () => { const timeoutError = new Error('Request timeout after 30 seconds'); timeoutError.timeout = true; req.destroy(); reject(timeoutError); }); if (postData) req.write(postData); req.end(); }); } // Upload an asset (image, audio, video) async uploadAsset(filePath) { console.log(`πŸ“€ Uploading asset: ${path.basename(filePath)}`); return new Promise((resolve, reject) => { const form = new FormData(); form.append('file', fs.createReadStream(filePath)); const options = { hostname: this.baseUrl, path: '/pub/v1/assets/v2/upload', method: 'POST', headers: { 'X-API-KEY': this.apiKey, ...form.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 >= 200 && res.statusCode < 300) { console.log(`βœ… Asset uploaded: ${parsedData.data.id}`); resolve(parsedData.data); } else { reject(new Error(`HTTP ${res.statusCode}: ${parsedData.message || responseData}`)); } } catch (error) { reject(new Error(`Parse error: ${error.message}`)); } }); }); req.on('error', reject); form.pipe(req); }); } // Create a character from an uploaded asset async createCharacter(assetId, characterName, characterType = 'Custom', prompt = '') { console.log(`🎭 Creating character: ${characterName}`); const data = { asset_id: assetId, character_type: characterType, name: characterName, prompt: prompt || `A detailed portrait of ${characterName}`, mode: 'balanced' }; const response = await this.makeApiRequest('/pub/v1/characters', data); console.log(`βœ… Character created: ${response.id}`); return response; } // Set character traits for consistent generation setCharacterTraits(traits) { this.currentCharacterTraits = traits; } // Generate puppet images with emotions and 4-point views using consistency system async generatePuppetImages(characterId, emotions, characterTraits = null) { // Normalize emotions parameter to handle null/undefined const emotionList = Array.isArray(emotions) && emotions.length ? emotions : ['happy', 'sad', 'excited', 'angry', 'surprised', 'neutral', 'thoughtful']; if (characterTraits) { this.currentCharacterTraits = characterTraits; } console.log(`🎨 Generating puppet images for character: ${characterId}`); const generatedImages = []; // Define 4-point views to generate for each character const views = [ { name: 'front', description: 'front view, facing camera directly' }, { name: 'left', description: 'left profile view, side angle showing left side of head' }, { name: 'right', description: 'right profile view, side angle showing right side of head' }, { name: 'back', description: 'rear view, back of head and body visible' } ]; // Generate all emotions with front view for (const emotion of emotionList) { console.log(` 🎭 Generating ${emotion} expression (front view)...`); try { const data = { aspect_ratio: '1:1', character: { character_id: characterId, mode: 'balanced' }, prompt: this.buildConsistentPrompt('emotion', emotion, 'front view, facing camera directly'), quality: 'Plus', steps: 30, cfg_scale: 9 }; const response = await this.makeApiRequest('/pub/v1/generations', [data]); if (response.media && response.media[0]) { generatedImages.push({ type: 'emotion', emotion: emotion, view: 'front', generation_id: response.generation_id, media_id: response.media[0].id, status: response.media[0].status }); console.log(` βœ… ${emotion} expression (front) initiated`); } // Rate limiting delay await this.delay(4000); } catch (error) { console.log(` ⚠️ ${emotion} expression failed: ${error.message}`); generatedImages.push({ type: 'emotion', emotion: emotion, view: 'front', status: 'failed', error: error.message }); } } // Generate 4-point views with neutral expression for (const view of views) { console.log(` πŸ“ Generating ${view.name} view (neutral)...`); try { const data = { aspect_ratio: '1:1', character: { character_id: characterId, mode: 'balanced' }, prompt: this.buildConsistentPrompt('view', 'neutral', view.description), quality: 'Plus', steps: 30, cfg_scale: 9 }; const response = await this.makeApiRequest('/pub/v1/generations', [data]); if (response.media && response.media[0]) { generatedImages.push({ type: 'view', emotion: 'neutral', view: view.name, generation_id: response.generation_id, media_id: response.media[0].id, status: response.media[0].status }); console.log(` βœ… ${view.name} view initiated`); } // Rate limiting delay await this.delay(4000); } catch (error) { console.log(` ⚠️ ${view.name} view failed: ${error.message}`); generatedImages.push({ type: 'view', emotion: 'neutral', view: view.name, status: 'failed', error: error.message }); } } return generatedImages; } // Generate a lipsync video async generateLipsyncVideo(characterId, audioAssetId, prompt = 'talking naturally') { console.log(`🎬 Generating lipsync video for character: ${characterId}`); const data = { aspect_ratio: '16:9', character: { character_id: characterId, mode: 'balanced' }, narrator: { audio_asset_id: audioAssetId }, prompt: { positive: `${prompt}, puppet character talking, clean background, high quality`, negative: 'nsfw, deformed, extra limbs, bad anatomy, text, worst quality' }, quality: 'Plus' }; try { const response = await this.makeApiRequest('/pub/v1/generations', [data]); console.log(`βœ… Lipsync video initiated: ${response.generation_id}`); return response; } catch (error) { console.error(`❌ Lipsync video failed: ${error.message}`); throw error; } } // Convert image to video using video_anyone async generateVideoFromImage(mediaId, prompt, duration = 5) { console.log(`πŸŽ₯ Converting image to video: ${mediaId}`); const data = { video_anyone: { media_id: mediaId, prompt: prompt.slice(0, 500), // Max 500 characters duration: duration }, aspect_ratio: '16:9', quality: 'Plus' }; try { const response = await this.makeApiRequest('/pub/v1/generations', [data]); console.log(`βœ… Video generation initiated: ${response.generation_id}`); return response; } catch (error) { console.error(`❌ Video generation failed: ${error.message}`); throw error; } } // Check generation status async getGenerationStatus(generationId) { try { const response = await this.makeApiRequest(`/pub/v1/generations/${generationId}`, null, 'GET'); return response; } catch (error) { console.error(`❌ Failed to get generation status: ${error.message}`); throw error; } } // Get media details async getMedia(mediaId) { try { const response = await this.makeApiRequest(`/pub/v1/media/${mediaId}`, null, 'GET'); return response; } catch (error) { console.error(`❌ Failed to get media: ${error.message}`); throw error; } } // Download generated media async downloadMedia(url, outputPath) { 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) => { fs.unlink(outputPath, () => {}); reject(error); }); }).on('error', reject); }); } // List available models async getModels() { return await this.makeApiRequest('/pub/v1/models', null, 'GET'); } // List available styles async getStyles() { return await this.makeApiRequest('/pub/v1/styles', null, 'GET'); } // List available voices async getVoices() { return await this.makeApiRequest('/pub/v1/voices', null, 'GET'); } // List characters async getCharacters() { return await this.makeApiRequest('/pub/v1/characters', null, 'GET'); } // Build consistent prompt using character traits and template system buildConsistentPrompt(type, emotion, viewDescription) { if (!this.currentCharacterTraits) { // Fallback to legacy system if no traits provided return { positive: this.buildPuppetPrompt(emotion, viewDescription), negative: this.buildNegativePrompt() }; } // Use consistency manager to build prompt with character traits const promptType = type === 'emotion' ? 'establishingShot' : 'profileViews'; const templateVariables = { 'VIEW_ANGLE': viewDescription, 'EMOTION_STATE': emotion, 'EMOTION_SPECIFIC_DETAILS': this.getEmotionSpecificDetails(emotion) }; return this.consistencyManager.buildCharacterPrompt( this.currentCharacterTraits, promptType, templateVariables ); } // Get emotion-specific details for enhanced prompts getEmotionSpecificDetails(emotion) { const emotionDetails = { 'happy': 'mouth slightly open in smile position, eyes bright and alert, eyebrows raised', 'sad': 'mouth downturned, eyes drooping, eyebrows lowered', 'excited': 'mouth wide open, eyes wide, eyebrows raised high, bouncy posture', 'angry': 'mouth in firm line or slightly open, eyes narrowed, eyebrows lowered', 'surprised': 'mouth open in O-shape, eyes wide, eyebrows raised high', 'neutral': 'mouth in relaxed position, eyes forward, eyebrows in natural position', 'thoughtful': 'mouth slightly pursed, eyes looking up or to side, one eyebrow raised' }; return emotionDetails[emotion] || 'natural puppet expression'; } // Build comprehensive puppet prompt with authentic mechanics (legacy fallback) buildPuppetPrompt(emotion, viewDescription) { const basePrompt = `realistic puppet character, Muppet-style construction, waist-up view, ${viewDescription}, ${emotion} expression, black contrasting background`; const puppetMechanics = [ 'felt or foam puppet material with visible texture', 'mechanical puppet mouth that can open and close', 'consistent finger count on each hand', 'puppet hand sticks or internal hand controls visible', 'mechanical eyebrows that can move up and down', 'no human-like facial movements or expressions', 'no lip movement or realistic mouth animation', 'controlled eye positioning without human-like movement', 'bouncy puppet-like posture and positioning', 'authentic puppet construction details', 'consistent puppet proportions and character design', 'consistent inner mouth color and tongue texture', 'proper hair or fur definition with puppet material texture', 'fabric seams and puppet construction elements visible', 'professional studio lighting on black background', 'sharp focus with puppet material textures clearly defined' ]; return `${basePrompt}, ${puppetMechanics.join(', ')}, high quality 4K puppet photography`; } // Build negative prompt to avoid unwanted elements buildNegativePrompt() { return [ 'nsfw', 'deformed', 'extra limbs', 'bad anatomy', 'distorted face', 'human facial expressions', 'realistic human skin', 'lip movement', 'cartoon style', 'animation style', 'disney style', 'anime style', 'unrealistic eye movement', 'human-like expressions', 'smooth skin', 'wrong number of fingers', 'extra fingers', 'missing fingers', 'text', 'watermark', 'worst quality', 'jpeg artifacts', 'blurry', 'duplicate', 'morbid', 'mutilated', 'poorly drawn hands', 'poorly drawn face', 'mutation', 'deformed', 'ugly', 'bad proportions', 'malformed limbs', 'missing arms', 'missing legs', 'extra arms', 'extra legs', 'fused fingers', 'too many fingers', 'long neck', 'cross-eyed', 'mutated hands', 'polar lowres', 'bad body', 'bad face', 'bad teeth', 'bad eyes', 'bad limbs', 'bad fingers' ].join(', '); } // Enhanced error classification methods isAuthenticationError(error) { return error.statusCode === 401 || error.errorCode === 'GN04' || error.message.toLowerCase().includes('invalid credentials') || error.message.toLowerCase().includes('unauthorized'); } isRetryableError(error) { // Don't retry authentication errors after first failure if (this.isAuthenticationError(error)) { return false; } // Retry on network errors, timeouts, and 5xx server errors return error.networkError || error.timeout || (error.statusCode >= 500 && error.statusCode < 600) || error.statusCode === 429; // Rate limiting } calculateDelay(retryCount) { // Exponential backoff with jitter const exponentialDelay = this.baseDelay * Math.pow(2, retryCount); const jitter = Math.random() * 0.1 * exponentialDelay; // 10% jitter return Math.min(exponentialDelay + jitter, this.maxDelay); } // Utility function for delays async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Method to refresh API credentials async refreshCredentials(newApiKey) { if (!newApiKey) { throw new Error('New API key is required for credential refresh'); } console.log('πŸ”„ Refreshing API credentials...'); const oldKey = this.apiKey; this.apiKey = newApiKey; // Reset authentication state this.authFailureCount = 0; this.lastAuthFailure = null; try { // Test the new credentials with a simple API call await this.testAuthentication(); console.log('βœ… Credentials refreshed successfully'); } catch (error) { // Restore old credentials if test fails this.apiKey = oldKey; throw new Error(`Credential refresh failed: ${error.message}`); } } // Test authentication with a simple API call async testAuthentication() { console.log('πŸ§ͺ Testing API authentication...'); try { const models = await this.getModels(); console.log(`βœ… Authentication successful - found ${models.length || 0} models`); return true; } catch (error) { console.error(`❌ Authentication test failed: ${error.message}`); throw error; } } // Ensure output directory exists async ensureOutputDirectory(dir = this.outputPath) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bermingham85/mcp-puppet-pipeline'

If you have feedback or need assistance with the MCP directory API, please join our Discord server