teszenofficial commited on
Commit
587bf27
·
verified ·
1 Parent(s): fd07f17

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +828 -134
app.py CHANGED
@@ -1,8 +1,11 @@
1
  import os
 
2
  import torch
3
  import json
 
 
4
  import re
5
- from fastapi import FastAPI
6
  from fastapi.responses import HTMLResponse
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel, Field
@@ -13,13 +16,26 @@ import torch.nn as nn
13
  import torch.nn.functional as F
14
  import sentencepiece as spm
15
 
16
- # ====================== CONFIGURACIÓN ======================
17
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
18
- print(f"📱 Dispositivo: {DEVICE}")
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  MODEL_REPO = "TeszenAI/MTP-3"
21
 
22
- # ====================== ARQUITECTURA DEL MODELO ======================
 
 
23
  class LayerNorm(nn.Module):
24
  def __init__(self, d_model: int, eps: float = 1e-5):
25
  super().__init__()
@@ -98,8 +114,8 @@ class PositionalEncoding(nn.Module):
98
  return x + self.pe[:, :x.size(1), :]
99
 
100
  class MTPModel(nn.Module):
101
- def __init__(self, vocab_size: int, d_model: int = 512, n_heads: int = 8,
102
- n_layers: int = 8, d_ff: int = 2048, dropout: float = 0.1, max_len: int = 512):
103
  super().__init__()
104
  self.vocab_size = vocab_size
105
  self.d_model = d_model
@@ -109,6 +125,7 @@ class MTPModel(nn.Module):
109
  self.blocks = nn.ModuleList([TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)])
110
  self.norm = LayerNorm(d_model)
111
  self.lm_head = nn.Linear(d_model, vocab_size)
 
112
  def forward(self, x, mask=None):
113
  if mask is None:
114
  mask = torch.tril(torch.ones(x.size(1), x.size(1))).unsqueeze(0).unsqueeze(0).to(x.device)
@@ -119,214 +136,891 @@ class MTPModel(nn.Module):
119
  x = self.norm(x)
120
  return self.lm_head(x)
121
 
122
- # ====================== DESCARGA DEL MODELO ======================
123
- print(f"📦 Cargando modelo...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  if os.path.exists("mtp_repo") and os.path.exists("mtp_repo/mtp_model.pt"):
 
126
  repo_path = "mtp_repo"
127
  else:
128
  try:
129
- repo_path = snapshot_download(repo_id=MODEL_REPO, repo_type="model", local_dir="mtp_repo", resume_download=True)
130
  except:
131
  repo_path = "mtp_repo"
132
 
133
- # Configuración
134
  config_path = os.path.join(repo_path, "config.json")
135
  if os.path.exists(config_path):
136
  with open(config_path, "r") as f:
137
  config = json.load(f)
138
  else:
139
- config = {"vocab_size": 10000, "d_model": 512, "n_heads": 8, "n_layers": 8, "d_ff": 2048, "dropout": 0.1, "max_len": 512}
 
 
 
 
 
 
 
 
140
 
141
- # Tokenizador
142
  tokenizer_path = os.path.join(repo_path, "mtp_tokenizer.model")
143
  if os.path.exists(tokenizer_path):
144
  sp = spm.SentencePieceProcessor()
145
  sp.load(tokenizer_path)
146
  VOCAB_SIZE = sp.get_piece_size()
147
  config["vocab_size"] = VOCAB_SIZE
 
148
  else:
149
  sp = None
150
- VOCAB_SIZE = 10000
 
 
 
 
 
151
 
152
  model = MTPModel(**config)
153
  model.to(DEVICE)
154
 
 
155
  model_path = os.path.join(repo_path, "mtp_model.pt")
156
  if os.path.exists(model_path):
157
  try:
158
  state_dict = torch.load(model_path, map_location=DEVICE)
159
  model.load_state_dict(state_dict)
160
- print("✅ Modelo cargado")
161
- except:
162
- print("⚠️ Error cargando pesos")
 
163
  model.eval()
164
 
165
- # ====================== API ======================
166
- app = FastAPI()
167
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
 
 
 
 
 
168
 
169
  class PromptRequest(BaseModel):
170
  text: str = Field(..., max_length=2000)
 
 
 
 
171
 
172
- class TokenizerWrapper:
173
- def __init__(self, sp_model):
174
- self.sp = sp_model
175
- def encode(self, text):
176
- if self.sp is None:
177
- return [ord(c) % 1000 for c in text[:200]]
178
- return self.sp.encode(text)
179
- def decode(self, tokens):
180
- if self.sp is None:
181
- return ''.join([chr(t % 128) if 32 <= t % 128 < 127 else ' ' for t in tokens])
182
- return self.sp.decode(tokens)
183
- def eos_id(self):
184
- return self.sp.eos_id() if self.sp else 3
185
 
186
- tokenizer = TokenizerWrapper(sp)
187
-
188
- # Diccionario de respuestas por categoría (fallback cuando el modelo alucina)
189
- RESPUESTAS_FALLBACK = {
190
- "marketing": "El marketing digital incluye estrategias como SEO, marketing en redes sociales, email marketing, publicidad pagada y marketing de contenidos. ¿Te gustaría que profundice en alguna de estas áreas?",
191
- "blackpink": "BLACKPINK es un grupo femenino de K-pop formado por Jisoo, Jennie, Rosé y Lisa. Tienen éxitos como 'Ddu-Du Ddu-Du', 'Kill This Love' y 'How You Like That'.",
192
- "bts": "BTS es un grupo masculino de K-pop formado por RM, Jin, Suga, J-Hope, Jimin, V y Jungkook. Son conocidos por éxitos como 'Dynamite', 'Butter' y 'Boy With Luv'.",
193
- "default": "Lo siento, no entendí bien tu pregunta. ¿Podrías reformularla? Estoy aquí para ayudarte con marketing, K-pop (BLACKPINK, BTS), tecnología, y más."
194
- }
195
-
196
- def detectar_tema(texto):
197
- texto = texto.lower()
198
- if "marketing" in texto or "seo" in texto or "publicidad" in texto or "redes sociales" in texto:
199
- return "marketing"
200
- if "blackpink" in texto or "jisoo" in texto or "jennie" in texto or "rosé" in texto or "lisa" in texto:
201
- return "blackpink"
202
- if "bts" in texto or "rm" in texto or "jin" in texto or "suga" in texto or "j-hope" in texto or "jimin" in texto or "v" in texto or "jungkook" in texto:
203
- return "bts"
204
- return None
205
-
206
- def generar_respuesta(prompt, max_length=200, temperature=0.7):
207
- formatted = f"### Instrucción:\n{prompt}\n\n### Respuesta:\n"
208
- input_ids = tokenizer.encode(formatted)
209
  generated = input_ids.copy()
210
  eos_id = tokenizer.eos_id()
211
 
212
- for _ in range(max_length):
213
- input_tensor = torch.tensor([generated[-model.max_len:]], dtype=torch.long).to(DEVICE)
 
 
 
 
214
  with torch.no_grad():
215
  logits = model(input_tensor)
216
  next_logits = logits[0, -1, :] / temperature
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  probs = F.softmax(next_logits, dim=-1)
219
  next_token = torch.multinomial(probs, 1).item()
220
 
 
221
  if next_token == eos_id:
222
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  generated.append(next_token)
 
 
 
 
 
 
 
224
 
 
225
  response = tokenizer.decode(generated)
 
 
226
  if "### Respuesta:" in response:
227
  response = response.split("### Respuesta:")[-1].strip()
 
 
 
 
228
 
229
- # Limpiar caracteres basura
230
- response = re.sub(r'[^\w\s\u00C0-\u00FF.,!?¿¡\-:;"]+', ' ', response)
 
 
 
 
 
231
  response = re.sub(r'\s+', ' ', response).strip()
232
 
233
- # Verificar si la respuesta es coherente
234
- if len(response) < 5 or "kSq" in response or "%" in response or "{" in response:
235
- tema = detectar_tema(prompt)
236
- if tema:
237
- response = RESPUESTAS_FALLBACK.get(tema, RESPUESTAS_FALLBACK["default"])
238
- else:
239
- response = RESPUESTAS_FALLBACK["default"]
 
 
 
 
 
 
 
240
 
241
  return response
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  @app.post("/generate")
244
  async def generate(req: PromptRequest):
 
 
 
245
  user_input = req.text.strip()
246
  if not user_input:
247
- return {"reply": ""}
 
 
 
 
248
 
249
  try:
250
- response = generar_respuesta(user_input)
251
- return {"reply": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  except Exception as e:
253
- print(f"Error: {e}")
254
- tema = detectar_tema(user_input)
255
- return {"reply": RESPUESTAS_FALLBACK.get(tema, RESPUESTAS_FALLBACK["default"])}
 
 
 
 
256
 
257
- @app.get("/")
258
- async def chat_ui():
259
- return HTMLResponse("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  <!DOCTYPE html>
261
- <html>
262
  <head>
263
- <title>MTP Asistente</title>
264
- <meta charset="UTF-8">
265
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
266
- <style>
267
- body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #131314; color: #e3e3e3; }
268
- #chat { height: 500px; border: 1px solid #333; overflow-y: auto; padding: 10px; margin-bottom: 10px; border-radius: 10px; background: #1E1F20; }
269
- .user { text-align: right; margin: 10px; }
270
- .user span { background: #4a9eff; padding: 8px 15px; border-radius: 18px; display: inline-block; }
271
- .bot { text-align: left; margin: 10px; }
272
- .bot span { background: #282a2c; padding: 8px 15px; border-radius: 18px; display: inline-block; }
273
- input { width: 80%; padding: 10px; border-radius: 25px; border: none; background: #1E1F20; color: white; }
274
- button { padding: 10px 20px; border-radius: 25px; border: none; background: #4a9eff; color: white; cursor: pointer; }
275
- .typing { opacity: 0.7; font-style: italic; }
276
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  </head>
278
  <body>
279
- <h1>🤖 MTP Asistente</h1>
280
- <div id="chat">
281
- <div class="bot"><span>¡Hola! Soy MTP. Puedo ayudarte con marketing, K-pop (BLACKPINK, BTS), tecnología y más. ¿Qué necesitas?</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
283
- <div>
284
- <input type="text" id="input" placeholder="Escribe tu mensaje..." autocomplete="off">
285
- <button onclick="sendMessage()">Enviar</button>
286
  </div>
287
- <script>
288
- const chat = document.getElementById('chat');
289
- const input = document.getElementById('input');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
- function addMessage(text, sender) {
292
- const div = document.createElement('div');
293
- div.className = sender;
294
- div.innerHTML = `<span>${text}</span>`;
295
- chat.appendChild(div);
296
- chat.scrollTop = chat.scrollHeight;
 
 
 
297
  }
298
 
299
- async function sendMessage() {
300
- const text = input.value.trim();
301
- if (!text) return;
302
- input.value = '';
303
- addMessage(text, 'user');
304
-
305
- const loadingDiv = document.createElement('div');
306
- loadingDiv.className = 'bot';
307
- loadingDiv.innerHTML = '<span class="typing">✍️ Pensando...</span>';
308
- chat.appendChild(loadingDiv);
309
- chat.scrollTop = chat.scrollHeight;
310
-
311
- try {
312
- const response = await fetch('/generate', {
313
- method: 'POST',
314
- headers: { 'Content-Type': 'application/json' },
315
- body: JSON.stringify({ text: text })
316
- });
317
- const data = await response.json();
318
- loadingDiv.remove();
319
- addMessage(data.reply, 'bot');
320
- } catch (error) {
321
- loadingDiv.innerHTML = '<span class="typing">❌ Error de conexión</span>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
  }
324
-
325
- input.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); });
326
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  </body>
328
  </html>
329
- """)
330
 
331
  if __name__ == "__main__":
332
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
1
  import os
2
+ import sys
3
  import torch
4
  import json
5
+ import time
6
+ import gc
7
  import re
8
+ from fastapi import FastAPI, Request
9
  from fastapi.responses import HTMLResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from pydantic import BaseModel, Field
 
16
  import torch.nn.functional as F
17
  import sentencepiece as spm
18
 
19
+ # ======================
20
+ # CONFIGURACIÓN DE DISPOSITIVO
21
+ # ======================
22
+ if torch.cuda.is_available():
23
+ DEVICE = "cuda"
24
+ print("✅ GPU NVIDIA detectada. Usando CUDA.")
25
+ else:
26
+ DEVICE = "cpu"
27
+ print("⚠️ GPU no detectada. Usando CPU (puede ser más lento).")
28
+
29
+ if DEVICE == "cpu":
30
+ torch.set_num_threads(max(1, os.cpu_count() // 2))
31
+
32
+ torch.set_grad_enabled(False)
33
 
34
  MODEL_REPO = "TeszenAI/MTP-3"
35
 
36
+ # ======================
37
+ # ARQUITECTURA DEL MODELO MEJORADA
38
+ # ======================
39
  class LayerNorm(nn.Module):
40
  def __init__(self, d_model: int, eps: float = 1e-5):
41
  super().__init__()
 
114
  return x + self.pe[:, :x.size(1), :]
115
 
116
  class MTPModel(nn.Module):
117
+ def __init__(self, vocab_size: int, d_model: int = 256, n_heads: int = 8,
118
+ n_layers: int = 6, d_ff: int = 1024, dropout: float = 0.1, max_len: int = 512):
119
  super().__init__()
120
  self.vocab_size = vocab_size
121
  self.d_model = d_model
 
125
  self.blocks = nn.ModuleList([TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)])
126
  self.norm = LayerNorm(d_model)
127
  self.lm_head = nn.Linear(d_model, vocab_size)
128
+
129
  def forward(self, x, mask=None):
130
  if mask is None:
131
  mask = torch.tril(torch.ones(x.size(1), x.size(1))).unsqueeze(0).unsqueeze(0).to(x.device)
 
136
  x = self.norm(x)
137
  return self.lm_head(x)
138
 
139
+ # ======================
140
+ # NLP UTILITIES - PROCESAMIENTO DE LENGUAJE NATURAL
141
+ # ======================
142
+ class NLPProcessor:
143
+ """Procesador de lenguaje natural para entender mejor las intenciones"""
144
+
145
+ @staticmethod
146
+ def detect_intent(text):
147
+ """Detecta la intención del usuario"""
148
+ text_lower = text.lower()
149
+
150
+ intents = {
151
+ 'saludo': ['hola', 'buenas', 'que tal', 'cómo estás', 'hey', 'saludos'],
152
+ 'despedida': ['adiós', 'chao', 'hasta luego', 'nos vemos', 'bye'],
153
+ 'agradecimiento': ['gracias', 'gracias por', 'te agradezco', 'muchas gracias'],
154
+ 'pregunta': ['qué es', 'cómo funciona', 'por qué', 'cuándo', 'dónde', 'quién'],
155
+ 'ayuda': ['ayuda', 'necesito ayuda', 'puedes ayudarme', 'me ayudas'],
156
+ 'presentacion': ['quién eres', 'qué eres', 'presentate', 'eres'],
157
+ 'capacidad': ['qué puedes hacer', 'funciones', 'capacidades', 'que sabes hacer'],
158
+ 'sentimiento': ['estoy triste', 'estoy feliz', 'me siento', 'emocionado']
159
+ }
160
+
161
+ for intent, keywords in intents.items():
162
+ for keyword in keywords:
163
+ if keyword in text_lower:
164
+ return intent
165
+ return 'general'
166
+
167
+ @staticmethod
168
+ def should_stop(response, min_length=30, max_length=200):
169
+ """Determina si la respuesta debe terminar"""
170
+
171
+ # Palabras que indican final de respuesta
172
+ stop_phrases = [
173
+ '¿alguna otra pregunta?', '¿en qué más puedo ayudarte?',
174
+ '¿necesitas ayuda con algo más?', '¿tienes alguna otra duda?',
175
+ 'espero haberte ayudado', 'que tengas un buen día',
176
+ 'hasta luego', 'adiós', 'saludos', 'gracias por consultar'
177
+ ]
178
+
179
+ # Si es demasiado corta, continuar
180
+ if len(response) < min_length:
181
+ return False
182
+
183
+ # Si excede el máximo, cortar
184
+ if len(response) > max_length:
185
+ return True
186
+
187
+ # Verificar frases de parada
188
+ for phrase in stop_phrases:
189
+ if phrase in response.lower():
190
+ return True
191
+
192
+ # Verificar si termina con puntuación adecuada
193
+ if len(response) > 50:
194
+ last_chars = response[-10:]
195
+ # Termina con punto, signo de interrogación o exclamación
196
+ if any(last_chars.rstrip().endswith(p) for p in ['.', '?', '!', '…']):
197
+ # Contar oraciones completas
198
+ sentences = re.split(r'[.!?]+', response)
199
+ if len(sentences) >= 2: # Al menos 2 oraciones completas
200
+ return True
201
+
202
+ return False
203
+
204
+ @staticmethod
205
+ def clean_response(text):
206
+ """Limpia y mejora la respuesta"""
207
+ # Eliminar repeticiones excesivas
208
+ text = re.sub(r'(\b\w+\b)(?:\s+\1\b)+', r'\1', text)
209
+
210
+ # Corregir espaciado
211
+ text = re.sub(r'\s+([.,!?;:])', r'\1', text)
212
+
213
+ # Asegurar mayúscula al inicio
214
+ if text and text[0].islower():
215
+ text = text[0].upper() + text[1:]
216
+
217
+ # Agregar punto final si no tiene
218
+ if text and not text[-1] in '.!?':
219
+ text += '.'
220
+
221
+ return text.strip()
222
+
223
+ @staticmethod
224
+ def extract_key_info(text):
225
+ """Extrae información clave del texto"""
226
+ # Detectar números
227
+ numbers = re.findall(r'\d+(?:\.\d+)?', text)
228
+
229
+ # Detectar emails
230
+ emails = re.findall(r'[\w\.-]+@[\w\.-]+\.\w+', text)
231
+
232
+ # Detectar URLs
233
+ urls = re.findall(r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+', text)
234
+
235
+ return {
236
+ 'has_numbers': bool(numbers),
237
+ 'has_emails': bool(emails),
238
+ 'has_urls': bool(urls),
239
+ 'numbers': numbers,
240
+ 'emails': emails,
241
+ 'urls': urls
242
+ }
243
+
244
+ # ======================
245
+ # DESCARGA Y CARGA DEL MODELO
246
+ # ======================
247
+ def download_with_retry(repo_id, local_dir, max_retries=3):
248
+ for attempt in range(max_retries):
249
+ try:
250
+ print(f"📦 Intento {attempt + 1}/{max_retries} - Descargando modelo...")
251
+ repo_path = snapshot_download(
252
+ repo_id=repo_id,
253
+ repo_type="model",
254
+ local_dir=local_dir,
255
+ resume_download=True,
256
+ local_files_only=False
257
+ )
258
+ print(f"✅ Modelo descargado")
259
+ return repo_path
260
+ except Exception as e:
261
+ print(f"⚠️ Error: {str(e)[:100]}")
262
+ if attempt < max_retries - 1:
263
+ time.sleep(3)
264
+ else:
265
+ raise
266
+ return local_dir
267
+
268
+ print(f"🚀 Cargando modelo...")
269
 
270
  if os.path.exists("mtp_repo") and os.path.exists("mtp_repo/mtp_model.pt"):
271
+ print("📁 Modelo en caché")
272
  repo_path = "mtp_repo"
273
  else:
274
  try:
275
+ repo_path = download_with_retry(MODEL_REPO, "mtp_repo", max_retries=3)
276
  except:
277
  repo_path = "mtp_repo"
278
 
279
+ # Cargar configuración
280
  config_path = os.path.join(repo_path, "config.json")
281
  if os.path.exists(config_path):
282
  with open(config_path, "r") as f:
283
  config = json.load(f)
284
  else:
285
+ config = {
286
+ "vocab_size": 2000,
287
+ "d_model": 256,
288
+ "n_heads": 8,
289
+ "n_layers": 6,
290
+ "d_ff": 1024,
291
+ "dropout": 0.1,
292
+ "max_len": 512
293
+ }
294
 
295
+ # Cargar tokenizador
296
  tokenizer_path = os.path.join(repo_path, "mtp_tokenizer.model")
297
  if os.path.exists(tokenizer_path):
298
  sp = spm.SentencePieceProcessor()
299
  sp.load(tokenizer_path)
300
  VOCAB_SIZE = sp.get_piece_size()
301
  config["vocab_size"] = VOCAB_SIZE
302
+ print(f"✅ Tokenizador: {VOCAB_SIZE} tokens")
303
  else:
304
  sp = None
305
+ VOCAB_SIZE = config.get("vocab_size", 2000)
306
+
307
+ print(f"🧠 Inicializando modelo...")
308
+ print(f" → Vocabulario: {VOCAB_SIZE}")
309
+ print(f" → Dimensión: {config['d_model']}")
310
+ print(f" → Capas: {config['n_layers']}")
311
 
312
  model = MTPModel(**config)
313
  model.to(DEVICE)
314
 
315
+ # Cargar pesos
316
  model_path = os.path.join(repo_path, "mtp_model.pt")
317
  if os.path.exists(model_path):
318
  try:
319
  state_dict = torch.load(model_path, map_location=DEVICE)
320
  model.load_state_dict(state_dict)
321
+ print("✅ Pesos cargados")
322
+ except Exception as e:
323
+ print(f"⚠️ Error cargando pesos: {e}")
324
+
325
  model.eval()
326
 
327
+ param_count = sum(p.numel() for p in model.parameters())
328
+ print(f"✅ Modelo listo: {param_count:,} parámetros ({param_count/1e6:.1f}M)")
329
+
330
+ # ======================
331
+ # API CONFIG
332
+ # ======================
333
+ app = FastAPI(title="MTP API - Versión Mejorada", description="API con NLP integrado", version="2.0")
334
+
335
+ app.add_middleware(
336
+ CORSMiddleware,
337
+ allow_origins=["*"],
338
+ allow_methods=["*"],
339
+ allow_headers=["*"],
340
+ )
341
 
342
  class PromptRequest(BaseModel):
343
  text: str = Field(..., max_length=2000)
344
+ max_tokens: int = Field(default=150, ge=10, le=300)
345
+ temperature: float = Field(default=0.7, ge=0.1, le=2.0)
346
+ top_k: int = Field(default=50, ge=1, le=100)
347
+ top_p: float = Field(default=0.9, ge=0.1, le=1.0)
348
 
349
+ # Inicializar NLP
350
+ nlp = NLPProcessor()
 
 
 
 
 
 
 
 
 
 
 
351
 
352
+ # ======================
353
+ # GENERACIÓN INTELIGENTE MEJORADA
354
+ # ======================
355
+ def generate_response_intelligent(model, tokenizer, prompt, max_length=150, temperature=0.7, top_k=50, top_p=0.9, device='cpu'):
356
+ model.eval()
357
+
358
+ # Detectar intención para ajustar comportamiento
359
+ intent = nlp.detect_intent(prompt)
360
+
361
+ # Ajustar temperatura según intención
362
+ if intent == 'despedida':
363
+ temperature = 0.5 # Más determinista
364
+ max_length = min(max_length, 60) # Respuestas cortas
365
+ elif intent == 'pregunta':
366
+ temperature = 0.6 # Más preciso
367
+ elif intent == 'agradecimiento':
368
+ temperature = 0.5
369
+ max_length = min(max_length, 50)
370
+
371
+ formatted_prompt = f"### Instrucción:\n{prompt}\n\n### Respuesta:\n"
372
+ input_ids = tokenizer.encode(formatted_prompt)
 
 
373
  generated = input_ids.copy()
374
  eos_id = tokenizer.eos_id()
375
 
376
+ # Contadores para control de parada
377
+ consecutive_punctuation = 0
378
+ last_chars = []
379
+
380
+ for step in range(max_length):
381
+ input_tensor = torch.tensor([generated[-model.max_len:]], dtype=torch.long).to(device)
382
  with torch.no_grad():
383
  logits = model(input_tensor)
384
  next_logits = logits[0, -1, :] / temperature
385
 
386
+ # Top-k filtering
387
+ if top_k > 0:
388
+ indices_to_remove = next_logits < torch.topk(next_logits, top_k)[0][..., -1, None]
389
+ next_logits[indices_to_remove] = float('-inf')
390
+
391
+ # Top-p filtering
392
+ if top_p < 1.0:
393
+ sorted_logits, sorted_indices = torch.sort(next_logits, descending=True)
394
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
395
+ sorted_indices_to_remove = cumulative_probs > top_p
396
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
397
+ sorted_indices_to_remove[..., 0] = 0
398
+ indices_to_remove = sorted_indices[sorted_indices_to_remove]
399
+ next_logits[indices_to_remove] = float('-inf')
400
+
401
  probs = F.softmax(next_logits, dim=-1)
402
  next_token = torch.multinomial(probs, 1).item()
403
 
404
+ # Detener en EOS
405
  if next_token == eos_id:
406
  break
407
+
408
+ # Detener si hay demasiados signos de puntuación seguidos
409
+ token_str = tokenizer.decode([next_token]) if hasattr(tokenizer, 'decode') else str(next_token)
410
+ if token_str in '.!?':
411
+ consecutive_punctuation += 1
412
+ if consecutive_punctuation >= 3:
413
+ break
414
+ else:
415
+ consecutive_punctuation = 0
416
+
417
+ # Guardar últimos caracteres para análisis
418
+ last_chars.append(token_str)
419
+ if len(last_chars) > 20:
420
+ last_chars.pop(0)
421
+
422
+ # Detectar bucles de repetición
423
+ if len(last_chars) >= 10:
424
+ last_str = ''.join(last_chars[-5:])
425
+ if last_str in ''.join(last_chars[:-5]):
426
+ break
427
+
428
  generated.append(next_token)
429
+
430
+ # Verificar si ya es suficiente (para respuestas cortas)
431
+ current_response = tokenizer.decode(generated)
432
+ if "### Respuesta:" in current_response:
433
+ response_part = current_response.split("### Respuesta:")[-1].strip()
434
+ if nlp.should_stop(response_part, min_length=20, max_length=max_length):
435
+ break
436
 
437
+ # Decodificar respuesta
438
  response = tokenizer.decode(generated)
439
+
440
+ # Extraer la parte de la respuesta
441
  if "### Respuesta:" in response:
442
  response = response.split("### Respuesta:")[-1].strip()
443
+ elif "Respuesta:" in response:
444
+ response = response.split("Respuesta:")[-1].strip()
445
+ elif "[/INST]" in response:
446
+ response = response.split("[/INST]")[-1].strip()
447
 
448
+ # Limpiar y mejorar respuesta
449
+ garbage_words = ['foompañances', 'ciudadores', 'mejtedon', 'calportedon', 'rápidodcor', 'baon', 'domol']
450
+ for word in garbage_words:
451
+ response = response.replace(word, '')
452
+
453
+ # Limpiar caracteres especiales
454
+ response = re.sub(r'[^\w\s\u00C0-\u00FF\u0100-\u017F.,!?¿¡()\-:;"\']+', ' ', response)
455
  response = re.sub(r'\s+', ' ', response).strip()
456
 
457
+ # Aplicar NLP a la respuesta
458
+ response = nlp.clean_response(response)
459
+
460
+ # Respuestas por defecto según intención si está vacía
461
+ if len(response) < 3:
462
+ default_responses = {
463
+ 'saludo': "¡Hola! ¿En qué puedo ayudarte hoy?",
464
+ 'despedida': "¡Hasta luego! Que tengas un excelente día.",
465
+ 'agradecimiento': "¡De nada! Estoy aquí para ayudarte cuando lo necesites.",
466
+ 'ayuda': "Claro, estoy aquí para ayudarte. ¿Qué necesitas saber?",
467
+ 'presentacion': "Soy MTP, un asistente virtual creado para responder preguntas y ayudarte con información.",
468
+ 'general': "Entendido. ¿Hay algo específico en lo que pueda ayudarte?"
469
+ }
470
+ response = default_responses.get(intent, default_responses['general'])
471
 
472
  return response
473
 
474
+ # ======================
475
+ # ENDPOINTS
476
+ # ======================
477
+ ACTIVE_REQUESTS = 0
478
+
479
+ class TokenizerWrapper:
480
+ def __init__(self, sp_model):
481
+ self.sp = sp_model
482
+ def encode(self, text):
483
+ if self.sp is None:
484
+ return [ord(c) % 1000 for c in text[:200]]
485
+ return self.sp.encode(text)
486
+ def decode(self, tokens):
487
+ if self.sp is None:
488
+ return ''.join([chr(t % 128) if 32 <= t % 128 < 127 else ' ' for t in tokens])
489
+ return self.sp.decode(tokens)
490
+ def eos_id(self):
491
+ return self.sp.eos_id() if self.sp else 3
492
+ def bos_id(self):
493
+ return self.sp.bos_id() if self.sp else 2
494
+ def pad_id(self):
495
+ return self.sp.pad_id() if self.sp else 0
496
+
497
+ tokenizer_wrapper = TokenizerWrapper(sp)
498
+
499
  @app.post("/generate")
500
  async def generate(req: PromptRequest):
501
+ global ACTIVE_REQUESTS
502
+ ACTIVE_REQUESTS += 1
503
+
504
  user_input = req.text.strip()
505
  if not user_input:
506
+ ACTIVE_REQUESTS -= 1
507
+ return {"reply": "", "tokens_generated": 0, "intent": None}
508
+
509
+ # Detectar intención
510
+ intent = nlp.detect_intent(user_input)
511
 
512
  try:
513
+ response = generate_response_intelligent(
514
+ model, tokenizer_wrapper, user_input,
515
+ max_length=req.max_tokens,
516
+ temperature=req.temperature,
517
+ top_k=req.top_k,
518
+ top_p=req.top_p,
519
+ device=DEVICE
520
+ )
521
+
522
+ # Extraer información clave
523
+ key_info = nlp.extract_key_info(response)
524
+
525
+ return {
526
+ "reply": response,
527
+ "tokens_generated": len(response.split()),
528
+ "model": "MTP-Intelligent",
529
+ "intent": intent,
530
+ "has_numbers": key_info['has_numbers'],
531
+ "has_emails": key_info['has_emails']
532
+ }
533
  except Exception as e:
534
+ print(f"Error: {e}")
535
+ return {"reply": "Lo siento, ocurrió un error.", "error": str(e), "intent": intent}
536
+ finally:
537
+ ACTIVE_REQUESTS -= 1
538
+ if DEVICE == "cuda":
539
+ torch.cuda.empty_cache()
540
+ gc.collect()
541
 
542
+ @app.get("/health")
543
+ def health_check():
544
+ return {
545
+ "status": "healthy",
546
+ "model": "MTP-Intelligent",
547
+ "device": DEVICE,
548
+ "active_requests": ACTIVE_REQUESTS,
549
+ "vocab_size": VOCAB_SIZE
550
+ }
551
+
552
+ @app.get("/info")
553
+ def model_info():
554
+ return {
555
+ "model_name": "MTP-Intelligent",
556
+ "version": "2.0",
557
+ "architecture": config,
558
+ "parameters": sum(p.numel() for p in model.parameters()),
559
+ "device": DEVICE,
560
+ "nlp_enabled": True
561
+ }
562
+
563
+ @app.post("/analyze")
564
+ async def analyze_intent(req: PromptRequest):
565
+ """Endpoint para analizar intención sin generar respuesta"""
566
+ intent = nlp.detect_intent(req.text)
567
+ return {
568
+ "text": req.text,
569
+ "intent": intent,
570
+ "confidence": 0.85 # Por ahora fijo, se puede mejorar
571
+ }
572
+
573
+ # ======================
574
+ # INTERFAZ WEB MEJORADA
575
+ # ======================
576
+ @app.get("/", response_class=HTMLResponse)
577
+ def chat_ui():
578
+ return """
579
  <!DOCTYPE html>
580
+ <html lang="es">
581
  <head>
582
+ <meta charset="UTF-8">
583
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
584
+ <title>MTP - Asistente Inteligente</title>
585
+ <link rel="preconnect" href="https://fonts.googleapis.com">
586
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
587
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
588
+ <style>
589
+ :root {
590
+ --bg-color: #131314;
591
+ --surface-color: #1E1F20;
592
+ --accent-color: #4a9eff;
593
+ --text-primary: #e3e3e3;
594
+ --text-secondary: #9aa0a6;
595
+ --user-bubble: #282a2c;
596
+ --success-color: #00c853;
597
+ }
598
+ * { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; }
599
+ body {
600
+ margin: 0;
601
+ background-color: var(--bg-color);
602
+ font-family: 'Inter', sans-serif;
603
+ color: var(--text-primary);
604
+ height: 100dvh;
605
+ display: flex;
606
+ flex-direction: column;
607
+ overflow: hidden;
608
+ }
609
+ header {
610
+ padding: 12px 20px;
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: space-between;
614
+ background: rgba(19, 19, 20, 0.85);
615
+ backdrop-filter: blur(12px);
616
+ position: fixed;
617
+ top: 0;
618
+ width: 100%;
619
+ z-index: 50;
620
+ border-bottom: 1px solid rgba(255,255,255,0.05);
621
+ }
622
+ .brand-wrapper {
623
+ display: flex;
624
+ align-items: center;
625
+ gap: 12px;
626
+ cursor: pointer;
627
+ }
628
+ .brand-logo {
629
+ width: 32px;
630
+ height: 32px;
631
+ border-radius: 50%;
632
+ background: linear-gradient(135deg, #4a9eff, #00c853);
633
+ }
634
+ .brand-text {
635
+ font-weight: 500;
636
+ font-size: 1.05rem;
637
+ display: flex;
638
+ align-items: center;
639
+ gap: 8px;
640
+ }
641
+ .version-badge {
642
+ font-size: 0.75rem;
643
+ background: rgba(74, 158, 255, 0.15);
644
+ color: #8ab4f8;
645
+ padding: 2px 8px;
646
+ border-radius: 12px;
647
+ font-weight: 600;
648
+ }
649
+ .chat-scroll {
650
+ flex: 1;
651
+ overflow-y: auto;
652
+ padding: 80px 20px 40px 20px;
653
+ display: flex;
654
+ flex-direction: column;
655
+ gap: 30px;
656
+ max-width: 850px;
657
+ margin: 0 auto;
658
+ width: 100%;
659
+ scroll-behavior: smooth;
660
+ }
661
+ .msg-row {
662
+ display: flex;
663
+ gap: 16px;
664
+ width: 100%;
665
+ opacity: 0;
666
+ transform: translateY(10px);
667
+ animation: slideUpFade 0.4s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
668
+ }
669
+ .msg-row.user { justify-content: flex-end; }
670
+ .msg-row.bot { justify-content: flex-start; align-items: flex-start; }
671
+ .msg-content {
672
+ line-height: 1.6;
673
+ font-size: 1rem;
674
+ word-wrap: break-word;
675
+ max-width: 85%;
676
+ }
677
+ .user .msg-content {
678
+ background-color: var(--user-bubble);
679
+ padding: 10px 18px;
680
+ border-radius: 18px;
681
+ border-top-right-radius: 4px;
682
+ color: #fff;
683
+ }
684
+ .bot .msg-content-wrapper {
685
+ display: flex;
686
+ flex-direction: column;
687
+ gap: 8px;
688
+ width: 100%;
689
+ }
690
+ .bot .msg-text {
691
+ padding-top: 6px;
692
+ color: var(--text-primary);
693
+ }
694
+ .bot-avatar {
695
+ width: 34px;
696
+ height: 34px;
697
+ min-width: 34px;
698
+ border-radius: 50%;
699
+ background: linear-gradient(135deg, #4a9eff, #00c853);
700
+ box-shadow: 0 2px 6px rgba(0,0,0,0.2);
701
+ }
702
+ .bot-actions {
703
+ display: flex;
704
+ gap: 10px;
705
+ opacity: 0;
706
+ transition: opacity 0.3s;
707
+ margin-top: 5px;
708
+ }
709
+ .action-btn {
710
+ background: transparent;
711
+ border: none;
712
+ color: var(--text-secondary);
713
+ cursor: pointer;
714
+ padding: 4px;
715
+ border-radius: 4px;
716
+ display: flex;
717
+ align-items: center;
718
+ transition: color 0.2s, background 0.2s;
719
+ }
720
+ .action-btn:hover {
721
+ color: var(--text-primary);
722
+ background: rgba(255,255,255,0.08);
723
+ }
724
+ .action-btn svg { width: 16px; height: 16px; fill: currentColor; }
725
+ .typing-cursor::after {
726
+ content: '';
727
+ display: inline-block;
728
+ width: 10px;
729
+ height: 10px;
730
+ background: var(--accent-color);
731
+ border-radius: 50%;
732
+ margin-left: 5px;
733
+ vertical-align: middle;
734
+ animation: blink 1s infinite;
735
+ }
736
+ .footer-container {
737
+ padding: 0 20px 20px 20px;
738
+ background: linear-gradient(to top, var(--bg-color) 85%, transparent);
739
+ position: relative;
740
+ z-index: 60;
741
+ }
742
+ .input-box {
743
+ max-width: 850px;
744
+ margin: 0 auto;
745
+ background: var(--surface-color);
746
+ border-radius: 28px;
747
+ padding: 8px 10px 8px 20px;
748
+ display: flex;
749
+ align-items: center;
750
+ border: 1px solid rgba(255,255,255,0.1);
751
+ transition: border-color 0.2s, box-shadow 0.2s;
752
+ }
753
+ .input-box:focus-within {
754
+ border-color: rgba(74, 158, 255, 0.5);
755
+ box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.1);
756
+ }
757
+ #userInput {
758
+ flex: 1;
759
+ background: transparent;
760
+ border: none;
761
+ color: white;
762
+ font-size: 1rem;
763
+ font-family: inherit;
764
+ padding: 10px 0;
765
+ }
766
+ #mainBtn {
767
+ background: var(--accent-color);
768
+ color: white;
769
+ border: none;
770
+ width: 36px;
771
+ height: 36px;
772
+ border-radius: 50%;
773
+ display: flex;
774
+ align-items: center;
775
+ justify-content: center;
776
+ cursor: pointer;
777
+ margin-left: 8px;
778
+ transition: transform 0.2s;
779
+ }
780
+ #mainBtn:hover { transform: scale(1.05); background: #3a7ed4; }
781
+ .disclaimer {
782
+ text-align: center;
783
+ font-size: 0.75rem;
784
+ color: #666;
785
+ margin-top: 12px;
786
+ }
787
+ @keyframes slideUpFade {
788
+ from { opacity: 0; transform: translateY(15px); }
789
+ to { opacity: 1; transform: translateY(0); }
790
+ }
791
+ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
792
+ @keyframes pulseAvatar {
793
+ 0% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.4); }
794
+ 70% { box-shadow: 0 0 0 8px rgba(74, 158, 255, 0); }
795
+ 100% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0); }
796
+ }
797
+ .pulsing { animation: pulseAvatar 1.5s infinite; }
798
+ .intent-badge {
799
+ font-size: 0.7rem;
800
+ background: rgba(0, 200, 83, 0.15);
801
+ color: #00c853;
802
+ padding: 2px 8px;
803
+ border-radius: 12px;
804
+ display: inline-block;
805
+ margin-top: 5px;
806
+ }
807
+ ::-webkit-scrollbar { width: 8px; }
808
+ ::-webkit-scrollbar-track { background: transparent; }
809
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
810
+ </style>
811
  </head>
812
  <body>
813
+ <header>
814
+ <div class="brand-wrapper" onclick="location.reload()">
815
+ <div class="brand-logo"></div>
816
+ <div class="brand-text">
817
+ MTP <span class="version-badge">Inteligente</span>
818
+ </div>
819
+ </div>
820
+ </header>
821
+ <div id="chatScroll" class="chat-scroll">
822
+ <div class="msg-row bot" style="animation-delay: 0.1s;">
823
+ <div class="bot-avatar"></div>
824
+ <div class="msg-content-wrapper">
825
+ <div class="msg-text">
826
+ ¡Hola! Soy MTP, tu asistente inteligente. ¿En qué puedo ayudarte hoy?
827
+ </div>
828
+ </div>
829
+ </div>
830
+ </div>
831
+ <div class="footer-container">
832
+ <div class="input-box">
833
+ <input type="text" id="userInput" placeholder="Escribe tu mensaje..." autocomplete="off">
834
+ <button id="mainBtn" onclick="handleBtnClick()">➤</button>
835
  </div>
836
+ <div class="disclaimer">
837
+ MTP usa NLP para entender mejor tu consulta • Respuestas inteligentes
 
838
  </div>
839
+ </div>
840
+ <script>
841
+ const chatScroll = document.getElementById('chatScroll');
842
+ const userInput = document.getElementById('userInput');
843
+ const mainBtn = document.getElementById('mainBtn');
844
+ let isGenerating = false;
845
+ let abortController = null;
846
+ let typingTimeout = null;
847
+ let lastUserPrompt = "";
848
+
849
+ function scrollToBottom() {
850
+ chatScroll.scrollTop = chatScroll.scrollHeight;
851
+ }
852
+
853
+ function setBtnState(state) {
854
+ if (state === 'sending') {
855
+ mainBtn.innerHTML = "⏹";
856
+ isGenerating = true;
857
+ } else {
858
+ mainBtn.innerHTML = "➤";
859
+ isGenerating = false;
860
+ abortController = null;
861
+ }
862
+ }
863
+
864
+ function handleBtnClick() {
865
+ if (isGenerating) {
866
+ stopGeneration();
867
+ } else {
868
+ sendMessage();
869
+ }
870
+ }
871
+
872
+ function stopGeneration() {
873
+ if (abortController) abortController.abort();
874
+ if (typingTimeout) clearTimeout(typingTimeout);
875
+ const activeCursor = document.querySelector('.typing-cursor');
876
+ if (activeCursor) activeCursor.classList.remove('typing-cursor');
877
+ const activeAvatar = document.querySelector('.pulsing');
878
+ if (activeAvatar) activeAvatar.classList.remove('pulsing');
879
+ setBtnState('idle');
880
+ userInput.focus();
881
+ }
882
+
883
+ async function sendMessage(textOverride = null) {
884
+ const text = textOverride || userInput.value.trim();
885
+ if (!text || isGenerating) return;
886
+
887
+ lastUserPrompt = text;
888
+ if (!textOverride) {
889
+ userInput.value = '';
890
+ addMessage(text, 'user');
891
+ }
892
+
893
+ setBtnState('sending');
894
+ abortController = new AbortController();
895
+
896
+ const botRow = document.createElement('div');
897
+ botRow.className = 'msg-row bot';
898
+ const avatar = document.createElement('div');
899
+ avatar.className = 'bot-avatar pulsing';
900
+ const wrapper = document.createElement('div');
901
+ wrapper.className = 'msg-content-wrapper';
902
+ const msgText = document.createElement('div');
903
+ msgText.className = 'msg-text';
904
+ wrapper.appendChild(msgText);
905
+ botRow.appendChild(avatar);
906
+ botRow.appendChild(wrapper);
907
+ chatScroll.appendChild(botRow);
908
+ scrollToBottom();
909
+
910
+ try {
911
+ const response = await fetch('/generate', {
912
+ method: 'POST',
913
+ headers: { 'Content-Type': 'application/json' },
914
+ body: JSON.stringify({
915
+ text: text,
916
+ max_tokens: 200,
917
+ temperature: 0.7,
918
+ top_k: 50,
919
+ top_p: 0.9
920
+ }),
921
+ signal: abortController.signal
922
+ });
923
+
924
+ const data = await response.json();
925
+ if (!isGenerating) return;
926
 
927
+ avatar.classList.remove('pulsing');
928
+ const reply = data.reply || "No entendí eso.";
929
+
930
+ // Mostrar intención detectada si está disponible
931
+ if (data.intent && data.intent !== 'general') {
932
+ const intentSpan = document.createElement('div');
933
+ intentSpan.className = 'intent-badge';
934
+ intentSpan.textContent = `🎯 Intención: ${data.intent}`;
935
+ wrapper.appendChild(intentSpan);
936
  }
937
 
938
+ await typeWriter(msgText, reply);
939
+ if (isGenerating) {
940
+ addActions(wrapper, reply);
941
+ setBtnState('idle');
942
+ }
943
+ } catch (error) {
944
+ if (error.name === 'AbortError') {
945
+ msgText.textContent += " [Detenido]";
946
+ } else {
947
+ avatar.classList.remove('pulsing');
948
+ msgText.textContent = "Error de conexión. Intenta de nuevo.";
949
+ msgText.style.color = "#ff8b8b";
950
+ setBtnState('idle');
951
+ }
952
+ }
953
+ }
954
+
955
+ function addMessage(text, sender) {
956
+ const row = document.createElement('div');
957
+ row.className = `msg-row ${sender}`;
958
+ const content = document.createElement('div');
959
+ content.className = 'msg-content';
960
+ content.textContent = text;
961
+ row.appendChild(content);
962
+ chatScroll.appendChild(row);
963
+ scrollToBottom();
964
+ }
965
+
966
+ function typeWriter(element, text, speed = 10) {
967
+ return new Promise(resolve => {
968
+ let i = 0;
969
+ element.classList.add('typing-cursor');
970
+ function type() {
971
+ if (!isGenerating) {
972
+ element.classList.remove('typing-cursor');
973
+ resolve();
974
+ return;
975
+ }
976
+ if (i < text.length) {
977
+ element.textContent += text.charAt(i);
978
+ i++;
979
+ scrollToBottom();
980
+ typingTimeout = setTimeout(type, speed + Math.random() * 5);
981
+ } else {
982
+ element.classList.remove('typing-cursor');
983
+ resolve();
984
  }
985
  }
986
+ type();
987
+ });
988
+ }
989
+
990
+ function addActions(wrapperElement, textToCopy) {
991
+ const actionsDiv = document.createElement('div');
992
+ actionsDiv.className = 'bot-actions';
993
+
994
+ const copyBtn = document.createElement('button');
995
+ copyBtn.className = 'action-btn';
996
+ copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
997
+ copyBtn.onclick = () => { navigator.clipboard.writeText(textToCopy); };
998
+
999
+ const regenBtn = document.createElement('button');
1000
+ regenBtn.className = 'action-btn';
1001
+ regenBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`;
1002
+ regenBtn.onclick = () => { sendMessage(lastUserPrompt); };
1003
+
1004
+ actionsDiv.appendChild(copyBtn);
1005
+ actionsDiv.appendChild(regenBtn);
1006
+ wrapperElement.appendChild(actionsDiv);
1007
+ requestAnimationFrame(() => actionsDiv.style.opacity = "1");
1008
+ scrollToBottom();
1009
+ }
1010
+
1011
+ userInput.addEventListener('keydown', (e) => {
1012
+ if (e.key === 'Enter') handleBtnClick();
1013
+ });
1014
+
1015
+ window.onload = () => userInput.focus();
1016
+ </script>
1017
  </body>
1018
  </html>
1019
+ """
1020
 
1021
  if __name__ == "__main__":
1022
+ port = int(os.environ.get("PORT", 7860))
1023
+ print(f"\n🚀 MTP Inteligente iniciado en puerto {port}")
1024
+ print(f"🌐 http://0.0.0.0:{port}")
1025
+
1026
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")