104 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
import puppeteer from 'puppeteer';
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import { mcpConfigs } from './mcp-configs.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const existingMp4s = fs.readdirSync(path.join(__dirname, 'output'))
.filter(f => f.endsWith('.mp4'))
.map(f => f.replace('.mp4', ''));
const newConfigs = mcpConfigs.filter(c => !existingMp4s.includes(c.id));
console.log(`Generating ${newConfigs.length} new videos...`);
function generateHTML(config) {
const { name, color, question, statLabel, statValue, statLabel2, statValue2, rows, insight } = config;
return `<!DOCTYPE html><html><head><meta charset="UTF-8">
<meta name="viewport" content="width=1920, height=1080">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#0a0a12;--card:#12121c;--border:rgba(255,255,255,0.08);--text:#fff;--dim:#888;--accent:${color};--green:#22c55e}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:var(--bg);width:1920px;height:1080px;overflow:hidden;display:flex;align-items:center;justify-content:center}
.scene{width:1920px;height:1080px;display:flex;align-items:center;justify-content:center;background:radial-gradient(ellipse at 50% 0%,${color}15 0%,transparent 60%)}
.chat{width:900px;background:var(--card);border:1px solid var(--border);border-radius:24px;overflow:hidden;box-shadow:0 50px 100px rgba(0,0,0,0.5)}
.hdr{padding:20px 28px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px}
.hdr .d{width:10px;height:10px;border-radius:50%;background:var(--accent)}
.hdr span{font-size:15px;font-weight:600;color:var(--dim)}
.msgs{padding:28px;display:flex;flex-direction:column;gap:20px}
.m{padding:16px 20px;border-radius:16px;font-size:15px;line-height:1.5;max-width:85%;opacity:0;transform:translateY(10px)}
.u{background:var(--accent);color:#fff;align-self:flex-end;border-bottom-right-radius:4px;animation:fu .5s ease forwards .3s}
.a{background:rgba(255,255,255,0.05);color:var(--text);align-self:flex-start;border-bottom-left-radius:4px;animation:fu .5s ease forwards 1s}
.sc{display:flex;gap:24px;margin:12px 0;opacity:0;animation:fu .5s ease forwards 1.5s}
.s{background:rgba(255,255,255,0.05);padding:14px 20px;border-radius:12px;flex:1;border:1px solid var(--border)}
.sl{font-size:12px;color:var(--dim);margin-bottom:4px}
.sv{font-size:28px;font-weight:700;color:var(--accent)}
.dr{display:flex;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);font-size:14px;opacity:0}
.dr:nth-child(1){animation:fu .4s ease forwards 2s}
.dr:nth-child(2){animation:fu .4s ease forwards 2.3s}
.dr:nth-child(3){animation:fu .4s ease forwards 2.6s}
.dr .l{color:var(--text)}.dr .v{color:var(--dim)}
.ins{margin-top:16px;padding:14px 18px;background:linear-gradient(135deg,${color}15,${color}05);border-left:3px solid var(--accent);border-radius:0 12px 12px 0;font-size:13px;color:var(--dim);line-height:1.5;opacity:0;animation:fu .5s ease forwards 3s}
.ins strong{color:var(--green)}
@keyframes fu{to{opacity:1;transform:translateY(0)}}
</style></head><body><div class="scene"><div class="chat"><div class="hdr"><div class="d"></div><span>${name} MCP</span></div>
<div class="msgs"><div class="m u">${question}</div><div class="m a">
<div class="sc"><div class="s"><div class="sl">${statLabel}</div><div class="sv">${statValue}</div></div>
<div class="s"><div class="sl">${statLabel2}</div><div class="sv">${statValue2}</div></div></div>
${rows.map(r=>`<div class="dr"><span class="l">${r.label}</span><span class="v">${r.value}</span></div>`).join('')}
<div class="ins">${insight}</div></div></div></div></div></body></html>`;
}
async function main() {
const browser = await puppeteer.launch({ headless: 'shell', args: ['--no-sandbox','--disable-setuid-sandbox','--disable-gpu'] });
for (let i = 0; i < newConfigs.length; i++) {
const config = newConfigs[i];
const start = Date.now();
process.stdout.write(`[${i+1}/${newConfigs.length}] ${config.name}... `);
try {
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
const html = generateHTML(config);
const tmpHtml = path.join(__dirname, `tmp-${config.id}.html`);
fs.writeFileSync(tmpHtml, html);
await page.goto('file://' + tmpHtml, { waitUntil: 'networkidle0', timeout: 10000 });
await new Promise(r => setTimeout(r, 3500));
const framesDir = path.join(__dirname, `fr-${config.id}`);
fs.mkdirSync(framesDir, { recursive: true });
// 90 frames = 3sec at 30fps
for (let f = 0; f < 90; f++) {
await page.screenshot({ path: path.join(framesDir, `f${String(f).padStart(4,'0')}.png`), type: 'png' });
await new Promise(r => setTimeout(r, 33));
}
const outPath = path.join(__dirname, 'output', `${config.id}.mp4`);
execSync(`ffmpeg -y -framerate 30 -i "${framesDir}/f%04d.png" -c:v libx264 -pix_fmt yuv420p -crf 25 -preset ultrafast "${outPath}" 2>/dev/null`);
fs.rmSync(framesDir, { recursive: true });
fs.unlinkSync(tmpHtml);
const landingOut = '/Users/jakeshore/.clawdbot/workspace/mcpengine-repo/landing-pages/output';
fs.mkdirSync(landingOut, { recursive: true });
fs.copyFileSync(outPath, path.join(landingOut, `${config.id}.mp4`));
await page.close();
console.log(`done ${((Date.now()-start)/1000).toFixed(1)}s`);
} catch(err) {
console.log(`ERR: ${err.message}`);
}
}
await browser.close();
console.log('\nAll done!');
}
main().catch(console.error);