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