← back to Ventura Claw

server/connectors/elevenlabs.js

88 lines

// ElevenLabs — REAL. Text-to-speech generation + voice library + user account.
// Free tier: 10K characters/month. Voice cloning available on paid tiers.
function key(creds) {
  const k = creds?.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
  if (!k) throw new Error("ELEVENLABS_API_KEY not set");
  return k;
}

async function call(method, path, body, creds, accept = "application/json") {
  const r = await fetch("https://api.elevenlabs.io/v1" + path, {
    method,
    headers: {
      "xi-api-key": key(creds),
      "Content-Type": "application/json",
      "Accept": accept,
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (accept === "audio/mpeg") {
    if (!r.ok) {
      const txt = await r.text();
      throw new Error(`elevenlabs ${path} ${r.status}: ${txt.slice(0, 200)}`);
    }
    const buf = Buffer.from(await r.arrayBuffer());
    return { audio_mp3_base64: buf.toString("base64"), bytes: buf.length };
  }
  const d = r.status === 204 ? {} : await r.json();
  if (!r.ok) throw new Error(`elevenlabs ${path} ${r.status}: ${d.detail?.message || d.message || JSON.stringify(d).slice(0, 200)}`);
  return d;
}

module.exports = {
  meta: {
    id: "elevenlabs",
    name: "ElevenLabs",
    category: "ai-voice",
    docsUrl: "https://elevenlabs.io/docs/api-reference/introduction",
    auth: "api_key",
    realImpl: true,
  },
  fields: [
    { key: "ELEVENLABS_API_KEY", label: "API Key", type: "password", required: true, hint: "elevenlabs.io → Profile → API Keys (free tier: 10K chars/month)" },
  ],
  configured(c) { return !!(c?.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY); },

  async health(c) {
    if (!this.configured(c)) return { ok: false, reason: "no_token" };
    try {
      const u = await call("GET", "/user", null, c);
      return {
        ok: true,
        subscription: u.subscription?.tier || "free",
        chars_used: u.subscription?.character_count || 0,
        chars_limit: u.subscription?.character_limit || 0,
      };
    } catch (e) { return { ok: false, reason: e.message }; }
  },

  // Read actions
  async voicesList(c) {
    const d = await call("GET", "/voices", null, c);
    return { voices: (d.voices || []).map(v => ({
      id: v.voice_id, name: v.name, category: v.category,
      labels: v.labels || {}, description: v.description || "",
      preview_url: v.preview_url || "",
    })) };
  },

  async user(c) { return call("GET", "/user", null, c); },

  async modelsList(c) { return call("GET", "/models", null, c); },

  // Write actions (TTS generation counts against free-tier quota)
  async textToSpeech(c, args) {
    const { voice_id, text, model_id, stability, similarity_boost } = args || {};
    if (!voice_id) throw new Error("voice_id required");
    if (!text)     throw new Error("text required");
    return call("POST", `/text-to-speech/${voice_id}`, {
      text: String(text).slice(0, 5000),
      model_id: model_id || "eleven_turbo_v2_5",
      voice_settings: {
        stability: Number.isFinite(stability) ? stability : 0.5,
        similarity_boost: Number.isFinite(similarity_boost) ? similarity_boost : 0.75,
      },
    }, c, "audio/mpeg");
  },
};