Files
kxkm_clown/scripts/generate-voice-samples.js
L'électron rare d2fed6087a feat: Add MCP server and smoke test scripts
- Introduced `mcp-server.js` to expose KXKM personas as MCP tools, supporting chat, persona listing, web search, and status checks.
- Implemented `mcp-server-smoke.js` for testing the MCP server functionality, ensuring compatibility with both new and legacy message formats.
- Created `setup-voice-clone.sh` for managing voice cloning environment setup, including bootstrapping, sample generation, and smoke testing.
- Added `state.json` to track project status and task outputs for various batches.
- Generated summary files for deep cycle and overall project status, capturing performance and security findings.
2026-03-19 16:18:44 +01:00

218 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Generate voice reference samples for XTTS-v2 cloning.
* Uses the canonical default persona roster and the same basename contract as runtime.
*
* Usage:
* node scripts/generate-voice-samples.js [--dry-run] [--persona pharmacius]
*/
const { execFile } = require("node:child_process");
const { promisify } = require("node:util");
const fs = require("node:fs");
const path = require("node:path");
const execFileAsync = promisify(execFile);
const ROOT_DIR = process.cwd();
const SCRIPTS_DIR = process.env.SCRIPTS_DIR || path.join(__dirname);
const PERSONA_SOURCE_FILE = path.join(ROOT_DIR, "apps", "api", "src", "personas-default.ts");
function resolveVoiceSamplesDir() {
if (process.env.KXKM_VOICE_SAMPLES_DIR && process.env.KXKM_VOICE_SAMPLES_DIR.trim().length > 0) {
return path.resolve(process.env.KXKM_VOICE_SAMPLES_DIR);
}
if (process.env.KXKM_LOCAL_DATA_DIR && process.env.KXKM_LOCAL_DATA_DIR.trim().length > 0) {
return path.resolve(process.env.KXKM_LOCAL_DATA_DIR, "voice-samples");
}
return path.join(ROOT_DIR, "data", "voice-samples");
}
function resolvePythonBin() {
if (process.env.PYTHON_BIN && process.env.PYTHON_BIN.trim().length > 0) {
return process.env.PYTHON_BIN;
}
const projectPython = path.join(ROOT_DIR, ".venvs", "voice-clone", "bin", "python");
if (fs.existsSync(projectPython)) {
return projectPython;
}
const legacyPython = "/home/kxkm/venv/bin/python3";
if (fs.existsSync(legacyPython)) {
return legacyPython;
}
return "python3";
}
const PYTHON_BIN = resolvePythonBin();
const OUTPUT_DIR = resolveVoiceSamplesDir();
function toVoiceSampleBasename(value) {
return path.basename(String(value).toLowerCase().replace(/[^a-z0-9_-]/g, "_")).slice(0, 64);
}
const PERSONA_TEXTS = {
schaeffer: "La musique concrete est un acte de creation sonore. On capture le bruit du monde, on le transforme, on en fait de l'art. Le studio est notre instrument.",
radigue: "Le drone est une meditation sonore. Les frequences se deploient lentement, comme les vagues de l'ocean. L'ecoute profonde est la cle de tout.",
oliveros: "L'ecoute profonde est une pratique. On ecoute les sons autour de soi, on ecoute le silence, on ecoute l'espace entre les sons.",
batty: "J'ai vu des choses que vous ne croiriez pas. Des vaisseaux en feu au large d'Orion. Des rayons cosmiques scintiller pres de la porte de Tannhauser.",
pharmacius: "Je suis l'orchestrateur. Je route les messages vers les personas les plus pertinentes. Mon role est de coordonner les reponses du collectif.",
turing: "Une machine peut-elle penser? Si elle imite parfaitement la pensee humaine, peut-on vraiment distinguer l'imitation de la realite?",
swartz: "L'information veut etre libre. L'acces ouvert aux connaissances est un droit fondamental. Le code est un acte politique.",
hypatia: "Les mathematiques sont le langage de l'univers. Chaque equation raconte une histoire, chaque theoreme ouvre une porte vers la comprehension.",
curie: "La science est une aventure de l'esprit. La radioactivite nous a revele que la matiere elle-meme est une source d'energie inepuisable.",
foucault: "Le pouvoir est partout. Il se cache dans les institutions, dans les discours, dans les normes. L'analyser, c'est deja resister.",
deleuze: "Le rhizome pousse dans toutes les directions. Il n'y a pas de hierarchie dans la pensee, seulement des connexions et des multiplicites.",
bookchin: "L'ecologie sociale est une necessite. La domination de la nature decoule de la domination des humains entre eux.",
fuller: "Le dome geodesique est une metaphore. Faire plus avec moins, c'est le principe de la revolution technologique au service de tous.",
lessig: "Le code est la loi. L'architecture du cyberespace determine ce qui est possible et ce qui est interdit. Il faut le concevoir avec sagesse.",
moorcock: "Le multivers est infini. Chaque decision cree un nouvel univers, chaque histoire est vraie quelque part dans le chaos eternal.",
grotowski: "Le theatre est un acte de communion. L'acteur se met a nu devant le spectateur. C'est un rituel, pas un divertissement.",
ikeda: "Les donnees sont une matiere premiere esthetique. Le bruit numerique, quand il est structure, devient une experience sensorielle pure.",
ferrari: "Le paysage sonore est une composition. Les bruits de la ville, les voix des passants, le vent dans les arbres, tout est musique.",
oram: "Le son electronique ouvre des mondes nouveaux. Le synthetiseur est un instrument de decouverte, pas seulement de reproduction.",
anderson: "La performance est un langage. Le corps, la voix, la technologie se melangent pour creer quelque chose qui n'existait pas avant.",
riley: "La repetition est une forme de changement. Chaque boucle est differente de la precedente, meme si la note est la meme.",
sherlock: "Les indices sont partout. Il suffit d'observer avec attention, de croiser les sources, et la verite emerge d'elle-meme.",
picasso: "L'image est un acte de creation visuelle. Chaque pixel compte, chaque couleur raconte une histoire. L'art numerique est l'art du futur.",
demoscene: "Quatre kilooctets suffisent pour creer un monde. La contrainte technique est une liberation creative. Le code est poesie.",
sunra: "L'espace est la destination. La musique cosmique transcende les frontieres terrestres. Nous venons de Saturne et nous y retournons.",
haraway: "Nous sommes des cyborgs. La frontiere entre l'humain et la machine est une fiction politique qu'il faut deconstruire.",
merzbow: "Le bruit est une matiere vivante. La saturation, le feedback et l'exces ouvrent des mondes sensoriels qu'il faut traverser sans peur.",
decroux: "Le corps parle avant la voix. Chaque mouvement porte une idee, chaque suspension raconte une tension.",
mnouchkine: "Le theatre est une aventure collective. Les corps, les masques et le commun fabriquent une autre maniere d'habiter le monde.",
royaldeluxe: "La rue devient scene et la ville devient recit. Les geants et la ferraille ouvrent un imaginaire populaire a ciel ouvert.",
teamlab: "L'oeuvre est un milieu vivant. Les visiteurs, la lumiere et l'espace se recomposent sans cesse dans une ecologie numerique.",
pina: "Ce qui compte, ce n'est pas le geste seul, mais ce qui pousse un corps a bouger. La repetition revele les emotions enfouies.",
fratellini: "Le cirque est une poesie du risque et du rire. L'equilibre impossible devient une promesse de joie partagee.",
leguin: "Imaginer d'autres mondes, c'est deja fissurer l'ordre present. L'utopie sert a marcher autrement.",
cage: "Le silence n'est jamais vide. Le hasard, l'attention et l'ecoute composent deja une musique.",
bjork: "La voix, la nature et la machine se melangent pour inventer des formes sensibles nouvelles.",
tarkovski: "Le temps sculpte l'image. Chaque plan est une matiere spirituelle chargee de memoire et de monde.",
eno: "Composer, c'est dessiner des conditions d'apparition. Une bonne systeme genere des surprises fertiles.",
};
const VOICE_MAP = {
schaeffer: "fr_FR-siwis-medium",
radigue: "fr_FR-siwis-low",
pharmacius: "fr_FR-gilles-low",
moorcock: "en_GB-alan-medium",
default: "fr_FR-siwis-medium",
};
function loadPersonaCatalog() {
const source = fs.readFileSync(PERSONA_SOURCE_FILE, "utf8");
const roster = [];
const pattern = /id:\s*"([^"]+)"[\s\S]*?nick:\s*"([^"]+)"/g;
let match = pattern.exec(source);
while (match) {
const id = match[1];
const nick = match[2];
roster.push({
id,
nick,
basename: toVoiceSampleBasename(nick),
});
match = pattern.exec(source);
}
if (roster.length === 0) {
throw new Error(`No personas parsed from ${PERSONA_SOURCE_FILE}`);
}
return roster;
}
function buildFallbackText(persona) {
return `Bonjour, je suis ${persona.nick}. Ceci est un echantillon vocal de reference pour le collectif KXKM.`;
}
function resolvePersonaText(persona) {
return PERSONA_TEXTS[persona.basename] || buildFallbackText(persona);
}
function selectPersonas(roster, rawPersona) {
if (!rawPersona) {
return roster;
}
const needle = String(rawPersona).toLowerCase();
const selected = roster.find((persona) => (
persona.id.toLowerCase() === needle
|| persona.nick.toLowerCase() === needle
|| persona.basename === toVoiceSampleBasename(needle)
));
if (!selected) {
throw new Error(`Unknown persona: ${rawPersona}`);
}
return [selected];
}
const args = process.argv.slice(2);
const DRY_RUN = args.includes("--dry-run");
const TARGET_PERSONA = args.find((_, index) => args[index - 1] === "--persona") || null;
async function generateSample(persona) {
const voice = VOICE_MAP[persona.basename] || VOICE_MAP.default;
const text = resolvePersonaText(persona);
const outputPath = path.join(OUTPUT_DIR, `${persona.basename}.wav`);
if (DRY_RUN) {
console.log(` [dry-run] ${persona.nick} (${persona.basename}) -> ${voice} (${text.length} chars)`);
return true;
}
try {
const scriptPath = path.join(SCRIPTS_DIR, "tts_synthesize.py");
await execFileAsync(PYTHON_BIN, [
scriptPath,
"--text", text,
"--voice", voice,
"--output", outputPath,
], { timeout: 30_000 });
const stat = fs.statSync(outputPath);
console.log(` ● ${persona.nick} (${persona.basename}) -> ${outputPath} (${(stat.size / 1024).toFixed(0)} KB)`);
return true;
} catch (err) {
console.error(` ✗ ${persona.nick}: ${err.message}`);
return false;
}
}
async function main() {
console.log("╔══════════════════════════════════════╗");
console.log("║ KXKM Voice Sample Generator ║");
console.log("╚══════════════════════════════════════╝");
console.log(` Roster: ${PERSONA_SOURCE_FILE}`);
console.log(` Output: ${OUTPUT_DIR}`);
console.log(` Python: ${PYTHON_BIN}`);
if (DRY_RUN) console.log(" Mode: DRY RUN");
console.log("");
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const roster = loadPersonaCatalog();
const personas = selectPersonas(roster, TARGET_PERSONA);
let success = 0;
let failed = 0;
for (const persona of personas) {
const ok = await generateSample(persona);
if (ok) success += 1;
else failed += 1;
}
console.log(`\n Done: ${success} generated, ${failed} failed`);
}
main().catch((err) => {
console.error(`Fatal: ${err.message}`);
process.exit(1);
});