from deep_translator import GoogleTranslator import gradio as gr import pandas as pd import duckdb import time import re import threading from concurrent.futures import ThreadPoolExecutor # ── Configuración ───────────────────────────────────────────────────────────── PARQUET_PATH = 'vesta_master_database.parquet' con_parquet = duckdb.connect() translator_es_en = GoogleTranslator(source='es', target='en') translator_en_es = GoogleTranslator(source='en', target='es') # ── Mapa completo de los 266 nutrientes ────────────────────────────────────── NUTRIENT_MAP = { # 01. ENERGÍA 'Energy': '01. ENERGÍA', 'Energy (Atwater General Factors)': '01. ENERGÍA', 'Energy (Atwater Specific Factors)': '01. ENERGÍA', # 02. AGUA Y CENIZA 'Water': '02. AGUA Y CENIZA', 'Ash': '02. AGUA Y CENIZA', 'Nitrogen': '02. AGUA Y CENIZA', 'Specific Gravity': '02. AGUA Y CENIZA', # 03. PROTEÍNAS 'Protein': '03. PROTEÍNAS', 'Alanine': '03. PROTEÍNAS', 'Arginine': '03. PROTEÍNAS', 'Aspartic acid': '03. PROTEÍNAS', 'Cystine': '03. PROTEÍNAS', 'Cysteine': '03. PROTEÍNAS', 'Glutamic acid': '03. PROTEÍNAS', 'Glutamine': '03. PROTEÍNAS', 'Glutathione': '03. PROTEÍNAS', 'Glycine': '03. PROTEÍNAS', 'Histidine': '03. PROTEÍNAS', 'Hydroxyproline': '03. PROTEÍNAS', 'Isoleucine': '03. PROTEÍNAS', 'Leucine': '03. PROTEÍNAS', 'Lysine': '03. PROTEÍNAS', 'Methionine': '03. PROTEÍNAS', 'Phenylalanine': '03. PROTEÍNAS', 'Proline': '03. PROTEÍNAS', 'Serine': '03. PROTEÍNAS', 'Taurine': '03. PROTEÍNAS', 'Threonine': '03. PROTEÍNAS', 'Tryptophan': '03. PROTEÍNAS', 'Tyrosine': '03. PROTEÍNAS', 'Valine': '03. PROTEÍNAS', # 04. CARBOHIDRATOS 'Carbohydrate, by difference': '04. CARBOHIDRATOS', 'Carbohydrate, by summation': '04. CARBOHIDRATOS', 'Carbohydrate, other': '04. CARBOHIDRATOS', 'Fiber, total dietary': '04. CARBOHIDRATOS', 'Fiber, insoluble': '04. CARBOHIDRATOS', 'Fiber, soluble': '04. CARBOHIDRATOS', 'High Molecular Weight Dietary Fiber (HMWDF)': '04. CARBOHIDRATOS', 'Low Molecular Weight Dietary Fiber (LMWDF)': '04. CARBOHIDRATOS', 'Total Sugars': '04. CARBOHIDRATOS', 'Sugars, added': '04. CARBOHIDRATOS', 'Sugars, total including NLEA': '04. CARBOHIDRATOS', 'Starch': '04. CARBOHIDRATOS', 'Sucrose': '04. CARBOHIDRATOS', 'Glucose': '04. CARBOHIDRATOS', 'Fructose': '04. CARBOHIDRATOS', 'Lactose': '04. CARBOHIDRATOS', 'Maltose': '04. CARBOHIDRATOS', 'Galactose': '04. CARBOHIDRATOS', 'Ribose': '04. CARBOHIDRATOS', 'Sorbitol': '04. CARBOHIDRATOS', 'Xylitol': '04. CARBOHIDRATOS', 'Inositol': '04. CARBOHIDRATOS', 'Stachyose': '04. CARBOHIDRATOS', 'Verbascose': '04. CARBOHIDRATOS', 'Beta-glucan': '04. CARBOHIDRATOS', 'Beta-glucans': '04. CARBOHIDRATOS', 'Lignin': '04. CARBOHIDRATOS', # 05. LÍPIDOS 'Total lipid (fat)': '05. LÍPIDOS', 'Cholesterol': '05. LÍPIDOS', 'Fatty acids, total saturated': '05. LÍPIDOS', 'Fatty acids, total monounsaturated': '05. LÍPIDOS', 'Fatty acids, total polyunsaturated': '05. LÍPIDOS', 'Fatty acids, total trans': '05. LÍPIDOS', 'Fatty acids, total trans-monoenoic': '05. LÍPIDOS', 'Fatty acids, total trans-dienoic': '05. LÍPIDOS', 'Fatty acids, total trans-polyenoic': '05. LÍPIDOS', # 05a. SFA 'SFA 4:0':'05a. SFA (Saturados)','SFA 5:0':'05a. SFA (Saturados)', 'SFA 6:0':'05a. SFA (Saturados)','SFA 7:0':'05a. SFA (Saturados)', 'SFA 8:0':'05a. SFA (Saturados)','SFA 9:0':'05a. SFA (Saturados)', 'SFA 10:0':'05a. SFA (Saturados)','SFA 11:0':'05a. SFA (Saturados)', 'SFA 12:0':'05a. SFA (Saturados)','SFA 13:0':'05a. SFA (Saturados)', 'SFA 14:0':'05a. SFA (Saturados)','SFA 15:0':'05a. SFA (Saturados)', 'SFA 16:0':'05a. SFA (Saturados)','SFA 17:0':'05a. SFA (Saturados)', 'SFA 18:0':'05a. SFA (Saturados)','SFA 20:0':'05a. SFA (Saturados)', 'SFA 21:0':'05a. SFA (Saturados)','SFA 22:0':'05a. SFA (Saturados)', 'SFA 23:0':'05a. SFA (Saturados)','SFA 24:0':'05a. SFA (Saturados)', # 05b. MUFA 'MUFA 12:1':'05b. MUFA (Monoinsaturados)','MUFA 14:1':'05b. MUFA (Monoinsaturados)', 'MUFA 14:1 c':'05b. MUFA (Monoinsaturados)','MUFA 15:1':'05b. MUFA (Monoinsaturados)', 'MUFA 16:1':'05b. MUFA (Monoinsaturados)','MUFA 16:1 c':'05b. MUFA (Monoinsaturados)', 'MUFA 17:1':'05b. MUFA (Monoinsaturados)','MUFA 17:1 c':'05b. MUFA (Monoinsaturados)', 'MUFA 18:1':'05b. MUFA (Monoinsaturados)','MUFA 18:1 c':'05b. MUFA (Monoinsaturados)', 'MUFA 18:1-11 t (18:1t n-7)':'05b. MUFA (Monoinsaturados)', 'MUFA 20:1':'05b. MUFA (Monoinsaturados)','MUFA 20:1 c':'05b. MUFA (Monoinsaturados)', 'MUFA 22:1':'05b. MUFA (Monoinsaturados)','MUFA 22:1 c':'05b. MUFA (Monoinsaturados)', 'MUFA 22:1 n-11':'05b. MUFA (Monoinsaturados)','MUFA 22:1 n-9':'05b. MUFA (Monoinsaturados)', 'MUFA 24:1 c':'05b. MUFA (Monoinsaturados)', # 05c. PUFA 'PUFA 18:2':'05c. PUFA (Poliinsaturados)','PUFA 18:2 CLAs':'05c. PUFA (Poliinsaturados)', 'PUFA 18:2 c':'05c. PUFA (Poliinsaturados)','PUFA 18:2 i':'05c. PUFA (Poliinsaturados)', 'PUFA 18:2 n-6 c,c':'05c. PUFA (Poliinsaturados)','PUFA 18:3':'05c. PUFA (Poliinsaturados)', 'PUFA 18:3 c':'05c. PUFA (Poliinsaturados)','PUFA 18:3 n-3 c,c,c (ALA)':'05c. PUFA (Poliinsaturados)', 'PUFA 18:3 n-6 c,c,c':'05c. PUFA (Poliinsaturados)','PUFA 18:3i':'05c. PUFA (Poliinsaturados)', 'PUFA 18:4':'05c. PUFA (Poliinsaturados)','PUFA 20:2 c':'05c. PUFA (Poliinsaturados)', 'PUFA 20:2 n-6 c,c':'05c. PUFA (Poliinsaturados)','PUFA 20:3':'05c. PUFA (Poliinsaturados)', 'PUFA 20:3 c':'05c. PUFA (Poliinsaturados)','PUFA 20:3 n-3':'05c. PUFA (Poliinsaturados)', 'PUFA 20:3 n-6':'05c. PUFA (Poliinsaturados)','PUFA 20:3 n-9':'05c. PUFA (Poliinsaturados)', 'PUFA 20:4':'05c. PUFA (Poliinsaturados)','PUFA 20:4 n-6':'05c. PUFA (Poliinsaturados)', 'PUFA 20:4c':'05c. PUFA (Poliinsaturados)','PUFA 20:5 n-3 (EPA)':'05c. PUFA (Poliinsaturados)', 'PUFA 20:5c':'05c. PUFA (Poliinsaturados)','PUFA 21:5':'05c. PUFA (Poliinsaturados)', 'PUFA 22:2':'05c. PUFA (Poliinsaturados)','PUFA 22:3':'05c. PUFA (Poliinsaturados)', 'PUFA 22:4':'05c. PUFA (Poliinsaturados)','PUFA 22:5 c':'05c. PUFA (Poliinsaturados)', 'PUFA 22:5 n-3 (DPA)':'05c. PUFA (Poliinsaturados)','PUFA 22:6 c':'05c. PUFA (Poliinsaturados)', 'PUFA 22:6 n-3 (DHA)':'05c. PUFA (Poliinsaturados)', # 05d. TFA 'TFA 14:1 t':'05d. TFA (Trans)','TFA 16:1 t':'05d. TFA (Trans)', 'TFA 18:1 t':'05d. TFA (Trans)','TFA 18:2 t':'05d. TFA (Trans)', 'TFA 18:2 t not further defined':'05d. TFA (Trans)','TFA 18:2 t,t':'05d. TFA (Trans)', 'TFA 18:3 t':'05d. TFA (Trans)','TFA 20:1 t':'05d. TFA (Trans)', 'TFA 22:1 t':'05d. TFA (Trans)', # 05e. ESTEROLES 'Phytosterols':'05e. ESTEROLES','Phytosterols, other':'05e. ESTEROLES', 'Beta-sitosterol':'05e. ESTEROLES','Beta-sitostanol':'05e. ESTEROLES', 'Campesterol':'05e. ESTEROLES','Campestanol':'05e. ESTEROLES', 'Brassicasterol':'05e. ESTEROLES','Stigmastadiene':'05e. ESTEROLES', 'Delta-5-avenasterol':'05e. ESTEROLES','Delta-7-Stigmastenol':'05e. ESTEROLES', 'Ergosterol':'05e. ESTEROLES','Ergosta-5,7-dienol':'05e. ESTEROLES', 'Ergosta-7,22-dienol':'05e. ESTEROLES','Ergosta-7-enol':'05e. ESTEROLES', # 06. MINERALES 'Calcium, Ca':'06. MINERALES','Iron, Fe':'06. MINERALES', 'Magnesium, Mg':'06. MINERALES','Phosphorus, P':'06. MINERALES', 'Potassium, K':'06. MINERALES','Sodium, Na':'06. MINERALES', 'Zinc, Zn':'06. MINERALES','Copper, Cu':'06. MINERALES', 'Manganese, Mn':'06. MINERALES','Selenium, Se':'06. MINERALES', 'Fluoride, F':'06. MINERALES','Chromium, Cr':'06. MINERALES', 'Iodine, I':'06. MINERALES','Molybdenum, Mo':'06. MINERALES', 'Boron, B':'06. MINERALES','Cobalt, Co':'06. MINERALES', 'Nickel, Ni':'06. MINERALES','Chlorine, Cl':'06. MINERALES', 'Salt, NaCl':'06. MINERALES', # 07. VITAMINAS LIPOSOLUBLES 'Vitamin A, IU':'07. VITAMINAS LIPOSOLUBLES','Vitamin A, RAE':'07. VITAMINAS LIPOSOLUBLES', 'Retinol':'07. VITAMINAS LIPOSOLUBLES','Carotene, alpha':'07. VITAMINAS LIPOSOLUBLES', 'Carotene, beta':'07. VITAMINAS LIPOSOLUBLES','Carotene, gamma':'07. VITAMINAS LIPOSOLUBLES', 'Cryptoxanthin, beta':'07. VITAMINAS LIPOSOLUBLES','Lycopene':'07. VITAMINAS LIPOSOLUBLES', 'Lutein + zeaxanthin':'07. VITAMINAS LIPOSOLUBLES','Lutein':'07. VITAMINAS LIPOSOLUBLES', 'Zeaxanthin':'07. VITAMINAS LIPOSOLUBLES','Phytoene':'07. VITAMINAS LIPOSOLUBLES', 'Phytofluene':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin D (D2 + D3)':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin D (D2 + D3), International Units':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin D2 (ergocalciferol)':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin D3 (cholecalciferol)':'07. VITAMINAS LIPOSOLUBLES', '25-hydroxycholecalciferol':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin E (alpha-tocopherol)':'07. VITAMINAS LIPOSOLUBLES', 'Tocopherol, beta':'07. VITAMINAS LIPOSOLUBLES','Tocopherol, gamma':'07. VITAMINAS LIPOSOLUBLES', 'Tocopherol, delta':'07. VITAMINAS LIPOSOLUBLES','Tocotrienol, alpha':'07. VITAMINAS LIPOSOLUBLES', 'Tocotrienol, beta':'07. VITAMINAS LIPOSOLUBLES','Tocotrienol, gamma':'07. VITAMINAS LIPOSOLUBLES', 'Tocotrienol, delta':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin K (phylloquinone)':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin K (Menaquinone-4)':'07. VITAMINAS LIPOSOLUBLES', 'Vitamin K (Dihydrophylloquinone)':'07. VITAMINAS LIPOSOLUBLES', # 08. VITAMINAS HIDROSOLUBLES 'Vitamin C, total ascorbic acid':'08. VITAMINAS HIDROSOLUBLES', 'Thiamin':'08. VITAMINAS HIDROSOLUBLES','Riboflavin':'08. VITAMINAS HIDROSOLUBLES', 'Niacin':'08. VITAMINAS HIDROSOLUBLES','Pantothenic acid':'08. VITAMINAS HIDROSOLUBLES', 'Vitamin B-6':'08. VITAMINAS HIDROSOLUBLES','Vitamin B-12':'08. VITAMINAS HIDROSOLUBLES', 'Vitamin B-12, added':'08. VITAMINAS HIDROSOLUBLES', 'Folate, total':'08. VITAMINAS HIDROSOLUBLES','Folate, food':'08. VITAMINAS HIDROSOLUBLES', 'Folate, DFE':'08. VITAMINAS HIDROSOLUBLES','Folic acid':'08. VITAMINAS HIDROSOLUBLES', '5-methyl tetrahydrofolate (5-MTHF)':'08. VITAMINAS HIDROSOLUBLES', '5-Formyltetrahydrofolic acid (5-HCOH4':'08. VITAMINAS HIDROSOLUBLES', '10-Formyl folic acid (10HCOFA)':'08. VITAMINAS HIDROSOLUBLES', 'Biotin':'08. VITAMINAS HIDROSOLUBLES','Choline, total':'08. VITAMINAS HIDROSOLUBLES', 'Choline, free':'08. VITAMINAS HIDROSOLUBLES', 'Choline, from phosphocholine':'08. VITAMINAS HIDROSOLUBLES', 'Choline, from phosphotidyl choline':'08. VITAMINAS HIDROSOLUBLES', 'Choline, from glycerophosphocholine':'08. VITAMINAS HIDROSOLUBLES', 'Choline, from sphingomyelin':'08. VITAMINAS HIDROSOLUBLES', 'Betaine':'08. VITAMINAS HIDROSOLUBLES','Niacin equivalent':'08. VITAMINAS HIDROSOLUBLES', # 09. FITONUTRIENTES Y BIOACTIVOS 'Caffeine':'09. FITONUTRIENTES Y BIOACTIVOS','Theobromine':'09. FITONUTRIENTES Y BIOACTIVOS', 'Alcohol, ethyl':'09. FITONUTRIENTES Y BIOACTIVOS','Oxalic acid':'09. FITONUTRIENTES Y BIOACTIVOS', 'Citric acid':'09. FITONUTRIENTES Y BIOACTIVOS','Malic acid':'09. FITONUTRIENTES Y BIOACTIVOS', 'Lactic acid':'09. FITONUTRIENTES Y BIOACTIVOS','Acetic acid':'09. FITONUTRIENTES Y BIOACTIVOS', 'Pyruvic acid':'09. FITONUTRIENTES Y BIOACTIVOS','Quinic acid':'09. FITONUTRIENTES Y BIOACTIVOS', 'Daidzein':'09. FITONUTRIENTES Y BIOACTIVOS','Daidzin':'09. FITONUTRIENTES Y BIOACTIVOS', 'Genistein':'09. FITONUTRIENTES Y BIOACTIVOS','Genistin':'09. FITONUTRIENTES Y BIOACTIVOS', 'Glycitin':'09. FITONUTRIENTES Y BIOACTIVOS', 'Epigallocatechin-3-gallate':'09. FITONUTRIENTES Y BIOACTIVOS', 'Ergothioneine':'09. FITONUTRIENTES Y BIOACTIVOS', } # ── Catálogo maestro (cargado una vez al inicio) ───────────────────────────── _MASTER_CATALOG = None _catalog_lock = threading.Lock() def get_master_catalog() -> pd.DataFrame: global _MASTER_CATALOG with _catalog_lock: if _MASTER_CATALOG is not None: return _MASTER_CATALOG df = con_parquet.execute(f""" SELECT DISTINCT Nutriente, MAX(Unidad) AS Unidad FROM read_parquet('{PARQUET_PATH}') WHERE Nutriente IS NOT NULL AND TRIM(Nutriente) != '' GROUP BY Nutriente ORDER BY Nutriente ASC """).df() df['Categoria'] = df['Nutriente'].map(NUTRIENT_MAP).fillna('10. OTROS') _MASTER_CATALOG = df return _MASTER_CATALOG def get_nutrients(food_name: str) -> pd.DataFrame: """LEFT JOIN catálogo maestro ← datos del alimento. Siempre devuelve todas las filas.""" df_food = con_parquet.execute(f""" SELECT Nutriente, ANY_VALUE(Cantidad) AS Cantidad FROM read_parquet('{PARQUET_PATH}') WHERE Alimento = ? GROUP BY Nutriente """, [food_name]).df() catalog = get_master_catalog().copy() df_full = catalog.merge(df_food[['Nutriente', 'Cantidad']], on='Nutriente', how='left') df_full = df_full.sort_values(['Categoria', 'Nutriente']).reset_index(drop=True) return df_full # ── Mapa interno label → nombre EN (compartido entre funciones) ─────────────── _label_to_en: dict[str, str] = {} # ── Función de búsqueda ─────────────────────────────────────────────────────── def buscar_alimentos(termino: str): global _label_to_en if not termino or not termino.strip(): return ( gr.update(choices=[], value=None), "⚠️ Escribe un ingrediente para buscar.", ) t0 = time.time() try: term_en = translator_es_en.translate(termino.strip()).lower() words = term_en.split() where = " AND ".join(["Alimento ILIKE ?" for _ in words]) query_db = f""" SELECT Alimento, COUNT(DISTINCT Nutriente) AS nutrientes_unicos FROM read_parquet('{PARQUET_PATH}') WHERE {where} GROUP BY Alimento ORDER BY nutrientes_unicos DESC LIMIT 100 """ df_results = con_parquet.execute(query_db, [f"%{w}%" for w in words]).df() if df_results.empty: return ( gr.update(choices=[], value=None), f"❌ Sin resultados para «{termino}» (buscado como: {term_en})", ) names_en = df_results['Alimento'].tolist() counts = df_results['nutrientes_unicos'].tolist() with ThreadPoolExecutor(max_workers=1) as exc: names_es = exc.submit(lambda: translator_en_es.translate_batch(names_en)).result() labels = [f"[{cnt}] {es} ({en})" for es, en, cnt in zip(names_es, names_en, counts)] _label_to_en = {lbl: en for lbl, en in zip(labels, names_en)} elapsed = time.time() - t0 status = f"✅ {len(labels)} resultado(s) para «{termino}» — {elapsed:.2f}s" return gr.update(choices=labels, value=labels[0]), status except Exception as e: return gr.update(choices=[], value=None), f"❌ Error: {e}" # ── Función ver nutrientes ──────────────────────────────────────────────────── def ver_nutrientes(label_seleccionado: str | None): if not label_seleccionado: return "Selecciona un alimento primero.", pd.DataFrame() food_en = _label_to_en.get(label_seleccionado) # Fallback: extraer el nombre EN del paréntesis final del label if not food_en: m = re.search(r'\(([^)]+)\)$', label_seleccionado) food_en = m.group(1) if m else label_seleccionado try: df = get_nutrients(food_en) total = len(df) tiene = int(df['Cantidad'].notna().sum()) resumen = ( f"**{food_en}**\n\n" f"Nutrientes con datos: **{tiene}** / {total}  |  " f"Sin datos: {total - tiene}" ) tabla = df[['Categoria', 'Nutriente', 'Cantidad', 'Unidad']].copy() tabla['Cantidad'] = tabla['Cantidad'].apply( lambda x: f"{x:.4g}" if pd.notna(x) else "" ) return resumen, tabla except Exception as e: return f"❌ Error al cargar nutrientes: {e}", pd.DataFrame() # ── Interfaz Gradio ─────────────────────────────────────────────────────────── CSS = """ #buscar-btn { background: #378ADD !important; color: white !important; } #buscar-btn:hover { background: #185FA5 !important; } #ver-btn { background: #1D9E75 !important; color: white !important; } #ver-btn:hover { background: #0F6E56 !important; } #tabla-nutrientes { font-size: 13px; } """ with gr.Blocks(title="Buscador de Nutrientes Vesta", css=CSS) as demo: gr.Markdown("## 🥗 Buscador de Nutrientes Vesta") gr.Markdown( "Busca alimentos en español o inglés y consulta su perfil completo de nutrientes." ) with gr.Row(): txt_buscar = gr.Textbox( label="Ingrediente", placeholder="ej: fresa, pollo, aguacate, chocolate...", value="fresa", scale=4, ) btn_buscar = gr.Button("🔍 Buscar", variant="primary", scale=1, elem_id="buscar-btn") txt_status = gr.Markdown(value="") with gr.Row(): dd_alimento = gr.Dropdown( label="Alimento encontrado — ordenados por riqueza nutricional ↓", choices=[], interactive=True, scale=5, ) btn_ver = gr.Button("👁 Ver nutrientes", variant="secondary", scale=1, elem_id="ver-btn") md_resumen = gr.Markdown(value="") tbl_nutrientes = gr.Dataframe( label="Perfil nutricional completo", headers=["Categoría", "Nutriente", "Cantidad", "Unidad"], datatype=["str", "str", "str", "str"], interactive=False, wrap=False, elem_id="tabla-nutrientes", ) # ── Eventos ─────────────────────────────────────────────────────────────── btn_buscar.click( fn=buscar_alimentos, inputs=txt_buscar, outputs=[dd_alimento, txt_status], ) txt_buscar.submit( fn=buscar_alimentos, inputs=txt_buscar, outputs=[dd_alimento, txt_status], ) btn_ver.click( fn=ver_nutrientes, inputs=dd_alimento, outputs=[md_resumen, tbl_nutrientes], ) # Al cambiar selección en dropdown limpiar tabla anterior dd_alimento.change( fn=lambda _: ("", pd.DataFrame()), inputs=dd_alimento, outputs=[md_resumen, tbl_nutrientes], ) # ── Pre-cargar catálogo en background ──────────────────────────────────────── threading.Thread(target=get_master_catalog, daemon=True).start() if __name__ == "__main__": demo.launch()