Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100#!/usr/bin/env node// chillwave/bin/preview.mjs — render a preview mp4 from cover + audio.//// Single still cover (1024x1024) + audio mp3 → 1080x1080 mp4 with a// slow ken-burns drift (subtle zoom-in across the whole track). No// chrome, no title overlay — the cover speaks.//// Usage:// node bin/preview.mjs --slug wanderbeach// node bin/preview.mjs --slug wanderbeach --size 720x720// node bin/preview.mjs --slug wanderbeach --zoom 1.12 (default 1.08)
import { existsSync } from "node:fs";import { spawnSync } from "node:child_process";import { resolve, dirname } from "node:path";import { fileURLToPath } from "node:url";
const HERE = dirname(fileURLToPath(import.meta.url));const LANE = resolve(HERE, "..");const REPO = resolve(LANE, "../..");
const flags = {};for (let i = 2; i < process.argv.length; i++) { const a = process.argv[i]; if (!a.startsWith("--")) continue; const next = process.argv[i + 1]; if (next === undefined || next.startsWith("--")) flags[a.slice(2)] = true; else { flags[a.slice(2)] = next; i++; }}
const SLUG = flags.slug || "helpabeach";const SIZE = flags.size || "1080x1080";const ZOOM_END = Number(flags.zoom ?? 1.08); // final zoom factorconst FPS = Number(flags.fps ?? 30);
const COVER = `${LANE}/out/${SLUG}-cover.png`;const AUDIO = `${LANE}/out/${SLUG}.mp3`;const OUT = `${LANE}/out/${SLUG}-preview.mp4`;
if (!existsSync(COVER)) { console.error(`✗ cover missing: ${COVER.replace(REPO + "/", "")}`); console.error(` run: node bin/gen-illy.mjs --slug ${SLUG}`); process.exit(1);}if (!existsSync(AUDIO)) { console.error(`✗ audio missing: ${AUDIO.replace(REPO + "/", "")}`); console.error(` run: node bin/render.mjs --slug ${SLUG}`); process.exit(1);}
// Probe audio duration so we know how many frames to render.const probe = spawnSync("ffprobe", [ "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", AUDIO,], { encoding: "utf8" });if (probe.status !== 0) { console.error(`✗ ffprobe failed: ${probe.stderr}`); process.exit(1);}const DURATION = parseFloat(probe.stdout.trim());const FRAMES = Math.ceil(DURATION * FPS);
console.log(`▸ ${SLUG} preview · ${SIZE} · ${DURATION.toFixed(1)}s · ${FRAMES} frames`);
const [W, H] = SIZE.split("x").map(Number);
// zoompan needs the input upscaled so the zoom doesn't pixelate.// We pre-upscale to 4x then zoompan in that space and crop down.// Center the zoom and let it slowly drift toward an off-center point// (slightly down-right) so the eye moves over the sand + fingers// across the duration.const upW = W * 4, upH = H * 4;const filter = `scale=${upW}:${upH}:flags=lanczos,` + `zoompan=` + `z='1+(${(ZOOM_END - 1).toFixed(4)})*on/${FRAMES}':` + `x='iw/2-(iw/zoom/2) + (0.05*iw*on/${FRAMES})':` + `y='ih/2-(ih/zoom/2) + (0.04*ih*on/${FRAMES})':` + `d=1:s=${W}x${H}:fps=${FPS},` + `format=yuv420p`;
const r = spawnSync("ffmpeg", [ "-hide_banner", "-y", "-loglevel", "error", "-loop", "1", "-framerate", String(FPS), "-i", COVER, "-i", AUDIO, "-vf", filter, "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-c:a", "aac", "-b:a", "192k", "-pix_fmt", "yuv420p", "-shortest", "-movflags", "+faststart", OUT,], { stdio: "inherit" });
if (r.status !== 0) { console.error(`✗ ffmpeg failed (exit ${r.status})`); process.exit(1);}console.log(`✓ ${OUT.replace(REPO + "/", "")}`);