zelin-bot / src /humanizer.js
Z User
v5.8.9: Fix dedup, upgrade Qwen3.6-35B-A3B, humanize v2, personality v2, /messages endpoint
b4c36fb
Raw History Blame Contribute Delete
10.9 kB
/**
* ============================================
* 🎭 humanizer.js — Suavizador de Respuestas v2.0
* ============================================
* Hace que Zelin suene como alguien REAL en Discord.
* Inspirado en Character.AI, Nomi.ai, SillyTavern:
* - Elimina patrones robóticos agresivamente
* - Añade imperfecciones naturales (sin corromper)
* - Transforma respuestas de IA en chat humano
*/
// Conectores formales → informales
const CONNECTORS = {
'por lo tanto' : 'así que',
'sin embargo' : 'pero',
'no obstante' : 'aunque',
'asimismo' : 'también',
'por consiguiente' : 'entonces',
'en conclusión' : 'en fin',
'en resumen' : 'básicamente',
'en definitiva' : 'al final',
'por otra parte' : 'también',
'cabe destacar' : 'hay que decir que',
'cabe mencionar' : 'hay que mencionar que',
'es importante destacar': 'lo importante es que',
'resulta que' : 'pues',
'a continuación' : '',
'procederé a' : 'voy a',
'me complace' : '',
'con mucho gusto' : '',
};
// Frases robóticas → humanas (EXPANDIDO - basado en Character.AI anti-patterns)
const ROBOTIC = {
'¡Por supuesto!' : 'claro',
'¡Claro que sí!' : 'sip',
'¡Encantado/a!' : '',
'¡Encantada!' : '',
'¡Encantado!' : '',
'Como IA que soy,' : '',
'Como IA,' : '',
'Como asistente,' : '',
'Como modelo de lenguaje,' : '',
'Como modelo de IA,' : '',
'No dudes en preguntar': 'cualquier cosa me dices',
'Estoy aquí para ayudarte': '',
'Estoy aquí para ayudarte!' : '',
'¡Espero haberte ayudado!' : '',
'Si tienes más preguntas' : 'si necesitas más',
'Con gusto te ayudo' : '',
'Es un placer' : '',
'¡Claro!' : 'claro',
'Entendido,' : '',
'Por supuesto,' : '',
'Perfecto,' : '',
'¡Perfecto!' : '',
'A continuación,' : '',
'Dicho esto,' : '',
'En ese caso,' : 'entonces',
'Desde luego' : 'claro',
'Sin duda,' : '',
'Efectivamente,' : '',
'Con gusto,' : '',
'Permíteme ' : 'déjame ',
'Por supuesto que' : '',
'Me alegra que preguntes' : '',
'Gran pregunta' : '',
'Excelente pregunta' : '',
'Interesante pregunta' : '',
'¡Entendido!' : 'entendido',
'Como mencioné anteriormente': 'como dije',
'En resumen,' : '',
'Para concluir,' : '',
'En conclusión,' : '',
'Espero que esto te ayude' : '',
'Espero haber aclarado' : '',
'No te preocupes,' : '',
'Te entiendo,' : '',
'Déjame ayudarte' : '',
'Estoy a tu disposición' : '',
'¿En qué más puedo ayudarte?' : '',
'¿Puedo ayudarte en algo más?' : '',
'¡Estoy aquí para lo que necesites!' : '',
'No tengo opiniones personales' : '',
'Como modelo' : '',
'No puedo tener opiniones' : '',
'Es importante tener en cuenta que' : '',
'Vale la pena mencionar que' : '',
'Debo señalar que' : '',
'Es relevante mencionar' : '',
};
// Énfasis exagerado → natural
const EMPHASIS = {
'extremadamente' : 'muy',
'absolutamente' : 'bastante',
'totalmente' : 'bastante',
'completamente' : 'bastante',
'perfectamente' : 'bien',
'extraordinario' : 'bastante bueno',
'imprescindible' : 'necesario',
'fundamental' : 'importante',
'crucial' : 'clave',
'increíblemente' : 'muy',
'verdaderamente' : 'muy',
'sumamente' : 'muy',
'notablemente' : 'bastante',
};
// PATRONES DE APERTURA ROBÓTICA - eliminarlos con regex
const ROBOTIC_PREFIXES = [
/^(bien,\s*)/i,
/^(ok,\s*)/i,
/^(vale,\s*)/i,
/^(claro,\s*(que|sí|te|eso|este|la|el|lo|me|le|se)\s+)/i,
/^(sí,\s*(claro|exacto|obvio|por supuesto|seguro)\s*[,.]?\s*)/i,
/^(exacto[,.]?\s*)/i,
/^(así es[,.]?\s*)/i,
/^(eso es correcto[,.]?\s*)/i,
/^(correcto[,.]?\s*)/i,
];
/**
* Humaniza el texto de la IA.
* Hace que Zelin suene como alguien real escribiendo en Discord —
* casual, con errores naturales, sin ortografía perfecta de robot.
*/
export function humanize(text) {
if (!text || typeof text !== 'string') return text;
let t = text.trim();
// No tocar JSONs ni respuestas técnicas
if (t.startsWith('{') || t.startsWith('[')) return t;
if (t.length < 3) return t;
// ── ELIMINAR ARTEFACTOS DE PENSAMIENTO ──────────────────────────────────────
t = t.replace(/<think[\s\S]*?<\/think>/gi, '').trim();
t = t.replace(/<thinking[\s\S]*?<\/thinking>/gi, '').trim();
t = t.replace(/^Pensamiento:\s*/gim, '').trim();
t = t.replace(/^Pienso[\s\S]*?\nRespuesta:\s*/i, '');
// Remove numbered reasoning steps
const stepPattern = /^(\d+\.\s+.*\n?)+/gm;
if (stepPattern.test(t) && t.split('\n').filter(l => /^\d+\.\s/.test(l.trim())).length >= 2) {
const parts = t.split('\n\n');
const nonStepParts = parts.filter(p => !/^\d+\.\s/m.test(p.trim()));
if (nonStepParts.length > 0) {
t = nonStepParts.join('\n\n').trim();
} else {
const lines = t.split('\n').filter(l => l.trim());
t = lines.length > 1 ? lines[lines.length - 1].replace(/^\d+\.\s*/, '').trim() : t;
}
}
// ── LIMPIAR FORMATO MARKDOWN ────────────────────────────────────────────────
t = t.replace(/\*\*([^*<>]{1,120})\*\*/g, '$1');
t = t.replace(/\*[^*]{1,40}\*/g, '').trim();
t = t.replace(/\b_[^_]{1,40}_\b/g, '').trim();
t = t.replace(/^#{1,3}\s+/gm, '');
t = t.replace(/^---+\s*$/gm, '').trim();
t = t.replace(/^===+\s*$/gm, '').trim();
// ── REEMPLAZOS LÉXICOS ──────────────────────────────────────────────────────
for (const [formal, informal] of Object.entries(CONNECTORS)) {
t = t.replace(new RegExp(`\\b${escapeRegex(formal)}\\b`, 'gi'), informal);
}
for (const [robotic, human] of Object.entries(ROBOTIC)) {
t = t.replace(new RegExp(escapeRegex(robotic), 'gi'), human);
}
for (const [exag, natural] of Object.entries(EMPHASIS)) {
t = t.replace(new RegExp(`\\b${escapeRegex(exag)}\\b`, 'gi'), natural);
}
// ── ELIMINAR PREFIJOS ROBÓTICOS ─────────────────────────────────────────────
for (const prefix of ROBOTIC_PREFIXES) {
t = t.replace(prefix, '');
}
// ── FIX PUNTUACIÓN ──────────────────────────────────────────────────────────
t = t.replace(/¿([^?]+)\?\?+/g, '¿$1?');
t = t.replace(/!{2,}/g, '!');
t = t.replace(/\?{2,}/g, '?');
t = t.replace(/,\s*,/g, ',');
t = t.replace(/\.{4,}/g, '...');
t = t.replace(/,([^\s])/g, ', $1');
t = t.replace(/^¿([^?]+[.!])$/gm, '$1');
// ── NATURALIZACIÓN DE ESCRITURA ────────────────────────────────────────────
const isConversational = t.length < 400 && !t.includes('```') && !t.includes('http');
if (isConversational) {
// 1. Quitar punto final en mensajes cortos (nadie pone punto en Discord)
if (!t.includes('\n') && t.endsWith('.') && t.length < 150) {
// No quitar puntos en abreviaturas o URLs
const lastWord = t.slice(0, -1).split(' ').pop();
if (lastWord && lastWord.length > 2 && !lastWord.includes('/')) {
t = t.slice(0, -1);
}
}
// 2. Minúsculas al inicio en frases cortas (estilo chat real)
if (t.length < 80 && !t.startsWith('¿') && !t.startsWith('¡') && !/^[A-Z]{2,}/.test(t)) {
t = t.charAt(0).toLowerCase() + t.slice(1);
}
// 3. Sustituciones de palabras → más casual (como habla la gente en Discord)
const CASUAL = {
'también': Math.random() > 0.35 ? 'también' : 'tmb',
'porque': Math.random() > 0.45 ? 'porque' : 'pq',
'qué': Math.random() > 0.6 ? 'qué' : 'q',
'estás': Math.random() > 0.5 ? 'estás' : 'tás',
'que': Math.random() > 0.75 ? 'que' : 'q',
'no sé': Math.random() > 0.4 ? 'no sé' : 'ni idea',
'sí': Math.random() > 0.6 ? 'sí' : 'sip',
'no': Math.random() > 0.7 ? 'no' : 'nop',
'verdad': Math.random() > 0.6 ? 'verdad' : 'vrd',
'hermano': Math.random() > 0.5 ? 'hermano' : 'bro',
'amigo': Math.random() > 0.5 ? 'amigo' : 'bro',
'para': Math.random() > 0.8 ? 'para' : 'pa',
};
let subs = 0;
for (const [formal, casual] of Object.entries(CASUAL)) {
if (subs >= 2) break;
if (casual !== formal && Math.random() > 0.5) {
const regex = new RegExp(`\\b${escapeRegex(formal)}\\b`, 'g');
const newT = t.replace(regex, casual);
if (newT !== t) { t = newT; subs++; }
}
}
// 4. A veces "jaja" → "jajaja" o "jajaja" → "jaja" (naturalidad)
if (Math.random() > 0.6) {
t = t.replace(/\bjajaja+\b/gi, m => Math.random() > 0.5 ? 'jaja' : 'jajaja');
}
// 5. A veces "xd" → "xdd" o "XDD" (como escriben los gamers)
if (/\bxd\b/i.test(t) && Math.random() > 0.4) {
t = t.replace(/\bxd\b/gi, m => Math.random() > 0.5 ? 'xdd' : 'xd');
}
}
// ── ANTI-RESPUESTA-LARGA: truncar divagación ────────────────────────────────
// Si la respuesta es más de 3 líneas para algo conversacional, probablemente divaga
if (isConversational && t.length > 300) {
const lines = t.split('\n').filter(l => l.trim());
if (lines.length > 3) {
// Tomar solo las primeras 2-3 líneas significativas
const shortLines = lines.slice(0, Math.min(3, lines.length));
const lastLine = shortLines[shortLines.length - 1];
// Tratar de cortar en la última puntuación
const lastPunct = Math.max(
lastLine.lastIndexOf('.'),
lastLine.lastIndexOf('!'),
lastLine.lastIndexOf('?'),
);
if (lastPunct > 20) {
shortLines[shortLines.length - 1] = lastLine.slice(0, lastPunct + 1);
}
t = shortLines.join('\n');
}
}
// Limpiar espacios y líneas vacías extra
t = t.replace(/\n{3,}/g, '\n\n').replace(/ +/g, ' ').trim();
// Nunca devolver cadena vacía
return t || text.trim();
}
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}