← back to Animate Museum Posts
src/animateWithGrokFrames.js
206 lines
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import GIFEncoder from 'gif-encoder-2';
import sharp from 'sharp';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const GROK_API_KEY = process.env.GROK_API_KEY;
const GROK_API_URL = 'https://api.x.ai/v1';
export async function createAIAnimation(imagePath, metadata) {
try {
if (!GROK_API_KEY) {
throw new Error('GROK_API_KEY not configured');
}
console.log('Creating AI animation using Grok vision capabilities...');
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
// Ask Grok to describe how the painting should be animated
const animationResponse = await axios.post(
`${GROK_API_URL}/chat/completions`,
{
model: 'grok-2-vision-1212',
messages: [
{
role: 'system',
content: 'You are an expert at bringing classical paintings to life through animation.'
},
{
role: 'user',
content: [
{
type: 'text',
text: `Analyze this painting "${metadata.title}" and describe exactly how to animate it. Focus on one subtle movement that would bring it to life. For example: water rippling, flames flickering, fabric swaying, eyes blinking, or light changing. Be specific about what moves and how. Keep it under 50 words.`
},
{
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
]
}
],
max_tokens: 100,
temperature: 0.7
},
{
headers: {
'Authorization': `Bearer ${GROK_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
const animationConcept = animationResponse.data.choices[0].message.content;
console.log('Animation concept:', animationConcept);
// Since Grok can't generate actual video, we'll create a sophisticated animated GIF
// with effects that simulate the suggested animation
const outputDir = path.join(__dirname, '..', 'output');
const timestamp = new Date().getTime();
const outputPath = path.join(outputDir, `ai_animated_${timestamp}.gif`);
const width = 500;
const height = 500;
const frameCount = 20;
// Load and prepare the base image
const baseImage = await sharp(imagePath)
.resize(width, height, { fit: 'cover' })
.jpeg()
.toBuffer();
// Create GIF encoder
const encoder = new GIFEncoder(width, height, 'neuquant');
encoder.setDelay(100); // 100ms between frames
encoder.setRepeat(0); // Loop forever
encoder.setQuality(10);
const stream = fs.createWriteStream(outputPath);
encoder.createReadStream().pipe(stream);
encoder.start();
// Generate frames with subtle animations based on the concept
for (let i = 0; i < frameCount; i++) {
const progress = i / frameCount;
// Create frame with effects based on animation concept
let frameBuffer;
if (animationConcept.toLowerCase().includes('water') || animationConcept.toLowerCase().includes('rippl')) {
// Water ripple effect
const wave = Math.sin(progress * Math.PI * 4) * 5;
frameBuffer = await sharp(baseImage)
.modulate({
brightness: 1 + (Math.sin(progress * Math.PI * 2) * 0.05),
saturation: 1 + (Math.cos(progress * Math.PI * 2) * 0.1)
})
.blur(Math.max(0.3, Math.abs(wave) * 0.3))
.raw()
.toBuffer();
} else if (animationConcept.toLowerCase().includes('flame') || animationConcept.toLowerCase().includes('fire')) {
// Fire flicker effect
frameBuffer = await sharp(baseImage)
.modulate({
brightness: 0.9 + (Math.random() * 0.2),
hue: Math.floor(Math.random() * 10 - 5)
})
.raw()
.toBuffer();
} else if (animationConcept.toLowerCase().includes('light') || animationConcept.toLowerCase().includes('glow')) {
// Lighting change effect
const lightIntensity = 0.8 + (Math.sin(progress * Math.PI * 2) * 0.3);
frameBuffer = await sharp(baseImage)
.modulate({
brightness: lightIntensity
})
.gamma(Math.max(1.0, Math.min(3.0, 1.0 + (Math.sin(progress * Math.PI * 2) * 0.2))))
.raw()
.toBuffer();
} else if (animationConcept.toLowerCase().includes('wind') || animationConcept.toLowerCase().includes('sway')) {
// Swaying effect with slight distortion
const skewAmount = Math.sin(progress * Math.PI * 2) * 2;
frameBuffer = await sharp(baseImage)
.rotate(skewAmount, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
.resize(width, height, { fit: 'cover' })
.raw()
.toBuffer();
} else {
// Default: subtle zoom and brightness pulse
const zoomFactor = 1 + (Math.sin(progress * Math.PI * 2) * 0.02);
const size = Math.round(width * zoomFactor);
frameBuffer = await sharp(baseImage)
.resize(size, size, { fit: 'cover' })
.extract({
left: Math.round((size - width) / 2),
top: Math.round((size - height) / 2),
width: width,
height: height
})
.modulate({
brightness: 0.95 + (Math.sin(progress * Math.PI * 2) * 0.05)
})
.raw()
.toBuffer();
}
// Convert to RGBA for GIF encoder
const rgbaBuffer = Buffer.alloc(width * height * 4);
for (let j = 0; j < frameBuffer.length; j += 3) {
const pixelIndex = Math.floor(j / 3) * 4;
rgbaBuffer[pixelIndex] = frameBuffer[j]; // R
rgbaBuffer[pixelIndex + 1] = frameBuffer[j + 1]; // G
rgbaBuffer[pixelIndex + 2] = frameBuffer[j + 2]; // B
rgbaBuffer[pixelIndex + 3] = 255; // A
}
encoder.addFrame(rgbaBuffer);
}
encoder.finish();
return new Promise((resolve) => {
stream.on('finish', () => {
console.log(`AI-animated GIF created: ${outputPath}`);
// Check file size
const stats = fs.statSync(outputPath);
console.log(`File size: ${(stats.size / 1024).toFixed(2)} KB`);
resolve({
originalImage: imagePath,
animatedImage: outputPath,
artworkMetadata: metadata,
isAnimated: true,
animationType: 'ai-enhanced',
animationConcept: animationConcept
});
});
});
} catch (error) {
console.error('Error creating AI animation:', error.message);
return {
originalImage: imagePath,
isStatic: true,
artworkMetadata: metadata,
error: error.message
};
}
}