TeszenAI/MTP_2
0
1import os2import sys3import torch4import pickle5import time6import gc7from fastapi import FastAPI, Request8from fastapi.responses import HTMLResponse, StreamingResponse9from fastapi.middleware.cors import CORSMiddleware10from pydantic import BaseModel, Field11from huggingface_hub import snapshot_download12import uvicorn13 14# ======================15# CONFIGURACIÓN DE DISPOSITIVO16# ======================17if torch.cuda.is_available():18 DEVICE = "cuda"19 print("✅ GPU NVIDIA detectada. Usando CUDA.")20else:21 DEVICE = "cpu"22 print("⚠️ GPU no detectada. Usando CPU (puede ser más lento).")23 24# Optimización de hilos para CPU25if DEVICE == "cpu":26 torch.set_num_threads(max(1, os.cpu_count() // 2))27 28torch.set_grad_enabled(False)29 30MODEL_REPO = "TeszenAI/MTP-4"31 32# ======================33# DESCARGA Y CARGA DEL MODELO34# ======================35print(f"📦 Descargando modelo desde {MODEL_REPO}...")36repo_path = snapshot_download(37 repo_id=MODEL_REPO,38 repo_type="model",39 local_dir="mtp_repo"40)41 42sys.path.insert(0, repo_path)43 44# Importar modelo y tokenizer45from model import MTPMiniModel46from tokenizer import MTPTokenizer47 48print("🔧 Cargando tensores y configuración...")49with open(os.path.join(repo_path, "mtp_mini.pkl"), "rb") as f:50 model_data = pickle.load(f)51 52tokenizer = MTPTokenizer(os.path.join(repo_path, "mtp_tokenizer.model"))53VOCAB_SIZE = tokenizer.sp.get_piece_size()54config = model_data["config"]55 56# Detectar si el modelo usa SwiGLU57use_swiglu = config["model"].get("use_swiglu", False)58 59print(f"🧠 Inicializando modelo MTP 4...")60print(f" → Vocabulario: {VOCAB_SIZE}")61print(f" → Dimensión: {config['model']['d_model']}")62print(f" → Capas: {config['model']['n_layers']}")63print(f" → Cabezas: {config['model']['n_heads']}")64print(f" → SwiGLU: {'✓' if use_swiglu else '✗'}")65 66model = MTPMiniModel(67 vocab_size=VOCAB_SIZE,68 d_model=config["model"]["d_model"],69 n_layers=config["model"]["n_layers"],70 n_heads=config["model"]["n_heads"],71 d_ff=config["model"]["d_ff"],72 max_seq_len=config["model"]["max_seq_len"],73 dropout=0.0,74 use_swiglu=use_swiglu75)76 77model.load_state_dict(model_data["model_state_dict"])78model.eval()79 80# Cuantización para CPU81if DEVICE == "cpu":82 print("⚡ Aplicando cuantización dinámica para CPU...")83 model = torch.quantization.quantize_dynamic(84 model, 85 {torch.nn.Linear}, 86 dtype=torch.qint887 )88 89model.to(DEVICE)90 91param_count = sum(p.numel() for p in model.parameters())92print(f"✅ Modelo cargado: {param_count:,} parámetros ({param_count/1e6:.1f}M)")93 94# ======================95# API CONFIG96# ======================97app = FastAPI(98 title="MTP 4 API",99 description="API para modelo de lenguaje MTP 4 con RoPE, RMSNorm y SwiGLU",100 version="4.0"101)102 103app.add_middleware(104 CORSMiddleware,105 allow_origins=["*"],106 allow_methods=["*"],107 allow_headers=["*"],108)109 110class PromptRequest(BaseModel):111 text: str = Field(..., max_length=2000, description="Texto de entrada")112 max_tokens: int = Field(default=150, ge=10, le=300, description="Tokens máximos a generar")113 temperature: float = Field(default=0.7, ge=0.1, le=2.0, description="Temperatura de muestreo")114 top_k: int = Field(default=40, ge=1, le=100, description="Top-k sampling")115 top_p: float = Field(default=0.92, ge=0.1, le=1.0, description="Top-p (nucleus) sampling")116 repetition_penalty: float = Field(default=1.15, ge=1.0, le=2.0, description="Penalización por repetición")117 min_length: int = Field(default=20, ge=5, le=100, description="Longitud mínima de respuesta")118 119def build_prompt(user_input: str) -> str:120 """Construye el prompt en el formato del modelo"""121 return f"### Instrucción:\n{user_input}\n\n### Respuesta:\n"122 123# ======================124# ⚡ GESTIÓN DE CARGA125# ======================126ACTIVE_REQUESTS = 0127MAX_CONCURRENT_REQUESTS = 3128 129@app.post("/generate")130async def generate(req: PromptRequest):131 """Endpoint principal de generación de texto con control de calidad"""132 global ACTIVE_REQUESTS133 134 if ACTIVE_REQUESTS >= MAX_CONCURRENT_REQUESTS:135 return {136 "reply": "El servidor está ocupado. Por favor, intenta de nuevo en unos segundos.",137 "error": "too_many_requests",138 "active_requests": ACTIVE_REQUESTS139 }140 141 ACTIVE_REQUESTS += 1142 143 # Ajuste dinámico bajo carga144 dyn_max_tokens = req.max_tokens145 dyn_temperature = req.temperature146 147 if ACTIVE_REQUESTS > 1:148 print(f"⚠️ Carga alta ({ACTIVE_REQUESTS} requests). Ajustando parámetros.")149 dyn_max_tokens = min(dyn_max_tokens, 120)150 dyn_temperature = max(0.6, dyn_temperature * 0.95)151 152 user_input = req.text.strip()153 if not user_input:154 ACTIVE_REQUESTS -= 1155 return {"reply": "", "tokens_generated": 0}156 157 full_prompt = build_prompt(user_input)158 tokens = [tokenizer.bos_id()] + tokenizer.encode(full_prompt)159 input_ids = torch.tensor([tokens], device=DEVICE)160 161 try:162 start_time = time.time()163 164 with torch.no_grad():165 output_ids = model.generate(166 input_ids,167 max_new_tokens=dyn_max_tokens,168 temperature=dyn_temperature,169 top_k=req.top_k,170 top_p=req.top_p,171 repetition_penalty=req.repetition_penalty,172 min_length=req.min_length,173 eos_token_id=tokenizer.eos_id()174 )175 176 gen_tokens = output_ids[0, len(tokens):].tolist()177 178 # Filtro de seguridad mejorado179 safe_tokens = []180 for t in gen_tokens:181 if 0 <= t < VOCAB_SIZE and t != tokenizer.eos_id():182 safe_tokens.append(t)183 elif t == tokenizer.eos_id():184 break185 186 response = tokenizer.decode(safe_tokens).strip()187 188 # Limpiar marcadores de sección189 if "###" in response:190 response = response.split("###")[0].strip()191 192 # Remover repeticiones al final193 if response.endswith(("...", ". . .", "…")):194 response = response.rstrip(".")195 196 generation_time = time.time() - start_time197 tokens_per_second = len(safe_tokens) / generation_time if generation_time > 0 else 0198 199 return {200 "reply": response,201 "tokens_generated": len(safe_tokens),202 "generation_time": round(generation_time, 2),203 "tokens_per_second": round(tokens_per_second, 1),204 "model": "MTP-4",205 "device": DEVICE206 }207 208 except Exception as e:209 print(f"❌ Error durante generación: {e}")210 import traceback211 traceback.print_exc()212 return {213 "reply": "Lo siento, ocurrió un error al procesar tu solicitud.",214 "error": str(e)215 }216 217 finally:218 ACTIVE_REQUESTS -= 1219 if DEVICE == "cuda":220 torch.cuda.empty_cache()221 gc.collect()222 223# ======================224# 📡 STREAMING SSE225# ======================226@app.get("/generate_sse")227def generate_sse(228 text: str,229 max_tokens: int = 150,230 temperature: float = 0.7,231 top_k: int = 40,232 top_p: float = 0.92,233 repetition_penalty: float = 1.15234):235 """Endpoint de streaming con Server-Sent Events mejorado"""236 global ACTIVE_REQUESTS237 238 if ACTIVE_REQUESTS >= MAX_CONCURRENT_REQUESTS:239 def error_stream():240 yield "data:[ERROR: Servidor ocupado]\n\n"241 return StreamingResponse(error_stream(), media_type="text/event-stream")242 243 ACTIVE_REQUESTS += 1244 245 def event_stream():246 try:247 full_prompt = build_prompt(text)248 tokens = [tokenizer.bos_id()] + tokenizer.encode(full_prompt)249 input_ids = torch.tensor([tokens], device=DEVICE)250 generated_tokens = []251 252 # Ajuste dinámico253 limit = min(100 if ACTIVE_REQUESTS > 1 else max_tokens, 200)254 temp = max(0.6, temperature * 0.95) if ACTIVE_REQUESTS > 1 else temperature255 256 for step in range(limit):257 with torch.no_grad():258 logits, _ = model(input_ids)259 logits = logits[:, -1, :VOCAB_SIZE].clone()260 261 # Aplicar repetition penalty262 if repetition_penalty != 1.0:263 for token_id in set(input_ids[0].tolist()):264 if logits[0, token_id] < 0:265 logits[0, token_id] *= repetition_penalty266 else:267 logits[0, token_id] /= repetition_penalty268 269 # Temperature scaling270 logits = logits / temp271 272 # Top-k filtering273 if top_k > 0:274 v, _ = torch.topk(logits, min(top_k, logits.size(-1)))275 logits[logits < v[:, [-1]]] = float('-inf')276 277 # Top-p (nucleus) filtering278 if top_p < 1.0:279 sorted_logits, sorted_indices = torch.sort(logits, descending=True)280 cumulative_probs = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1)281 sorted_indices_to_remove = cumulative_probs > top_p282 sorted_indices_to_remove[:, 1:] = sorted_indices_to_remove[:, :-1].clone()283 sorted_indices_to_remove[:, 0] = 0284 indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)285 logits[indices_to_remove] = float('-inf')286 287 # Sample288 probs = torch.softmax(logits, dim=-1)289 next_id = torch.multinomial(probs, num_samples=1).item()290 291 if next_id == tokenizer.eos_id():292 break293 294 if 0 <= next_id < VOCAB_SIZE:295 generated_tokens.append(next_id)296 token_text = tokenizer.decode([next_id])297 298 # Limpiar salida299 if "###" in token_text:300 break301 302 yield f"data:{token_text}\n\n"303 304 input_ids = torch.cat(305 [input_ids, torch.tensor([[next_id]], device=DEVICE)],306 dim=1307 )308 time.sleep(0.02) # Control de velocidad309 310 yield "data:[DONE]\n\n"311 312 except Exception as e:313 print(f"❌ Error en streaming: {e}")314 yield f"data:[ERROR: {str(e)}]\n\n"315 316 finally:317 ACTIVE_REQUESTS -= 1318 if DEVICE == "cuda":319 torch.cuda.empty_cache()320 gc.collect()321 322 return StreamingResponse(event_stream(), media_type="text/event-stream")323 324# ======================325# 📊 ENDPOINTS DE INFORMACIÓN326# ======================327@app.get("/health")328def health_check():329 """Check del estado del servicio"""330 memory_info = {}331 if DEVICE == "cuda":332 memory_info = {333 "gpu_memory_allocated_mb": round(torch.cuda.memory_allocated() / 1024**2, 2),334 "gpu_memory_reserved_mb": round(torch.cuda.memory_reserved() / 1024**2, 2)335 }336 337 return {338 "status": "healthy",339 "model": "MTP-4",340 "device": DEVICE,341 "active_requests": ACTIVE_REQUESTS,342 "max_concurrent_requests": MAX_CONCURRENT_REQUESTS,343 "vocab_size": VOCAB_SIZE,344 "parameters": sum(p.numel() for p in model.parameters()),345 **memory_info346 }347 348@app.get("/info")349def model_info():350 """Información detallada del modelo"""351 improvements = [352 "RoPE (Rotary Position Embedding)",353 "RMSNorm (Root Mean Square Normalization)",354 "Label Smoothing (0.1)",355 "Repetition Penalty",356 "Early Stopping",357 "EOS Loss Weight",358 "Length Control",359 "Gradient Accumulation"360 ]361 362 if config["model"].get("use_swiglu", False):363 improvements.append("SwiGLU Activation")364 365 return {366 "model_name": "MTP-4",367 "version": "4.0",368 "architecture": {369 "d_model": config["model"]["d_model"],370 "n_layers": config["model"]["n_layers"],371 "n_heads": config["model"]["n_heads"],372 "d_ff": config["model"]["d_ff"],373 "max_seq_len": config["model"]["max_seq_len"],374 "vocab_size": VOCAB_SIZE,375 "use_swiglu": config["model"].get("use_swiglu", False),376 "dropout": config["model"]["dropout"]377 },378 "parameters": sum(p.numel() for p in model.parameters()),379 "parameters_human": f"{sum(p.numel() for p in model.parameters())/1e6:.1f}M",380 "device": DEVICE,381 "improvements": improvements,382 "training_config": {383 "batch_size": config["training"]["batch_size"],384 "accumulation_steps": config["training"]["accumulation_steps"],385 "learning_rate": config["training"]["learning_rate"],386 "weight_decay": config["training"]["weight_decay"],387 "epochs": config["training"]["epochs"]388 }389 }390 391@app.get("/config")392def get_config():393 """Obtener configuración completa del modelo"""394 return {395 "model": config["model"],396 "training": config["training"],397 "data": config["data"],398 "generation": config.get("generation", {})399 }400 401# ======================402# 🎨 INTERFAZ WEB MEJORADA403# ======================404@app.get("/", response_class=HTMLResponse)405def chat_ui():406 return """407<!DOCTYPE html>408<html lang="es">409<head>410<meta charset="UTF-8">411<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">412<title>MTP 4 - Chat Interface</title>413<link rel="preconnect" href="https://fonts.googleapis.com">414<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>415<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">416<style>417:root {418 --bg-color: #0a0a0b;419 --surface-color: #1a1a1c;420 --accent-color: #6366f1;421 --text-primary: #e8e8ea;422 --text-secondary: #9ca3af;423 --user-bubble: #2d2d30;424 --success-color: #10b981;425 --warning-color: #f59e0b;426 --error-color: #ef4444;427 --logo-url: url('https://i.postimg.cc/yxS54PF3/IMG-3082.jpg');428}429* { 430 box-sizing: border-box; 431 outline: none; 432 -webkit-tap-highlight-color: transparent; 433}434body {435 margin: 0;436 background: linear-gradient(135deg, #0a0a0b 0%, #1a1a1c 100%);437 font-family: 'Inter', sans-serif;438 color: var(--text-primary);439 height: 100dvh;440 display: flex;441 flex-direction: column;442 overflow: hidden;443}444header {445 padding: 14px 24px;446 display: flex;447 align-items: center;448 justify-content: space-between;449 background: rgba(26, 26, 28, 0.9);450 backdrop-filter: blur(16px);451 position: fixed;452 top: 0;453 width: 100%;454 z-index: 50;455 border-bottom: 1px solid rgba(99, 102, 241, 0.1);456}457.brand-wrapper {458 display: flex;459 align-items: center;460 gap: 14px;461 cursor: pointer;462}463.brand-logo {464 width: 36px;465 height: 36px;466 border-radius: 50%;467 background-image: var(--logo-url);468 background-size: cover;469 background-position: center;470 border: 2px solid rgba(99, 102, 241, 0.3);471 box-shadow: 0 0 12px rgba(99, 102, 241, 0.2);472}473.brand-text {474 font-weight: 600;475 font-size: 1.15rem;476 display: flex;477 align-items: center;478 gap: 10px;479 background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);480 -webkit-background-clip: text;481 -webkit-text-fill-color: transparent;482 background-clip: text;483}484.version-badge {485 font-size: 0.75rem;486 background: linear-gradient(135deg, rgba(99, 102, 241, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);487 color: #a5b4fc;488 padding: 3px 10px;489 border-radius: 14px;490 font-weight: 700;491 border: 1px solid rgba(99, 102, 241, 0.3);492}493.status-indicator {494 width: 10px;495 height: 10px;496 border-radius: 50%;497 background: var(--success-color);498 animation: pulse 2s infinite;499 box-shadow: 0 0 8px var(--success-color);500}501@keyframes pulse {502 0%, 100% { opacity: 1; transform: scale(1); }503 50% { opacity: 0.7; transform: scale(0.95); }504}505.chat-scroll {506 flex: 1;507 overflow-y: auto;508 padding: 90px 24px 50px 24px;509 display: flex;510 flex-direction: column;511 gap: 32px;512 max-width: 900px;513 margin: 0 auto;514 width: 100%;515 scroll-behavior: smooth;516}517.msg-row {518 display: flex;519 gap: 18px;520 width: 100%;521 opacity: 0;522 transform: translateY(12px);523 animation: slideUpFade 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;524}525.msg-row.user { justify-content: flex-end; }526.msg-row.bot { justify-content: flex-start; align-items: flex-start; }527.msg-content {528 line-height: 1.65;529 font-size: 1rem;530 word-wrap: break-word;531 max-width: 85%;532}533.user .msg-content {534 background: linear-gradient(135deg, #2d2d30 0%, #3a3a3d 100%);535 padding: 12px 20px;536 border-radius: 20px;537 border-top-right-radius: 6px;538 color: #fff;539 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);540}541.bot .msg-content-wrapper {542 display: flex;543 flex-direction: column;544 gap: 10px;545 width: 100%;546}547.bot .msg-text {548 padding-top: 8px;549 color: var(--text-primary);550 white-space: pre-wrap;551}552.bot-avatar {553 width: 38px;554 height: 38px;555 min-width: 38px;556 border-radius: 50%;557 background-image: var(--logo-url);558 background-size: cover;559 box-shadow: 0 0 16px rgba(99, 102, 241, 0.4);560 border: 2px solid rgba(99, 102, 241, 0.3);561}562.bot-actions {563 display: flex;564 gap: 12px;565 opacity: 0;566 transition: opacity 0.3s;567 margin-top: 6px;568}569.action-btn {570 background: rgba(99, 102, 241, 0.1);571 border: 1px solid rgba(99, 102, 241, 0.2);572 color: var(--text-secondary);573 cursor: pointer;574 padding: 6px 12px;575 border-radius: 8px;576 display: flex;577 align-items: center;578 transition: all 0.2s;579 font-size: 0.85rem;580}581.action-btn:hover {582 color: var(--accent-color);583 background: rgba(99, 102, 241, 0.15);584 border-color: rgba(99, 102, 241, 0.4);585}586.action-btn svg { 587 width: 16px; 588 height: 16px; 589 fill: currentColor; 590 margin-right: 5px;591}592.typing-cursor::after {593 content: '';594 display: inline-block;595 width: 3px;596 height: 18px;597 background: var(--accent-color);598 margin-left: 3px;599 vertical-align: middle;600 animation: blink 0.8s infinite;601}602.footer-container {603 padding: 0 24px 24px 24px;604 background: linear-gradient(to top, rgba(10, 10, 11, 0.95) 85%, transparent);605 position: relative;606 z-index: 60;607}608.input-box {609 max-width: 900px;610 margin: 0 auto;611 background: var(--surface-color);612 border-radius: 30px;613 padding: 10px 12px 10px 24px;614 display: flex;615 align-items: center;616 border: 1px solid rgba(99, 102, 241, 0.2);617 transition: all 0.3s;618 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);619}620.input-box:focus-within {621 border-color: rgba(99, 102, 241, 0.6);622 box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15), 0 4px 20px rgba(0, 0, 0, 0.4);623}624#userInput {625 flex: 1;626 background: transparent;627 border: none;628 color: white;629 font-size: 1rem;630 font-family: inherit;631 padding: 10px 0;632 resize: none;633 max-height: 120px;634}635#mainBtn {636 background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);637 color: white;638 border: none;639 width: 40px;640 height: 40px;641 border-radius: 50%;642 display: flex;643 align-items: center;644 justify-content: center;645 cursor: pointer;646 margin-left: 10px;647 transition: all 0.2s;648 box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);649}650#mainBtn:hover { 651 transform: scale(1.05); 652 box-shadow: 0 6px 16px rgba(99, 102, 241, 0.5);653}654#mainBtn:disabled {655 opacity: 0.6;656 cursor: not-allowed;657 transform: scale(1);658}659.disclaimer {660 text-align: center;661 font-size: 0.75rem;662 color: #6b7280;663 margin-top: 14px;664}665.stats-badge {666 font-size: 0.7rem;667 color: var(--text-secondary);668 margin-top: 6px;669 font-family: 'Monaco', monospace;670 background: rgba(99, 102, 241, 0.05);671 padding: 4px 8px;672 border-radius: 6px;673 display: inline-block;674}675@keyframes slideUpFade {676 from { opacity: 0; transform: translateY(18px); }677 to { opacity: 1; transform: translateY(0); }678}679@keyframes blink { 680 0%, 100% { opacity: 1; } 681 50% { opacity: 0.3; } 682}683@keyframes pulseAvatar {684 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.5); }685 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }686 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }687}688.pulsing { animation: pulseAvatar 1.5s infinite; }689::-webkit-scrollbar { width: 10px; }690::-webkit-scrollbar-track { background: transparent; }691::-webkit-scrollbar-thumb { 692 background: rgba(99, 102, 241, 0.3); 693 border-radius: 5px; 694}695::-webkit-scrollbar-thumb:hover { background: rgba(99, 102, 241, 0.5); }696.error-message {697 color: var(--error-color);698 font-size: 0.9rem;699 padding: 10px 14px;700 background: rgba(239, 68, 68, 0.1);701 border-radius: 10px;702 margin-top: 10px;703 border: 1px solid rgba(239, 68, 68, 0.2);704}705</style>706</head>707<body>708<header>709 <div class="brand-wrapper" onclick="location.reload()">710 <div class="brand-logo"></div>711 <div class="brand-text">712 MTP <span class="version-badge">4.0</span>713 </div>714 </div>715 <div class="status-indicator" title="Sistema operativo"></div>716</header>717<div id="chatScroll" class="chat-scroll">718 <div class="msg-row bot" style="animation-delay: 0.1s;">719 <div class="bot-avatar"></div>720 <div class="msg-content-wrapper">721 <div class="msg-text">722¡Hola! Soy MTP 4, un modelo de lenguaje avanzado con arquitectura Transformer optimizada. 723 724Características principales:725• RoPE - Rotary Position Embedding para mejor contexto726• RMSNorm - Normalización estable y eficiente727• SwiGLU - Función de activación mejorada728• Control inteligente de repetición y coherencia729• Generación fluida y natural730 731¿En qué puedo ayudarte hoy?732 </div>733 </div>734 </div>735</div>736<div class="footer-container">737 <div class="input-box">738 <textarea id="userInput" placeholder="Escribe un mensaje..." rows="1" autocomplete="off"></textarea>739 <button id="mainBtn" onclick="handleBtnClick()"></button>740 </div>741 <div class="disclaimer">742 MTP 4 puede cometer errores. Considera verificar la información importante.743 </div>744</div>745<script>746const chatScroll = document.getElementById('chatScroll');747const userInput = document.getElementById('userInput');748const mainBtn = document.getElementById('mainBtn');749let isGenerating = false;750let abortController = null;751let typingTimeout = null;752let lastUserPrompt = "";753const ICON_SEND = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"></path></svg>`;754const ICON_STOP = `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="0"><rect x="2" y="2" width="20" height="20" rx="4" ry="4"></rect></svg>`;755mainBtn.innerHTML = ICON_SEND;756 757// Auto-resize textarea758userInput.addEventListener('input', function() {759 this.style.height = 'auto';760 this.style.height = Math.min(this.scrollHeight, 120) + 'px';761});762 763function scrollToBottom() {764 chatScroll.scrollTop = chatScroll.scrollHeight;765}766 767function setBtnState(state) {768 if (state === 'sending') {769 mainBtn.innerHTML = ICON_STOP;770 mainBtn.disabled = false;771 isGenerating = true;772 } else if (state === 'disabled') {773 mainBtn.disabled = true;774 isGenerating = false;775 } else {776 mainBtn.innerHTML = ICON_SEND;777 mainBtn.disabled = false;778 isGenerating = false;779 abortController = null;780 }781}782 783function handleBtnClick() {784 if (isGenerating) {785 stopGeneration();786 } else {787 sendMessage();788 }789}790 791function stopGeneration() {792 if (abortController) abortController.abort();793 if (typingTimeout) clearTimeout(typingTimeout);794 const activeCursor = document.querySelector('.typing-cursor');795 if (activeCursor) activeCursor.classList.remove('typing-cursor');796 const activeAvatar = document.querySelector('.pulsing');797 if (activeAvatar) activeAvatar.classList.remove('pulsing');798 setBtnState('idle');799 userInput.focus();800}801 802async function sendMessage(textOverride = null) {803 const text = textOverride || userInput.value.trim();804 if (!text) return;805 806 lastUserPrompt = text;807 808 if (!textOverride) {809 userInput.value = '';810 userInput.style.height = 'auto';811 addMessage(text, 'user');812 }813 814 setBtnState('sending');815 abortController = new AbortController();816 817 const botRow = document.createElement('div');818 botRow.className = 'msg-row bot';819 820 const avatar = document.createElement('div');821 avatar.className = 'bot-avatar pulsing'; 822 823 const wrapper = document.createElement('div');824 wrapper.className = 'msg-content-wrapper';825 826 const msgText = document.createElement('div');827 msgText.className = 'msg-text'; 828 829 wrapper.appendChild(msgText);830 botRow.appendChild(avatar);831 botRow.appendChild(wrapper);832 chatScroll.appendChild(botRow);833 scrollToBottom();834 835 try {836 const startTime = performance.now();837 838 const response = await fetch('/generate', {839 method: 'POST',840 headers: { 'Content-Type': 'application/json' },841 body: JSON.stringify({ 842 text: text,843 max_tokens: 150,844 temperature: 0.7,845 top_k: 40,846 top_p: 0.92,847 repetition_penalty: 1.15,848 min_length: 20849 }),850 signal: abortController.signal851 });852 853 const data = await response.json();854 855 if (!isGenerating) return; 856 857 avatar.classList.remove('pulsing');858 859 if (data.error) {860 msgText.innerHTML = `<span style="color: var(--error-color);">Error: ${data.error}</span>`;861 setBtnState('idle');862 return;863 }864 865 const reply = data.reply || "No entendí eso.";866 const endTime = performance.now();867 const totalTime = ((endTime - startTime) / 1000).toFixed(2);868 869 await typeWriter(msgText, reply);870 871 if (isGenerating) {872 // Agregar estadísticas873 const stats = document.createElement('div');874 stats.className = 'stats-badge';875 stats.textContent = `${data.tokens_generated} tokens • ${data.tokens_per_second} t/s • ${totalTime}s • ${data.device}`;876 wrapper.appendChild(stats);877 878 addActions(wrapper, reply);879 setBtnState('idle');880 }881 } catch (error) {882 if (error.name === 'AbortError') {883 msgText.textContent += " [Detenido]";884 } else {885 console.error('Error:', error);886 avatar.classList.remove('pulsing');887 msgText.innerHTML = `<span style="color: var(--error-color);">Error de conexión. Por favor, intenta de nuevo.</span>`;888 setBtnState('idle');889 }890 }891}892 893function addMessage(text, sender) {894 const row = document.createElement('div');895 row.className = `msg-row ${sender}`;896 897 const content = document.createElement('div');898 content.className = 'msg-content';899 content.textContent = text;900 901 row.appendChild(content);902 chatScroll.appendChild(row);903 scrollToBottom();904}905 906function typeWriter(element, text, speed = 12) {907 return new Promise(resolve => {908 let i = 0;909 element.classList.add('typing-cursor');910 911 function type() {912 if (!isGenerating) {913 element.classList.remove('typing-cursor');914 resolve();915 return;916 }917 918 if (i < text.length) {919 element.textContent += text.charAt(i);920 i++;921 scrollToBottom();922 typingTimeout = setTimeout(type, speed + Math.random() * 5);923 } else {924 element.classList.remove('typing-cursor');925 resolve();926 }927 }928 929 type();930 });931}932 933function addActions(wrapperElement, textToCopy) {934 const actionsDiv = document.createElement('div');935 actionsDiv.className = 'bot-actions';936 937 const copyBtn = document.createElement('button');938 copyBtn.className = 'action-btn';939 copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>Copiar`;940 copyBtn.onclick = () => {941 navigator.clipboard.writeText(textToCopy).then(() => {942 copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>Copiado`;943 setTimeout(() => {944 copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>Copiar`;945 }, 2000);946 });947 };948 949 const regenBtn = document.createElement('button');950 regenBtn.className = 'action-btn';951 regenBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>Regenerar`;952 regenBtn.onclick = () => {953 sendMessage(lastUserPrompt);954 };955 956 actionsDiv.appendChild(copyBtn);957 actionsDiv.appendChild(regenBtn);958 wrapperElement.appendChild(actionsDiv);959 960 requestAnimationFrame(() => actionsDiv.style.opacity = "1");961 scrollToBottom();962}963 964userInput.addEventListener('keydown', (e) => {965 if (e.key === 'Enter' && !e.shiftKey) {966 e.preventDefault();967 handleBtnClick();968 }969});970 971window.onload = () => {972 userInput.focus();973 974 // Cargar info del modelo975 fetch('/info')976 .then(r => r.json())977 .then(data => {978 console.log('MTP 4 cargado:', data);979 })980 .catch(e => console.error('Error cargando info:', e));981};982</script>983</body>984</html>985"""986 987if __name__ == "__main__":988 port = int(os.environ.get("PORT", 7860))989 print(f"\n🚀 Iniciando servidor MTP 4...")990 print(f"🌐 Interfaz web: http://0.0.0.0:{port}")991 print(f"📡 API docs: http://0.0.0.0:{port}/docs")992 print(f"📊 Health check: http://0.0.0.0:{port}/health")993 print(f"ℹ️ Model info: http://0.0.0.0:{port}/info")994 print(f"\n✅ Sistema listo. Presiona Ctrl+C para detener.")995 996 uvicorn.run(997 app,998 host="0.0.0.0",999 port=port,1000 log_level="info"1001 )