teszenofficial commited on
Commit
7f6b010
·
verified ·
1 Parent(s): bd272de

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +559 -0
app.py ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import uvicorn
4
+ from fastapi import FastAPI
5
+ from fastapi.responses import HTMLResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from pydantic import BaseModel
8
+ from huggingface_hub import hf_hub_download
9
+ from llama_cpp import Llama
10
+
11
+ # ======================
12
+ # CONFIGURACIÓN DEL MODELO (Gemma 2B CPU)
13
+ # ======================
14
+ print("⚙️ Configurando entorno para CPU...")
15
+
16
+ # Usamos Gemma 2B Instruct en formato GGUF (Quantized).
17
+ # Gemma 2B es muy ligero y rápido en CPU.
18
+ REPO_ID = "TheBloke/gemma-2b-it-GGUF"
19
+ FILENAME = "gemma-2b-it.Q4_K_M.gguf"
20
+
21
+ print(f"📦 Descargando/Verificando modelo: {FILENAME}...")
22
+
23
+ try:
24
+ # Descarga el modelo a la caché local de Hugging Face
25
+ model_path = hf_hub_download(
26
+ repo_id=REPO_ID,
27
+ filename=FILENAME
28
+ )
29
+
30
+ # Cargar el modelo en memoria (Motor llama.cpp)
31
+ # n_ctx=2048: Gemma maneja bien contexto, 2048 es seguro para CPU spaces gratis.
32
+ llm = Llama(
33
+ model_path=model_path,
34
+ n_ctx=2048,
35
+ n_threads=max(1, os.cpu_count() - 1),
36
+ verbose=False
37
+ )
38
+ print("✅ Modelo Gemma 2B GGUF cargado correctamente en CPU.")
39
+
40
+ except Exception as e:
41
+ print(f"❌ Error crítico cargando el modelo: {e}")
42
+ raise e
43
+
44
+ # ======================
45
+ # FASTAPI
46
+ # ======================
47
+ app = FastAPI(
48
+ title="MTP Gemma 2B CPU",
49
+ description="Versión optimizada para CPU (Gemma 2B GGUF)",
50
+ version="3.0"
51
+ )
52
+
53
+ app.add_middleware(
54
+ CORSMiddleware,
55
+ allow_origins=["*"],
56
+ allow_methods=["*"],
57
+ allow_headers=["*"],
58
+ )
59
+
60
+ class PromptRequest(BaseModel):
61
+ text: str
62
+ max_tokens: int = 512
63
+ temperature: float = 0.7
64
+ top_p: float = 0.9
65
+
66
+ SYSTEM_PROMPT = (
67
+ "Eres MTP Gemma, una inteligencia artificial avanzada desarrollada por Teszen AI. "
68
+ "Tu objetivo es ser útil, preciso y amigable. "
69
+ "Responde siempre en formato Markdown bien estructurado. "
70
+ "Si te preguntan quién eres, responde que eres MTP Gemma de Teszen AI."
71
+ )
72
+
73
+ # ======================
74
+ # ENDPOINT DE GENERACIÓN
75
+ # ======================
76
+ @app.post("/generate")
77
+ def generate(req: PromptRequest):
78
+ try:
79
+ # Formato de Prompt específico para GEMMA (Instruction Tuned)
80
+ # Formato: <start_of_turn>user\n{prompt}<end_of_turn>\n<start_of_turn>model
81
+
82
+ full_prompt = f"<start_of_turn>user\n{SYSTEM_PROMPT}\n\n{req.text}<end_of_turn>\n<start_of_turn>model"
83
+
84
+ output = llm(
85
+ full_prompt,
86
+ max_tokens=req.max_tokens,
87
+ temperature=req.temperature,
88
+ top_p=req.top_p,
89
+ stop=["<end_of_turn>", "user"], # Tokens de parada específicos de Gemma
90
+ echo=False
91
+ )
92
+
93
+ reply = output["choices"][0]["text"].strip()
94
+
95
+ return {"reply": reply}
96
+
97
+ except Exception as e:
98
+ print(f"Error en generación: {e}")
99
+ return {"reply": f"❌ Error interno del servidor: {str(e)}"}
100
+
101
+ # ======================
102
+ # INTERFAZ WEB (UI PREMIUM)
103
+ # ======================
104
+ @app.get("/", response_class=HTMLResponse)
105
+ def chat_ui():
106
+ return """
107
+ <!DOCTYPE html>
108
+ <html lang="es">
109
+ <head>
110
+ <meta charset="UTF-8">
111
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
112
+ <title>MTP Gemma 2B | Teszen AI</title>
113
+
114
+ <!-- Fuentes e Iconos -->
115
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
116
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
117
+
118
+ <!-- Markdown y Highlight.js para código -->
119
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
120
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css">
121
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
122
+
123
+ <style>
124
+ :root {
125
+ --bg-color: #0f1012;
126
+ --chat-bg: #161719;
127
+ --input-bg: #202124;
128
+ --primary: #d64a4a; /* Gemma suele asociarse con tonos rojizos/Google, o mantengo el azul si prefieres, he puesto rojo suave para diferenciar */
129
+ --primary-glow: rgba(214, 74, 74, 0.4);
130
+ --text-main: #e8eaed;
131
+ --text-secondary: #9aa0a6;
132
+ --user-bubble: #2b2d31;
133
+ --bot-bubble: transparent;
134
+ --border: #303134;
135
+ }
136
+
137
+ /* Sobreescribimos a azul si prefieres mantener la identidad de Teszen */
138
+ :root {
139
+ --primary: #4a9eff;
140
+ --primary-glow: rgba(74, 158, 255, 0.4);
141
+ }
142
+
143
+ * { box-sizing: border-box; outline: none; }
144
+
145
+ body {
146
+ margin: 0;
147
+ font-family: 'Outfit', sans-serif;
148
+ background-color: var(--bg-color);
149
+ color: var(--text-main);
150
+ height: 100vh;
151
+ display: flex;
152
+ flex-direction: column;
153
+ overflow: hidden;
154
+ }
155
+
156
+ /* --- Header --- */
157
+ header {
158
+ padding: 15px 24px;
159
+ background: rgba(15, 16, 18, 0.85);
160
+ backdrop-filter: blur(12px);
161
+ border-bottom: 1px solid var(--border);
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: space-between;
165
+ z-index: 100;
166
+ }
167
+
168
+ .brand {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 12px;
172
+ }
173
+
174
+ .logo-container {
175
+ position: relative;
176
+ width: 42px;
177
+ height: 42px;
178
+ }
179
+
180
+ .logo {
181
+ width: 100%;
182
+ height: 100%;
183
+ border-radius: 50%;
184
+ object-fit: cover;
185
+ border: 2px solid var(--primary);
186
+ box-shadow: 0 0 15px var(--primary-glow);
187
+ }
188
+
189
+ .brand-text h1 {
190
+ margin: 0;
191
+ font-size: 1.1rem;
192
+ font-weight: 600;
193
+ letter-spacing: 0.5px;
194
+ }
195
+
196
+ .brand-text span {
197
+ font-size: 0.75rem;
198
+ color: var(--primary);
199
+ background: rgba(74, 158, 255, 0.1);
200
+ padding: 2px 8px;
201
+ border-radius: 6px;
202
+ margin-left: 8px;
203
+ }
204
+
205
+ .status-dot {
206
+ width: 8px;
207
+ height: 8px;
208
+ background: #00ff88;
209
+ border-radius: 50%;
210
+ box-shadow: 0 0 8px #00ff88;
211
+ }
212
+
213
+ /* --- Chat Area --- */
214
+ #chat-container {
215
+ flex: 1;
216
+ padding: 20px;
217
+ overflow-y: auto;
218
+ scroll-behavior: smooth;
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 20px;
222
+ max-width: 900px;
223
+ margin: 0 auto;
224
+ width: 100%;
225
+ }
226
+
227
+ .message {
228
+ display: flex;
229
+ gap: 16px;
230
+ opacity: 0;
231
+ transform: translateY(10px);
232
+ animation: slideIn 0.3s forwards;
233
+ }
234
+
235
+ @keyframes slideIn {
236
+ to { opacity: 1; transform: translateY(0); }
237
+ }
238
+
239
+ .avatar {
240
+ width: 36px;
241
+ height: 36px;
242
+ border-radius: 50%;
243
+ flex-shrink: 0;
244
+ display: flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ background: #333;
248
+ overflow: hidden;
249
+ }
250
+
251
+ .avatar img { width: 100%; height: 100%; object-fit: cover; }
252
+ .avatar i { font-size: 1.1rem; color: #fff; }
253
+
254
+ .bot-avatar { background: transparent; border: 1px solid var(--primary); }
255
+ .user-avatar { background: var(--border); }
256
+
257
+ .content {
258
+ flex: 1;
259
+ max-width: 85%;
260
+ font-size: 0.98rem;
261
+ line-height: 1.6;
262
+ }
263
+
264
+ .user-msg { flex-direction: row-reverse; }
265
+
266
+ .user-msg .content {
267
+ background: var(--user-bubble);
268
+ padding: 12px 18px;
269
+ border-radius: 18px 4px 18px 18px;
270
+ color: #fff;
271
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
272
+ }
273
+
274
+ .bot-msg .content {
275
+ background: var(--bot-bubble);
276
+ padding: 0 10px;
277
+ color: var(--text-main);
278
+ }
279
+
280
+ /* Markdown Styles within Bot Message */
281
+ .bot-msg .content p { margin-top: 0; margin-bottom: 10px; }
282
+ .bot-msg .content pre {
283
+ background: #1e1e1e !important;
284
+ padding: 15px;
285
+ border-radius: 12px;
286
+ overflow-x: auto;
287
+ border: 1px solid #333;
288
+ font-family: 'JetBrains Mono', monospace;
289
+ font-size: 0.9rem;
290
+ }
291
+ .bot-msg .content code {
292
+ font-family: 'JetBrains Mono', monospace;
293
+ background: rgba(255,255,255,0.1);
294
+ padding: 2px 5px;
295
+ border-radius: 4px;
296
+ font-size: 0.85em;
297
+ }
298
+ .bot-msg .content ul, .bot-msg .content ol { padding-left: 20px; }
299
+ .bot-msg .content li { margin-bottom: 5px; }
300
+
301
+ /* --- Footer / Input --- */
302
+ .input-area {
303
+ padding: 20px;
304
+ background: var(--bg-color);
305
+ border-top: 1px solid var(--border);
306
+ }
307
+
308
+ .input-wrapper {
309
+ max-width: 900px;
310
+ margin: 0 auto;
311
+ position: relative;
312
+ background: var(--input-bg);
313
+ border-radius: 24px;
314
+ padding: 8px 8px 8px 20px;
315
+ display: flex;
316
+ align-items: flex-end;
317
+ border: 1px solid transparent;
318
+ transition: border-color 0.3s, box-shadow 0.3s;
319
+ }
320
+
321
+ .input-wrapper:focus-within {
322
+ border-color: var(--primary);
323
+ box-shadow: 0 0 15px rgba(74, 158, 255, 0.15);
324
+ }
325
+
326
+ textarea {
327
+ flex: 1;
328
+ background: transparent;
329
+ border: none;
330
+ color: white;
331
+ font-family: inherit;
332
+ font-size: 1rem;
333
+ resize: none;
334
+ max-height: 150px;
335
+ padding: 12px 0;
336
+ height: 48px; /* Altura inicial */
337
+ }
338
+
339
+ textarea::placeholder { color: var(--text-secondary); }
340
+
341
+ .btn-send {
342
+ width: 42px;
343
+ height: 42px;
344
+ border: none;
345
+ border-radius: 50%;
346
+ background: var(--primary);
347
+ color: white;
348
+ cursor: pointer;
349
+ margin-left: 10px;
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ transition: transform 0.2s, background 0.2s;
354
+ }
355
+
356
+ .btn-send:hover { background: #3a8ee6; transform: scale(1.05); }
357
+ .btn-send:disabled { background: #444; cursor: not-allowed; transform: none; }
358
+
359
+ /* --- Typing Indicator --- */
360
+ .typing {
361
+ display: flex;
362
+ gap: 4px;
363
+ padding: 10px 0;
364
+ display: none; /* Hidden by default */
365
+ }
366
+ .dot {
367
+ width: 6px;
368
+ height: 6px;
369
+ background: var(--text-secondary);
370
+ border-radius: 50%;
371
+ animation: bounce 1.4s infinite ease-in-out both;
372
+ }
373
+ .dot:nth-child(1) { animation-delay: -0.32s; }
374
+ .dot:nth-child(2) { animation-delay: -0.16s; }
375
+
376
+ @keyframes bounce {
377
+ 0%, 80%, 100% { transform: scale(0); }
378
+ 40% { transform: scale(1); }
379
+ }
380
+
381
+ /* Scrollbar custom */
382
+ ::-webkit-scrollbar { width: 8px; }
383
+ ::-webkit-scrollbar-track { background: transparent; }
384
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
385
+ ::-webkit-scrollbar-thumb:hover { background: #444; }
386
+ </style>
387
+ </head>
388
+
389
+ <body>
390
+
391
+ <header>
392
+ <div class="brand">
393
+ <div class="logo-container">
394
+ <!-- Foto de Perfil con fallback a icono si falla la carga -->
395
+ <img src="https://i.postimg.cc/yxS54PF3/IMG-3082.jpg"
396
+ class="logo"
397
+ alt="MTP Gemma"
398
+ onerror="this.onerror=null; this.src='https://cdn-icons-png.flaticon.com/512/4712/4712027.png'">
399
+ </div>
400
+ <div class="brand-text">
401
+ <h1>MTP Gemma <span>2B CPU</span></h1>
402
+ </div>
403
+ </div>
404
+ <div title="Online" class="status-dot"></div>
405
+ </header>
406
+
407
+ <div id="chat-container">
408
+ <!-- Mensaje de Bienvenida -->
409
+ <div class="message bot-msg">
410
+ <div class="avatar bot-avatar">
411
+ <img src="https://i.postimg.cc/yxS54PF3/IMG-3082.jpg" onerror="this.style.display='none';this.nextElementSibling.style.display='block'">
412
+ <i class="fa-solid fa-robot" style="display:none"></i>
413
+ </div>
414
+ <div class="content">
415
+ <p>Hola, soy <strong>MTP Gemma</strong>. ✨<br>
416
+ Modelo 2B optimizado para CPU. ¿En qué puedo ayudarte hoy?</p>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <!-- Indicador de escribiendo (oculto por defecto) -->
422
+ <div id="typing-indicator" style="padding-left: 70px; display: none;">
423
+ <div class="typing">
424
+ <div class="dot"></div>
425
+ <div class="dot"></div>
426
+ <div class="dot"></div>
427
+ </div>
428
+ </div>
429
+
430
+ <div class="input-area">
431
+ <div class="input-wrapper">
432
+ <textarea id="userInput" placeholder="Escribe tu mensaje aquí..." rows="1"></textarea>
433
+ <button id="sendBtn" class="btn-send" onclick="sendMessage()">
434
+ <i class="fa-solid fa-paper-plane"></i>
435
+ </button>
436
+ </div>
437
+ </div>
438
+
439
+ <script>
440
+ const chatContainer = document.getElementById('chat-container');
441
+ const userInput = document.getElementById('userInput');
442
+ const sendBtn = document.getElementById('sendBtn');
443
+ const typingIndicator = document.getElementById('typing-indicator');
444
+
445
+ // Auto-resize del textarea
446
+ userInput.addEventListener('input', function() {
447
+ this.style.height = 'auto';
448
+ this.style.height = (this.scrollHeight) + 'px';
449
+ if(this.value === '') this.style.height = '48px';
450
+ });
451
+
452
+ // Enviar con Enter (Shift+Enter para salto de línea)
453
+ userInput.addEventListener('keydown', (e) => {
454
+ if (e.key === 'Enter' && !e.shiftKey) {
455
+ e.preventDefault();
456
+ sendMessage();
457
+ }
458
+ });
459
+
460
+ function appendMessage(text, isUser) {
461
+ const div = document.createElement('div');
462
+ div.className = `message ${isUser ? 'user-msg' : 'bot-msg'}`;
463
+
464
+ let avatarHTML = '';
465
+ if (isUser) {
466
+ avatarHTML = `
467
+ <div class="avatar user-avatar">
468
+ <i class="fa-solid fa-user"></i>
469
+ </div>`;
470
+ } else {
471
+ avatarHTML = `
472
+ <div class="avatar bot-avatar">
473
+ <img src="https://i.postimg.cc/yxS54PF3/IMG-3082.jpg" onerror="this.style.display='none';this.nextElementSibling.style.display='block'">
474
+ <i class="fa-solid fa-robot" style="display:none"></i>
475
+ </div>`;
476
+ }
477
+
478
+ // Procesar Markdown si es bot, texto plano si es usuario
479
+ let contentHTML = '';
480
+ if (isUser) {
481
+ contentHTML = text.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\\n/g, "<br>");
482
+ } else {
483
+ contentHTML = marked.parse(text);
484
+ }
485
+
486
+ div.innerHTML = `
487
+ ${avatarHTML}
488
+ <div class="content">${contentHTML}</div>
489
+ `;
490
+
491
+ chatContainer.appendChild(div);
492
+
493
+ // Resaltar código si hay bloques
494
+ if (!isUser) {
495
+ div.querySelectorAll('pre code').forEach((block) => {
496
+ hljs.highlightElement(block);
497
+ });
498
+ }
499
+
500
+ chatContainer.scrollTop = chatContainer.scrollHeight;
501
+ }
502
+
503
+ async function sendMessage() {
504
+ const text = userInput.value.trim();
505
+ if (!text) return;
506
+
507
+ // UI Updates
508
+ userInput.value = '';
509
+ userInput.style.height = '48px';
510
+ userInput.disabled = true;
511
+ sendBtn.disabled = true;
512
+
513
+ appendMessage(text, true);
514
+
515
+ // Mostrar Typing Indicator
516
+ typingIndicator.style.display = 'block';
517
+ chatContainer.scrollTop = chatContainer.scrollHeight;
518
+
519
+ try {
520
+ const response = await fetch('/generate', {
521
+ method: 'POST',
522
+ headers: { 'Content-Type': 'application/json' },
523
+ body: JSON.stringify({ text: text })
524
+ });
525
+
526
+ const data = await response.json();
527
+
528
+ // Ocultar Typing Indicator
529
+ typingIndicator.style.display = 'none';
530
+
531
+ if (data.reply) {
532
+ appendMessage(data.reply, false);
533
+ } else {
534
+ appendMessage("❌ Error: No se recibió respuesta.", false);
535
+ }
536
+
537
+ } catch (error) {
538
+ typingIndicator.style.display = 'none';
539
+ appendMessage(`❌ Error de conexión: ${error.message}`, false);
540
+ } finally {
541
+ userInput.disabled = false;
542
+ sendBtn.disabled = false;
543
+ userInput.focus();
544
+ }
545
+ }
546
+ </script>
547
+
548
+ </body>
549
+ </html>
550
+ """
551
+
552
+ # ======================
553
+ # EJECUCIÓN
554
+ # ======================
555
+ if __name__ == "__main__":
556
+ port = int(os.environ.get("PORT", 7860))
557
+ # Para Spaces con Docker, 0.0.0.0 es necesario
558
+ uvicorn.run(app, host="0.0.0.0", port=port)
559
+