CoolFace
Apppublic

TeszenAI/MTP_2

sourceHugging Faceapache-2.0updated 9mo agoView on Hugging Face
0likes
app.py1001 linesDownload Raw Back to root
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    )