From 5bf5beb22a9e5869d9800c398202fc94b856ae35 Mon Sep 17 00:00:00 2001 From: Jeffrey Alan Scudder Date: Mon, 30 Mar 2026 14:23:51 -0700 Subject: [PATCH] Add ElevenLabs TTS provider with scream mode (low stability, max style) say:eleven for normal speech, say:scream defaults to ElevenLabs with stability=0.1 and style=1.0 for intense vocal delivery. Scream colon also works with say:openai:scream for gpt-4o-mini-tts instructions path. Co-Authored-By: Claude Opus 4.6 (1M context) --- system/netlify/functions/say.js | 62 ++++++++++++++++++- .../public/aesthetic.computer/disks/say.mjs | 21 ++++--- .../public/aesthetic.computer/lib/speech.mjs | 1 + 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/system/netlify/functions/say.js b/system/netlify/functions/say.js index fd81c5fba..d8bb2e1df 100644 --- a/system/netlify/functions/say.js +++ b/system/netlify/functions/say.js @@ -94,6 +94,59 @@ async function generateOpenAI(text, gender, set, instructions) { }; } +// ElevenLabs voice mapping (premade voice IDs) +const ELEVEN_VOICES = { + male: [ + "SOYHLrjzK2X1ezoPC6cr", // Harry - Fierce Warrior + "IKne3meq5aSn9XLyUdCD", // Charlie - Deep, Confident, Energetic + "N2lVS1w4EtoT3dr4eOWO", // Callum - Husky Trickster + "TX3LPaxmHKxFdv7VOQHJ", // Liam - Energetic + ], + female: [ + "EXAVITQu4vr4xnSDxMaL", // Sarah + "FGY2WhTYpPnrIDTdsKH5", // Laura + "cgSgspJ2msm6clMCkdW9", // Jessica + ], + neutral: [ + "SAz9YHcvj6GT2YYXdXww", // River - Relaxed, Neutral + "cjVigY5qzO86Huf0OWal", // Eric + "bIHbv24MWmeRgasZH58o", // Will + ], +}; + +// Generate audio with ElevenLabs TTS +async function generateElevenLabs(text, gender, set, scream) { + const voiceList = ELEVEN_VOICES[gender] || ELEVEN_VOICES.neutral; + const voiceId = voiceList[set % voiceList.length]; + + const voiceSettings = scream + ? { stability: 0.1, similarity_boost: 0.7, style: 1.0, use_speaker_boost: true } + : { stability: 0.5, similarity_boost: 0.75, style: 0.4, use_speaker_boost: true }; + + const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, { + method: "POST", + headers: { + "xi-api-key": process.env.ELEVENLABS_API_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text, + model_id: "eleven_multilingual_v2", + voice_settings: voiceSettings, + }), + }); + + if (!response.ok) { + const err = await response.text(); + throw new Error(`ElevenLabs API error ${response.status}: ${err}`); + } + + return { + buffer: Buffer.from(await response.arrayBuffer()), + voiceId: `eleven-${voiceId.slice(0, 8)}`, + }; +} + // Generate audio with Google Cloud TTS async function generateGoogle(text, gender, set, isSSML) { // Fetch GCP key from URL @@ -163,13 +216,16 @@ exports.handler = async (event) => { const set = parseInt(body.voice?.split(":")[1]) || 0; const gender = body.voice?.split(":")[0]?.toLowerCase() || "neutral"; - // Provider: "openai" (default), "google" + // Provider: "openai" (default), "google", "eleven" // Can be set via body.provider or defaults to openai const provider = body.provider || "openai"; // Instructions for gpt-4o-mini-tts emotional/style control (OpenAI only) const instructions = provider === "openai" ? (body.instructions || null) : null; + // Scream mode for ElevenLabs (low stability, max style) + const scream = body.scream === true; + // Cache bust: if true, skip cache lookup and regenerate const bustCache = body.bust === true; @@ -183,7 +239,7 @@ exports.handler = async (event) => { } // Build voice identifier for cache key - const voiceSpec = `${provider}-${gender}-${set}`; + const voiceSpec = `${provider}-${gender}-${set}${scream ? "-scream" : ""}`; const cacheKey = getCacheKey(provider, voiceSpec, text, instructions); try { @@ -213,6 +269,8 @@ exports.handler = async (event) => { let result; if (provider === "google") { result = await generateGoogle(text, gender, set, isSSML); + } else if (provider === "eleven") { + result = await generateElevenLabs(text, gender, set, scream); } else { result = await generateOpenAI(text, gender, set, instructions); } diff --git a/system/public/aesthetic.computer/disks/say.mjs b/system/public/aesthetic.computer/disks/say.mjs index b83929a99..0d3472f3f 100644 --- a/system/public/aesthetic.computer/disks/say.mjs +++ b/system/public/aesthetic.computer/disks/say.mjs @@ -14,9 +14,10 @@ let text = ""; let lastSpoken = ""; let status = "idle"; // idle, speaking, error -let provider = "openai"; // "openai" or "google" +let provider = "openai"; // "openai", "google", or "eleven" let gender = "neutral"; let instructions = null; +let scream = false; // 🥾 Boot function boot({ params, colon }) { @@ -32,11 +33,16 @@ function boot({ params, colon }) { for (const part of parts) { if (part === "google") provider = "google"; else if (part === "openai") provider = "openai"; + else if (part === "eleven") provider = "eleven"; else if (part === "male") gender = "male"; else if (part === "female") gender = "female"; else if (part === "scream") { - provider = "openai"; - instructions = "Deliver this as a blood-curdling scream. Shriek at the absolute top of your lungs with your voice cracking. Pure primal rage. Do NOT speak normally — only scream, raw and unhinged."; + scream = true; + if (provider === "openai") { + instructions = "Deliver this as a blood-curdling scream. Shriek at the absolute top of your lungs with your voice cracking. Pure primal rage. Do NOT speak normally — only scream, raw and unhinged."; + } else if (provider !== "eleven") { + provider = "eleven"; // Default scream to ElevenLabs + } } } console.log(`Provider: ${provider}, Gender: ${gender}`); @@ -50,8 +56,8 @@ function paint({ wipe, ink, write, screen }) { // Note: Top-left corner is reserved for prompt HUD label // Provider indicator (below HUD area) - const providerColor = instructions ? "red" : provider === "google" ? "cyan" : "lime"; - const providerLabel = instructions ? `[${provider} SCREAM]` : `[${provider}]`; + const providerColor = scream ? "red" : provider === "eleven" ? "orange" : provider === "google" ? "cyan" : "lime"; + const providerLabel = scream ? `[${provider} SCREAM]` : `[${provider}]`; ink(providerColor).write(providerLabel, { x: 6, y: 18 }); // Instructions @@ -86,11 +92,12 @@ function act({ event: e, speak }) { const voice = `${gender}:0`; - console.log(`🗣️ Speaking: "${text}" with ${provider}, voice: ${voice}${instructions ? " [SCREAM]" : ""}`); + console.log(`🗣️ Speaking: "${text}" with ${provider}, voice: ${voice}${scream ? " [SCREAM]" : ""}`); speak(text, voice, "cloud", { volume: 1, - provider: provider, + provider, instructions, + scream, }); } diff --git a/system/public/aesthetic.computer/lib/speech.mjs b/system/public/aesthetic.computer/lib/speech.mjs index 9c68a8e13..377a42526 100644 --- a/system/public/aesthetic.computer/lib/speech.mjs +++ b/system/public/aesthetic.computer/lib/speech.mjs @@ -201,6 +201,7 @@ function speak(words, voice, mode = "local", opts = {}) { }; if (opts.instructions) payload.instructions = opts.instructions; + if (opts.scream) payload.scream = true; // Create a promise that resolves when the fetch completes let fetchResolve; -- 2.51.2