daqc commited on
Commit
ba0efb9
·
verified ·
1 Parent(s): 655b309

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +480 -0
  2. requirements.txt +13 -0
app.py ADDED
@@ -0,0 +1,480 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ from gradio import ChatMessage
4
+ import torch
5
+ import torch._dynamo
6
+ from transformers import AutoModelForCausalLM, AutoTokenizer
7
+ from threading import Thread
8
+ from huggingface_hub import hf_hub_download, login
9
+ from dotenv import load_dotenv
10
+ import re
11
+ from llama_cpp import Llama
12
+ from typing import Iterator
13
+
14
+ # Cargar variables de entorno
15
+ load_dotenv()
16
+
17
+ # Configurar token de Hugging Face
18
+ HF_TOKEN = os.getenv("HF_TOKEN")
19
+ if HF_TOKEN:
20
+ login(token=HF_TOKEN)
21
+
22
+ # Intentar importar spaces solo si estamos en un espacio de Hugging Face
23
+ try:
24
+ import spaces
25
+ SPACES_AVAILABLE = True
26
+ except ImportError:
27
+ SPACES_AVAILABLE = False
28
+
29
+ # Desactivar TorchDynamo para evitar errores de compilación
30
+ torch._dynamo.config.suppress_errors = True
31
+ torch._dynamo.disable()
32
+
33
+ # Configuración
34
+ MODEL_ID = "somosnlp-hackathon-2025/iberotales-gemma-3-1b-it-es"
35
+ GGUF_MODEL_ID = "somosnlp-hackathon-2025/iberotales-gemma-3-1b-it-es-finetune-gguf"
36
+ GGUF_FILENAME = "gemma-3-finetune.Q8_0.gguf"
37
+ GGUF_REVISION = "main"
38
+ MAX_MAX_NEW_TOKENS = 2048
39
+ DEFAULT_MAX_NEW_TOKENS = 2048
40
+
41
+ # Verificar si estamos en un espacio de Hugging Face
42
+ IS_HF_SPACE = any([
43
+ os.getenv("SPACE_ID") is not None,
44
+ os.getenv("SPACE_AUTHOR_NAME") is not None,
45
+ os.getenv("SPACE_REPO_NAME") is not None,
46
+ os.getenv("SPACE_HOST") is not None,
47
+ ])
48
+
49
+ # System prompt personalizado
50
+ DEFAULT_SYSTEM_MESSAGE = """Resuelve el siguiente problema.
51
+ Primero, piensa en voz alta qué debes hacer, paso por paso y de forma resumida, entre <think> y </think>.
52
+ Luego, da la respuesta final entre <SOLUTION> y </SOLUTION>.
53
+ No escribas nada fuera de ese formato."""
54
+
55
+ # Base de datos de personajes por país con banderas
56
+ PERSONAJES_POR_PAIS = {
57
+ "🇦🇷 Argentina": [
58
+ {"nombre": "La Difunta Correa", "imagen": "images/ar1.jpg", "descripcion": "Santa popular que murió de sed siguiendo a su esposo reclutado"},
59
+ {"nombre": "El Lobizón", "imagen": "images/ar2.jpg", "descripcion": "Hombre lobo de la tradición gaucha, séptimo hijo varón maldito"},
60
+ {"nombre": "La Telesita", "imagen": "images/ar3.webp", "descripcion": "Bailarina folklórica que se aparece en festivales y zambas"}
61
+ ],
62
+ "🇧🇴 Bolivia": [
63
+ {"nombre": "El Tío del Cerro Rico", "imagen": "images/bo1.webp", "descripcion": "Señor de las minas que protege y castiga a los mineros"},
64
+ {"nombre": "El Ekeko", "imagen": "images/bo2.jpg", "descripcion": "Dios aymara de la abundancia y la fortuna con jorobas"},
65
+ {"nombre": "El Jichi", "imagen": "images/bo3.webp", "descripcion": "Serpiente protectora de ríos y lagunas en la cultura andina"}
66
+ ]
67
+ };
68
+
69
+ # Variables globales
70
+ model = None
71
+ tokenizer = None
72
+ current_personajes = [] # Para mantener el estado de los personajes actuales
73
+
74
+ def load_model():
75
+ """Cargar modelo y tokenizador"""
76
+ global model, tokenizer
77
+
78
+ if torch.cuda.is_available():
79
+ try:
80
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
81
+ model = AutoModelForCausalLM.from_pretrained(
82
+ MODEL_ID,
83
+ torch_dtype=torch.float32,
84
+ device_map="auto",
85
+ trust_remote_code=True,
86
+ )
87
+ if tokenizer.pad_token is None:
88
+ tokenizer.pad_token = tokenizer.eos_token
89
+ return True
90
+ except Exception as e:
91
+ print(f"Error GPU: {e}")
92
+ return False
93
+ else:
94
+ try:
95
+ local_model_path = os.path.join("models", GGUF_FILENAME)
96
+ if os.path.exists(local_model_path):
97
+ model_path = local_model_path
98
+ else:
99
+ model_path = hf_hub_download(
100
+ repo_id=GGUF_MODEL_ID,
101
+ filename=GGUF_FILENAME,
102
+ revision=GGUF_REVISION,
103
+ local_dir="./models",
104
+ force_download=False,
105
+ resume_download=True
106
+ )
107
+ tokenizer = AutoTokenizer.from_pretrained("google/gemma-3-1b-it")
108
+ model = Llama(
109
+ model_path=model_path,
110
+ n_ctx=2048,
111
+ n_threads=4,
112
+ n_gpu_layers=0
113
+ )
114
+ return True
115
+ except Exception as e:
116
+ print(f"Error GGUF: {e}")
117
+ return False
118
+
119
+ model_loaded = load_model()
120
+
121
+ def format_chat_history(messages: list, exclude_last_user: bool = True) -> list:
122
+ """Formatea el historial de chat para el modelo"""
123
+ formatted_history = []
124
+ messages_to_process = messages[:]
125
+ if exclude_last_user and messages_to_process and messages_to_process[-1].get("role") == "user":
126
+ messages_to_process = messages_to_process[:-1]
127
+
128
+ for message in messages_to_process:
129
+ current_role = message.get("role")
130
+ current_content = message.get("content", "").strip()
131
+
132
+ if current_role == "assistant" and message.get("metadata"):
133
+ continue
134
+ if not current_content:
135
+ continue
136
+
137
+ if formatted_history and formatted_history[-1]["role"] == current_role:
138
+ formatted_history[-1]["content"] += "\n\n" + current_content
139
+ else:
140
+ formatted_history.append({
141
+ "role": current_role,
142
+ "content": current_content
143
+ })
144
+
145
+ return formatted_history
146
+
147
+ def stream_iberotales_response(
148
+ user_message: str,
149
+ messages: list,
150
+ system_message: str = DEFAULT_SYSTEM_MESSAGE,
151
+ max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
152
+ temperature: float = 0.7,
153
+ top_p: float = 0.95,
154
+ top_k: int = 50,
155
+ repetition_penalty: float = 1.2,
156
+ ) -> Iterator[list]:
157
+ """Genera respuesta con streaming"""
158
+ global model, tokenizer
159
+
160
+ if model is None or tokenizer is None:
161
+ messages.append(ChatMessage(role="assistant", content="Error: Modelo no disponible."))
162
+ yield messages
163
+ return
164
+
165
+ try:
166
+ chat_history = format_chat_history(messages, exclude_last_user=True)
167
+ conversation = []
168
+ if system_message.strip():
169
+ conversation.append({"role": "system", "content": system_message.strip()})
170
+ conversation.extend(chat_history)
171
+ conversation.append({"role": "user", "content": user_message})
172
+
173
+ # Validar alternancia
174
+ for i in range(1, len(conversation)):
175
+ if conversation[i]["role"] == conversation[i-1]["role"] and conversation[i-1]["role"] != "system":
176
+ messages.append(ChatMessage(role="assistant", content="Error: Reinicia la conversación."))
177
+ yield messages
178
+ return
179
+
180
+ prompt = tokenizer.apply_chat_template(conversation, tokenize=False, add_generation_prompt=True)
181
+ response = model(
182
+ prompt,
183
+ max_tokens=max_new_tokens,
184
+ temperature=temperature,
185
+ top_p=top_p,
186
+ top_k=top_k,
187
+ repeat_penalty=repetition_penalty,
188
+ stream=True
189
+ )
190
+
191
+ full_response = ""
192
+ thinking_message_index = None
193
+ solution_message_index = None
194
+ in_think_block = False
195
+ in_solution_block = False
196
+ thinking_complete = False
197
+
198
+ for chunk in response:
199
+ if chunk["choices"][0]["finish_reason"] is None:
200
+ new_text = chunk["choices"][0]["text"]
201
+ full_response += new_text
202
+
203
+ # Procesar pensamiento
204
+ if "<think>" in full_response and not thinking_complete:
205
+ if not in_think_block:
206
+ in_think_block = True
207
+ if thinking_message_index is None:
208
+ messages.append(ChatMessage(
209
+ role="assistant",
210
+ content="",
211
+ metadata={"title": "🤔 Pensando..."}
212
+ ))
213
+ thinking_message_index = len(messages) - 1
214
+
215
+ think_start = full_response.find("<think>") + 7
216
+ if "</think>" in full_response:
217
+ think_end = full_response.find("</think>")
218
+ current_thinking = full_response[think_start:think_end].strip()
219
+ thinking_complete = True
220
+ in_think_block = False
221
+ else:
222
+ current_thinking = full_response[think_start:].strip()
223
+
224
+ if thinking_message_index is not None:
225
+ messages[thinking_message_index] = ChatMessage(
226
+ role="assistant",
227
+ content=current_thinking,
228
+ metadata={"title": "🤔 Pensando..."}
229
+ )
230
+ yield messages
231
+
232
+ # Procesar solución
233
+ if "<SOLUTION>" in full_response:
234
+ if not in_solution_block:
235
+ in_solution_block = True
236
+ if solution_message_index is None:
237
+ messages.append(ChatMessage(role="assistant", content=""))
238
+ solution_message_index = len(messages) - 1
239
+
240
+ solution_start = full_response.find("<SOLUTION>") + 10
241
+ if "</SOLUTION>" in full_response:
242
+ solution_end = full_response.find("</SOLUTION>")
243
+ current_solution = full_response[solution_start:solution_end].strip()
244
+ in_solution_block = False
245
+ else:
246
+ current_solution = full_response[solution_start:].strip()
247
+
248
+ if solution_message_index is not None and current_solution:
249
+ messages[solution_message_index] = ChatMessage(
250
+ role="assistant",
251
+ content=current_solution
252
+ )
253
+ yield messages
254
+
255
+ # Respuesta sin formato
256
+ if full_response.strip() and solution_message_index is None:
257
+ clean_response = full_response
258
+ if "<think>" in clean_response and "</think>" in clean_response:
259
+ clean_response = re.sub(r'<think>.*?</think>', '', clean_response, flags=re.DOTALL)
260
+ if "<SOLUTION>" in clean_response and "</SOLUTION>" in clean_response:
261
+ clean_response = re.sub(r'<SOLUTION>(.*?)</SOLUTION>', r'\1', clean_response, flags=re.DOTALL)
262
+
263
+ clean_response = clean_response.strip()
264
+ if clean_response:
265
+ messages.append(ChatMessage(role="assistant", content=clean_response))
266
+ yield messages
267
+
268
+ except Exception as e:
269
+ messages.append(ChatMessage(role="assistant", content=f"Error: {str(e)}"))
270
+ yield messages
271
+
272
+ def user_message(msg: str, history: list) -> tuple[str, list]:
273
+ """Añade mensaje del usuario al historial"""
274
+ history.append(ChatMessage(role="user", content=msg))
275
+ return "", history
276
+
277
+ def actualizar_personajes(pais_seleccionado):
278
+ """Actualiza la galería de personajes según el país seleccionado"""
279
+ global current_personajes
280
+ personajes = PERSONAJES_POR_PAIS.get(pais_seleccionado, [])
281
+ current_personajes = personajes # Guardamos el estado actual
282
+
283
+ if not personajes:
284
+ return [], "Selecciona un país para ver sus personajes"
285
+
286
+ # Crear lista de imágenes y etiquetas para la galería
287
+ imagenes = []
288
+ for p in personajes:
289
+ if os.path.exists(p["imagen"]):
290
+ imagenes.append((p["imagen"], f"{p['nombre']}: {p['descripcion']}"))
291
+ else:
292
+ # Imagen placeholder si no existe
293
+ imagenes.append(("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjYyIvPjx0ZXh0IHg9IjUwIiB5PSI1MCIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjEyIiBmaWxsPSIjNjY2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSI+SW1hZ2VuPC90ZXh0Pjwvc3ZnPg==", f"{p['nombre']}: {p['descripcion']}"))
294
+
295
+ return imagenes, f"Personajes de {pais_seleccionado}"
296
+
297
+ def crear_prompt_desde_personaje(evt: gr.SelectData):
298
+ """Crea un prompt basado en el personaje seleccionado"""
299
+ global current_personajes
300
+
301
+ try:
302
+ if evt.index is not None and evt.index < len(current_personajes):
303
+ personaje = current_personajes[evt.index]
304
+ return f"Crea una historia sobre {personaje['nombre']}, {personaje['descripcion']}" #si alguien lee esto, cambiar el dataste a cuenta en lugar de crea
305
+ else:
306
+ return "Crea una historia sobre un personaje mítico"
307
+ except Exception as e:
308
+ print(f"Error al crear prompt: {e}")
309
+ return "Crea una historia sobre un personaje mítico"
310
+
311
+ # Aplicar decorador @spaces.GPU si es necesario
312
+ if IS_HF_SPACE and SPACES_AVAILABLE and torch.cuda.is_available():
313
+ stream_iberotales_response = spaces.GPU(stream_iberotales_response)
314
+
315
+ # CSS personalizado para mejorar la apariencia
316
+ custom_css = """
317
+ .gradio-container {
318
+ max-width: 1400px !important;
319
+ margin: auto;
320
+ padding-top: 1.5rem;
321
+ }
322
+ #galeria .grid-wrap {
323
+ max-height: 350px;
324
+ overflow-y: auto;
325
+ }
326
+ #galeria .grid-container {
327
+ grid-template-columns: repeat(1, 1fr) !important;
328
+ gap: 0.5rem;
329
+ }
330
+ #galeria .thumbnail-item {
331
+ aspect-ratio: 1;
332
+ max-height: 100px;
333
+ }
334
+ #galeria .thumbnail-item img {
335
+ object-fit: cover;
336
+ width: 100%;
337
+ height: 100%;
338
+ border-radius: 8px;
339
+ }
340
+ .header-info {
341
+ background: linear-gradient(135deg, #2c3e50 0%, #1a1a2e 100%);
342
+ color: white;
343
+ padding: 1rem;
344
+ border-radius: 12px;
345
+ margin-bottom: 1rem;
346
+ text-align: center;
347
+ }
348
+ """
349
+
350
+ # Crear la interfaz
351
+ with gr.Blocks(fill_height=True, title="Iberotales", css=custom_css) as demo:
352
+ # Header con información del proyecto
353
+ with gr.Row():
354
+ with gr.Column():
355
+ gr.HTML("""
356
+ <div class="header-info">
357
+ <h1>📚 Iberotales</h1>
358
+ <p><strong>Autor:</strong> David Quispe &nbsp;|&nbsp; <a href="https://github.com/mcdaqc/Iberotales" target="_blank" style="text-decoration: none;">GitHub</a> &nbsp;|&nbsp; <a href="https://huggingface.co/somosnlp-hackathon-2025/iberotales-gemma-3-1b-it-es" target="_blank" style="text-decoration: none;">Modelo</a> &nbsp;|&nbsp; <a href="https://huggingface.co/somosnlp-hackathon-2025/iberotales-gemma-3-1b-it-es-finetune-gguf" target="_blank" style="text-decoration: none;">GGUF</a></p>
359
+ <p><em>Alineando modelos de lenguaje con la narrativa de mitos y leyendas de Iberoamérica.</em></p>
360
+ <p><em>Hackathon SomosNLP 2025</em></p>
361
+ </div>
362
+ """)
363
+
364
+ with gr.Row():
365
+ # Panel izquierdo - Pokédex de personajes
366
+ with gr.Column(scale=1, min_width=320):
367
+ gr.Markdown("### 🗃️ Pokédex de Personajes")
368
+
369
+ pais_dropdown = gr.Dropdown(
370
+ choices=list(PERSONAJES_POR_PAIS.keys()),
371
+ value="🇦🇷 Argentina",
372
+ label="País",
373
+ container=False
374
+ )
375
+
376
+ galeria_personajes = gr.Gallery(
377
+ value=[],
378
+ label="Personajes",
379
+ show_label=False,
380
+ elem_id="galeria",
381
+ columns=1,
382
+ rows=4,
383
+ height=350,
384
+ object_fit="cover",
385
+ preview=False # Esto evita que se expanda automáticamente
386
+ )
387
+
388
+ # Panel derecho - Chat
389
+ with gr.Column(scale=2):
390
+ chatbot = gr.Chatbot(
391
+ type="messages",
392
+ show_label=False,
393
+ height=400,
394
+ avatar_images=(None, "🏛️")
395
+ )
396
+
397
+ with gr.Row():
398
+ input_box = gr.Textbox(
399
+ placeholder="Escribe tu historia o selecciona un personaje...",
400
+ show_label=False,
401
+ scale=4,
402
+ container=False
403
+ )
404
+ send_button = gr.Button("📤", scale=1, variant="primary")
405
+
406
+ with gr.Row():
407
+ clear_button = gr.Button("🗑️ Limpiar", scale=1, size="sm")
408
+
409
+ with gr.Column(scale=3):
410
+ with gr.Row():
411
+ max_tokens = gr.Slider(100, MAX_MAX_NEW_TOKENS, DEFAULT_MAX_NEW_TOKENS, label="Tokens", container=False)
412
+ temperature = gr.Slider(0.1, 2.0, 0.7, label="Temp", container=False)
413
+
414
+ # Variables de estado
415
+ msg_store = gr.State("")
416
+
417
+ # Eventos
418
+ def submit_message(msg, history):
419
+ if not msg.strip():
420
+ return msg, history
421
+ return "", user_message(msg, history)[1]
422
+
423
+ def generate_response(msg, history, max_tok, temp):
424
+ yield from stream_iberotales_response(msg, history, DEFAULT_SYSTEM_MESSAGE, max_tok, temp)
425
+
426
+ # Actualizar personajes cuando cambia el país
427
+ pais_dropdown.change(
428
+ fn=actualizar_personajes,
429
+ inputs=[pais_dropdown],
430
+ outputs=[galeria_personajes, gr.Textbox(visible=False)]
431
+ )
432
+
433
+ # Cargar personajes iniciales
434
+ demo.load(
435
+ fn=actualizar_personajes,
436
+ inputs=[pais_dropdown],
437
+ outputs=[galeria_personajes, gr.Textbox(visible=False)]
438
+ )
439
+
440
+ # Crear prompt desde galería
441
+ galeria_personajes.select(
442
+ fn=crear_prompt_desde_personaje,
443
+ outputs=[input_box]
444
+ )
445
+
446
+ # Envío de mensajes
447
+ input_box.submit(
448
+ lambda msg, hist: (msg, submit_message(msg, hist)[1]),
449
+ inputs=[input_box, chatbot],
450
+ outputs=[msg_store, chatbot],
451
+ queue=False
452
+ ).then(
453
+ generate_response,
454
+ inputs=[msg_store, chatbot, max_tokens, temperature],
455
+ outputs=chatbot
456
+ )
457
+
458
+ send_button.click(
459
+ lambda msg, hist: (msg, submit_message(msg, hist)[1]),
460
+ inputs=[input_box, chatbot],
461
+ outputs=[msg_store, chatbot],
462
+ queue=False
463
+ ).then(
464
+ generate_response,
465
+ inputs=[msg_store, chatbot, max_tokens, temperature],
466
+ outputs=chatbot
467
+ )
468
+
469
+ clear_button.click(
470
+ lambda: ([], "", ""),
471
+ outputs=[chatbot, input_box, msg_store],
472
+ queue=False
473
+ )
474
+
475
+ # Lanzar aplicación
476
+ if __name__ == "__main__":
477
+ if model_loaded:
478
+ demo.launch(share=False, show_error=True)
479
+ else:
480
+ print("Error al cargar el modelo.")
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ torch>=2.0.0
3
+ transformers>=4.36.0
4
+ huggingface_hub>=0.20.0
5
+ llama-cpp-python>=0.2.0
6
+ python-dotenv
7
+ accelerate
8
+ huggingface-hub
9
+ protobuf
10
+ sentencepiece
11
+ gguf
12
+ hf_xet
13
+ pillow