// 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/.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); } } }