import os import gradio as gr from gradio import ChatMessage import torch import torch._dynamo from transformers import AutoModelForCausalLM, AutoTokenizer from huggingface_hub import hf_hub_download import re from llama_cpp import Llama from typing import Iterator import spaces # Desactivar TorchDynamo para evitar errores de compilación torch._dynamo.config.suppress_errors = True torch._dynamo.disable() # Configuración MODEL_ID = "somosnlp-hackathon-2025/iberotales-gemma-3-1b-it-es" GGUF_MODEL_ID = "somosnlp-hackathon-2025/iberotales-gemma-3-1b-it-es-finetune-gguf" GGUF_FILENAME = "gemma-3-finetune.Q8_0.gguf" GGUF_REVISION = "main" MAX_MAX_NEW_TOKENS = 2048 DEFAULT_MAX_NEW_TOKENS = 1024 # System prompt personalizado DEFAULT_SYSTEM_MESSAGE = """Resuelve el siguiente problema. Primero, piensa en voz alta qué debes hacer, paso por paso y de forma resumida, entre y . Luego, da la respuesta final entre y . No escribas nada fuera de ese formato.""" # Base de datos de personajes por país con banderas PERSONAJES_POR_PAIS = { "🇦🇷 Argentina": [ {"nombre": "La Difunta Correa", "imagen": "images/ar1.jpg", "descripcion": "Santa popular que murió de sed siguiendo a su esposo reclutado"}, {"nombre": "El Lobizón", "imagen": "images/ar2.jpg", "descripcion": "Hombre lobo de la tradición gaucha, séptimo hijo varón maldito"}, {"nombre": "La Telesita", "imagen": "images/ar3.webp", "descripcion": "Bailarina folklórica que se aparece en festivales y zambas"} ], "🇧🇴 Bolivia": [ {"nombre": "El Tío del Cerro Rico", "imagen": "images/bo1.webp", "descripcion": "Señor de las minas que protege y castiga a los mineros"}, {"nombre": "El Ekeko", "imagen": "images/bo2.jpg", "descripcion": "Dios aymara de la abundancia y la fortuna con jorobas"}, {"nombre": "El Jichi", "imagen": "images/bo3.webp", "descripcion": "Serpiente protectora de ríos y lagunas en la cultura andina"} ], "🇧🇷 Brasil": [ {"nombre": "Curupira", "imagen": "images/br1.jpeg", "descripcion": "Protector del bosque amazónico con pies al revés"}, {"nombre": "Saci-Pererê", "imagen": "images/br2.jpg", "descripcion": "Duende travieso de una pierna que fuma pipa"}, {"nombre": "Yebá Bëló", "imagen": "images/br3.jpg", "descripcion": "Abuela del mundo en mitología desana, creadora del universo"} ], "🇨🇱 Chile": [ {"nombre": "Guallipén", "imagen": "images/ch1.webp", "descripcion": "Carnero gigante que habita en los ríos de Chiloé"}, {"nombre": "Colo Colo", "imagen": "images/ch2.webp", "descripcion": "Ser maléfico nacido de huevo de gallo empollado por serpiente"}, {"nombre": "Cuchivilu", "imagen": "images/ch3.webp", "descripcion": "Cerdo marino con cola de pez que habita en Chiloé"} ], "🇨🇴 Colombia": [ {"nombre": "El Guaca", "imagen": "images/co1.jpg", "descripcion": "Espíritu guardián de tesoros enterrados por indígenas"}, {"nombre": "Chiminigagua", "imagen": "images/co2.webp", "descripcion": "Dios creador muisca, fuente de luz y vida universal"}, {"nombre": "Jukumari", "imagen": "images/co3.jpg", "descripcion": "Oso gigante mitológico de los Andes colombianos"} ], "🇨🇷 Costa Rica": [ {"nombre": "El Micomalo", "imagen": "images/cr1.jpg", "descripcion": "Mono gigante y feroz que ataca a los cazadores"}, {"nombre": "La Carreta sin Bueyes", "imagen": "images/crr2.jpg", "descripcion": "Carreta fantasma que recorre caminos sin animales tirando"}, {"nombre": "El Sisimiqui", "imagen": "images/cr3.webp", "descripcion": "Duende pequeño y travieso de los bosques costarricenses"} ], "🇨🇺 Cuba": [ {"nombre": "El Güije", "imagen": "images/cu1.jpg", "descripcion": "Duende negro de aguas dulces que ahoga a los bañistas"}, {"nombre": "Itiba Cahubaba", "imagen": "images/cu2.jpg", "descripcion": "Madre primordial taína de donde brotaron las aguas"}, {"nombre": "Guabancex", "imagen": "images/cu3.jpg", "descripcion": "Diosa taína de los huracanes y vientos destructores"} ], "🇪🇨 Ecuador": [ {"nombre": "Salun", "imagen": "images/ec1.jpg", "descripcion": "Espíritu shamán shuar que guía en visiones espirituales"}, {"nombre": "Nunkui", "imagen": "images/ec2.jpg", "descripcion": "Diosa shuar de la fertilidad de la tierra y agricultura"}, {"nombre": "Yacuruna", "imagen": "images/ec3.jpg", "descripcion": "Hombre serpiente amazónico señor de las aguas dulces"} ], "🇪🇸 España": [ {"nombre": "Ojáncanu", "imagen": "images/es1.jpg", "descripcion": "Gigante cíclope cántabro de fuerza descomunal"}, {"nombre": "Los Carantos", "imagen": "images/es2.jpg", "descripcion": "Espíritus gallegos que anuncian desgracias familiares"}, {"nombre": "Cuegle", "imagen": "images/es3.jpg", "descripcion": "Ser asturiano de tres ojos que protege casas del mal"} ], "🇬🇹 Guatemala": [ {"nombre": "Camalotz", "imagen": "images/gu1.jpg", "descripcion": "Dios murciélago maya señor de las cuevas y la muerte"}, {"nombre": "La Siguanaba", "imagen": "images/gu2.jpg", "descripcion": "Mujer hermosa que muestra rostro de calavera a infieles"}, {"nombre": "Gucumatz", "imagen": "images/gu3.jpg", "descripcion": "Serpiente emplumada maya, equivalente a Quetzalcóatl"} ], "🇭🇳 Honduras": [ {"nombre": "Icelaca", "imagen": "images/ho1.jpg", "descripcion": "Serpiente gigante que habita en cuevas hondureñas"}, {"nombre": "Cihuateteo", "imagen": "images/ho2.jpg", "descripcion": "Espíritus de mujeres muertas en parto, guerreras divinas"}, {"nombre": "Comelenguas", "imagen": "images/ho3.webp", "descripcion": "Demonio que devora lenguas de personas dormidas"} ], "🇲🇽 México": [ {"nombre": "Quetzalcóatl", "imagen": "images/me1.jpg", "descripcion": "Serpiente emplumada, dios del viento y la sabiduría"}, {"nombre": "Tlacatecolotl", "imagen": "images/me2.jpg", "descripcion": "Brujo nahuatl que se transforma en búho gigante"}, {"nombre": "Tlalocan", "imagen": "images/me3.jpg", "descripcion": "Paraíso acuático de Tláloc donde van los ahogados"} ], "🇳🇮 Nicaragua": [ {"nombre": "La Mocuana", "imagen": "images/ni1.webp", "descripcion": "Espíritu de mujer sin cabeza que vaga de noche"}, {"nombre": "El Guácimo Renco", "imagen": "images/ni2.jpg", "descripcion": "Árbol maldito que se transforma y persigue viajeros"}, {"nombre": "La Mona Bruja", "imagen": "images/ni3.jpg", "descripcion": "Bruja transformada en mona que roba niños pequeños"} ], "🇵🇦 Panamá": [ {"nombre": "Humantahú", "imagen": "images/pa1.jpg", "descripcion": "Chamán kuna protector de la naturaleza y animales"}, {"nombre": "La Silampa", "imagen": "images/pa2.jpg", "descripcion": "Mujer fantasma que seduce y mata a hombres solitarios"}, {"nombre": "El Chivato", "imagen": "images/pa3.jpg", "descripcion": "Cabro diabólico que aparece en encrucijadas nocturnas"} ], "🇵🇾 Paraguay": [ {"nombre": "Mbói Tata", "imagen": "images/py1.png", "descripcion": "Serpiente de fuego protectora de campos y esteros"}, {"nombre": "Urutau", "imagen": "images/py2.jpg", "descripcion": "Ave fantasma cuyo canto anuncia tragedias familiares"}, {"nombre": "Tajy", "imagen": "images/py3.jpg", "descripcion": "Espíritu del lapacho florido en la mitología guaraní"} ], "🇵🇪 Perú": [ {"nombre": "Wiracocha", "imagen": "images/pe1.jpg", "descripcion": "Dios creador inca señor de todas las cosas vivientes"}, {"nombre": "El Muki", "imagen": "images/pe2.webp", "descripcion": "Duende minero que protege vetas de oro y plata"}, {"nombre": "Los Hermanos Ayar", "imagen": "images/pe3.jpg", "descripcion": "Cuatro hermanos fundadores míticos del imperio inca"} ], "🇵🇹 Portugal": [ {"nombre": "El Marialva", "imagen": "images/po1.jpg", "descripcion": "Caballero galante y seductor de la tradición portuguesa"}, {"nombre": "El Pez Milagroso", "imagen": "images/po2.webp", "descripcion": "Pez mágico que concede deseos a pescadores devotos"}, {"nombre": "La Cueva da Moeda", "imagen": "images/po3.webp", "descripcion": "Cueva encantada llena de tesoros moros perdidos"} ], "🇵🇷 Puerto Rico": [ {"nombre": "Yúcahu", "imagen": "images/pr1.jpg", "descripcion": "Dios taíno de la yuca y protector de cosechas"}, {"nombre": "Atabey", "imagen": "images/pr2.jpg", "descripcion": "Diosa madre taína de las aguas dulces y fertilidad"}, {"nombre": "Anacacuya", "imagen": "images/pr3.png", "descripcion": "Estrella taína guía de navegantes y pescadores"} ], "🇩🇴 República Dominicana": [ {"nombre": "El Galipote", "imagen": "images/rd1.webp", "descripcion": "Brujo que se transforma en animal para hacer maldades"}, {"nombre": "El Lugaru", "imagen": "images/rd2.webp", "descripcion": "Hombre lobo dominicano que ataca ganado y personas"}, {"nombre": "Papa Legba", "imagen": "images/rd3.jpg", "descripcion": "Loa vudú guardián de encrucijadas y comunicaciones"} ], "🇺🇾 Uruguay": [ {"nombre": "El Yasy Yateré", "imagen": "images/ur1.jpg", "descripcion": "Duende rubio protector de niños con bastón mágico"}, {"nombre": "El Ñandú Barriga Blanca", "imagen": "images/ur2.jpg", "descripcion": "Ñandú gigante y sagrado de las pampas uruguayas"}, {"nombre": "El Pombero", "imagen": "images/ur3.jpg", "descripcion": "Duende travieso protector de aves y la naturaleza"} ], "🇻🇪 Venezuela": [ {"nombre": "Amalivaca", "imagen": "images/ve1.jpg", "descripcion": "Héroe cultural tamanaco creador del río Orinoco"}, {"nombre": "Osemma", "imagen": "images/ve2.jpg", "descripcion": "Espíritu warao de las palmeras de moriche"}, {"nombre": "La Sayona", "imagen": "images/ve3.webp", "descripcion": "Mujer vengativa que persigue y castiga a infieles"} ] }; # Variables globales model = None tokenizer = None current_personajes = [] # CSS personalizado custom_css = """ .gradio-container { max-width: 1400px !important; margin: auto; padding-top: 1.5rem; } #galeria .grid-wrap { max-height: 350px; overflow-y: auto; } #galeria .grid-container { grid-template-columns: repeat(1, 1fr) !important; gap: 0.5rem; } #galeria .thumbnail-item { aspect-ratio: 1; max-height: 100px; } #galeria .thumbnail-item img { object-fit: cover; width: 100%; height: 100%; border-radius: 8px; } .header-info { background: linear-gradient(135deg, #2c3e50 0%, #1a1a2e 100%); color: white; padding: 1rem; border-radius: 12px; margin-bottom: 1rem; text-align: center; } """ def load_model(): """Cargar modelo GGUF""" global model, tokenizer try: # Descargar modelo GGUF print("Descargando modelo GGUF...") model_path = hf_hub_download( repo_id=GGUF_MODEL_ID, filename=GGUF_FILENAME, revision=GGUF_REVISION, local_dir="./models", ) print(f"Modelo descargado en: {model_path}") # Cargar modelo GGUF model = Llama( model_path=model_path, n_ctx=2048, n_batch=512, n_threads=4, n_gpu_layers=0, chat_format="gemma" ) # Cargar tokenizer tokenizer = AutoTokenizer.from_pretrained("google/gemma-2-2b-it") print("Modelo cargado exitosamente") return True except Exception as e: print(f"Error al cargar modelo: {e}") return False def generate_response( user_message: str, system_message: str = DEFAULT_SYSTEM_MESSAGE, max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS, temperature: float = 0.7, top_p: float = 0.95, ): """Genera respuesta usando el modelo GGUF""" global model if model is None: return "Error: Modelo no disponible." try: # Crear mensajes messages = [ {"role": "system", "content": system_message}, {"role": "user", "content": user_message} ] # Generar respuesta response = model.create_chat_completion( messages=messages, max_tokens=max_new_tokens, temperature=temperature, top_p=top_p, stream=False ) full_response = response['choices'][0]['message']['content'] # Procesar respuesta con formato thinking_part = "" solution_part = "" # Extraer pensamiento think_match = re.search(r'(.*?)', full_response, re.DOTALL) if think_match: thinking_part = think_match.group(1).strip() # Extraer solución solution_match = re.search(r'(.*?)', full_response, re.DOTALL) if solution_match: solution_part = solution_match.group(1).strip() # Construir respuesta formateada messages = [] if thinking_part: messages.append(ChatMessage( role="assistant", content=thinking_part, metadata={"title": "🤔 Pensando..."} )) if solution_part: messages.append(ChatMessage( role="assistant", content=solution_part )) elif not thinking_part: # Si no hay formato específico, usar respuesta completa clean_response = re.sub(r'.*?', '', full_response, flags=re.DOTALL) clean_response = re.sub(r'(.*?)', r'\1', clean_response, flags=re.DOTALL) messages.append(ChatMessage( role="assistant", content=clean_response.strip() )) return messages except Exception as e: return [ChatMessage(role="assistant", content=f"Error: {str(e)}")] def crear_imagen_placeholder(): """Crea una imagen placeholder simple usando PIL""" try: from PIL import Image, ImageDraw, ImageFont import io import base64 # Crear imagen simple img = Image.new('RGB', (100, 100), color='lightgray') draw = ImageDraw.Draw(img) # Dibujar texto simple draw.text((10, 40), "No Image", fill='black') # Convertir a bytes buffer = io.BytesIO() img.save(buffer, format='PNG') img_str = base64.b64encode(buffer.getvalue()).decode() return f"data:image/png;base64,{img_str}" except: # Fallback: retornar None si no se puede crear la imagen return None def actualizar_personajes(pais_seleccionado): """Actualiza la galería de personajes según el país seleccionado""" global current_personajes if not pais_seleccionado: return gr.Gallery(value=None, visible=False), "Selecciona un país para ver sus personajes" personajes = PERSONAJES_POR_PAIS.get(pais_seleccionado, []) current_personajes = personajes if not personajes: return gr.Gallery(value=None, visible=False), "No hay personajes disponibles para este país" # Crear lista de imágenes válidas para la galería imagenes_validas = [] for i, personaje in enumerate(personajes): imagen_path = personaje["imagen"] # Verificar si la imagen existe if os.path.exists(imagen_path) and os.path.isfile(imagen_path): try: # Verificar que es una imagen válida from PIL import Image with Image.open(imagen_path) as img: img.verify() # Verificar que la imagen es válida imagenes_validas.append(imagen_path) except Exception as e: print(f"Error al verificar imagen {imagen_path}: {e}") # Skip esta imagen si hay error continue else: print(f"Imagen no encontrada: {imagen_path}") # Skip imágenes que no existen continue # Si no hay imágenes válidas, mostrar mensaje if not imagenes_validas: return gr.Gallery( value=None, visible=True, label=f"⚠️ Imágenes no encontradas para {pais_seleccionado}" ), f"Las imágenes para {pais_seleccionado} no están disponibles" # Actualizar la galería con las imágenes válidas return gr.Gallery( value=imagenes_validas, visible=True, label=f"Personajes de {pais_seleccionado}" ), f"Mostrando {len(imagenes_validas)} personajes de {pais_seleccionado}" def crear_prompt_desde_personaje(evt: gr.SelectData): """Crea un prompt basado en el personaje seleccionado""" global current_personajes try: if evt.index is not None and 0 <= evt.index < len(current_personajes): personaje = current_personajes[evt.index] prompt = f"Crea una historia sobre {personaje['nombre']}: {personaje['descripcion']}" return prompt else: return "Crea una historia sobre un personaje mítico" except Exception as e: print(f"Error al crear prompt: {e}") return "Crea una historia sobre un personaje mítico" def chat_function(message, history, max_tokens, temperature): """Función principal del chat""" if not message.strip(): return history, "" # Agregar mensaje del usuario al historial history = history + [ChatMessage(role="user", content=message)] # Generar respuesta response_messages = generate_response(message, DEFAULT_SYSTEM_MESSAGE, max_tokens, temperature) # Agregar respuestas al historial for msg in response_messages: history = history + [msg] return history, "" # Cargar modelo al inicio print("Iniciando carga del modelo...") model_loaded = load_model() # Crear la interfaz con componentes renderizables with gr.Blocks(title="Iberotales", css=custom_css) as demo: # Header con información del proyecto gr.HTML("""

📚 Iberotales

Autor: David Quispe  |  GitHub  |  Modelo  |  GGUF

Alineando modelos de lenguaje con la narrativa de mitos y leyendas de Iberoamérica.

Hackathon SomosNLP 2025

""") with gr.Row(): # Panel izquierdo - Pokédex de personajes with gr.Column(scale=1, min_width=320): gr.Markdown("### 🗃️ Pokédex de Personajes") pais_dropdown = gr.Dropdown( choices=list(PERSONAJES_POR_PAIS.keys()), value="🇵🇾 Paraguay", label="País", container=False ) # Inicializar la galería vacía galeria_personajes = gr.Gallery( value=None, label="Selecciona un país", show_label=True, elem_id="galeria", columns=1, rows=4, height=350, type="filepath" # Especificar el tipo explícitamente ) # Mensaje de estado status_msg = gr.Textbox( value="Selecciona un país para ver sus personajes", show_label=False, interactive=False, container=False ) # Panel derecho - Chat with gr.Column(scale=2): chatbot = gr.Chatbot( type="messages", show_label=False, height=400, avatar_images=(None, "🏛️"), value=[] ) with gr.Row(): input_box = gr.Textbox( placeholder="Escribe tu historia o selecciona un personaje...", show_label=False, scale=4, container=False ) send_button = gr.Button("📤", scale=1, variant="primary") with gr.Row(): clear_button = gr.Button("🗑️ Limpiar", scale=1, size="sm") with gr.Column(scale=3): with gr.Row(): max_tokens = gr.Slider(100, MAX_MAX_NEW_TOKENS, DEFAULT_MAX_NEW_TOKENS, label="Tokens", container=False) temperature = gr.Slider(0.1, 2.0, 0.7, label="Temp", container=False) # Eventos # Actualizar personajes cuando cambia el país pais_dropdown.change( fn=actualizar_personajes, inputs=[pais_dropdown], outputs=[galeria_personajes, status_msg] ) # Cargar personajes iniciales demo.load( fn=actualizar_personajes, inputs=[pais_dropdown], outputs=[galeria_personajes, status_msg] ) # Crear prompt desde galería galeria_personajes.select( fn=crear_prompt_desde_personaje, outputs=[input_box] ) # Envío de mensajes input_box.submit( fn=chat_function, inputs=[input_box, chatbot, max_tokens, temperature], outputs=[chatbot, input_box] ) send_button.click( fn=chat_function, inputs=[input_box, chatbot, max_tokens, temperature], outputs=[chatbot, input_box] ) clear_button.click( fn=lambda: ([], ""), outputs=[chatbot, input_box], queue=False ) # Lanzar aplicación if __name__ == "__main__": if model_loaded: print("Lanzando aplicación...") demo.launch(share=False, show_error=True) else: print("Error: No se pudo cargar el modelo. Revisa la configuración.")