← back to Animate Museum Posts

scripts/test-wave-metamorphosis.js

177 lines

#!/usr/bin/env node
/**
 * Test metamorphosis animation with The Great Wave off Kanagawa
 * This should create DRAMATIC wave motion, not just panning
 */

import Replicate from 'replicate';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import sharp from 'sharp';
import dotenv from 'dotenv';
import { execSync } from 'child_process';

dotenv.config();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, '..');

const OUTPUT_DIR = path.join(projectRoot, 'output');
if (!fs.existsSync(OUTPUT_DIR)) {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}

async function downloadGreatWave() {
  console.log('Downloading The Great Wave off Kanagawa...');

  // Met Museum version (public domain, no restrictions)
  const imageUrl = 'https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg';

  const response = await axios.get(imageUrl, {
    responseType: 'arraybuffer',
    headers: {
      'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)'
    }
  });
  const imagePath = path.join(OUTPUT_DIR, 'great_wave_original.jpg');
  fs.writeFileSync(imagePath, response.data);

  console.log('Downloaded Great Wave image from Met Museum');
  return imagePath;
}

async function createMetamorphosisVideo(imagePath) {
  console.log('\n=== CREATING METAMORPHOSIS VIDEO ===');
  console.log('Goal: Waves should CRASH, MOVE, TRANSFORM - not just pan!');

  const replicate = new Replicate({
    auth: process.env.REPLICATE_API_TOKEN,
  });

  // Prepare image for SVD (1024x576)
  const optimizedPath = path.join(OUTPUT_DIR, 'wave_optimized.jpg');
  await sharp(imagePath)
    .resize(1024, 576, { fit: 'cover', position: 'center' })
    .jpeg({ quality: 95 })
    .toFile(optimizedPath);

  const imageBuffer = fs.readFileSync(optimizedPath);
  const base64Image = imageBuffer.toString('base64');
  const dataUri = `data:image/jpeg;base64,${base64Image}`;

  console.log('\nSending to Stable Video Diffusion with MAXIMUM motion...');
  console.log('Settings:');
  console.log('  - motion_bucket_id: 255 (MAXIMUM)');
  console.log('  - cond_aug: 0.1 (high variation)');
  console.log('  - fps: 6 (slower = more dramatic)');
  console.log('\nPlease wait 60-180 seconds...');

  const startTime = Date.now();

  const output = await replicate.run(
    "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
    {
      input: {
        input_image: dataUri,
        video_length: "25_frames_with_svd_xt",
        sizing_strategy: "maintain_aspect_ratio",
        frames_per_second: 6,  // Slower for dramatic effect
        motion_bucket_id: 255,  // MAXIMUM motion
        cond_aug: 0.1,  // Higher variation
        decoding_t: 14,
        seed: Math.floor(Math.random() * 1000000)
      }
    }
  );

  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
  console.log(`\nVideo generated in ${elapsed}s`);

  // Download the video
  const videoResponse = await axios.get(output, { responseType: 'arraybuffer' });
  const timestamp = Date.now();
  const videoPath = path.join(OUTPUT_DIR, `wave_metamorphosis_${timestamp}.mp4`);
  fs.writeFileSync(videoPath, videoResponse.data);

  // Check video properties
  const probeCmd = `ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`;
  const duration = parseFloat(execSync(probeCmd, { stdio: 'pipe' }).toString().trim());
  const stats = fs.statSync(videoPath);

  console.log(`\nVideo created:`);
  console.log(`  - Path: ${videoPath}`);
  console.log(`  - Duration: ${duration.toFixed(2)}s`);
  console.log(`  - Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);

  // Convert to H.264 for Twitter
  const h264Path = videoPath.replace('.mp4', '_h264.mp4');
  console.log('\nConverting to Twitter-compatible H.264...');
  execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264Path}"`, { stdio: 'pipe' });
  fs.unlinkSync(videoPath);
  fs.renameSync(h264Path, videoPath);

  // Cleanup
  fs.unlinkSync(optimizedPath);

  return videoPath;
}

async function postToTwitter(videoPath) {
  console.log('\n=== POSTING TO TWITTER ===');

  const { postToX } = await import('../src/postToX.js');

  // Create animationResult object matching postToX signature
  const animationResult = {
    animatedVideo: videoPath,
    artworkMetadata: {
      title: "The Great Wave off Kanagawa",
      artist: "Katsushika Hokusai",
      year: "1831",
      museum: "Public Domain Collection"
    },
    isAnimated: true
  };

  const result = await postToX(animationResult);

  console.log(`\nTweet posted successfully!`);
  console.log(`Tweet ID: ${result.data.id}`);
  console.log(`URL: https://twitter.com/goodquestionai/status/${result.data.id}`);

  return result;
}

async function main() {
  console.log(`
╔═══════════════════════════════════════════════════════════╗
║     THE GREAT WAVE - METAMORPHOSIS TEST                   ║
║     Testing maximum motion SVD animation                  ║
╚═══════════════════════════════════════════════════════════╝
`);

  try {
    // 1. Download the Great Wave image
    const imagePath = await downloadGreatWave();

    // 2. Create metamorphosis video
    const videoPath = await createMetamorphosisVideo(imagePath);

    // 3. Post to Twitter
    await postToTwitter(videoPath);

    console.log('\n=== TEST COMPLETE ===');
    console.log('Check the tweet to see if waves are actually moving/transforming');
    console.log('Not just panning across the image\n');

  } catch (error) {
    console.error('Error:', error.message);
    console.error(error.stack);
  }
}

main();