Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283#!/usr/bin/env node// gecs/bin/sing.mjs — jeffrey-pvc sung vocal for kernel panic. Same// proven pipeline as pop/hippyhayzard/bin/sing.mjs (say → align →// score-pitch → score-stretch) but tuned HOT for 100-gecs hyperpop://// • TTS: stability 0.55 (still ≥0.5 — clone stays jeffrey; per the// jeffrey_pvc_settings memory anything lower drifts), style 0.55// (more bratty / less measured), speed 1.10 (rapid delivery).// • score-pitch: heavier vibrato (6.5Hz × 22c) for the autotune-wobble// gecs vocal tic; the .np already does the chipmunk lift in the// chorus by writing the melody up at A4-F#5.// • score-stretch: 168 BPM so the sung line packs tight inside the// 84.4s bed (162 left some chorus held notes ringing past the kit).// • mix: aggressive comp + presence EQ (+5dB ~3.5kHz) + light tape-// style saturation; no reverb (gecs vox sit dry on top of the// bit-crushed bed). Bed hard-ducked under the vocal (ratio 10).//// Word order MUST match pop/gecs/gecs.txt strictly (175 words).//// Usage:// node pop/gecs/bin/sing.mjs # full sung song// node pop/gecs/bin/sing.mjs --force # bypass say cache
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";import { resolve, dirname } from "node:path";import { spawnSync } from "node:child_process";import { fileURLToPath } from "node:url";
const argv = process.argv.slice(2);const flags = {};for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith("--")) { const k = a.slice(2), n = argv[i + 1]; if (n !== undefined && !n.startsWith("--")) { flags[k] = n; i++; } else flags[k] = true; }}
const HERE = dirname(fileURLToPath(import.meta.url));const G = resolve(HERE, "..");const POP = resolve(G, "..");const REL = (p) => p.replace(POP + "/", "");
const SCORE = `${G}/gecs.np`;const LYRIC = `${G}/gecs.txt`;const OUT_DIR = `${G}/out`;const TMP = `${OUT_DIR}/.tmp`;mkdirSync(TMP, { recursive: true });
const VOCAL = `${OUT_DIR}/gecs-vocal.mp3`;const VOCAL_ALIGN = `${VOCAL}.alignment.json`;const WORDS = `${OUT_DIR}/gecs-vocal-words.json`;const PITCHED = `${OUT_DIR}/gecs-pitched.mp3`;const PITCHED_ALIGN = `${OUT_DIR}/gecs-pitched-alignment.json`;const STRETCHED = `${OUT_DIR}/gecs-stretched.mp3`;const BED = `${OUT_DIR}/gecs.mp3`;const FINAL = resolve(process.cwd(), flags.out || `${OUT_DIR}/gecs-song.mp3`);const FORCE = !!flags.force;
for (const [p, what] of [[SCORE, "score"], [LYRIC, "lyric"]]) { if (!existsSync(p)) { console.error(`✗ ${what} missing: ${p}`); process.exit(1); }}if (!existsSync(BED)) { console.error(`✗ bed missing: ${BED}\n run: node pop/gecs/bin/render.mjs`); process.exit(1);}
function run(cmd, args, cwd = POP, soft = false) { const r = spawnSync(cmd, args, { cwd, stdio: ["ignore", "inherit", "inherit"] }); if (r.status !== 0 && !soft) { console.error(`✗ ${cmd} ${args.slice(0, 3).join(" ")}… failed (exit ${r.status})`); process.exit(1); } return r.status === 0;}function step(name, fn) { const t0 = Date.now(); process.stdout.write(`▸ ${name}\n`); fn(); console.log(` ${((Date.now() - t0) / 1000).toFixed(1)}s`);}
console.log(`━━━ gecs · sing (jeffrey-pvc hyperpop) ━━━\n`);
// ── 1. say × N — one ElevenLabs call PER SECTION with section-specific// voice settings. Then concatenate the per-section mp3s into VOCAL and// merge per-section alignment JSONs into VOCAL_ALIGN (with cumulative// time offsets) so steps 2-5 see a single contiguous vocal as before.//// verse — stability 0.72, style 0.30, speed 1.10 (solid + rapid)// chorus — stability 0.58, style 0.65, speed 1.05 (excited, big)// breakdown — stability 0.50, style 0.85, speed 0.95 (shouted, loose)//// say.mjs already content-hash-caches per call body, so unchanged// sections skip the API on re-run. `--force` invalidates all.
// Parse gecs.txt into ORDERED sections (chorus appears 3× — sing each).function parseSections(txt) { const out = []; let cur = null; for (const raw of txt.split("\n")) { const t = raw.trim(); if (!t) continue; const m = t.match(/^(verse \d+|chorus|breakdown|hook|intro|outro|bridge)$/i); if (m) { if (cur) out.push(cur); const name = m[1].toLowerCase(); const kind = name.startsWith("verse") ? "verse" : name === "chorus" ? "chorus" : name === "breakdown" ? "breakdown" : name; cur = { name, kind, lines: [] }; } else if (cur) { cur.lines.push(t); } } if (cur) out.push(cur); return out;}
const SECTION_VOICE = { verse: { stability: "0.72", style: "0.30", speed: "1.10" }, chorus: { stability: "0.58", style: "0.65", speed: "1.05" }, breakdown: { stability: "0.50", style: "0.85", speed: "0.95" },};
const SECTIONS_TXT = parseSections(readFileSync(LYRIC, "utf8"));const SECTION_DIR = `${TMP}/sections`;mkdirSync(SECTION_DIR, { recursive: true });
step(`1 · say × ${SECTIONS_TXT.length} (per-section voice settings)`, () => { // Number each section occurrence so the temp file paths are unique // (3 chorus calls → chorus-1.txt, chorus-2.txt, chorus-3.txt etc). const counts = {}; for (const sec of SECTIONS_TXT) { counts[sec.name] = (counts[sec.name] || 0) + 1; sec.idx = counts[sec.name]; sec.slug = sec.name.replace(/\s+/g, ""); // "verse 1" → "verse1" sec.txtPath = `${SECTION_DIR}/${sec.slug}-${sec.idx}.txt`; sec.mp3Path = `${SECTION_DIR}/${sec.slug}-${sec.idx}.mp3`; sec.alignPath = `${sec.mp3Path}.alignment.json`; writeFileSync(sec.txtPath, sec.lines.join("\n") + "\n"); } // Fire each section sequentially (parallel hammers the API + machine). for (const sec of SECTIONS_TXT) { const v = SECTION_VOICE[sec.kind] || SECTION_VOICE.verse; const args = ["bin/say.mjs", REL(sec.txtPath), "--stability", v.stability, "--similarity", "0.92", "--style", v.style, "--speed", v.speed, "--out", REL(sec.mp3Path), "--timestamps"]; if (FORCE) args.push("--force"); process.stdout.write(` · ${sec.name}(${sec.idx}) [${sec.kind}: stab ${v.stability} · sty ${v.style} · spd ${v.speed}]\n`); const r = spawnSync("node", args, { cwd: POP, stdio: ["ignore", "inherit", "inherit"] }); if (r.status !== 0) { console.error(`✗ say failed for ${sec.name}(${sec.idx})`); process.exit(1); } }});
// ── 2. concat per-section mp3s + merge alignments → VOCAL + WORDS ────const lyricWords = readFileSync(LYRIC, "utf8") .split("\n") .filter((l) => l.trim() && !/^(verse \d+|hook|intro|outro|bridge|chorus|breakdown)$/i.test(l.trim())) .join(" ") .split(/\s+/).map((w) => w.replace(/[^a-zA-Z0-9]/g, "")).filter(Boolean);const expected = lyricWords.length;
step(`2 · concat sections + merge alignments (target ${expected} words)`, () => { // Convert each section mp3 → 48k wav, measure exact duration, and // stitch with ffmpeg concat. Wav avoids the mp3-concat priming gap. const wavList = []; let offsetSec = 0; const mergedWords = []; for (const sec of SECTIONS_TXT) { const wav = `${SECTION_DIR}/${sec.slug}-${sec.idx}.wav`; run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-i", sec.mp3Path, "-ar", "48000", "-ac", "1", wav]); wavList.push(wav); // word offsets: shift this section's words by cumulative offset if (!existsSync(sec.alignPath)) { console.error(`✗ alignment missing for ${sec.name}(${sec.idx})`); process.exit(1); } const a = JSON.parse(readFileSync(sec.alignPath, "utf8")); const offMs = Math.round(offsetSec * 1000); for (const w of (a.words || [])) { mergedWords.push({ text: w.text.replace(/[^a-zA-Z0-9]/g, ""), fromMs: w.fromMs + offMs, toMs: w.toMs + offMs, }); } const probe = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", wav], { encoding: "utf8" }); offsetSec += parseFloat(probe.stdout.trim()) || 0; } // ffmpeg concat demuxer requires a manifest const manifest = `${SECTION_DIR}/manifest.txt`; writeFileSync(manifest, wavList.map((p) => `file '${p}'`).join("\n") + "\n"); const combinedWav = `${SECTION_DIR}/combined.wav`; run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", manifest, "-ar", "48000", "-ac", "1", combinedWav]); // combined wav → mp3 for VOCAL (so the downstream pipeline gets mp3) run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-i", combinedWav, "-c:a", "libmp3lame", "-q:a", "2", VOCAL]); // Synthesize a combined alignment json (matches format of single say.mjs) writeFileSync(VOCAL_ALIGN, JSON.stringify({ source: "elevenlabs/with-timestamps/multi-section", sections: SECTIONS_TXT.map((s) => ({ name: s.name, idx: s.idx, kind: s.kind })), words: mergedWords, }, null, 2)); writeFileSync(WORDS, JSON.stringify(mergedWords, null, 0)); console.log(` ✓ ${mergedWords.length} words across ${SECTIONS_TXT.length} sections · combined ${offsetSec.toFixed(1)}s vocal`);});
// ── 3. score-pitch (WORLD f0 → the .np melody, heavy hyperpop vibrato) ──step("3 · score-pitch (WORLD · hyperpop wobble 6.5Hz/22c)", () => { run("node", ["bin/score-pitch.mjs", "--slug", "gecs", "--section", "all", "--score", REL(SCORE), "--vocal", REL(VOCAL), "--words", REL(WORDS), "--transpose", "0", "--vibrato-hz", "6.5", "--vibrato-cents", "22", "--out", REL(PITCHED)]);});
// ── 4. score-stretch (pack into the 84.4s bed) ────────────────────────step("4 · score-stretch (rubberband · 168 BPM → fits 84.4s bed)", () => { run("node", ["bin/score-stretch.mjs", "--slug", "gecs", "--section", "all", "--score", REL(SCORE), "--in", REL(PITCHED), "--alignment", REL(PITCHED_ALIGN), "--bpm", "168", "--max-stretch", "8.0", "--out", REL(STRETCHED)]);});
// ── 5. hyperpop mix: dry, hot, in-your-face. Lay over the bed. ────────step("5 · treat + mix onto the bed (intro = 4 bars @ 165 BPM)", () => { const lead = `${TMP}/lead.wav`; run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-i", STRETCHED, "-ar", "48000", "-ac", "1", lead]);
// intro = 4 bars @ 165 BPM = 4 * (60/165*4) = 5.818 s const introMs = Math.round(4 * (60 / 165 * 4) * 1000);
const bp = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", BED], { encoding: "utf8" }); const bedDur = parseFloat(bp.stdout.trim()) || 84.4; const fadeStart = Math.max(0, bedDur - 1.0);
// VOCAL chain (less airy): // highpass 80Hz only (was 110Hz — that was eating chest warmth) // acompressor (thresh -18dB, ratio 4, gentle attack — keep body) // equalizer +2.5dB 250Hz (RESTORE chest, not de-mud — was -3dB) // equalizer +2dB ~2.5kHz (presence cut-through, not the harsh 3.5k +5dB) // equalizer -2dB 6kHz (KILL the airy/breathy top end) // // BED stays at parity (0.85 vol) and only gets light ducking (ratio 3, // higher threshold) — earlier ratio-10/thresh-0.015 was making the bed // disappear the moment any vocal hit. Now bed is heard THROUGH vocal. const fc = `[1:a]highpass=f=80,` + `acompressor=threshold=-18dB:ratio=4:attack=8:release=160:makeup=4:knee=4,` + `equalizer=f=250:t=q:w=1.0:g=2.5,` + `equalizer=f=2500:t=q:w=1.4:g=2,` + `equalizer=f=6000:t=q:w=1.4:g=-2,` + `acompressor=threshold=-10dB:ratio=2.5:attack=4:release=80:makeup=1.5,` + `volume=1.35,` + `adelay=${introMs}|${introMs},` + `apad=whole_dur=${bedDur.toFixed(3)}[vox];` + `[vox]asplit=2[voxM][voxK];` + `[0:a]volume=0.95[bedlo];` + `[bedlo][voxK]sidechaincompress=threshold=0.08:ratio=3:attack=10:release=200:makeup=1[bedd];` + `[bedd][voxM]amix=inputs=2:duration=first:normalize=0,` + `atrim=duration=${bedDur.toFixed(3)},` + `afade=t=out:st=${fadeStart.toFixed(3)}:d=1.0,` + `loudnorm=I=-11:TP=-1.0:LRA=8,aresample=48000[out]`;
run("ffmpeg", ["-hide_banner", "-y", "-loglevel", "error", "-i", BED, "-i", lead, "-filter_complex", fc, "-map", "[out]", "-c:a", "libmp3lame", "-q:a", "2", FINAL]);});
mkdirSync(dirname(FINAL), { recursive: true });const probe = spawnSync("ffprobe", ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", FINAL], { encoding: "utf8" });const sz = (readFileSync(FINAL).length / 1024).toFixed(0);console.log(`\n✓ ${FINAL} (${sz} KB · ${parseFloat(probe.stdout || 0).toFixed(1)}s · jeffrey-pvc hyperpop sung on the bed)`);copyFileSync(FINAL, `${OUT_DIR}/gecs-song.mp3`);