|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import gradio as gr |
|
|
import torch |
|
|
from transformers import AutoModelForVision2Seq |
|
|
from transformers import LlavaOneVisionProcessor |
|
|
from PIL import Image |
|
|
import re |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_ID = "lmms-lab/llava-onevision-1.5-8b-instruct" |
|
|
|
|
|
print("⏳ Cargando modelo local con trust_remote_code=True...") |
|
|
|
|
|
processor = LlavaOneVisionProcessor.from_pretrained(MODEL_ID, trust_remote_code=True) |
|
|
model = AutoModelForVision2Seq.from_pretrained( |
|
|
MODEL_ID, |
|
|
trust_remote_code=True, |
|
|
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, |
|
|
device_map="auto" |
|
|
) |
|
|
|
|
|
print("✅ Modelo LLaVA-OneVision cargado correctamente.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_macros(text): |
|
|
"""Extrae proteínas, carbohidratos y grasas del texto generado.""" |
|
|
def find_value(keyword): |
|
|
m = re.search(rf"{keyword}[^0-9]*([0-9]+)", text.lower()) |
|
|
return int(m.group(1)) if m else 0 |
|
|
p, c, f = find_value("prote"), find_value("carb"), find_value("gras") |
|
|
kcal = p * 4 + c * 4 + f * 9 if any([p, c, f]) else 0 |
|
|
return {"protein": p, "carbs": c, "fat": f, "kcal": kcal} |
|
|
|
|
|
|
|
|
def build_macro_card(macros): |
|
|
"""Genera el HTML visual con barras de progreso tipo dashboard.""" |
|
|
if not any(macros.values()): |
|
|
return "<div class='card'>⚖️ No se pudieron estimar los macros.</div>" |
|
|
|
|
|
def bar_html(value, color): |
|
|
width = min(value, 100) |
|
|
return f""" |
|
|
<div class='bar-bg'> |
|
|
<div class='bar-fill' style='width:{width}%; background:{color};'></div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return f""" |
|
|
<div class='card'> |
|
|
<h2>🍽️ Estimación Nutricional</h2> |
|
|
<div class='macro'><span>💪 Proteínas</span><span>{macros['protein']} g</span></div> |
|
|
{bar_html(macros['protein'], '#b25eff')} |
|
|
<div class='macro'><span>🥔 Carbohidratos</span><span>{macros['carbs']} g</span></div> |
|
|
{bar_html(macros['carbs'], '#00f0ff')} |
|
|
<div class='macro'><span>🥑 Grasas</span><span>{macros['fat']} g</span></div> |
|
|
{bar_html(macros['fat'], '#ff5efb')} |
|
|
<div class='macro kcal'><span>🔥 Calorías Totales</span><span>{macros['kcal']} kcal</span></div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
def analyze_food(image, text_prompt="Describe esta comida y estima sus calorías, proteínas, carbohidratos y grasas."): |
|
|
"""Procesa la imagen localmente con el modelo y devuelve descripción + macros.""" |
|
|
try: |
|
|
inputs = processor(text=text_prompt, images=image, return_tensors="pt").to(model.device) |
|
|
out = model.generate(**inputs, max_new_tokens=400) |
|
|
answer = processor.decode(out[0], skip_special_tokens=True) |
|
|
|
|
|
macros = extract_macros(answer) |
|
|
card = build_macro_card(macros) |
|
|
return f"<div class='desc'>{answer}</div>{card}" |
|
|
|
|
|
except Exception as e: |
|
|
return f"<div class='card error'>⚠️ Error: {e}</div>" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_interface(): |
|
|
with gr.Blocks(css=""" |
|
|
/* --- DARK NEON THEME --- */ |
|
|
body { |
|
|
background: radial-gradient(circle at 20% 20%, #0d001f, #000); |
|
|
color: #fff; |
|
|
font-family: 'Inter', sans-serif; |
|
|
} |
|
|
.gradio-container {background: transparent !important;} |
|
|
.card { |
|
|
backdrop-filter: blur(12px); |
|
|
background: rgba(30, 0, 60, 0.3); |
|
|
border: 1px solid rgba(200, 100, 255, 0.2); |
|
|
border-radius: 16px; |
|
|
padding: 1.2em; |
|
|
margin-top: 1em; |
|
|
box-shadow: 0 0 25px rgba(180, 0, 255, 0.15); |
|
|
} |
|
|
h1,h2 {color:#c18fff;} |
|
|
.bar-bg { |
|
|
width:100%; height:8px; border-radius:6px; |
|
|
background:rgba(255,255,255,0.1); margin:4px 0 12px 0; |
|
|
overflow:hidden; |
|
|
} |
|
|
.bar-fill {height:100%; border-radius:6px; transition:width 1s ease;} |
|
|
.macro {display:flex; justify-content:space-between; font-size:0.95em;} |
|
|
.kcal {font-weight:600; color:#ffb3ff;} |
|
|
.desc { |
|
|
background:rgba(255,255,255,0.05); |
|
|
padding:1em; border-radius:10px; line-height:1.5em; |
|
|
box-shadow:inset 0 0 20px rgba(180,0,255,0.1); |
|
|
} |
|
|
button { |
|
|
background:linear-gradient(90deg,#b25eff,#00f0ff); |
|
|
color:#fff; border:none; border-radius:12px; |
|
|
font-weight:600; transition:opacity .2s; |
|
|
} |
|
|
button:hover {opacity:0.8;} |
|
|
""") as demo: |
|
|
|
|
|
gr.Markdown(""" |
|
|
<h1>💜 NasFit Vision AI</h1> |
|
|
<p>Analiza tus comidas con IA y obtené tu ficha nutricional instantánea.</p> |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
img = gr.Image(label="📸 Imagen del plato", type="pil") |
|
|
txt = gr.Textbox(label="💬 Instrucción (opcional)", placeholder="Ej: ¿Cuántas calorías tiene este plato?") |
|
|
btn = gr.Button("🔍 Analizar", variant="primary") |
|
|
with gr.Column(scale=1): |
|
|
out = gr.HTML(label="🧠 Resultado") |
|
|
|
|
|
btn.click(analyze_food, [img, txt], out) |
|
|
|
|
|
return demo |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo = build_interface() |
|
|
demo.launch() |