File size: 10,947 Bytes
ee826ee
 
b4c36fb
ee826ee
b4c36fb
 
 
 
 
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b4c36fb
ee826ee
 
b4c36fb
ee826ee
 
 
 
 
 
b4c36fb
 
ee826ee
 
b4c36fb
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b4c36fb
 
 
 
 
 
 
 
 
 
 
 
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
b4c36fb
 
 
 
ee826ee
 
b4c36fb
 
 
 
 
 
 
 
 
 
 
 
 
ee826ee
 
 
 
 
 
 
 
 
 
 
 
b4c36fb
ee826ee
cde5d07
 
b4c36fb
cde5d07
b4c36fb
 
cde5d07
 
 
 
 
 
 
 
 
 
 
 
b4c36fb
ee826ee
 
 
 
 
 
 
b4c36fb
ee826ee
 
 
 
 
 
 
 
 
 
b4c36fb
 
 
 
 
 
ee826ee
 
 
 
 
 
 
 
 
 
 
 
b4c36fb
 
 
 
 
 
 
ee826ee
 
b4c36fb
 
ee826ee
 
 
b4c36fb
ee826ee
b4c36fb
 
 
 
 
 
 
 
 
 
 
 
ee826ee
 
 
b4c36fb
 
ee826ee
 
 
 
 
 
b4c36fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/**
 * ============================================
 * 🎭 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, '\\$&');
}