104 lines
5.7 KiB
JavaScript
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);
|