← 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();
}