c-lipovae / app.py
fc28's picture
Keep gradio_client bool-schema patch (needed under gradio 5 too)
58da3cb verified
Raw
History Blame Contribute Delete
39.1 kB
"""C-LipoVAE interactive demo — Hugging Face Space.
A leakage-safe conditional variational autoencoder for binary phenotype
stratification of high-dimensional omics data. Editorial-quality UI
with per-metric interpretive commentary.
"""
from __future__ import annotations
# ---------------------------------------------------------------
# Patch gradio_client to tolerate bool JSON-schema nodes.
# A long-standing bug (present in gradio_client shipped with both
# Gradio 4.x and 5.x): a schema node of `additionalProperties: True`
# — emitted for pandas DataFrame components — reaches `get_type` /
# `_json_schema_to_python_type` as a bare bool and crashes the
# startup api-info generation with
# "TypeError: argument of type 'bool' is not iterable".
# We short-circuit bool schema nodes so startup succeeds.
# Must run before gradio builds the app; import order matters.
# ---------------------------------------------------------------
try:
import gradio_client.utils as _gcu
_orig_j2p = _gcu._json_schema_to_python_type
def _patched_j2p(schema, defs=None):
if isinstance(schema, bool):
return "Any" if schema else "None"
return _orig_j2p(schema, defs)
_gcu._json_schema_to_python_type = _patched_j2p
_orig_get_type = _gcu.get_type
def _patched_get_type(schema):
if isinstance(schema, bool):
return "Any" if schema else "None"
return _orig_get_type(schema)
_gcu.get_type = _patched_get_type
except Exception:
pass
from pathlib import Path
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from clipovae import (
active_units,
c2st_auc,
encode_null,
fit_cvae,
integrated_gradients,
mmd2_rbf,
reconstruct,
sym_kl_gauss,
)
APP_ROOT = Path(__file__).parent
EXAMPLE_MAT = APP_ROOT / "examples" / "wbc_matrix.csv"
EXAMPLE_LAB = APP_ROOT / "examples" / "wbc_labels.csv"
MAX_SAMPLES = 2000
MAX_FEATURES = 2000
MAX_CLASSES = 4
# ------------------------------------------------------------------
# Interpretation helpers
# ------------------------------------------------------------------
def auc_verdict(auc: float) -> tuple[str, str, str]:
"""Return (label, color, sentence) for a C2ST AUC value.
Bands follow the interpretation convention used in the C-LipoVAE
manuscript, with numeric anchors from real datasets:
supervised-comparable (AUC >= 0.90) ← WBC benchmark = 0.965
strongly discriminative (0.80-0.90) ← TCM PD/NPD = 0.877
moderately discriminative (0.70-0.80)
weakly discriminative (0.60-0.70) ← PIVUS sex = 0.644
chance-level (< 0.60)
"""
if auc >= 0.90:
return ("Supervised-comparable encoding", "#0f766e",
"The null-condition latent carries near-complete "
"class-discriminative information (5-fold CV C2ST AUC "
f"= {auc:.3f}), of the same order as a direct "
"supervised classifier trained on the raw features. "
"Consistent with a well-separated, low-noise class "
"boundary.")
if auc >= 0.80:
return ("Strongly discriminative latent", "#16a34a",
"The leakage-safe latent geometry retains a "
"substantial fraction of the class signal from the "
"input space (5-fold CV C2ST AUC = "
f"{auc:.3f}); consistent with recoverable, "
"reproducible class-conditional structure.")
if auc >= 0.70:
return ("Moderately discriminative latent", "#ca8a04",
"The null-condition latent captures partial "
"class-discriminative structure (5-fold CV C2ST AUC "
f"= {auc:.3f}). Consistent with a real but incomplete "
"phenotype signature; feature attributions should be "
"corroborated with an external cohort.")
if auc >= 0.60:
return ("Weakly discriminative latent", "#d97706",
"The latent carries limited class-discriminative "
f"signal (5-fold CV C2ST AUC = {auc:.3f}). Consistent "
"with a biologically weak effect, small sample size, "
"or noisy input features. Interpret integrated-"
"gradient attributions with caution.")
return ("Chance-level encoding", "#dc2626",
f"The null-condition latent (5-fold CV C2ST AUC = {auc:.3f}) "
"does not encode the class label beyond chance. Consider a "
"higher-capacity model, more training data, or verifying "
"that the input features actually carry the phenotype.")
def au_verdict(n_au: int, d_latent: int) -> tuple[str, str, str]:
ratio = n_au / max(d_latent, 1)
if ratio >= 0.75:
return ("No posterior collapse", "#0f766e",
f"All {n_au} of {d_latent} latent dimensions have "
"per-dimension KL > 0.01 nats; full latent capacity "
"is being used and the prior is not dominating the "
"posterior.")
if ratio >= 0.5:
return ("Partial posterior collapse", "#ca8a04",
f"Only {n_au} of {d_latent} dimensions are active "
"(per-dim KL > 0.01). The effective latent dimension "
f"is {n_au}; the remaining dimensions have collapsed "
"onto the prior and contribute no information.")
return ("Severe posterior collapse", "#dc2626",
f"Only {n_au} of {d_latent} dimensions carry non-trivial "
"KL. The β regulariser is over-constraining the encoder. "
"Try a lower β (e.g. 0.5) or a larger training set.")
def r2_verdict(r2: float) -> tuple[str, str, str]:
if r2 >= 0.30:
return ("High reconstruction fidelity", "#0f766e",
"The CVAE recovers a substantial fraction of input "
f"variance (R²_rel = {r2:.2f} vs. mean-baseline), so "
"the latent code retains most feature-level "
"information alongside its discriminative signal.")
if r2 >= 0.10:
return ("Moderate reconstruction fidelity", "#ca8a04",
f"R²_rel = {r2:.2f}: the latent preserves the "
"informative signal but sacrifices some raw-feature "
"detail. Typical for β = 1.0 CVAEs tuned for "
"separability over autoencoding fidelity.")
return ("Low reconstruction fidelity", "#d97706",
f"R²_rel = {r2:.2f} indicates the bottleneck is "
"aggressive relative to the input complexity. Consider "
"increasing the latent dimension d or lowering β.")
def mmd_note(mmd: float) -> str:
if mmd >= 0.20:
s = ("large distributional shift between the class-conditional "
"latent clouds")
elif mmd >= 0.05:
s = "clear distributional shift"
else:
s = ("small distributional shift; the two classes overlap "
"substantially in the latent")
return (f"Squared MMD with a Gaussian-RBF kernel at "
f"median-heuristic bandwidth: {s}. Reported alongside "
"the C2ST AUC as an independent, classifier-free "
"separation check.")
def kl_note(kl: float) -> str:
if kl >= 10.0:
s = "well-separated Gaussian fits"
elif kl >= 2.0:
s = "clearly distinguishable Gaussian fits"
else:
s = "overlapping Gaussian fits"
return (f"Symmetrised KL divergence between multivariate-Gaussian "
f"fits to the two class-conditional latent clouds: {s}. "
"Larger values mean the class-conditional posteriors are "
"further apart in Gaussian-approximation terms.")
# ------------------------------------------------------------------
# Biological interpretation: auto-detect the dataset domain from the
# feature names and provide dataset-appropriate framing.
# ------------------------------------------------------------------
def detect_domain(feature_names: list[str]) -> str:
"""Rough classifier based on feature-name lexicon."""
joined = " ".join(feature_names[:100]).lower()
lipid_prefixes = ("pc ", "pe ", "ps ", "pi ", "pg ", "lpc ", "lpe ",
"sm ", "cer ", "cerp ", "tag ", "tag-", "tg ",
"dag ", "dg ", "cholesterol", "ffa ", "coq")
if any(f.strip().lower().startswith(lipid_prefixes)
for f in feature_names[:40]):
return "lipidomics"
wbc_terms = ("radius", "texture", "perimeter", "area",
"concavity", "concave points", "smoothness",
"compactness", "fractal dimension")
if sum(t in joined for t in wbc_terms) >= 4:
return "wbc_morphometry"
if any(f.startswith("ENSG") or f.startswith("ENST")
for f in feature_names[:100]):
return "transcriptomics"
if any(f.upper().startswith(("P0", "P1", "Q9", "Q1"))
and len(f) == 6 for f in feature_names[:100]):
return "proteomics"
if any("HMDB" in f.upper() or "kegg" in f.lower()
or "cid" in f.lower() for f in feature_names[:100]):
return "metabolomics"
return "generic"
def biology_html(domain: str, top_feats: list[str], auc: float) -> str:
"""Domain-appropriate biological framing of the top drivers."""
top3 = ", ".join(f"<code>{t}</code>" for t in top_feats[:3])
if domain == "lipidomics":
body = (
"The top-ranked features are the strongest nonlinear "
"drivers of the class contrast in the leakage-safe latent. "
f"Species carrying names such as {top3} identify the "
"lipid classes whose abundance shift most strongly "
"co-varies with the phenotype. When the leading species "
"are glycerophospholipids (<code>PC</code>, <code>PE</code>, "
"<code>PS</code>, <code>PI</code>), the signature "
"typically points to plasma-membrane remodeling and "
"polyunsaturated-fatty-acid trafficking; when "
"sphingomyelins (<code>SM</code>) dominate, ceramide-"
"signalling and insulin-resistance pathways are the "
"conventional biological reading; when triacylglycerols "
"(<code>TAG</code>) dominate, adipose storage and hepatic "
"lipogenesis are implicated. Corroborate directly against "
"the class-conditional log-fold-change and against an "
"external cohort before making mechanistic claims."
)
header = "Lipidomic interpretation"
elif domain == "wbc_morphometry":
body = (
"This is the Wisconsin Breast Cancer benchmark — "
"quantitative cell-nucleus morphometry from fine-needle "
"aspirates (radius, texture, area, concavity, concave "
f"points, and so on). The top-ranked drivers ({top3}) "
"are the same nuclear-atypia measurements that "
"pathologists use to grade cytological samples: nuclei "
"that are larger, more irregular, and more concave "
"correspond to the malignant class. C-LipoVAE recovers "
"these classical drivers <strong>without being told the "
"class label at evaluation time</strong> — a direct "
"sanity-check that the leakage-safe protocol still "
"extracts biologically meaningful signal, and a "
"reproduction of what is arguably the best-studied "
"supervised benchmark in medical machine learning."
)
header = "Cytological interpretation (WBC benchmark)"
elif domain == "transcriptomics":
body = (
"The top-ranked features are Ensembl transcripts / genes "
f"({top3}). Their differential representation between "
"classes suggests programme-level transcriptional changes "
"— consider enrichment against MSigDB Hallmark or KEGG "
"gene-sets before drawing pathway conclusions."
)
header = "Transcriptomic interpretation"
elif domain == "proteomics":
body = (
"The top-ranked features are UniProt-style protein "
f"identifiers ({top3}). These are the strongest nonlinear "
"drivers of the class separation; annotate them via "
"UniProt / Reactome and cross-check with an orthogonal "
"proteomic panel."
)
header = "Proteomic interpretation"
elif domain == "metabolomics":
body = (
"The top-ranked features are metabolomic identifiers "
f"({top3}). Map them to HMDB / KEGG to build a "
"pathway-level interpretation; check the direction of "
"shift against the class-conditional means before making "
"biological claims."
)
header = "Metabolomic interpretation"
else:
body = (
"The top-ranked features are the strongest nonlinear "
f"drivers of the class contrast in the latent ({top3}). "
"Because the leakage-safe protocol excludes label "
"information at evaluation time, these are the features "
"that the encoder learned to compress into a "
"class-informative geometry — a data-driven candidate "
"signature rather than a supervised discriminator. "
"Interpret in the context of the actual measurement "
"platform, and validate the direction of each feature "
"against the class-conditional means before making "
"mechanistic claims."
)
header = "Signature interpretation"
strength = ("These interpretations are conditional on the AUC "
"band shown above: the higher the C2ST AUC, the more "
"confidence one can place in the top drivers as a "
"reproducible signature. For AUC < 0.70 the "
"attributions should be treated as exploratory only.")
return f"""
<div class="biology">
<div class="biology-eyebrow">Biological interpretation</div>
<div class="biology-title">{header}</div>
<p class="biology-body">{body}</p>
<div class="biology-caveat">{strength}</div>
</div>
"""
# ------------------------------------------------------------------
# IO helpers
# ------------------------------------------------------------------
def _read_matrix(path: str) -> tuple[np.ndarray, list[str], list[str]]:
df = pd.read_csv(path, index_col=0)
return (df.values.astype(np.float32),
df.index.astype(str).tolist(),
df.columns.astype(str).tolist())
def _read_labels(path: str) -> pd.Series:
df = pd.read_csv(path)
if df.shape[1] < 2:
raise gr.Error("labels.csv must have >=2 columns: "
"sample_id, label.")
return df.set_index(df.columns[0])[df.columns[1]]
def _align(X: np.ndarray, ids: list[str],
labels: pd.Series) -> tuple[np.ndarray, np.ndarray]:
common = [s for s in ids if s in labels.index]
if not common:
raise gr.Error("No sample IDs match between matrix and labels.")
row = {s: i for i, s in enumerate(ids)}
return X[[row[s] for s in common]], labels.loc[common].astype(int).values
def _r2_rel(X: np.ndarray, Xh: np.ndarray) -> float:
mse_r = float(np.mean((X - Xh) ** 2))
mse_b = float(np.mean((X - X.mean(0, keepdims=True)) ** 2))
return 1.0 - mse_r / max(mse_b, 1e-12)
# ------------------------------------------------------------------
# Plot helpers
# ------------------------------------------------------------------
def _plot_latent(Z: np.ndarray, y: np.ndarray) -> plt.Figure:
palette = ["#4338ca", "#ea580c", "#0f766e", "#c026d3"]
Z2 = PCA(n_components=2, random_state=0).fit_transform(Z) \
if Z.shape[1] >= 2 else np.hstack([Z, np.zeros_like(Z)])
fig, ax = plt.subplots(figsize=(6.5, 5.0), dpi=110)
fig.patch.set_facecolor("#fafafa")
ax.set_facecolor("#ffffff")
for i, cls in enumerate(sorted(np.unique(y))):
m = y == cls
ax.scatter(Z2[m, 0], Z2[m, 1], s=42, alpha=0.78,
color=palette[i % len(palette)],
label=f"Class {cls} (n={int(m.sum())})",
edgecolor="white", linewidth=0.7, zorder=3)
ax.set_xlabel("PC1 of null-condition latent",
fontsize=11, color="#374151")
ax.set_ylabel("PC2 of null-condition latent",
fontsize=11, color="#374151")
ax.set_title("Leakage-safe latent geometry",
fontsize=13, fontweight="600", color="#111827", pad=12)
ax.legend(frameon=False, fontsize=10, loc="best")
for s in ("top", "right"):
ax.spines[s].set_visible(False)
for s in ("left", "bottom"):
ax.spines[s].set_color("#d1d5db")
ax.grid(alpha=0.25, linestyle=":", zorder=1)
ax.tick_params(colors="#6b7280")
fig.tight_layout()
return fig
# ------------------------------------------------------------------
# Main callback
# ------------------------------------------------------------------
def run_pipeline(matrix_file, labels_file,
beta: float, d_latent: int, epochs: int,
log2: bool, seed: int):
if not matrix_file or not labels_file:
raise gr.Error("Please upload both matrix.csv and labels.csv, "
"or click 'Load example (WBC)'.")
try:
X_raw, ids, feats = _read_matrix(matrix_file)
except Exception as e:
raise gr.Error(f"Cannot read matrix CSV: {e}")
try:
labels = _read_labels(labels_file)
except Exception as e:
raise gr.Error(f"Cannot read labels CSV: {e}")
X_raw, y = _align(X_raw, ids, labels)
n, p = X_raw.shape
if n > MAX_SAMPLES:
raise gr.Error(f"n={n} exceeds the CPU-space cap of "
f"{MAX_SAMPLES}. Install locally for large data.")
if p > MAX_FEATURES:
raise gr.Error(f"p={p} exceeds the CPU-space cap of "
f"{MAX_FEATURES}. Install locally.")
n_cls = int(y.max()) + 1
if n_cls > MAX_CLASSES:
raise gr.Error(f"{n_cls} classes; the demo supports "
f"<= {MAX_CLASSES}.")
if np.bincount(y).min() < 3:
raise gr.Error("At least 3 samples per class are required.")
if log2:
X_raw = np.log2(np.clip(X_raw, 1.0, None))
X = StandardScaler().fit_transform(X_raw).astype(np.float32)
model = fit_cvae(X, y, beta=float(beta), d_latent=int(d_latent),
epochs=int(epochs), lr=1e-3, batch_size=16,
seed=int(seed), verbose=False)
Z = encode_null(model, X)
auc_m, auc_s = c2st_auc(Z, y, n_splits=5, seed=int(seed))
mmd, _g = mmd2_rbf(Z[y == 0], Z[y == 1])
kl = sym_kl_gauss(Z[y == 0], Z[y == 1])
n_au, _ = active_units(model, X, y)
r2 = _r2_rel(X, reconstruct(model, X, y))
z_diff = np.abs(Z[y == 1].mean(0) - Z[y == 0].mean(0))
tgt = int(np.argmax(z_diff))
ig = integrated_gradients(model, X, y, target_dim=tgt, steps=50)
ig_abs = np.mean(np.abs(ig), axis=0)
ord_ = np.argsort(ig_abs)[::-1][:15]
top_df = pd.DataFrame({
"Rank": list(range(1, len(ord_) + 1)),
"Feature": [feats[i] for i in ord_],
"Mean |IG|": [f"{ig_abs[i]:.4f}" for i in ord_],
})
auc_lbl, auc_col, auc_txt = auc_verdict(auc_m)
au_lbl, au_col, au_txt = au_verdict(n_au, int(d_latent))
r2_lbl, r2_col, r2_txt = r2_verdict(r2)
mmd_txt = mmd_note(mmd)
kl_txt = kl_note(kl)
domain = detect_domain(feats)
top_feats = [feats[i] for i in ord_]
biology_block = biology_html(domain, top_feats, auc_m)
# Verdict + metric grid rendered as HTML for full styling control
verdict_html = f"""
<div class="verdict-hero">
<div class="verdict-eyebrow">Latent-space verdict</div>
<div class="verdict-label" style="color:{auc_col}">{auc_lbl}</div>
<div class="verdict-sub">
5-fold&nbsp;CV&nbsp;C2ST&nbsp;AUC&nbsp;<strong>{auc_m:.3f}&nbsp;±&nbsp;{auc_s:.3f}</strong>
&nbsp;·&nbsp; n&nbsp;=&nbsp;{n}&nbsp;·&nbsp; p&nbsp;=&nbsp;{p}
&nbsp;·&nbsp; classes&nbsp;=&nbsp;{n_cls}
</div>
<div class="verdict-interp">{auc_txt}</div>
<div class="verdict-anchor">
<strong>Reference anchors from the C-LipoVAE manuscript</strong> —
Wisconsin&nbsp;Breast&nbsp;Cancer&nbsp;0.965&nbsp;(supervised-comparable) ·
TCM&nbsp;PD/NPD&nbsp;0.877&nbsp;(strongly&nbsp;discriminative) ·
PIVUS&nbsp;sex&nbsp;0.644&nbsp;(weakly&nbsp;discriminative).
</div>
</div>
<div class="metric-grid">
<div class="metric">
<div class="metric-name">C2ST AUC</div>
<div class="metric-value">{auc_m:.3f}
<span class="metric-sd">± {auc_s:.3f}</span></div>
<div class="metric-badge" style="background:{auc_col}15;color:{auc_col}">{auc_lbl}</div>
<div class="metric-note">{auc_txt}</div>
</div>
<div class="metric">
<div class="metric-name">MMD² (RBF)</div>
<div class="metric-value">{mmd:.3f}</div>
<div class="metric-note">{mmd_txt}</div>
</div>
<div class="metric">
<div class="metric-name">Symmetrised KL</div>
<div class="metric-value">{kl:.2f}<span class="metric-unit">&nbsp;nats</span></div>
<div class="metric-note">{kl_txt}</div>
</div>
<div class="metric">
<div class="metric-name">Active units</div>
<div class="metric-value">{n_au}<span class="metric-unit">&nbsp;/&nbsp;{d_latent}</span></div>
<div class="metric-badge" style="background:{au_col}15;color:{au_col}">{au_lbl}</div>
<div class="metric-note">{au_txt}</div>
</div>
<div class="metric">
<div class="metric-name">R²<sub>rel</sub></div>
<div class="metric-value">{r2:.3f}</div>
<div class="metric-badge" style="background:{r2_col}15;color:{r2_col}">{r2_lbl}</div>
<div class="metric-note">{r2_txt}</div>
</div>
<div class="metric">
<div class="metric-name">Most-discriminative axis</div>
<div class="metric-value">z<sub>{tgt}</sub></div>
<div class="metric-note">Latent dimension with the largest |μ<sub>1</sub>&nbsp;−&nbsp;μ<sub>0</sub>|
under null-condition encoding. Integrated-gradient attribution
is computed against this axis, and reported below as the most
likely nonlinear discriminative-feature signature.</div>
</div>
</div>
<div class="protocol-note">
<div class="protocol-title">Why these numbers are trustworthy</div>
All separability metrics are computed on
<code>encode_null(model, X)</code>: the class label is replaced by a
zero vector at evaluation time so it cannot leak into the encoder.
This <em>null-condition</em> protocol removes the label side-channel
that inflates naive CVAE evaluations.
</div>
{biology_block}
"""
return verdict_html, _plot_latent(Z, y), top_df
def load_example():
return str(EXAMPLE_MAT), str(EXAMPLE_LAB)
# ------------------------------------------------------------------
# UI
# ------------------------------------------------------------------
HERO = """
<div class="hero">
<div class="hero-badge">Open-source · MIT · Reproducible</div>
<h1 class="hero-title">C-LipoVAE</h1>
<p class="hero-tag">A <strong>leakage-safe conditional VAE</strong>
for binary phenotype stratification of high-dimensional omics data.</p>
<p class="hero-sub">
Upload a feature matrix and binary labels. The model trains a
conditional variational autoencoder, evaluates its latent space
<span class="hl-yellow">without</span> feeding the label back in,
and returns publication-grade separability metrics with
interpretation.
</p>
<div class="hero-links">
<a href="https://github.com/23008613g/C-LipoVAE" target="_blank"
rel="noopener">↗ GitHub</a>
&nbsp;·&nbsp;
<a href="https://github.com/23008613g/C-LipoVAE#quick-start"
target="_blank" rel="noopener">↗ Quick start</a>
</div>
</div>
<div class="glance">
<div class="glance-item"><span>1</span>
Upload <code>matrix.csv</code> + <code>labels.csv</code>
(or load the WBC example)</div>
<div class="glance-item"><span>2</span>
C-LipoVAE trains a β-VAE (default β = 1.0, d = 8, 150 epochs)</div>
<div class="glance-item"><span>3</span>
Metrics are computed under a null-condition encoding — no label
leakage</div>
<div class="glance-item"><span>4</span>
Get an interpretive verdict plus integrated-gradient
attributions</div>
</div>
"""
CSS = """
:root { color-scheme: light; }
.gradio-container { max-width: 1160px !important;
font-family: 'Inter', 'SF Pro Text',
-apple-system, BlinkMacSystemFont,
'Segoe UI', system-ui, sans-serif;
background: linear-gradient(180deg,#fafbff 0%,
#f4f4f8 100%) !important; }
/* HERO */
.hero { padding: 42px 44px 26px 44px;
background: linear-gradient(135deg,#0f172a 0%,#1e1b4b 45%,
#4c1d95 100%);
border-radius: 22px; color: #f8fafc; margin: 8px 0 4px 0;
box-shadow: 0 10px 40px -8px rgba(30,27,75,.35); }
.hero-badge { display: inline-block; font-size: 11px; letter-spacing: .12em;
text-transform: uppercase; color: #c7d2fe;
background: rgba(255,255,255,.08); padding: 5px 12px;
border-radius: 999px; margin-bottom: 20px; }
.hero-title { font-size: 44px; margin: 0 0 8px 0; font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(90deg,#fff 0%,#c7d2fe 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent; }
.hero-tag { font-size: 19px; margin: 4px 0 8px 0; color: #e0e7ff;
font-weight: 300; }
.hero-tag strong { font-weight: 600; color: #fff; }
.hero-sub { font-size: 14.5px; max-width: 780px; margin: 6px 0 14px 0;
color: #cbd5e1; line-height: 1.6; }
.hero-sub .hl-yellow { color: #fde68a; font-weight: 600; }
.hero-links { font-size: 13.5px; color: #a5b4fc; }
.hero-links a { color: #ddd6fe; text-decoration: none; }
.hero-links a:hover { color: #fff; text-decoration: underline; }
/* AT-A-GLANCE STRIP */
.glance { display: grid; grid-template-columns: repeat(4, 1fr);
gap: 12px; margin: 22px 0 10px 0; }
.glance-item { background: #ffffff; padding: 14px 16px 14px 46px;
border-radius: 12px; position: relative; font-size: 13px;
color: #334155; line-height: 1.5;
box-shadow: 0 1px 2px rgba(15,23,42,.05),
0 4px 12px -6px rgba(15,23,42,.08);
border: 1px solid #e5e7eb; }
.glance-item span { position: absolute; left: 12px; top: 14px;
width: 26px; height: 26px; border-radius: 8px;
background: linear-gradient(135deg,#4f46e5,#7c3aed);
color: #fff; font-weight: 600; font-size: 12.5px;
display: flex; align-items: center;
justify-content: center; }
.glance-item code { background: #f1f5f9; padding: 1px 5px;
border-radius: 4px; font-size: 12px; }
/* SECTION HEADERS */
.section-title { font-size: 12px; font-weight: 700;
letter-spacing: .16em; text-transform: uppercase;
color: #6366f1; margin: 24px 0 10px 4px; }
/* INPUTS PANEL */
.panel { background: #ffffff; padding: 18px; border-radius: 14px;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(15,23,42,.04); }
/* VERDICT HERO */
.verdict-hero { background: #ffffff; border-radius: 16px;
padding: 26px 30px; margin-bottom: 18px;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 24px -8px rgba(15,23,42,.10); }
.verdict-eyebrow { font-size: 11px; letter-spacing: .18em;
text-transform: uppercase; color: #6b7280;
font-weight: 700; }
.verdict-label { font-size: 34px; font-weight: 800;
letter-spacing: -0.02em; margin: 4px 0 6px 0;
line-height: 1.1; }
.verdict-sub { color: #4b5563; font-size: 14px;
font-family: 'JetBrains Mono', 'SF Mono',
'Menlo', monospace; }
.verdict-sub strong { color: #111827; }
.verdict-interp { margin-top: 14px; color: #1f2937; font-size: 14.5px;
line-height: 1.65; max-width: 780px; }
.verdict-anchor { margin-top: 14px; padding: 10px 14px;
background: #f3f4f6; border-radius: 8px;
color: #4b5563; font-size: 12.5px; line-height: 1.55;
border-left: 3px solid #6366f1; }
.verdict-anchor strong { color: #111827; }
/* METRIC GRID */
.metric-grid { display: grid; grid-template-columns: repeat(3, 1fr);
gap: 14px; margin-bottom: 20px; }
@media (max-width: 900px) {
.metric-grid { grid-template-columns: repeat(2, 1fr); }
.glance { grid-template-columns: repeat(2, 1fr); }
}
.metric { background: #ffffff; border-radius: 14px; padding: 18px 20px;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 2px rgba(15,23,42,.04),
0 4px 20px -10px rgba(15,23,42,.10);
display: flex; flex-direction: column; }
.metric-name { font-size: 11px; letter-spacing: .12em;
text-transform: uppercase; color: #6b7280;
font-weight: 700; }
.metric-value { font-size: 32px; font-weight: 800; margin: 4px 0 0 0;
color: #111827; line-height: 1.05; letter-spacing: -0.02em;
font-family: 'JetBrains Mono', 'SF Mono',
'Menlo', monospace; }
.metric-sd, .metric-unit { font-size: 15px; font-weight: 500;
color: #9ca3af; margin-left: 4px; }
.metric-badge { display: inline-block; margin: 8px 0 6px 0;
padding: 3px 10px; border-radius: 999px;
font-size: 11.5px; font-weight: 700;
letter-spacing: .04em; text-transform: uppercase;
align-self: flex-start; }
.metric-note { color: #4b5563; font-size: 12.5px; line-height: 1.55;
margin-top: 4px; }
/* BIOLOGY BLOCK */
.biology { background: linear-gradient(180deg,#ffffff 0%,#fdfaff 100%);
border-radius: 16px; padding: 22px 26px 20px 26px;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 24px -12px rgba(15,23,42,.12);
margin: 4px 0 18px 0; }
.biology-eyebrow { font-size: 11px; letter-spacing: .18em;
text-transform: uppercase; color: #7c3aed;
font-weight: 700; }
.biology-title { font-size: 20px; font-weight: 700; color: #111827;
margin: 4px 0 10px 0; letter-spacing: -0.01em; }
.biology-body { color: #1f2937; font-size: 14.5px; line-height: 1.7;
margin: 0 0 12px 0; max-width: 820px; }
.biology-body code { background: #f3f4f6; padding: 1px 6px;
border-radius: 4px; font-size: 12.8px;
color: #4338ca;
font-family: 'JetBrains Mono','SF Mono',monospace; }
.biology-body strong { color: #111827; font-weight: 700; }
.biology-caveat { color: #6b7280; font-size: 12.5px; line-height: 1.55;
padding: 8px 12px; background: #f9fafb;
border-radius: 6px; border-left: 2px solid #d1d5db; }
/* PROTOCOL NOTE */
.protocol-note { background: #f5f3ff; border-left: 3px solid #7c3aed;
border-radius: 8px; padding: 14px 18px;
margin: 6px 0 18px 0; font-size: 13px; color: #4c1d95;
line-height: 1.6; }
.protocol-note code { background: #ede9fe; padding: 1px 6px;
border-radius: 4px; font-size: 12.5px;
color: #5b21b6; }
.protocol-title { font-weight: 700; font-size: 12px;
letter-spacing: .12em; text-transform: uppercase;
color: #6d28d9; margin-bottom: 4px; }
/* BUTTONS */
button.primary { background: linear-gradient(135deg,#4f46e5,#7c3aed)
!important; color: #fff !important;
border: 0 !important; font-weight: 700 !important;
letter-spacing: .02em !important;
box-shadow: 0 4px 12px -3px rgba(79,70,229,.5) !important; }
button.primary:hover { filter: brightness(1.08); }
/* FOOTER */
.footer { text-align: center; color: #6b7280; font-size: 12px;
margin: 22px 0 10px 0; letter-spacing: .04em; }
/* Hide Gradio's built-in footer ("Built with Gradio" / "Settings"),
which is translated by browser locale. Our own English footer
replaces it above. */
footer,
.gradio-container footer,
div[class*="svelte-"] > footer,
button[title*="Setting"], button[title*="设置"], button[title*="設定"] {
display: none !important;
}
"""
FORCE_EN_JS = """
<script>
(function () {
// Force English UI text regardless of the browser locale that
// Gradio's i18n picks. Maps common Traditional / Simplified Chinese
// strings shown by Gradio 4.x components to their English equivalents.
const T = {
'拖放檔案至此處': 'Drop CSV file here',
'拖放檔案至此': 'Drop CSV file here',
'拖放文件至此处': 'Drop CSV file here',
'拖放文件至此': 'Drop CSV file here',
'點擊上傳': 'or click to upload',
'点击上传': 'or click to upload',
'- 或 -': '— or —',
'或': 'or',
'上傳檔案': 'Upload file',
'上传文件': 'Upload file',
'錯誤': 'Error',
'错误': 'Error',
'提交': 'Submit',
'送出': 'Submit',
'清除': 'Clear',
'刪除': 'Delete',
'删除': 'Delete',
'執行': 'Run',
'执行': 'Run',
'下載': 'Download',
'下载': 'Download',
'已完成': 'Done',
'完成': 'Done',
'處理中': 'Processing',
'处理中': 'Processing',
'請等待': 'Please wait',
'请等待': 'Please wait',
'使用Gradio建構': 'Built with Gradio',
'使用Gradio构建': 'Built with Gradio',
'設定': 'Settings',
'设置': 'Settings',
};
const swap = () => {
// Walk text nodes and translate any exact matches.
const walker = document.createTreeWalker(
document.body, NodeFilter.SHOW_TEXT, null, false);
let n;
while ((n = walker.nextNode())) {
const s = n.nodeValue.trim();
if (T[s] !== undefined && n.nodeValue !== T[s]) {
n.nodeValue = n.nodeValue.replace(s, T[s]);
}
}
// Also swap common aria-labels / titles.
document.querySelectorAll('[aria-label],[title]').forEach(el => {
const a = el.getAttribute('aria-label');
if (a && T[a.trim()]) el.setAttribute('aria-label', T[a.trim()]);
const t = el.getAttribute('title');
if (t && T[t.trim()]) el.setAttribute('title', T[t.trim()]);
});
};
const start = () => {
swap();
new MutationObserver(swap).observe(document.body, {
childList: true, subtree: true, characterData: true
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else { start(); }
})();
</script>
"""
def build_demo() -> gr.Blocks:
with gr.Blocks(title="C-LipoVAE — leakage-safe conditional VAE",
css=CSS, head=FORCE_EN_JS,
theme=gr.themes.Base(
primary_hue="indigo",
neutral_hue="slate")) as demo:
gr.HTML(HERO)
gr.HTML('<div class="section-title">Inputs</div>')
with gr.Group(elem_classes=["panel"]):
with gr.Row():
matrix_in = gr.File(label="Feature matrix (CSV)",
file_types=[".csv"])
labels_in = gr.File(label="Labels (CSV)",
file_types=[".csv"])
with gr.Row():
example_btn = gr.Button("Load example (Wisconsin "
"Breast Cancer)",
variant="secondary")
run_btn = gr.Button("Run C-LipoVAE",
variant="primary",
elem_classes=["primary"])
with gr.Accordion("Hyperparameters (advanced)", open=False):
with gr.Row():
beta = gr.Slider(0.1, 4.0, value=1.0, step=0.1,
label="β (KL weight)")
d_latent = gr.Slider(2, 16, value=8, step=1,
label="Latent dim (d)")
epochs = gr.Slider(20, 300, value=150, step=10,
label="Epochs")
with gr.Row():
seed = gr.Number(value=42, label="Seed",
precision=0)
log2 = gr.Checkbox(
label="Apply log₂ transform "
"(for raw LC-MS intensities)")
gr.HTML('<div class="section-title">Results</div>')
result_html = gr.HTML(value=(
'<div class="verdict-hero">'
'<div class="verdict-eyebrow">Awaiting run</div>'
'<div class="verdict-label" style="color:#6b7280">'
'— · —</div>'
'<div class="verdict-sub">'
'Click <strong>Load example</strong> then '
'<strong>Run C-LipoVAE</strong> to see the interpretive '
'verdict and metric breakdown.</div></div>'))
with gr.Row():
plot_out = gr.Plot(label="Leakage-safe latent (PCA-2)",
show_label=False)
table_out = gr.Dataframe(
headers=["Rank", "Feature", "Mean |IG|"],
label="Top-15 features by integrated-gradient "
"attribution",
interactive=False)
gr.HTML(
'<div class="footer">'
'Runs on Hugging Face free-tier CPU · uploaded files '
'are used only for the current inference call and are not '
'stored · MIT license · '
'<a href="https://github.com/23008613g/C-LipoVAE" '
'style="color:#6366f1;text-decoration:none">'
'github.com/23008613g/C-LipoVAE</a></div>')
example_btn.click(load_example, None, [matrix_in, labels_in])
run_btn.click(run_pipeline,
[matrix_in, labels_in,
beta, d_latent, epochs, log2, seed],
[result_html, plot_out, table_out])
return demo
if __name__ == "__main__":
build_demo().launch(server_name="0.0.0.0", server_port=7860)