← back to Crankd
lib/elevenlabs.js
52 lines
'use strict';
const fs = require('fs');
const path = require('path');
const PERSONAS_DIR = path.join(__dirname, '..', 'personas');
function loadPersona(slug) {
const idxPath = path.join(PERSONAS_DIR, 'index.json');
if (!fs.existsSync(idxPath)) throw new Error('personas/index.json missing');
const idx = JSON.parse(fs.readFileSync(idxPath, 'utf8'));
const meta = idx.personas[slug];
if (!meta) throw new Error(`unknown persona: ${slug}`);
const file = path.join(PERSONAS_DIR, meta.file);
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function listPersonas() {
const idx = JSON.parse(fs.readFileSync(path.join(PERSONAS_DIR, 'index.json'), 'utf8'));
return Object.entries(idx.personas).map(([slug, meta]) => ({ slug, ...meta }));
}
async function tts({ persona, text }) {
const p = loadPersona(persona);
if (!p.elevenlabs.voice_id) {
throw new Error(`persona '${persona}' has no voice_id yet — record samples + run PVC submission first`);
}
const key = process.env.ELEVENLABS_API_KEY;
if (!key) throw new Error('ELEVENLABS_API_KEY not set');
const url = `https://api.elevenlabs.io/v1/text-to-speech/${p.elevenlabs.voice_id}`;
const body = {
text,
model_id: p.elevenlabs.model_id || 'eleven_turbo_v2_5',
voice_settings: {
stability: p.elevenlabs.stability ?? 0.45,
similarity_boost: p.elevenlabs.similarity_boost ?? 0.75,
style: p.elevenlabs.style ?? 0.15,
use_speaker_boost: p.elevenlabs.use_speaker_boost ?? true,
},
};
const r = await fetch(url, {
method: 'POST',
headers: { 'xi-api-key': key, 'content-type': 'application/json', accept: 'audio/mpeg' },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`elevenlabs ${r.status}: ${await r.text()}`);
return Buffer.from(await r.arrayBuffer());
}
module.exports = { loadPersona, listPersonas, tts };