← back to Animate Museum Posts

scripts/wave-repeat-test.js

198 lines

#!/usr/bin/env node
/**
 * Post The Great Wave with different metamorphosis variations
 * every 5 minutes for 3 hours (36 posts)
 */

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';
import { postToX } from '../src/postToX.js';

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');

const TOTAL_POSTS = 36;  // 3 hours at 5 min intervals
const INTERVAL_MS = 5 * 60 * 1000;  // 5 minutes

let postCount = 0;
let successCount = 0;

async function downloadGreatWave() {
  const imagePath = path.join(OUTPUT_DIR, 'great_wave_source.jpg');

  if (fs.existsSync(imagePath)) {
    return imagePath;
  }

  console.log('Downloading The Great Wave...');
  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)' }
  });
  fs.writeFileSync(imagePath, response.data);
  return imagePath;
}

async function createMetamorphosisVariation(imagePath, variationNum) {
  console.log(`\n=== Creating Variation ${variationNum} ===`);

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

  // Prepare image
  const optimizedPath = path.join(OUTPUT_DIR, `wave_opt_${variationNum}.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}`;

  // Vary the motion parameters for different effects
  const seed = Math.floor(Math.random() * 1000000);
  const motionBucket = 200 + Math.floor(Math.random() * 55);  // 200-255 range
  const condAug = 0.05 + (Math.random() * 0.1);  // 0.05-0.15 range
  const fps = 5 + Math.floor(Math.random() * 4);  // 5-8 fps

  console.log(`Settings: motion=${motionBucket}, cond_aug=${condAug.toFixed(2)}, fps=${fps}, seed=${seed}`);
  console.log('Generating... (60-120s)');

  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: fps,
        motion_bucket_id: motionBucket,
        cond_aug: condAug,
        decoding_t: 14,
        seed: seed
      }
    }
  );

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

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

  // Convert to H.264
  const h264Path = videoPath.replace('.mp4', '_h264.mp4');
  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 postVariation(videoPath, variationNum) {
  const captions = [
    "Watch the waves crash and surge!",
    "Hokusai's masterpiece comes alive",
    "AI brings motion to ancient art",
    "The ocean awakens",
    "Waves in perpetual motion",
    "Art transformed by AI",
    "Feel the power of the sea",
    "Dynamic reinterpretation",
    "The wave never stops",
    "Ancient art, modern motion"
  ];

  const animationResult = {
    animatedVideo: videoPath,
    artworkMetadata: {
      title: "The Great Wave off Kanagawa",
      artist: "Katsushika Hokusai",
      year: "1831",
      museum: `Variation ${variationNum}/36 - ${captions[variationNum % captions.length]}`
    },
    isAnimated: true
  };

  const result = await postToX(animationResult);
  console.log(`Posted! Tweet ID: ${result.data.id}`);
  return result;
}

async function runOneIteration() {
  postCount++;
  console.log(`\n${'='.repeat(60)}`);
  console.log(`POST ${postCount}/${TOTAL_POSTS} - ${new Date().toISOString()}`);
  console.log(`${'='.repeat(60)}`);

  try {
    const imagePath = await downloadGreatWave();
    const videoPath = await createMetamorphosisVariation(imagePath, postCount);
    await postVariation(videoPath, postCount);
    successCount++;
    console.log(`Success! (${successCount}/${postCount} successful)`);
  } catch (error) {
    console.error(`Failed: ${error.message}`);
  }

  if (postCount < TOTAL_POSTS) {
    console.log(`\nNext post in 5 minutes...`);
    console.log(`Remaining: ${TOTAL_POSTS - postCount} posts, ~${((TOTAL_POSTS - postCount) * 5)} minutes`);
  }
}

async function main() {
  console.log(`
╔═══════════════════════════════════════════════════════════╗
║       THE GREAT WAVE - 3 HOUR METAMORPHOSIS TEST          ║
║       36 variations posted every 5 minutes                ║
╚═══════════════════════════════════════════════════════════╝

Duration: 3 hours
Interval: 5 minutes
Total Posts: 36
Start: ${new Date().toISOString()}
End (estimated): ${new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString()}
`);

  // First post immediately
  await runOneIteration();

  // Then schedule remaining posts
  const interval = setInterval(async () => {
    await runOneIteration();

    if (postCount >= TOTAL_POSTS) {
      clearInterval(interval);
      console.log(`\n${'='.repeat(60)}`);
      console.log('ALL POSTS COMPLETE!');
      console.log(`Success rate: ${successCount}/${TOTAL_POSTS} (${((successCount/TOTAL_POSTS)*100).toFixed(1)}%)`);
      console.log(`${'='.repeat(60)}\n`);
      process.exit(0);
    }
  }, INTERVAL_MS);
}

main().catch(console.error);