Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124#!/usr/bin/env node// chillwave/bin/sing.mjs — jeffrey-pvc SUNG vocal stem for helpabeach.//// 1. POST the lyric to the LOCAL say endpoint (say-local.mjs) with// ElevenLabs /with-timestamps → vocal.mp3 + per-word alignment.// 2. alignment → words.json// 3. score-pitch.mjs (WORLD f0 replacement) snaps jeffrey's natural// read onto helpabeach.vocal.np → a soft sung performance layer.//// Requires say-local.mjs running (node pop/chillwave/bin/say-local.mjs).//// Usage: node pop/chillwave/bin/sing.mjs [--force] [--transpose 0]
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";import { resolve, dirname } from "node:path";import { fileURLToPath } from "node:url";import { spawnSync } from "node:child_process";
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 n = process.argv[i + 1]; if (n === undefined || n.startsWith("--")) flags[a.slice(2)] = true; else { flags[a.slice(2)] = n; i++; }}const FORCE = flags.force === true;const TRANSPOSE = String(flags.transpose ?? 0);const SAY_URL = process.env.SAY_ENDPOINT || "http://127.0.0.1:8899/api/say";
const OUT = `${LANE}/out`;mkdirSync(OUT, { recursive: true });const VOCAL_MP3 = `${OUT}/helpabeach-vocal.mp3`;const WORDS_JSON = `${OUT}/helpabeach-vocal-words.json`;const PITCHED_MP3 = `${OUT}/helpabeach-vocal-pitched.mp3`;const VOCAL_NP = `${LANE}/helpabeach.vocal.np`;
// Lyric = the content lines of helpabeach.txt (skip the "whisper" tag).const lyric = readFileSync(`${LANE}/helpabeach.txt`, "utf8") .split("\n").map((l) => l.trim()) .filter((l) => l && l.toLowerCase() !== "whisper") .join(" ");console.log(`▸ lyric: "${lyric}"`);
// ── 1+2. local say (jeffrey-pvc + timestamps) → mp3 + words.json ─────if (FORCE || !existsSync(VOCAL_MP3) || !existsSync(WORDS_JSON)) { console.log(`▸ say-local → ${SAY_URL}`); const res = await fetch(SAY_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from: lyric, provider: "jeffrey", voice: "neutral:0", stability: 0.6, similarity: 0.92, style: 0.28, withTimestamps: true, }), }); if (!res.ok) { console.error(`✗ say-local ${res.status}: ${(await res.text()).slice(0, 300)}`); console.error(" is say-local.mjs running? node pop/chillwave/bin/say-local.mjs"); process.exit(1); } const j = await res.json(); writeFileSync(VOCAL_MP3, Buffer.from(j.audio, "base64")); // chars → words (runs of non-space), [first start, last end] const a = j.alignment || {}; const ch = a.characters || []; const st = a.character_start_times_seconds || []; const en = a.character_end_times_seconds || []; const words = []; let i = 0; while (i < ch.length) { if (/\s/.test(ch[i])) { i++; continue; } const from = st[i]; let txt = "", last = en[i]; while (i < ch.length && !/\s/.test(ch[i])) { txt += ch[i]; last = en[i]; i++; } words.push({ text: txt, fromMs: Math.round(from * 1000), toMs: Math.round(last * 1000) }); } writeFileSync(WORDS_JSON, JSON.stringify(words, null, 0)); console.log(`✓ ${VOCAL_MP3.replace(REPO + "/", "")} (${words.length} words)`);} else { console.log(`✓ cached vocal + words (use --force to regen)`);}
// ── 3. score-pitch → sung stem ───────────────────────────────────────console.log(`▸ score-pitch (WORLD f0 → helpabeach.vocal.np · transpose ${TRANSPOSE})`);const r = spawnSync("node", [ `${REPO}/pop/bin/score-pitch.mjs`, "--slug", "helpabeach", "--section", "all", "--score", VOCAL_NP, "--vocal", VOCAL_MP3, "--words", WORDS_JSON, "--transpose", TRANSPOSE, "--vibrato-hz", "5.0", "--vibrato-cents", "14", "--out", PITCHED_MP3,], { cwd: `${REPO}/pop`, stdio: ["ignore", "inherit", "inherit"] });if (r.status !== 0) { console.error("✗ score-pitch failed"); process.exit(1); }console.log(`✓ pitched → ${PITCHED_MP3.replace(REPO + "/", "")}`);
// ── 4. score-stretch → HOLD notes on the 70 BPM beat grid ────────────// This is what makes it actually SUNG and beat-aligned: each word is// rubberband-stretched (formant-preserving, no pitch change) to its// target beats from helpabeach.vocal.np at the track tempo, so the *5// phrase-ends sustain on pitch instead of reading as fast speech.const BPM = String(flags.bpm ?? 84);const PITCHED_ALIGN = `${OUT}/helpabeach-vocal-pitched-alignment.json`;const SUNG_MP3 = `${OUT}/helpabeach-vocal-sung.mp3`;console.log(`▸ score-stretch (rubberband → ${BPM} BPM beat grid · held notes)`);const r2 = spawnSync("node", [ `${REPO}/pop/bin/score-stretch.mjs`, "--slug", "helpabeach", "--section", "all", "--score", VOCAL_NP, "--in", PITCHED_MP3, "--alignment", PITCHED_ALIGN, "--bpm", BPM, "--max-stretch", "12.0", "--out", SUNG_MP3,], { cwd: `${REPO}/pop`, stdio: ["ignore", "inherit", "inherit"] });if (r2.status !== 0) { console.error("✗ score-stretch failed"); process.exit(1); }console.log(`\n✓ SUNG (beat-aligned) → ${SUNG_MP3.replace(REPO + "/", "")}`);console.log(` mix: node bin/render.mjs --slug helpabeach --vocal-stem ${SUNG_MP3.replace(LANE + "/", "")}`);