From c53f9039c0b2b41f2b2c6584b4f4b42deeaa88a3 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 15 Sep 2026 17:01:19 -0700 Subject: [PATCH] pals: animated turnarounds on every hit, 50/50 with stills; oven backdrop pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pals.aesthetic.computer now serves a fresh random pal per request, half the time a looping Seedance turnaround (webp / apng / mp4) instead of a still. 13 pals have turnarounds published (11 turntables + 2 glow pulses); animate-pals.mjs batches the rest via fal (--missing) and encodes webp with img2webp + a palette APNG, publish-pals-turnaround.mjs uploads all three and registers the slug in logo.mjs. The "jelly" motion melted the mark into blobs, so it is opt-in as "morph". oven: /kidlisp-backdrop.webp no longer 301-pins one daily clip — it 302s (no-store) to a random clip from a pool of top @jeffrey pieces (backdrop/kidlisp/pool/), warmed at startup and daily; /kidlisp-backdrop/pool shows status, POST /pool/warm renders. Co-Authored-By: Claude Fable 5.1 --- marketing/podcast/bin/animate-pals.mjs | 204 +++++++++++-- .../podcast/bin/publish-pals-turnaround.mjs | 87 +++--- oven/grabber.mjs | 133 ++++++++- oven/server.mjs | 66 ++++- system/backend/logo.mjs | 54 +++- system/netlify/functions/logo.js | 274 +++++++++--------- 6 files changed, 584 insertions(+), 234 deletions(-) mode change 100644 => 100755 marketing/podcast/bin/animate-pals.mjs mode change 100644 => 100755 marketing/podcast/bin/publish-pals-turnaround.mjs diff --git a/marketing/podcast/bin/animate-pals.mjs b/marketing/podcast/bin/animate-pals.mjs old mode 100644 new mode 100755 index d3d9b5c7eb..573cf8e15c --- a/marketing/podcast/bin/animate-pals.mjs +++ b/marketing/podcast/bin/animate-pals.mjs @@ -1,36 +1,178 @@ -// animate-pals.mjs — turn a still "pals" logo tile into a seamless LOOPING -// turnaround clip with Seedance 2.0. The trick: pass the same still as both -// the start frame (image) and the end frame (endImage) so the last frame -// snaps back to the first → the object spins 360° and loops cleanly. +#!/usr/bin/env node +// animate-pals.mjs — turn still "pals" logo tiles into seamless LOOPING +// turnaround clips with Seedance 2.0, then encode each clip as an animated +// WebP (hero) and an animated PNG (APNG, for hosts that only take PNG). // -// Usage: node bin/animate-pals.mjs [slug] (default: nat-amethyst) -// → ~/Desktop/pals-turnaround-.mp4 -import { generateShot } from "../../../pop/lib/fal.mjs"; -import { homedir } from "node:os"; +// The loop trick: the same still is passed as both the start frame (image) +// and the end frame (endImage), so the last frame snaps back to the first +// and the object returns to its starting pose. The encoder drops that +// duplicated last frame so the loop has no hitch. +// +// Usage: +// node bin/animate-pals.mjs one or more pals (e.g. nat-jade chrome) +// node bin/animate-pals.mjs --all every pal in system/backend/logo.mjs +// node bin/animate-pals.mjs --missing every pal without a local .mp4 yet +// node bin/animate-pals.mjs --tray nat|cf|ig|av|materials +// +// Flags: +// --tier fast|standard Seedance tier (default standard: nicer facets/light) +// --motion - - - - - - -`; - - return { - statusCode: 200, - headers: { - "Content-Type": "text/html", - }, - body: htmlResponse, - }; - } -} - -const randomPurple = () => { - const r = Math.floor(Math.random() * 50 + 50); // Values between 200 and 255 - const g = Math.floor(Math.random() * 25); // Values between 0 and 50 - const b = Math.floor(Math.random() * 50 + 50); // Values between 200 and 255 - return `rgb(${r},${g},${b})`; -}; + + // ── named assets → CDN ────────────────────────────────────────────── + const named = assetPath.match(/^pals-([a-z0-9-]+)\.(png|webp|apng|mp4)$/); + if (named) { + const [, slug, format] = named; + const url = format === "png" ? logoUrl(slug) : turnaroundUrl(slug, format); + return url ? redirect(url) : respond(404, { error: "Pal not found." }); + } + + // ── random animated (turnaround / auth0 are legacy aliases) ───────── + const random = assetPath.match(/^(?:random|turnaround|auth0)\.(webp|apng|mp4|json)$/); + if (random) { + const format = random[1]; + if (format === "json") { + const pal = randomPal({ previous }); + return { statusCode: 200, headers: { "Content-Type": "application/json", ...CORS, ...NO_CACHE }, body: JSON.stringify(pal) }; + } + const url = turnaroundUrl(null, format); + if (!url) return respond(404, { error: "No turnarounds published." }); + return format === "mp4" ? redirect(url) : proxy(url, format); + } + + // ── random still bytes (legacy /logo.png) ─────────────────────────── + if (assetPath === "logo.png" || assetPath === "random.png" || assetPath === "still.png") { + return proxy(randomPal({ previous, animated: false }).url, "png"); + } + + // ── bare endpoint: 50/50 still or animated, fresh each hit ────────── + const pal = randomPal({ previous }); + const userAgent = event.headers["user-agent"] || ""; + const accept = event.headers["accept"] || ""; + const isServer = /curl|wget|python-requests|node-fetch/i.test(userAgent); + const wantsImage = accept.startsWith("image/") || (accept.includes("image/*") && !accept.includes("text/html")); + + if (isServer || wantsImage) return proxy(pal.url, pal.format); + + const htmlResponse = ` + + + + + pals · ${pal.slug}${pal.animated ? " ↻" : ""} + + + + + pals ${pal.slug} + + + +`; + return { statusCode: 200, headers: { "Content-Type": "text/html", ...NO_CACHE }, body: htmlResponse }; +} + +const randomPurple = () => { + const r = Math.floor(Math.random() * 50 + 50); + const g = Math.floor(Math.random() * 25); + const b = Math.floor(Math.random() * 50 + 50); + return `rgb(${r},${g},${b})`; +}; -- 2.51.2