airwaves / web /hype.js
AndresCarreon's picture
AIRWAVES v0 — air-DJ (MediaPipe + Web Audio) + VoxCPM2 hype-man
860eb59 verified
Raw
History Blame Contribute Delete
3.3 kB
// hype.js — the VoxCPM2 AI hype-man. A voice bank designed in a club-MC persona
// (Spanglish energy, on-brand and Latin-flavored) is triggered INSTANTLY by
// deterministic musical events and ducked over the mix. The model owns the
// VOICE; deterministic code owns WHEN it speaks — the "her" pattern.
//
// Graceful fallback so the show always goes on:
// 1. pre-baked VoxCPM2 WAVs at hype/<key>.wav (bulletproof, instant)
// 2. browser SpeechSynthesis (last resort, dev/offline)
export const HYPE_LINES = {
hello: "Manos arriba — let's go!",
build: "Build it up… here it comes…",
drop: "¡Dale! Drop it!",
sweep: "Bring it up!",
mix: "Mix it — vamos!",
};
function b64ToBuf(b64) {
const bin = atob(b64), u = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
return u.buffer;
}
export class HypeMan {
constructor(engine) {
this.engine = engine;
this.ctx = engine.ctx;
this.bank = {}; // key -> AudioBuffer (VoxCPM2)
this.last = {}; // key -> last fire time (ctx seconds)
this.enabled = true;
// usable immediately via browser speech; upgrades to VoxCPM2 as load() fills the bank
this.source = ("speechSynthesis" in window) ? "speech" : "none";
this.bus = this.ctx.createGain();
this.bus.gain.value = 1.0;
this.bus.connect(this.ctx.destination);
}
// Non-blocking: prefer pre-baked static WAVs, then the live ZeroGPU endpoint.
// Any key not yet loaded simply falls back to browser speech in fire().
async load() {
let loaded = await this._loadPrebaked();
if (loaded < Object.keys(HYPE_LINES).length) loaded += await this._loadEndpoint();
if (loaded) this.source = "voxcpm";
return this.source;
}
async _loadPrebaked() {
let n = 0;
for (const key of Object.keys(HYPE_LINES)) {
try {
const r = await fetch(`hype/${key}.wav`, { cache: "force-cache" });
if (r.ok) { this.bank[key] = await this.ctx.decodeAudioData(await r.arrayBuffer()); n++; }
} catch (_) { /* not deployed — fall through */ }
}
return n;
}
async _loadEndpoint() {
try {
const r = await fetch("api/hype"); // VoxCPM2 on ZeroGPU (cached server-side)
if (!r.ok) return 0;
const j = await r.json();
let n = 0;
for (const item of (j.bank || [])) {
if (this.bank[item.key]) continue;
try { this.bank[item.key] = await this.ctx.decodeAudioData(b64ToBuf(item.wav_b64)); n++; } catch (_) {}
}
return n;
} catch (_) { return 0; }
}
fire(key, { cooldown = 5, duck = 0.4 } = {}) {
if (!this.enabled || !HYPE_LINES[key]) return;
const now = this.ctx.currentTime;
if (this.last[key] && now - this.last[key] < cooldown) return;
this.last[key] = now;
if (duck < 1) this.engine.duck(duck, 1.1);
const buf = this.bank[key];
if (buf) {
const s = this.ctx.createBufferSource();
s.buffer = buf; s.connect(this.bus); s.start();
} else if (this.source === "speech") {
const u = new SpeechSynthesisUtterance(HYPE_LINES[key]);
u.rate = 1.08; u.pitch = 1.15; u.volume = 1.0;
window.speechSynthesis.cancel();
window.speechSynthesis.speak(u);
}
}
}