nileshhanotia commited on
Commit
b43bae5
·
verified ·
1 Parent(s): cc90c67

Upload app (7).py

Browse files
Files changed (1) hide show
  1. app (7).py +905 -0
app (7).py ADDED
@@ -0,0 +1,905 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py
3
+ ======
4
+ Mutation Explainability Intelligence System
5
+ Gradio Space — explanation ALWAYS precedes the prediction panel.
6
+
7
+ Three models:
8
+ nileshhanotia/mutation-predictor-splice
9
+ nileshhanotia/mutation-predictor-v4
10
+ nileshhanotia/mutation-pathogenicity-predictor
11
+ """
12
+
13
+ from __future__ import annotations
14
+ import io
15
+ import json
16
+ import logging
17
+ import os
18
+ import tempfile
19
+ import time
20
+ import traceback
21
+ from functools import lru_cache
22
+
23
+ import gradio as gr
24
+ import numpy as np
25
+ import matplotlib
26
+ matplotlib.use("Agg")
27
+ import matplotlib.pyplot as plt
28
+ import matplotlib.gridspec as gridspec
29
+ from matplotlib.colors import LinearSegmentedColormap
30
+ import requests
31
+
32
+ from model_loader import ModelRegistry, encode_for_v2, find_mutation_pos
33
+ from explainability_engine import (
34
+ extract_splice_signals,
35
+ extract_v4_signals,
36
+ extract_classic_signals,
37
+ compute_cross_model_analysis,
38
+ V4Signals,
39
+ ClassicSignals,
40
+ )
41
+ from decision_engine import build_decision, DecisionResult
42
+
43
+ logging.basicConfig(
44
+ level=logging.INFO,
45
+ format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
46
+ )
47
+ logger = logging.getLogger("mutation_xai")
48
+
49
+
50
+ # ═══════════════════════════════════════════════════════════════════════════════
51
+ # Model registry — loaded once at startup
52
+ # ═══════════════════════════════════════════════════════════════════════════════
53
+
54
+ REGISTRY = ModelRegistry(hf_token=os.environ.get("HF_TOKEN"))
55
+
56
+
57
+ # ═══════════════════════════════════════════════════════════════════════════════
58
+ # Ensembl sequence fetch
59
+ # ═══════════════════════════════════════════════════════════════════════════════
60
+
61
+ ENSEMBL_URL = "https://rest.ensembl.org/sequence/region/human"
62
+ WINDOW_HALF = 49 # 49 + 1 + 49 = 99 bp
63
+
64
+
65
+ @lru_cache(maxsize=512)
66
+ def _fetch_ensembl(chrom: str, start: int, end: int) -> str:
67
+ chrom = chrom.lstrip("chrCHR").strip()
68
+ region = f"{chrom}:{start}..{end}:1"
69
+ url = f"{ENSEMBL_URL}/{region}"
70
+ for attempt in range(3):
71
+ try:
72
+ r = requests.get(url,
73
+ params={"content-type": "application/json"},
74
+ timeout=15)
75
+ if r.status_code == 429:
76
+ wait = int(r.headers.get("Retry-After", 5))
77
+ logger.warning(f"Ensembl rate-limited — waiting {wait}s")
78
+ time.sleep(wait)
79
+ continue
80
+ r.raise_for_status()
81
+ data = r.json()
82
+ if isinstance(data, list):
83
+ data = data[0]
84
+ return data.get("seq", "").upper()
85
+ except Exception as exc:
86
+ if attempt == 2:
87
+ raise RuntimeError(
88
+ f"Ensembl API failed after 3 attempts: {exc}")
89
+ time.sleep(1.5 * (2 ** attempt))
90
+ return ""
91
+
92
+
93
+ def fetch_window(chrom: str, pos: int, ref: str, alt: str):
94
+ """Fetch 99-bp window. Returns (ref_seq, mut_seq, mut_pos_in_window)."""
95
+ chrom_clean = chrom.strip().lstrip("chrCHR")
96
+ start = max(1, pos - WINDOW_HALF)
97
+ end = pos + WINDOW_HALF
98
+ raw = _fetch_ensembl(chrom_clean, start, end)
99
+
100
+ if not raw:
101
+ raise ValueError(
102
+ f"Empty sequence from Ensembl for chr{chrom}:{start}-{end}")
103
+
104
+ seq = (raw + "N" * 99)[:99]
105
+ mut_pos = max(0, min(98, pos - start))
106
+
107
+ genome_ref = seq[mut_pos] if mut_pos < len(seq) else "N"
108
+ if genome_ref.upper() != ref.upper():
109
+ logger.warning(
110
+ f"Reference mismatch at chr{chrom}:{pos}: "
111
+ f"Ensembl={genome_ref}, user={ref}. Using Ensembl sequence.")
112
+
113
+ mut_list = list(seq)
114
+ mut_list[mut_pos] = alt.upper()
115
+ mut_seq = "".join(mut_list)
116
+
117
+ return seq, mut_seq, mut_pos
118
+
119
+
120
+ # ═══════════════════════════════════════════════════════════════════════════════
121
+ # Colour palette & colour maps
122
+ # ═══════════════════════════════════════════════════════════════════════════════
123
+
124
+ _BG = "#0D1117"
125
+ _SURF = "#161B22"
126
+ _TEXT = "#E6EDF3"
127
+ _MUTED = "#7D8590"
128
+ _BLUE = "#58A6FF"
129
+ _GREEN = "#3FB950"
130
+ _RED = "#F85149"
131
+ _ORG = "#D29922"
132
+
133
+ _CMAP_ACT = LinearSegmentedColormap.from_list(
134
+ "act",
135
+ [(0.04, 0.22, 0.47), (0.96, 0.96, 0.96), (0.72, 0.05, 0.12)],
136
+ N=256)
137
+ _CMAP_SPLICE = LinearSegmentedColormap.from_list(
138
+ "splice",
139
+ [(0, "#f7f7f7"), (0.3, "#fee08b"), (0.6, "#fc8d59"), (1, "#d73027")])
140
+ _CMAP_GRAD = matplotlib.colormaps.get_cmap("PuOr")
141
+
142
+
143
+ # ═══════════════════════════════════════════════════════════════════════════════
144
+ # Visualisation helpers
145
+ # ═══════════════════════════════════════════════════════════════════════════════
146
+
147
+ def _pil(fig):
148
+ """Render matplotlib figure to PIL Image (required for gr.Image)."""
149
+ buf = io.BytesIO()
150
+ fig.savefig(buf, format="png", dpi=110, bbox_inches="tight",
151
+ facecolor=fig.get_facecolor())
152
+ buf.seek(0)
153
+ from PIL import Image
154
+ img = Image.open(buf).copy()
155
+ plt.close(fig)
156
+ return img
157
+
158
+
159
+ def _empty_pil():
160
+ fig, ax = plt.subplots(figsize=(4, 2), facecolor=_BG)
161
+ ax.set_facecolor(_BG)
162
+ ax.axis("off")
163
+ return _pil(fig)
164
+
165
+
166
+ def _style_ax(ax, title=""):
167
+ ax.set_title(title, color=_TEXT, fontsize=9, loc="left",
168
+ pad=4, fontweight="bold")
169
+ for sp in ["top", "right"]:
170
+ ax.spines[sp].set_visible(False)
171
+ ax.spines["left"].set_color("#333")
172
+ ax.spines["bottom"].set_color("#333")
173
+ ax.tick_params(colors=_TEXT, labelsize=7)
174
+
175
+
176
+ def _heatmap_pil(profile: np.ndarray, mutation_pos: int,
177
+ cmap, label: str, ylabel: str,
178
+ prob: float | None = None):
179
+ imp = profile.copy()
180
+ if imp.max() > 0:
181
+ imp /= imp.max()
182
+ fig, ax = plt.subplots(figsize=(15, 2.5), facecolor=_BG)
183
+ ax.set_facecolor(_BG)
184
+ im = ax.imshow(imp[np.newaxis, :], aspect="auto", cmap=cmap,
185
+ vmin=0, vmax=1, extent=[-0.5, 98.5, 0, 1])
186
+ if mutation_pos >= 0:
187
+ ax.axvline(x=mutation_pos, color=_GREEN, linewidth=2.0,
188
+ linestyle="--", label=f"Mutation pos {mutation_pos}")
189
+ ax.legend(fontsize=8, facecolor=_BG, labelcolor=_TEXT,
190
+ framealpha=0.6, loc="upper right")
191
+ cb = fig.colorbar(im, ax=ax, pad=0.01)
192
+ cb.set_label(ylabel, color=_TEXT, fontsize=8)
193
+ cb.ax.tick_params(colors=_TEXT, labelsize=7)
194
+ ax.set_xlabel("Nucleotide position (99-bp window)",
195
+ color=_TEXT, fontsize=9)
196
+ ax.set_xticks(range(0, 99, 10))
197
+ ax.set_yticks([])
198
+ title = label + (f" (prob={prob:.4f})" if prob is not None else "")
199
+ _style_ax(ax, title)
200
+ fig.tight_layout()
201
+ return _pil(fig)
202
+
203
+
204
+ def plot_splice_act(norm, pos, prob):
205
+ return _heatmap_pil(norm, pos, _CMAP_ACT,
206
+ "Splice Model — conv3 Activation Norm",
207
+ "Activation", prob)
208
+
209
+
210
+ def plot_v4_act(norm, pos, prob):
211
+ return _heatmap_pil(norm, pos, _CMAP_ACT,
212
+ "V4 Model — conv3 Activation Norm",
213
+ "Activation", prob)
214
+
215
+
216
+ def plot_classic_act(norm, pos, prob):
217
+ return _heatmap_pil(norm, pos, _CMAP_ACT,
218
+ "Classic Model — conv3 Activation Norm",
219
+ "Activation", prob)
220
+
221
+
222
+ def plot_splice_distance(ref_seq: str, mut_pos: int):
223
+ seq = (ref_seq.upper() + "N" * 99)[:99]
224
+ scores = np.zeros(99)
225
+ donors, acceptors = [], []
226
+ for i in range(len(seq) - 1):
227
+ if seq[i:i+2] == "GT": donors.append(i)
228
+ if seq[i:i+2] == "AG": acceptors.append(i)
229
+ for p in donors:
230
+ for d in range(-8, 9):
231
+ if 0 <= p+d < 99:
232
+ scores[p+d] = max(scores[p+d], 0.5)
233
+ for p in acceptors:
234
+ for d in range(-8, 9):
235
+ if 0 <= p+d < 99:
236
+ scores[p+d] = max(scores[p+d], 0.5)
237
+ for p in donors:
238
+ if 0 <= p < 99: scores[p] = 1.0
239
+ for p in acceptors:
240
+ if 0 <= p < 99: scores[p] = max(scores[p], 0.8)
241
+ return _heatmap_pil(scores, mut_pos, _CMAP_SPLICE,
242
+ "Splice Distance Risk Heatmap — GT/AG dinucleotides",
243
+ "Splice risk")
244
+
245
+
246
+ def plot_gradient(attr, pos, label):
247
+ return _heatmap_pil(attr, pos, _CMAP_GRAD,
248
+ f"Gradient Attribution — {label}",
249
+ "Attribution")
250
+
251
+
252
+ def plot_counterfactual(cf: dict):
253
+ table = cf.get("table", [])
254
+ orig_p = cf.get("original_probability", 0)
255
+ if not table:
256
+ fig, ax = plt.subplots(figsize=(8, 3), facecolor=_BG)
257
+ ax.set_facecolor(_BG)
258
+ ax.text(0.5, 0.5, "Counterfactual analysis not available",
259
+ color=_TEXT, ha="center", va="center",
260
+ transform=ax.transAxes, fontsize=11)
261
+ ax.axis("off")
262
+ return _pil(fig)
263
+
264
+ labels = [r["mutation"] for r in table]
265
+ probs = [r["probability"] for r in table]
266
+ p_max = cf.get("max_probability", max(probs))
267
+ p_min = cf.get("min_probability", min(probs))
268
+ colors = [_RED if r["probability"] == p_max
269
+ else _BLUE if r["probability"] == p_min
270
+ else "#74add1"
271
+ for r in table]
272
+
273
+ fig, ax = plt.subplots(figsize=(10, 3.5), facecolor=_BG)
274
+ ax.set_facecolor(_SURF)
275
+ bars = ax.bar(labels, probs, color=colors,
276
+ edgecolor="#30363D", linewidth=0.7)
277
+ ax.axhline(0.5, color=_MUTED, linestyle="--",
278
+ linewidth=1.0, label="Decision boundary (0.5)")
279
+ ax.axhline(orig_p, color=_ORG, linestyle="-.", linewidth=1.5,
280
+ label=f"Original mutation ({orig_p:.3f})")
281
+ ax.set_ylim(0, 1.05)
282
+ ax.set_xlabel("Alternative mutation", color=_TEXT, fontsize=10)
283
+ ax.set_ylabel("Pathogenicity probability", color=_TEXT, fontsize=10)
284
+ ax.tick_params(colors=_TEXT)
285
+ for sp in ["top", "right"]:
286
+ ax.spines[sp].set_visible(False)
287
+ ax.spines["left"].set_color("#333")
288
+ ax.spines["bottom"].set_color("#333")
289
+ for bar, p in zip(bars, probs):
290
+ ax.text(bar.get_x() + bar.get_width()/2,
291
+ bar.get_height() + 0.015,
292
+ f"{p:.3f}", ha="center", va="bottom",
293
+ fontsize=8, color=_TEXT)
294
+ ax.legend(fontsize=8, facecolor=_BG, labelcolor=_TEXT, framealpha=0.6)
295
+ ax.set_title(
296
+ f"Counterfactual Analysis | "
297
+ f"Causal importance: {cf.get('probability_range', 0):.4f} | "
298
+ f"Range: {p_min:.3f}–{p_max:.3f}",
299
+ color=_TEXT, fontsize=9, loc="left", pad=4, fontweight="bold")
300
+ fig.tight_layout()
301
+ return _pil(fig)
302
+
303
+
304
+ def plot_ablation(abl: dict):
305
+ keys = ["splice_delta", "region_delta", "mutation_delta", "sequence_delta"]
306
+ pkeys = ["splice_pct", "region_pct", "mutation_pct", "sequence_pct"]
307
+ labels = [
308
+ "Splice features\n(donor/acceptor/region)",
309
+ "Region flags\n(exon/intron)",
310
+ "Mutation type\n(one-hot)",
311
+ "Sequence context\n(conv features)",
312
+ ]
313
+ colors = [_RED, _ORG, _BLUE, _GREEN]
314
+ deltas = [abl.get(k, 0.0) for k in keys]
315
+ pcts = [abl.get(k, 0.0) for k in pkeys]
316
+
317
+ fig, ax = plt.subplots(figsize=(10, 3.5), facecolor=_BG)
318
+ ax.set_facecolor(_SURF)
319
+ bars = ax.barh(labels, deltas, color=colors,
320
+ edgecolor="#30363D", linewidth=0.7)
321
+ ax.set_xlabel("Probability delta (causal effect)",
322
+ color=_TEXT, fontsize=9)
323
+ ax.tick_params(colors=_TEXT)
324
+ for sp in ["top", "right"]:
325
+ ax.spines[sp].set_visible(False)
326
+ ax.spines["left"].set_color("#333")
327
+ ax.spines["bottom"].set_color("#333")
328
+ for bar, d, p in zip(bars, deltas, pcts):
329
+ ax.text(bar.get_width() + 0.002,
330
+ bar.get_y() + bar.get_height()/2,
331
+ f" Δ{d:.4f} ({p}%)",
332
+ va="center", color=_TEXT, fontsize=8)
333
+ ax.set_xlim(0, max(deltas) * 1.65 + 0.02)
334
+ ax.set_title(
335
+ f"Feature Ablation Causal Analysis | "
336
+ f"Baseline: {abl.get('baseline_probability', 0):.4f}",
337
+ color=_TEXT, fontsize=9, loc="left", pad=4, fontweight="bold")
338
+ fig.tight_layout()
339
+ return _pil(fig)
340
+
341
+
342
+ def plot_xai_metrics(cross, sp_prob, v4_prob, cl_prob):
343
+ """4-panel XAI metrics dashboard."""
344
+ fig = plt.figure(figsize=(14, 7), facecolor=_BG)
345
+ gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.45, wspace=0.35)
346
+
347
+ # ── TL: per-model probs ───────────────────────────────────────────────────
348
+ ax0 = fig.add_subplot(gs[0, 0])
349
+ ax0.set_facecolor(_SURF)
350
+ names = ["Splice", "V4", "Classic"]
351
+ probs = [sp_prob, v4_prob, cl_prob]
352
+ col0 = [_RED if p >= 0.5 else _BLUE for p in probs]
353
+ bars0 = ax0.bar(names, probs, color=col0,
354
+ edgecolor="#30363D", linewidth=0.7, width=0.5)
355
+ ax0.axhline(0.5, color=_MUTED, linestyle="--", linewidth=1.0, alpha=0.7)
356
+ ax0.set_ylim(0, 1.1)
357
+ for bar, p in zip(bars0, probs):
358
+ ax0.text(bar.get_x() + bar.get_width()/2,
359
+ bar.get_height() + 0.02,
360
+ f"{p:.4f}", ha="center", va="bottom",
361
+ color=_TEXT, fontsize=9)
362
+ ax0.set_ylabel("Pathogenicity probability", color=_TEXT, fontsize=9)
363
+ ax0.tick_params(colors=_TEXT)
364
+ for sp in ["top", "right"]:
365
+ ax0.spines[sp].set_visible(False)
366
+ ax0.spines["left"].set_color("#333")
367
+ ax0.spines["bottom"].set_color("#333")
368
+ _style_ax(ax0, "Per-model Probability")
369
+
370
+ # ── TR: XAI scores ────────────────────────────────────────────────────────
371
+ ax1 = fig.add_subplot(gs[0, 1])
372
+ ax1.set_facecolor(_SURF)
373
+ xai_labels = [
374
+ "Mut Peak Ratio\n(÷3 norm)",
375
+ "CF Magnitude",
376
+ "Cross-Model\nLocality",
377
+ "Signal\nConcentration",
378
+ "Explainability\nStrength",
379
+ ]
380
+ xai_raw = [
381
+ cross["mutation_peak_ratio"],
382
+ cross["counterfactual_magnitude"],
383
+ cross["cross_model_locality_score"],
384
+ cross["signal_concentration_index"],
385
+ cross["explainability_strength_score"],
386
+ ]
387
+ xai_norm = [
388
+ min(cross["mutation_peak_ratio"] / 3.0, 1.0),
389
+ min(cross["counterfactual_magnitude"], 1.0),
390
+ (cross["cross_model_locality_score"] + 1.0) / 2.0,
391
+ cross["signal_concentration_index"],
392
+ cross["explainability_strength_score"],
393
+ ]
394
+ col1 = [_GREEN if v >= 0.5 else _ORG if v >= 0.3 else _RED
395
+ for v in xai_norm]
396
+ bars1 = ax1.barh(xai_labels, xai_norm, color=col1,
397
+ edgecolor="#30363D", linewidth=0.7)
398
+ ax1.set_xlim(0, 1.35)
399
+ ax1.tick_params(colors=_TEXT, labelsize=7)
400
+ for sp in ["top", "right"]:
401
+ ax1.spines[sp].set_visible(False)
402
+ ax1.spines["left"].set_color("#333")
403
+ ax1.spines["bottom"].set_color("#333")
404
+ for bar, raw in zip(bars1, xai_raw):
405
+ ax1.text(bar.get_width() + 0.02,
406
+ bar.get_y() + bar.get_height()/2,
407
+ f"{raw:.3f}", va="center", color=_TEXT, fontsize=8)
408
+ _style_ax(ax1, "XAI Engine Metrics (normalised 0–1)")
409
+
410
+ # ── BL: cross-model activation overlap ────────────────────────────────────
411
+ ax2 = fig.add_subplot(gs[1, 0])
412
+ ax2.set_facecolor(_SURF)
413
+ x = np.arange(99)
414
+ sp_n = cross.get("_splice_norm", np.zeros(99))
415
+ v4_n = cross.get("_v4_norm", np.zeros(99))
416
+ cl_n = cross.get("_classic_norm", np.zeros(99))
417
+ ax2.plot(x, sp_n, color=_RED, linewidth=1.2, alpha=0.85, label="Splice")
418
+ ax2.plot(x, v4_n, color=_BLUE, linewidth=1.2, alpha=0.85, label="V4")
419
+ ax2.plot(x, cl_n, color=_GREEN, linewidth=1.2, alpha=0.85, label="Classic")
420
+ ax2.set_ylim(0, 1.15)
421
+ ax2.set_xlabel("Position (99-bp window)", color=_TEXT, fontsize=8)
422
+ ax2.set_ylabel("Norm. activation", color=_TEXT, fontsize=8)
423
+ ax2.tick_params(colors=_TEXT, labelsize=7)
424
+ for sp in ["top", "right"]:
425
+ ax2.spines[sp].set_visible(False)
426
+ ax2.spines["left"].set_color("#333")
427
+ ax2.spines["bottom"].set_color("#333")
428
+ ax2.legend(fontsize=7, facecolor=_BG, labelcolor=_TEXT,
429
+ framealpha=0.6, loc="upper right")
430
+ _style_ax(ax2, "Cross-model Activation Overlap")
431
+
432
+ # ── BR: summary text ──────────────────────────────────────────────────────
433
+ ax3 = fig.add_subplot(gs[1, 1])
434
+ ax3.set_facecolor(_SURF)
435
+ ax3.axis("off")
436
+ summary = "\n".join([
437
+ f"Activation pattern : {cross['activation_pattern_type']}",
438
+ f"Model agreement : {cross['model_agreement']}",
439
+ f"Probability std : {cross['prob_std']:.4f}",
440
+ "",
441
+ f"Splice prob : {sp_prob:.4f}",
442
+ f"V4 prob : {v4_prob:.4f}",
443
+ f"Classic prob : {cl_prob:.4f}",
444
+ "",
445
+ f"ESS score : {cross['explainability_strength_score']:.4f}",
446
+ f"Cross-model loc. : {cross['cross_model_locality_score']:.4f}",
447
+ ])
448
+ ax3.text(0.05, 0.95, summary, transform=ax3.transAxes,
449
+ color=_TEXT, fontsize=8, va="top",
450
+ fontfamily="monospace",
451
+ bbox=dict(facecolor="#21262D", edgecolor="#30363D",
452
+ alpha=0.8, boxstyle="round,pad=0.4"))
453
+ _style_ax(ax3, "Summary")
454
+
455
+ return _pil(fig)
456
+
457
+
458
+ # ═══════════════════════════════════════════════════════════════════════════════
459
+ # Pipeline
460
+ # ═══════════════════════════════════════════════════════════════════════════════
461
+
462
+ _EMPTY_PIL = _empty_pil()
463
+
464
+
465
+ def run_pipeline(chrom: str, pos_str: str,
466
+ ref: str, alt: str,
467
+ exon_flag: int, intron_flag: int):
468
+ """
469
+ Full XAI pipeline. Returns 13 outputs for the Gradio UI.
470
+ The ordering of computation enforces explanation-before-prediction:
471
+ Step 3: extract all internal signals
472
+ Step 4: run explainability engine
473
+ Step 5: build unified decision (uses step-4 results)
474
+ """
475
+
476
+ def _err(msg):
477
+ empty = _empty_pil()
478
+ return (
479
+ f"❌ **Error**\n\n{msg}",
480
+ msg,
481
+ empty, empty, empty, empty, empty,
482
+ empty, empty, empty, empty,
483
+ "{}", None,
484
+ )
485
+
486
+ # ── Validate input ────────────────────────────────────────────────────────
487
+ try:
488
+ pos = int(str(pos_str).strip())
489
+ except ValueError:
490
+ return _err(f"Invalid position: '{pos_str}'")
491
+
492
+ ref = ref.strip().upper()
493
+ alt = alt.strip().upper()
494
+ if len(ref) != 1 or ref not in "ACGTN":
495
+ return _err(f"Ref base must be a single nucleotide. Got: '{ref}'")
496
+ if len(alt) != 1 or alt not in "ACGTN":
497
+ return _err(f"Alt base must be a single nucleotide. Got: '{alt}'")
498
+ if ref == alt:
499
+ return _err("Reference and alternate bases are identical.")
500
+
501
+ exon_flag = int(exon_flag)
502
+ intron_flag = int(intron_flag)
503
+
504
+ # ── Step 1: Fetch 401-bp → trim to 99-bp window from Ensembl ─────────────
505
+ logger.info(f"Fetching chr{chrom}:{pos} {ref}>{alt}")
506
+ try:
507
+ ref_seq, mut_seq, mut_win_pos = fetch_window(chrom, pos, ref, alt)
508
+ except Exception as exc:
509
+ logger.warning(f"Ensembl fetch failed ({exc}). Using synthetic window.")
510
+ ref_seq = "N" * 49 + ref + "N" * 49
511
+ mut_seq = "N" * 49 + alt + "N" * 49
512
+ mut_win_pos = 49
513
+
514
+ # ── Step 2: Load models ───────────────────────────────────────────────────
515
+ try:
516
+ splice_model = REGISTRY.splice
517
+ v4_model = REGISTRY.v4
518
+ classic_model = REGISTRY.classic
519
+ except Exception as exc:
520
+ return _err(f"Model loading failed: {exc}")
521
+
522
+ # ── Step 3: Extract internal signals ─────────────────────────────────────
523
+ try:
524
+ logger.info("Extracting splice signals …")
525
+ splice_sig = extract_splice_signals(
526
+ splice_model, ref_seq, mut_seq, exon_flag, intron_flag)
527
+ except Exception as exc:
528
+ return _err(f"Splice model failed: {exc}\n{traceback.format_exc()}")
529
+
530
+ try:
531
+ logger.info("Extracting V4 signals …")
532
+ v4_sig = extract_v4_signals(
533
+ v4_model, ref_seq, mut_seq, exon_flag, intron_flag)
534
+ except Exception as exc:
535
+ logger.warning(f"V4 model failed ({exc}), using fallback.")
536
+ v4_sig = V4Signals(
537
+ probability=0.5, conv3_norm=np.zeros(99),
538
+ gradient_attribution=np.zeros(99),
539
+ mutation_pos=mut_win_pos,
540
+ mutation_peak_ratio=0.0, signal_concentration=0.0,
541
+ )
542
+
543
+ try:
544
+ logger.info("Extracting classic signals …")
545
+ classic_sig = extract_classic_signals(
546
+ classic_model, ref_seq, mut_seq, exon_flag, intron_flag)
547
+ except Exception as exc:
548
+ logger.warning(f"Classic model failed ({exc}), using fallback.")
549
+ classic_sig = ClassicSignals(
550
+ probability=0.5, conv3_norm=np.zeros(99),
551
+ importance_head=0.0, region_imp=np.zeros(2),
552
+ mutation_pos=mut_win_pos,
553
+ mutation_peak_ratio=0.0, signal_concentration=0.0,
554
+ )
555
+
556
+ # ── Step 4: Explainability engine — MANDATORY before decision ─────────────
557
+ logger.info("Running explainability engine …")
558
+ cross = compute_cross_model_analysis(splice_sig, v4_sig, classic_sig)
559
+
560
+ # ── Step 5: Unified decision (uses cross results — ordering guaranteed) ───
561
+ logger.info("Building unified decision …")
562
+ result: DecisionResult = build_decision(
563
+ chrom, pos, ref, alt,
564
+ splice_sig, v4_sig, classic_sig, cross)
565
+
566
+ # ── Step 6: Build visualisations ──────────────────────────────────────────
567
+ try:
568
+ xai_metrics = plot_xai_metrics(
569
+ cross, splice_sig.probability,
570
+ v4_sig.probability, classic_sig.probability)
571
+ splice_act = plot_splice_act(
572
+ splice_sig.conv3_norm, splice_sig.mutation_pos,
573
+ splice_sig.probability)
574
+ splice_dist = plot_splice_distance(ref_seq, splice_sig.mutation_pos)
575
+ v4_act = plot_v4_act(
576
+ v4_sig.conv3_norm, v4_sig.mutation_pos, v4_sig.probability)
577
+ classic_act = plot_classic_act(
578
+ classic_sig.conv3_norm, classic_sig.mutation_pos,
579
+ classic_sig.probability)
580
+ v4_grad = plot_gradient(
581
+ v4_sig.gradient_attribution, v4_sig.mutation_pos, "V4")
582
+ splice_grad = plot_gradient(
583
+ splice_sig.gradient_attribution, splice_sig.mutation_pos,
584
+ "Splice")
585
+ cf_plot = plot_counterfactual(splice_sig.counterfactual)
586
+ abl_plot = plot_ablation(splice_sig.ablation)
587
+ except Exception as exc:
588
+ logger.error(f"Visualisation error: {exc}\n{traceback.format_exc()}")
589
+ empty = _empty_pil()
590
+ xai_metrics = splice_act = splice_dist = v4_act = empty
591
+ classic_act = v4_grad = splice_grad = cf_plot = abl_plot = empty
592
+
593
+ # ── Step 7: Downloadable JSON ─────────────────────────────────────────────
594
+ json_str = result.report_json
595
+ try:
596
+ tmp = tempfile.NamedTemporaryFile(
597
+ mode="w", suffix=".json",
598
+ prefix=f"mutation_xai_{chrom}_{pos}_{ref}{alt}_",
599
+ delete=False, encoding="utf-8")
600
+ tmp.write(json_str)
601
+ tmp.close()
602
+ dl_path = tmp.name
603
+ except Exception:
604
+ dl_path = None
605
+
606
+ # ── Step 8: Explanation-first summary markdown ────────────────────────────
607
+ cf = splice_sig.counterfactual
608
+ abl = splice_sig.ablation
609
+ sp = splice_sig
610
+ cross_loc = cross["cross_model_locality_score"]
611
+
612
+ prob_icon = "🔴" if result.unified_probability >= 0.5 else "🟢"
613
+ conf_icon = {"High": "✅", "Moderate": "⚠️", "Low": "🔶"}.get(
614
+ result.confidence, "❓")
615
+
616
+ summary_md = f"""
617
+ ### {prob_icon} `{result.variant}`
618
+
619
+ | Field | Value |
620
+ |---|---|
621
+ | **Risk Tier** | `{result.risk_tier}` · {result.tier_desc} |
622
+ | **Unified Probability** | `{result.unified_probability:.4f}` |
623
+ | **Dominant Mechanism** | `{result.dominant_mechanism}` |
624
+ | **Confidence** | {conf_icon} `{result.confidence}` |
625
+
626
+ ---
627
+
628
+ #### 🔬 Explainability Engine Output
629
+
630
+ | Metric | Raw Value | Interpretation |
631
+ |---|---|---|
632
+ | Mutation Peak Ratio | `{cross["mutation_peak_ratio"]:.4f}` | {"Strongly localised to mutation site" if cross["mutation_peak_ratio"] > 2 else "Above-average localisation" if cross["mutation_peak_ratio"] > 1 else "Diffuse — signal not mutation-centred"} |
633
+ | Counterfactual Magnitude | `{cross["counterfactual_magnitude"]:.4f}` | {"Strong position-level causality" if cross["counterfactual_magnitude"] > 0.25 else "Moderate causality" if cross["counterfactual_magnitude"] > 0.10 else "Weak positional causality"} |
634
+ | Cross-model Locality | `{cross["cross_model_locality_score"]:.4f}` | {"Models align on same region" if cross_loc > 0.5 else "Partial alignment" if cross_loc > 0 else "Models attend to different regions"} |
635
+ | Signal Concentration Index | `{cross["signal_concentration_index"]:.4f}` | Fraction of activation energy at mutation site |
636
+ | **Explainability Strength (ESS)** | **`{cross["explainability_strength_score"]:.4f}`** | 0–1 composite quality score |
637
+ | Activation Pattern | `{cross["activation_pattern_type"]}` | Shape of conv3 profile |
638
+ | Model Agreement | `{cross["model_agreement"]}` | std={cross["prob_std"]:.4f} across models |
639
+
640
+ ---
641
+
642
+ #### 📊 Per-model Probabilities
643
+
644
+ | Model | Probability |
645
+ |---|---|
646
+ | `mutation-predictor-splice` | `{sp.probability:.4f}` · {sp.risk_tier} |
647
+ | `mutation-predictor-v4` | `{v4_sig.probability:.4f}` |
648
+ | `mutation-pathogenicity-predictor` | `{classic_sig.probability:.4f}` |
649
+
650
+ ---
651
+
652
+ #### ⚗️ Splice Signals
653
+
654
+ | Signal | Value |
655
+ |---|---|
656
+ | Splice aura score | `{sp.splice_aura_score:.4f}` |
657
+ | Donor importance | `{float(sp.splice_imp[0]):.4f}` |
658
+ | Acceptor importance | `{float(sp.splice_imp[1]):.4f}` |
659
+ | Nearest GT donor | `{sp.dist_donor if sp.dist_donor is not None else "N/A"} bp` — {sp.splice_risk_donor} |
660
+ | Nearest AG acceptor | `{sp.dist_acceptor if sp.dist_acceptor is not None else "N/A"} bp` — {sp.splice_risk_acceptor} |
661
+ | Counterfactual delta | `{cf.get("probability_range", 0):.4f}` |
662
+ | Dominant ablation feature | `{abl.get("dominant_feature", "—")}` |
663
+ """
664
+
665
+ logger.info("Pipeline complete.")
666
+
667
+ return (
668
+ summary_md, # ① explanation summary — FIRST
669
+ result.final_explanation, # ② final human-readable explanation
670
+ xai_metrics, # ③ XAI metrics dashboard
671
+ splice_act, # ④ splice conv3 heatmap
672
+ splice_dist, # ⑤ splice distance heatmap
673
+ v4_act, # ⑥ v4 conv3 heatmap
674
+ classic_act, # ⑦ classic conv3 heatmap
675
+ v4_grad, # ⑧ v4 gradient attribution
676
+ splice_grad, # ⑨ splice gradient attribution
677
+ cf_plot, # ⑩ counterfactual chart
678
+ abl_plot, # ⑪ feature ablation chart
679
+ json_str, # ⑫ JSON report text
680
+ dl_path, # ⑬ downloadable file
681
+ )
682
+
683
+
684
+ # ═══════════════════════════════════════════════════════════════════════════════
685
+ # Gradio UI
686
+ # ═══════════════════════════════════════════════════════════════════════════════
687
+
688
+ CSS = """
689
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@300;400;600;700&display=swap');
690
+ :root {
691
+ --bg:#0D1117; --surface:#161B22; --border:#30363D;
692
+ --text:#E6EDF3; --muted:#7D8590;
693
+ --blue:#58A6FF; --green:#3FB950; --red:#F85149; --orange:#D29922;
694
+ --font:'Inter',system-ui; --mono:'JetBrains Mono',monospace;
695
+ }
696
+ body,.gradio-container{background:var(--bg)!important;color:var(--text)!important;font-family:var(--font)!important;}
697
+ .xai-header{background:linear-gradient(135deg,#0D1117 0%,#161B22 60%,#1a2332 100%);
698
+ border-bottom:1px solid var(--border);padding:2rem 2.5rem 1.5rem;margin-bottom:1.5rem;}
699
+ .xai-header h1{font-size:1.7rem;font-weight:700;letter-spacing:-.03em;margin:0 0 .3rem;}
700
+ .xai-header h1 em{color:var(--blue);font-style:normal;}
701
+ .xai-header p{color:var(--muted);font-size:.82rem;margin:0;}
702
+ .section-title{font-size:.68rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
703
+ color:var(--muted);border-bottom:1px solid var(--border);padding-bottom:.4rem;margin-bottom:1rem;}
704
+ .gradio-textbox input,.gradio-textbox textarea,.gradio-number input{
705
+ background:#161B22!important;border:1px solid var(--border)!important;
706
+ color:var(--text)!important;border-radius:6px!important;
707
+ font-family:var(--mono)!important;font-size:.88rem!important;}
708
+ label span{color:var(--muted)!important;font-size:.76rem!important;font-weight:500!important;}
709
+ .run-btn{background:linear-gradient(135deg,#1f6feb 0%,#388bfd 100%)!important;
710
+ border:none!important;color:white!important;font-weight:700!important;
711
+ font-size:.92rem!important;border-radius:6px!important;letter-spacing:.04em!important;}
712
+ .run-btn:hover{transform:translateY(-1px)!important;box-shadow:0 4px 14px rgba(88,166,255,.35)!important;}
713
+ .explanation-panel{border:1px solid var(--blue)!important;border-radius:8px!important;
714
+ background:rgba(88,166,255,.04)!important;padding:1rem!important;}
715
+ .gradio-markdown table{border-collapse:collapse;width:100%;font-size:.83rem;}
716
+ .gradio-markdown th{background:#161B22;color:var(--muted);font-size:.68rem;
717
+ letter-spacing:.08em;text-transform:uppercase;padding:.45rem .7rem;border:1px solid var(--border);}
718
+ .gradio-markdown td{padding:.42rem .7rem;border:1px solid var(--border);
719
+ font-family:var(--mono);font-size:.80rem;}
720
+ .gradio-markdown code{background:#161B22;padding:1px 5px;border-radius:3px;
721
+ font-family:var(--mono);color:var(--blue);font-size:.85em;}
722
+ .gradio-image img{border-radius:6px;border:1px solid var(--border);}
723
+ .gradio-tabs button{font-size:.80rem!important;color:var(--muted)!important;
724
+ border-bottom:2px solid transparent!important;background:transparent!important;}
725
+ .gradio-tabs button[aria-selected=true]{color:var(--blue)!important;border-bottom-color:var(--blue)!important;}
726
+ .gradio-textbox textarea{font-family:var(--mono)!important;font-size:.76rem!important;line-height:1.5!important;}
727
+ """
728
+
729
+ HEADER_HTML = """
730
+ <div class="xai-header">
731
+ <h1>Mutation <em>Explainability</em> Intelligence System</h1>
732
+ <p>
733
+ Three-model ensemble &nbsp;·&nbsp; Explanation always before prediction &nbsp;·&nbsp;
734
+ conv3 activations &nbsp;·&nbsp; gradient attribution &nbsp;·&nbsp;
735
+ counterfactual analysis &nbsp;·&nbsp; feature ablation &nbsp;·&nbsp;
736
+ splice distance &nbsp;·&nbsp; cross-model locality
737
+ </p>
738
+ </div>
739
+ """
740
+
741
+ EXAMPLES = [
742
+ ["17", "43071077", "G", "A", 1, 0],
743
+ ["11", "5226929", "T", "C", 1, 0],
744
+ ["7", "117548628","T", "A", 1, 0],
745
+ ["3", "37053577", "A", "C", 0, 1],
746
+ ["19", "44908684", "G", "T", 1, 0],
747
+ ]
748
+
749
+
750
+ def build_ui() -> gr.Blocks:
751
+ with gr.Blocks(
752
+ title="Mutation Explainability Intelligence System",
753
+ css=CSS,
754
+ ) as demo:
755
+
756
+ gr.HTML(HEADER_HTML)
757
+
758
+ with gr.Row(equal_height=False):
759
+
760
+ # ── INPUT PANEL ───────────────────────────────────────────────────
761
+ with gr.Column(scale=1, min_width=280):
762
+ gr.HTML('<div class="section-title">Variant Input</div>')
763
+ chrom_in = gr.Textbox(label="Chromosome",
764
+ value="17", max_lines=1)
765
+ pos_in = gr.Textbox(label="Position (hg38, 1-based)",
766
+ value="43071077", max_lines=1)
767
+ with gr.Row():
768
+ ref_in = gr.Textbox(label="Ref Base", value="G",
769
+ max_lines=1)
770
+ alt_in = gr.Textbox(label="Alt Base", value="A",
771
+ max_lines=1)
772
+ with gr.Row():
773
+ exon_in = gr.Radio([0, 1], label="Exon flag", value=1)
774
+ intron_in = gr.Radio([0, 1], label="Intron flag", value=0)
775
+
776
+ run_btn = gr.Button("▶ Analyse Variant",
777
+ variant="primary",
778
+ elem_classes="run-btn")
779
+
780
+ gr.HTML('<div class="section-title" style="margin-top:1rem">'
781
+ 'Examples</div>')
782
+ gr.Examples(
783
+ examples=EXAMPLES,
784
+ inputs=[chrom_in, pos_in, ref_in, alt_in,
785
+ exon_in, intron_in],
786
+ label="",
787
+ examples_per_page=5,
788
+ )
789
+
790
+ # ── OUTPUT PANEL ──────────────────────────────────────────────────
791
+ with gr.Column(scale=3, min_width=640):
792
+
793
+ # ══════════════════════════════════════════════════════════════
794
+ # ① EXPLANATION PANEL — ALWAYS RENDERED FIRST
795
+ # Prediction score does not appear without this panel
796
+ # ══════════════════════════════════════════════════════════════
797
+ gr.HTML('<div class="section-title">'
798
+ '① Explanation &amp; Signal Analysis</div>')
799
+
800
+ summary_out = gr.Markdown(
801
+ value=(
802
+ "*Run an analysis to see the full explanation.*\n\n"
803
+ "*This panel always renders **before** the prediction score.*"
804
+ ),
805
+ elem_classes="explanation-panel",
806
+ )
807
+
808
+ final_exp_out = gr.Textbox(
809
+ label="Final Explanation (grounded in internal signals)",
810
+ lines=10, max_lines=18,
811
+ show_copy_button=True,
812
+ )
813
+
814
+ # ── ② XAI Metrics Dashboard ───────────────────────────────────
815
+ gr.HTML('<div class="section-title" style="margin-top:1.5rem">'
816
+ '② Explainability Metrics Panel</div>')
817
+ xai_metrics_plot = gr.Image(label="XAI Metrics Dashboard")
818
+
819
+ # ── ③ Internal model signal tabs ──────────────────────────────
820
+ gr.HTML('<div class="section-title" style="margin-top:1.5rem">'
821
+ '③ Internal Model Signals</div>')
822
+
823
+ with gr.Tabs():
824
+ with gr.TabItem("🔬 Splice Model"):
825
+ splice_act_plot = gr.Image(
826
+ label="conv3 Activation Heatmap — Splice")
827
+ splice_dist_plot = gr.Image(
828
+ label="Splice Distance Risk Heatmap")
829
+ splice_grad_plot = gr.Image(
830
+ label="Gradient Attribution — Splice")
831
+
832
+ with gr.TabItem("🧬 V4 Model"):
833
+ v4_act_plot = gr.Image(
834
+ label="conv3 Activation Heatmap — V4")
835
+ v4_grad_plot = gr.Image(
836
+ label="Gradient Attribution — V4")
837
+
838
+ with gr.TabItem("📊 Classic Model"):
839
+ classic_act_plot = gr.Image(
840
+ label="conv3 Activation Heatmap — Classic")
841
+
842
+ with gr.TabItem("⚗️ Causal Analysis"):
843
+ cf_plot = gr.Image(
844
+ label="Counterfactual Mutation Analysis")
845
+ abl_plot = gr.Image(
846
+ label="Feature Ablation Causal Chart")
847
+
848
+ with gr.TabItem("📋 JSON Report"):
849
+ json_out = gr.Textbox(
850
+ label="Structured JSON Report",
851
+ lines=30, max_lines=60,
852
+ show_copy_button=True,
853
+ )
854
+ dl_btn = gr.File(
855
+ label="⬇ Download JSON Report")
856
+
857
+ # ── Wire all outputs ──────────────────────────────────────────────────
858
+ all_outputs = [
859
+ summary_out, # ① explanation summary (always first)
860
+ final_exp_out, # ① detailed explanation text
861
+ xai_metrics_plot, # ② XAI dashboard
862
+ splice_act_plot, # ③ splice tab
863
+ splice_dist_plot,
864
+ v4_act_plot, # ③ v4 tab
865
+ classic_act_plot, # ③ classic tab
866
+ v4_grad_plot,
867
+ splice_grad_plot,
868
+ cf_plot, # ③ causal tab
869
+ abl_plot,
870
+ json_out, # ③ JSON tab
871
+ dl_btn,
872
+ ]
873
+
874
+ run_btn.click(
875
+ fn=run_pipeline,
876
+ inputs=[chrom_in, pos_in, ref_in, alt_in,
877
+ exon_in, intron_in],
878
+ outputs=all_outputs,
879
+ show_progress=True,
880
+ )
881
+
882
+ gr.HTML("""
883
+ <div style="text-align:center;color:#7D8590;font-size:.70rem;
884
+ padding:1rem;margin-top:1rem;border-top:1px solid #30363D;">
885
+ Mutation Explainability Intelligence System
886
+ &nbsp;·&nbsp;
887
+ Models: nileshhanotia/{mutation-predictor-splice,
888
+ mutation-predictor-v4, mutation-pathogenicity-predictor}
889
+ &nbsp;·&nbsp; For Research Use Only &nbsp;·&nbsp;
890
+ Not for Clinical Diagnosis
891
+ </div>
892
+ """)
893
+
894
+ return demo
895
+
896
+
897
+ demo = build_ui()
898
+
899
+ if __name__ == "__main__":
900
+ demo.launch(
901
+ server_name="0.0.0.0",
902
+ server_port=7860,
903
+ show_error=True,
904
+ share=False,
905
+ )