← back to Animate Museum Posts
src/animateWithGrok.js
180 lines
import { createLumaAnimation } from './animateWithLuma.js';
import { createHuggingFaceAnimation } from './animateWithHuggingFace.js';
import { createRunwayAnimation } from './animateWithRunway.js';
import { createRealAIVideo } from './animateWithReplicate.js';
import { createKenBurnsAnimation } from './animateKenBurns.js';
import { addBrandingToVideo, addArtworkOverlay } from './addBranding.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Track animation service success rates
const STATS_FILE = path.join(__dirname, '..', 'logs', 'animation-stats.json');
function loadStats() {
try {
if (fs.existsSync(STATS_FILE)) {
return JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
}
} catch (e) {}
return {
luma: { attempts: 0, successes: 0 },
huggingface: { attempts: 0, successes: 0 },
runway: { attempts: 0, successes: 0 },
replicate: { attempts: 0, successes: 0 },
kenburns: { attempts: 0, successes: 0 },
static: { attempts: 0, successes: 0 }
};
}
function saveStats(stats) {
const logsDir = path.join(__dirname, '..', 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
}
function recordAttempt(service, success) {
const stats = loadStats();
// Ensure the service exists in stats
if (!stats[service]) {
stats[service] = { attempts: 0, successes: 0 };
}
stats[service].attempts++;
if (success) stats[service].successes++;
saveStats(stats);
}
/**
* Animation cascade - tries services in order of cost/quality
* Order: HuggingFace (free) → Runway (quality) → Replicate (reliable) → KenBurns (local) → Static
*/
export async function animateArtwork(imagePath, metadata) {
console.log('Starting animation cascade...');
console.log('Goal: Make artwork COME ALIVE with real animation!');
// 0. Skip Luma for now (single frame output issue)
// TODO: Fix Luma model to generate actual video
console.log('[0/5] Skipping Luma (output issues, using SVD instead)');
// 1. Try HuggingFace (free tier)
if (process.env.HUGGINGFACE_TOKEN) {
console.log('\n[1/5] Trying HuggingFace (free)...');
try {
const result = await withTimeout(
createHuggingFaceAnimation(imagePath, metadata),
60000,
'HuggingFace timeout'
);
if (result.isAnimated) {
recordAttempt('huggingface', true);
console.log('HuggingFace animation succeeded!');
return result;
}
recordAttempt('huggingface', false);
} catch (error) {
console.log('HuggingFace failed:', error.message);
recordAttempt('huggingface', false);
}
} else {
console.log('[1/5] Skipping HuggingFace (no token)');
}
// 2. Try RunwayML (premium quality)
if (process.env.RUNWAY_API_TOKEN) {
console.log('\n[2/5] Trying RunwayML (premium)...');
try {
const result = await withTimeout(
createRunwayAnimation(imagePath, metadata),
180000, // 3 min for Runway
'RunwayML timeout'
);
if (result.isAnimated) {
recordAttempt('runway', true);
console.log('RunwayML animation succeeded!');
return result;
}
recordAttempt('runway', false);
} catch (error) {
console.log('RunwayML failed:', error.message);
recordAttempt('runway', false);
}
} else {
console.log('[2/5] Skipping RunwayML (no token)');
}
// 3. Try Replicate (reliable, $0.02/video) - Real AI animation!
if (process.env.REPLICATE_API_TOKEN) {
console.log('\n[3/5] Trying Replicate SVD ($0.02) - Real AI animation...');
try {
const result = await withTimeout(
createRealAIVideo(imagePath, metadata),
200000, // 3+ minutes for AI video generation
'Replicate timeout'
);
if (result.isAnimated && result.animatedVideo) {
recordAttempt('replicate', true);
console.log('Replicate animation succeeded!');
// Add GoodQuestion.AI branding
await addBrandingToVideo(result.animatedVideo, metadata);
return result;
}
recordAttempt('replicate', false);
} catch (error) {
console.log('Replicate failed:', error.message);
recordAttempt('replicate', false);
}
} else {
console.log('[3/5] Skipping Replicate (no token)');
}
// 4. Try Ken Burns (local, free)
console.log('\n[4/5] Trying Ken Burns (local)...');
try {
const result = await createKenBurnsAnimation(imagePath, metadata);
if (result.isAnimated) {
recordAttempt('kenburns', true);
console.log('Ken Burns animation succeeded!');
return result;
}
recordAttempt('kenburns', false);
} catch (error) {
console.log('Ken Burns failed:', error.message);
recordAttempt('kenburns', false);
}
// 5. Final fallback - static image
console.log('\n[5/5] Using static image (all animations failed)');
recordAttempt('static', true);
return {
originalImage: imagePath,
isStatic: true,
artworkMetadata: metadata,
animationType: 'static'
};
}
// Helper function with timeout
async function withTimeout(promise, ms, message) {
return Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms))
]);
}
export function createSimpleAnimation(imagePath, metadata) {
return animateArtwork(imagePath, metadata);
}
export function getAnimationStats() {
return loadStats();
}