""" app.py — Meta-LoRA Molecular Generator Nature-inspired light UI · Gradio 5 """ import gradio as gr import pandas as pd from inference import run_generation, load_models, mol_to_pil load_models() # ── Valid scaffold examples (verified with RDKit) ────────────────────────────── EXAMPLES = { " Caffeine-like (xanthines)": "\n".join([ "Cn1cnc2c1c(=O)n(C)c(=O)n2C", "Cn1cnc2[nH]c(=O)n(C)c2c1=O", "Cn1cnc2c1c(=O)[nH]c(=O)n2C", "O=c1[nH]c(=O)c2[nH]cnc2[nH]1", "CCn1cnc2c1c(=O)n(C)c(=O)n2C", ]), " Aspirin-like (salicylates)": "\n".join([ "CC(=O)Oc1ccccc1C(=O)O", "CC(=O)Oc1ccc(C)cc1C(=O)O", "CC(=O)Oc1cccc(C(=O)O)c1", "CC(=O)Oc1ccc(F)cc1C(=O)O", "CC(=O)Oc1ccc(Cl)cc1C(=O)O", ]), " Ibuprofen-like (arylpropionic)": "\n".join([ "CC(C)Cc1ccc(C(C)C(=O)O)cc1", "CC(C)Cc1ccc(C(C)C(=O)OC)cc1", "CC(C(=O)O)c1ccc(Cl)cc1", "CC(C(=O)O)c1cccc(F)c1", "CC(C(=O)O)c1ccc(CC)cc1", ]), } # ── Nature-inspired CSS ──────────────────────────────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@300;400;500&display=swap'); :root { --sage: #7C9E7E; --sage-light: #B8D4B9; --sage-pale: #EBF3EB; --mint: #C8E6C9; --forest: #3D6B40; --earth: #8B6F47; --clay: #C4956A; --cream: #FAFAF7; --warm-white: #F5F2ED; --text-dark: #2C3E2D; --text-mid: #5A7A5C; --text-soft: #8BA88C; --shadow-sm: 0 2px 12px rgba(61,107,64,0.10); --shadow-md: 0 6px 28px rgba(61,107,64,0.14); --radius: 16px; } /* ── Global ── */ body, .gradio-container { background: var(--cream) !important; font-family: 'DM Sans', sans-serif !important; color: var(--text-dark) !important; } /* Subtle leaf-pattern background */ .gradio-container::before { content: ''; position: fixed; inset: 0; background-image: radial-gradient(circle at 15% 20%, rgba(124,158,126,0.07) 0%, transparent 50%), radial-gradient(circle at 85% 75%, rgba(200,230,201,0.12) 0%, transparent 50%), radial-gradient(circle at 50% 50%, rgba(235,243,235,0.08) 0%, transparent 70%); pointer-events: none; z-index: 0; } /* ── Header ── */ .app-header { background: linear-gradient(135deg, var(--forest) 0%, var(--sage) 60%, #9EC4A0 100%); border-radius: var(--radius); padding: 36px 40px 32px; margin-bottom: 8px; position: relative; overflow: hidden; box-shadow: var(--shadow-md); } .app-header::after { content: '⬡ ⬡ ⬡'; position: absolute; right: 32px; top: 20px; font-size: 48px; opacity: 0.08; letter-spacing: 8px; color: white; } .app-header h1 { font-family: 'Lora', serif !important; font-size: 2rem !important; font-weight: 600 !important; color: white !important; margin: 0 0 8px !important; line-height: 1.2 !important; } .app-header p { color: rgba(255,255,255,0.85) !important; font-size: 0.95rem !important; margin: 0 !important; font-weight: 300 !important; max-width: 560px !important; } /* ── Panels ── */ .panel-card { background: white !important; border-radius: var(--radius) !important; padding: 28px !important; box-shadow: var(--shadow-sm) !important; border: 1px solid rgba(124,158,126,0.15) !important; } /* ── Labels ── */ label span, .label-wrap span { font-family: 'DM Sans', sans-serif !important; font-weight: 500 !important; font-size: 0.82rem !important; color: var(--text-mid) !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; } /* ── Dropdown ── */ .gr-dropdown, select { border: 1.5px solid var(--sage-light) !important; border-radius: 10px !important; background: var(--sage-pale) !important; color: var(--text-dark) !important; font-family: 'DM Sans', sans-serif !important; font-size: 0.9rem !important; transition: border-color 0.2s !important; } .gr-dropdown:focus, select:focus { border-color: var(--sage) !important; outline: none !important; box-shadow: 0 0 0 3px rgba(124,158,126,0.18) !important; } /* ── Textbox ── */ textarea, .gr-textbox textarea { border: 1.5px solid var(--sage-light) !important; border-radius: 12px !important; background: var(--sage-pale) !important; color: var(--text-dark) !important; font-family: 'DM Sans', sans-serif !important; font-size: 0.88rem !important; line-height: 1.7 !important; padding: 14px !important; transition: border-color 0.2s, box-shadow 0.2s !important; resize: vertical !important; } textarea:focus { border-color: var(--sage) !important; box-shadow: 0 0 0 3px rgba(124,158,126,0.18) !important; outline: none !important; background: white !important; } /* ── Slider ── */ input[type=range] { accent-color: var(--sage) !important; } .slider-container { background: transparent !important; } /* ── Generate button ── */ #gen-btn { background: linear-gradient(135deg, var(--forest), var(--sage)) !important; color: white !important; border: none !important; border-radius: 12px !important; font-family: 'DM Sans', sans-serif !important; font-size: 1rem !important; font-weight: 500 !important; padding: 14px 28px !important; letter-spacing: 0.03em !important; cursor: pointer !important; box-shadow: 0 4px 16px rgba(61,107,64,0.28) !important; transition: transform 0.15s, box-shadow 0.15s !important; width: 100% !important; } #gen-btn:hover { transform: translateY(-1px) !important; box-shadow: 0 8px 24px rgba(61,107,64,0.36) !important; } #gen-btn:active { transform: translateY(0) !important; } /* ── Error/status message ── */ #error-msg p { color: #C0392B !important; background: #FFF0EE !important; border: 1px solid #F5C6C2 !important; border-radius: 8px !important; padding: 10px 14px !important; font-size: 0.85rem !important; } /* ── Metrics card ── */ #metrics-box { background: linear-gradient(135deg, var(--sage-pale), #F0FAF0) !important; border: 1.5px solid var(--sage-light) !important; border-radius: var(--radius) !important; padding: 22px !important; } #metrics-box table { width: 100% !important; border-collapse: collapse !important; } #metrics-box th { color: var(--text-soft) !important; font-size: 0.78rem !important; text-transform: uppercase !important; letter-spacing: 0.06em !important; padding: 6px 10px !important; text-align: left !important; } #metrics-box td { padding: 7px 10px !important; font-size: 0.92rem !important; color: var(--text-dark) !important; border-bottom: 1px solid rgba(124,158,126,0.12) !important; } #metrics-box td:last-child { color: var(--forest) !important; font-weight: 600 !important; } /* ── Gallery ── */ .gallery-item, .thumbnail-item { border-radius: 12px !important; overflow: hidden !important; border: 1.5px solid var(--sage-light) !important; box-shadow: var(--shadow-sm) !important; background: white !important; transition: transform 0.15s, box-shadow 0.15s !important; } .gallery-item:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } /* ── Table ── */ .gr-dataframe table { border-collapse: separate !important; border-spacing: 0 4px !important; } .gr-dataframe th { background: var(--sage-pale) !important; color: var(--text-mid) !important; font-size: 0.78rem !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; padding: 10px 14px !important; border: none !important; } .gr-dataframe td { background: white !important; padding: 9px 14px !important; border-bottom: 1px solid var(--sage-pale) !important; font-size: 0.87rem !important; color: var(--text-dark) !important; } /* ── Section headings ── */ .section-title { font-family: 'Lora', serif !important; font-size: 1rem !important; font-weight: 600 !important; color: var(--forest) !important; margin-bottom: 14px !important; display: flex !important; align-items: center !important; gap: 8px !important; } /* ── Footer ── */ .app-footer { text-align: center !important; padding: 20px !important; color: var(--text-soft) !important; font-size: 0.78rem !important; border-top: 1px solid var(--sage-light) !important; margin-top: 8px !important; } /* ── Pill badge ── */ .pill { display: inline-block; background: var(--sage-pale); border: 1px solid var(--sage-light); color: var(--forest); border-radius: 20px; padding: 2px 12px; font-size: 0.75rem; font-weight: 500; margin: 2px; } """ def load_example(choice): return EXAMPLES.get(choice, "") def validate_smiles_input(smiles_text): from rdkit import Chem lines = [s.strip() for s in smiles_text.strip().split('\n') if s.strip()] if len(lines) < 3: return None, "❌ Enter at least 3 SMILES (one per line)." if len(lines) > 10: return None, "❌ Maximum 10 SMILES in the support set." valid = [] for smi in lines: if Chem.MolFromSmiles(smi) is None: return None, f"❌ Invalid SMILES: `{smi}`" valid.append(smi) return valid, None def generate_molecules(smiles_text, n_generate, progress=gr.Progress()): valid_smiles, err = validate_smiles_input(smiles_text) if err: return None, err, None, None TEMPERATURE = 0.8 # fixed — optimal balance of diversity and validity progress(0.15, desc="Encoding support set…") try: progress(0.40, desc="Generating molecules…") results = run_generation(valid_smiles, n=int(n_generate), temperature=TEMPERATURE) except Exception as e: return None, f"❌ Generation failed: {str(e)}", None, None progress(0.85, desc="Computing metrics…") m = results summary_md = f"""