Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| from PIL import Image | |
| import random | |
| import time | |
| import torch | |
| from transformers import LlavaNextForConditionalGeneration, AutoProcessor | |
| from peft import PeftModel | |
| import spaces | |
| import boto3 | |
| import uuid | |
| import json | |
| import datetime | |
| import os | |
| MODEL_ID = 'llava-hf/llava-v1.6-mistral-7b-hf' | |
| ADAPTER_ID = 'somosnlp-hackathon-2025/llava-v1.6-mistral-7b-memes-chilenos-small' | |
| BANNER_URL = "tralalelo-tralala-logo.png" | |
| processor = AutoProcessor.from_pretrained(MODEL_ID) | |
| processor.tokenizer.padding_side = "right" | |
| model_nuevo = LlavaNextForConditionalGeneration.from_pretrained( | |
| MODEL_ID, | |
| torch_dtype=torch.float16, | |
| device_map="auto" | |
| ) | |
| model_nuevo = PeftModel.from_pretrained(model_nuevo, ADAPTER_ID) | |
| model_nuevo = model_nuevo.eval() | |
| def explicar_meme_stream(img: Image.Image): | |
| if img is None: | |
| yield "❌ No hay imagen para analizar." | |
| return | |
| img = img.convert("RGB") | |
| img = img.resize((336, 336)) | |
| conversation = [ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "image"}, | |
| {"type": "text", "text": "Eres experto en memes chilenos. Observa la imagen y, si hay texto, interprétalo sin repetirlo. Analiza su sentido usando contexto cultural chileno. Responde según la instrucción."}, | |
| {"type": "text", "text": "Explica qué significa este meme en Chile, usando lenguaje coloquial chileno."}, | |
| ], | |
| } | |
| ] | |
| text_prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) | |
| inputs = processor(text=text_prompt, images=[img], return_tensors="pt").to(model_nuevo.device) | |
| with torch.no_grad(): | |
| generated_ids = model_nuevo.generate( | |
| input_ids=inputs["input_ids"], | |
| attention_mask=inputs["attention_mask"], | |
| pixel_values=inputs["pixel_values"], | |
| image_sizes=inputs["image_sizes"], | |
| max_new_tokens=256 | |
| ) | |
| respuesta = processor.batch_decode(generated_ids[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0] | |
| # Simula stream de escritura | |
| for i in range(1, len(respuesta) + 1): | |
| yield respuesta[:i] | |
| time.sleep(0.02) | |
| def cargar_imagen_ejemplo(nombre: str) -> Image.Image: | |
| if nombre == "🐶 Perro confundido existencialmente": | |
| return Image.open("perro.png") | |
| elif nombre == "🎉 Perro 18 con espíritu patriótico": | |
| return Image.open("perro18.png") | |
| elif nombre == "🛝 Resbalín": | |
| return Image.open("resbalin.jpg") | |
| elif nombre == "🍷 Santa Helena": | |
| return Image.open("santa.jpg") | |
| def subir_contribucion(img, explicacion_modelo, explicacion_usuario, pais, email): | |
| if img is None or not explicacion_modelo.strip() or not explicacion_usuario.strip() or not pais: | |
| return "❌ Debes subir una imagen, dejar tu explicación, seleccionar un país y tener una explicación generada por el modelo." | |
| now = datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S") | |
| unique_id = str(uuid.uuid4())[:8] | |
| filename_base = f"{now}-{unique_id}" | |
| # Guardamos imagen temporal | |
| image_path = f"/tmp/{filename_base}.png" | |
| img.save(image_path) | |
| # Guardamos texto temporal | |
| metadata = { | |
| "explicacion_modelo": explicacion_modelo, | |
| "explicacion_usuario": explicacion_usuario, | |
| "pais": pais, | |
| "email": email if email else None | |
| } | |
| text_path = f"/tmp/{filename_base}.json" | |
| with open(text_path, "w", encoding="utf-8") as f: | |
| json.dump(metadata, f, ensure_ascii=False, indent=2) | |
| # Subimos a S3 | |
| s3 = boto3.client( | |
| "s3", | |
| aws_access_key_id=os.environ["ACCESS_KEY"], | |
| aws_secret_access_key=os.environ["SECRET_KEY"] | |
| ) | |
| bucket = "tralalero-tralala-meme-aligned" | |
| s3.upload_file(image_path, bucket, f"contributions/images/{filename_base}.png") | |
| s3.upload_file(text_path, bucket, f"contributions/text/{filename_base}.json") | |
| # Eliminamos después de subir a S3 | |
| os.remove(image_path) | |
| os.remove(text_path) | |
| return "✅ ¡Gracias por tu aporte! Tu explicación fue enviada correctamente." | |
| def subir_contribucion_y_reset(img, explicacion_modelo, explicacion_usuario, pais, email): | |
| msg = subir_contribucion(img, explicacion_modelo, explicacion_usuario, pais, email) | |
| if msg.startswith("✅"): | |
| return msg, "", "", "", "", "" | |
| else: | |
| return msg, explicacion_usuario, pais, email, explicacion_modelo | |
| def reset_form(): | |
| return "", "", "", "", "" | |
| with gr.Blocks(theme=gr.themes.Base(), title="LLaVA Chile Memes") as demo: | |
| gr.Image(value=BANNER_URL, show_label=False, interactive=False, elem_id="banner", height=200, width=300) | |
| gr.Markdown( | |
| """ | |
| ## 🇨🇱 Tralalero Tralala Meme Align: Entendiendo memes con sabor chileno. | |
| **¿Podrías explicarle un meme chileno a una IA?** | |
| Ese fue nuestro desafío en la *Hackathon Somos NLP 2025*, y lo enfrentamos construyendo el primer modelo multimodal fine-tuneado para comprender memes chilenos con contexto cultural real. | |
| --- | |
| ## 🧠 ¿Qué son los memes? | |
| Los memes no son solo imágenes graciosas: son unidades culturales que condensan ideas, lenguaje, identidad y participación social. | |
| Según **Gleick** (2011), los memes son: | |
| > *“Nada más que ideas, imágenes, eslóganes, melodías, historias, recetas, habilidades o destrezas, leyendas y sistemas que habitan nuestras mentes.”* | |
| Por su parte, **Milner** (2012) los define como: | |
| > *“Artefactos mediáticos amateur, extensamente remezclados y recirculados por diferentes participantes en redes sociales.”* | |
| --- | |
| ### 🧠 Motivación | |
| Modelos como **LLaVA** se desempeñan bien en tareas como *image captioning* o *visual question answering (VQA)*, pero tienen dificultades para entender memes: estos son ambiguos, irónicos y profundamente dependientes del contexto cultural. Hasta ahora, no existía un dataset diseñado específicamente para abordar esta complejidad en el caso chileno. | |
| --- | |
| ### 🏗️ Metodología | |
| - **Dataset:** [`memes_instagram_chilenos_es_small`](https://huggingface.co/datasets/somosnlp-hackathon-2025/memes_instagram_chilenos_es_small) | |
| 🔹 1.194 memes reales chilenos (Instagram). | |
| 🔹 4 instrucciones por meme → 4.776 ejemplos. | |
| 🔹 Generados con GPT‑4o y command-r-plus (Cohere) + refinamiento humano. | |
| - **Modelo:** [`llava-v1.6-mistral-7b-memes-chilenos-small`](https://huggingface.co/somosnlp-hackathon-2025/llava-v1.6-mistral-7b-memes-chilenos-small) | |
| 🔸 Fine-tuning con LoRA. | |
| 🔸 2 épocas – GPU L40S. | |
| 🔸 Emisiones medidas: ~190 g CO₂eq. | |
| 🔸 Eval: BERTScore ≈ 0.73. | |
| --- | |
| ### 🌍 Impacto | |
| - Primer modelo en explicar memes con **lenguaje coloquial chileno.** | |
| - Abre camino a proyectos similares en otras regiones de LATAM. | |
| --- | |
| ### 👥 Equipo | |
| - [Andrés Sebastian](https://www.linkedin.com/in/andressebad/) | |
| - [Pedro Modinger](https://www.linkedin.com/in/pedromodingerh/) | |
| --- | |
| ### 🔮 Futuros Pasos | |
| - Extender dataset a memes de TikTok y otras regiones (Perú, Argentina, etc.). | |
| - Evaluación con humanos sobre humor y contexto. | |
| - Explorar estrategias como RAG con bases culturales y adaptación dinámica. | |
| --- | |
| """ | |
| ) | |
| gr.Markdown("### 👀 ¡Ahora pruébalo! Sube o selecciona un meme abajo y mira cómo lo explica la IA.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| ejemplo_selector = gr.Radio( | |
| label="🎯 O elige un clásico:", | |
| choices=["🐶 Perro confundido existencialmente", "🎉 Perro 18 con espíritu patriótico", "🛝 Resbalín", "🍷 Santa Helena"], | |
| interactive=True | |
| ) | |
| input_img = gr.Image(type="pil", label="📷 Sube tu meme chileno 🇨🇱 o elige uno arriba", height=336, width=336) | |
| output_label = gr.Textbox(label="🧠 Explicación cultural", interactive=False) | |
| btn = gr.Button("👀 Explica el meme") | |
| btn.click(fn=explicar_meme_stream, inputs=input_img, outputs=output_label) | |
| gr.Markdown("<hr style='margin-top:40px; margin-bottom:20px;'>") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 📬 ¿Te parece que la explicación puede mejorar? Contribuye con la tuya:") | |
| user_text = gr.Textbox(label="✍️ Tu explicación del meme", lines=4, placeholder="Ej: Este meme...") | |
| country = gr.Dropdown( | |
| label="🌎 País de origen", | |
| choices=[ | |
| "Chile", "Argentina", "Perú", "Colombia", "México", "Ecuador", "Bolivia", "Uruguay", "Paraguay", "Venezuela" | |
| ], | |
| interactive=True | |
| ) | |
| email = gr.Textbox(label="📧 Email (opcional)", placeholder="tucorreo@ejemplo.com", lines=1) | |
| submit_btn = gr.Button("📤 Enviar mi contribución") | |
| success_msg = gr.Textbox(label="✅ Estado del envío", interactive=False) | |
| submit_btn.click( | |
| fn=subir_contribucion_y_reset, | |
| inputs=[input_img, output_label, user_text, country, email], | |
| outputs=[success_msg, user_text, country, email, output_label] | |
| ) | |
| input_img.change(fn=reset_form, inputs=None, outputs=[output_label, user_text, country, email, success_msg]) | |
| ejemplo_selector.change( | |
| fn=lambda nombre: (cargar_imagen_ejemplo(nombre), "", "", "", "", ""), | |
| inputs=ejemplo_selector, | |
| outputs=[input_img, output_label, user_text, country, email, success_msg] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |