← back to Animate Museum Posts

scripts/post-3-wave-branded.js

139 lines

#!/usr/bin/env node
/**
 * Post 3 Great Wave videos with:
 * - Moderate motion (127)
 * - 2s intro + 2s outro branding
 */

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';
import { addBrandingToVideo } from '../src/addBranding.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');

async function createAndPost(num) {
  console.log(`\n${'='.repeat(50)}`);
  console.log(`POST ${num}/3 - ${new Date().toISOString()}`);
  console.log(`${'='.repeat(50)}`);

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

  // Get source image
  let imagePath = path.join(OUTPUT_DIR, 'great_wave_source.jpg');
  if (!fs.existsSync(imagePath)) {
    console.log('Downloading The Great Wave...');
    const response = await axios.get('https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg', {
      responseType: 'arraybuffer',
      headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)' }
    });
    fs.writeFileSync(imagePath, response.data);
  }

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

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

  // Moderate motion settings
  const seed = Math.floor(Math.random() * 1000000);
  console.log(`Generating with moderate motion (127), seed: ${seed}...`);

  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: 8,
        motion_bucket_id: 127,  // MODERATE motion
        cond_aug: 0.02,
        decoding_t: 14,
        seed: seed
      }
    }
  );
  console.log(`Generated in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);

  // Download video
  const videoResponse = await axios.get(output, { responseType: 'arraybuffer' });
  const timestamp = Date.now();
  const videoPath = path.join(OUTPUT_DIR, `wave_branded_${num}_${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);

  // Add 2s intro + 2s outro branding
  console.log('Adding GoodQuestion.AI branding (2s intro + 2s outro)...');
  const metadata = { title: "The Great Wave off Kanagawa", artist: "Katsushika Hokusai" };
  await addBrandingToVideo(videoPath, metadata);

  // Cleanup
  fs.unlinkSync(optimizedPath);

  // Post to Twitter
  console.log('Posting to Twitter...');
  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(`Posted! https://twitter.com/goodquestionai/status/${result.data.id}`);
  return result;
}

async function main() {
  console.log(`
╔═══════════════════════════════════════════════════════════╗
║     POSTING 3 GREAT WAVE VIDEOS WITH BRANDING             ║
║     - Moderate motion (127)                               ║
║     - 2s intro + 2s outro                                 ║
╚═══════════════════════════════════════════════════════════╝
`);

  for (let i = 1; i <= 3; i++) {
    try {
      await createAndPost(i);
      if (i < 3) {
        console.log('\nWaiting 15s before next post...');
        await new Promise(r => setTimeout(r, 15000));
      }
    } catch (err) {
      console.error(`Post ${i} failed:`, err.message);
    }
  }

  console.log('\n=== ALL 3 POSTS COMPLETE ===\n');
}

main().catch(console.error);