PypCoder commited on
Commit
4446f33
Β·
verified Β·
1 Parent(s): 21c9bd3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +324 -73
app.py CHANGED
@@ -1,86 +1,337 @@
1
- """
2
- SERAPH Playground β€” Gradio app entry point.
3
-
4
- Run with: python app.py
5
- """
6
  import gradio as gr
 
 
 
 
 
7
 
8
- from config import PORTFOLIO_URL
9
- from dataset import PROTEIN_DATASET
10
- from html_builder import build_result_html
11
- from inference import predict_structure, on_preset_change
12
- from styles import CUSTOM_CSS
13
-
14
- with gr.Blocks(css=CUSTOM_CSS, title="SERAPH β€” Protein Playground") as demo:
15
-
16
- # Top Branding Navigation Bar (back button + title + status)
17
- gr.HTML(f"""
18
- <div style='display: flex; justify-content: space-between; align-items: center; padding: 12px 0; margin-bottom: 20px; border-bottom: 1px solid rgba(255,255,255,0.08);'>
19
- <a href="{PORTFOLIO_URL}" target="_top" class="back-to-portfolio">
20
- &larr; Back to Portfolio
21
- </a>
22
- <div style='font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 1.1rem; color: #f5f5f7;'>
23
- SERAPH <span style='font-size: 0.75rem; font-family: "JetBrains Mono"; color: #8e8e93; font-weight: 400; margin-left: 8px;'>v1.0 β€’ ESM-2 + BiLSTM</span>
24
- </div>
25
- <div style='font-family: "JetBrains Mono", monospace; font-size: 0.72rem; padding: 4px 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 99px; color: #8e8e93;'>
26
- 🟒 Model Live
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  </div>
29
- """)
30
 
31
- gr.HTML("""
32
- <h1 class="hero-title">SERAPH Playground</h1>
33
- <p class="hero-subtitle">Predict 3-state protein secondary structure (Alpha Helix, Beta Sheet, Coil) using fine-tuned ESM-2 protein language embeddings.</p>
34
- """)
35
 
36
- with gr.Row():
37
- preset_dropdown = gr.Dropdown(
38
- choices=["-- Select a Preloaded Protein Preset (50 Available) --"] + [p["name"] for p in PROTEIN_DATASET],
39
- value="01. Human Myoglobin (Oxygen Storage)",
40
- label="SELECT FROM 50 PRELOADED PROTEIN STRUCTURES",
41
- interactive=True
42
- )
 
 
 
 
 
43
 
44
- with gr.Row():
45
- sequence_input = gr.Textbox(
46
- value=PROTEIN_DATASET[0]["sequence"],
47
- label="AMINO ACID SEQUENCE (FASTA / IUPAC)",
48
- lines=3,
49
- placeholder="Type or paste amino acid sequence..."
50
- )
51
 
52
- with gr.Row():
53
- predict_btn = gr.Button("Run SERAPH Prediction ⚑", elem_classes=["btn-magnetic"])
54
-
55
- output_html = gr.HTML(
56
- value=build_result_html(
57
- PROTEIN_DATASET[0]["sequence"],
58
- "C" * len(PROTEIN_DATASET[0]["sequence"]), # placeholder before click
59
- PROTEIN_DATASET[0]["true_ss"],
60
- None,
61
- PROTEIN_DATASET[0]["description"],
62
- PROTEIN_DATASET[0]["fun_fact"]
63
- )
64
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- # Event Handlers
67
- preset_dropdown.change(
68
- fn=on_preset_change,
69
- inputs=[preset_dropdown],
70
- outputs=[sequence_input]
71
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- predict_btn.click(
74
- fn=predict_structure,
75
- inputs=[sequence_input, preset_dropdown],
76
- outputs=[output_html, gr.State(), gr.State()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  )
78
 
79
- gr.HTML("""
80
- <div style='border-top: 1px solid rgba(255,255,255,0.08); margin-top: 40px; padding-top: 20px; text-align: center; font-size: 0.8rem; color: #55555a; font-family: "Plus Jakarta Sans";'>
81
- Built with PyTorch &amp; Hugging Face Spaces
82
- </div>
83
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- if __name__ == "__main__":
86
- demo.launch()
 
 
 
 
 
 
1
  import gradio as gr
2
+ import spaces
3
+ import torch
4
+ import torch.nn as nn
5
+ from huggingface_hub import hf_hub_download
6
+ from transformers import EsmModel, EsmTokenizer
7
 
8
+ from dataset import PROTEIN_DATASET, PROTEIN_MAP
9
+
10
+ # ──────────────────────────────────────────────────────────────────────────
11
+ # Model config
12
+ # ──────────────────────────────────────────────────────────────────────────
13
+ HF_REPO_ID = "PypCoder/SERAPH"
14
+ WEIGHTS_FILE = "SERAPH.pth"
15
+ ESM_MODEL_ID = "facebook/esm2_t6_8M_UR50D"
16
+ IDX_TO_LABEL = {0: 'H', 1: 'E', 2: 'C'}
17
+ LABEL_NAME = {'H': 'Alpha Helix', 'E': 'Beta Sheet', 'C': 'Coil / Loop'}
18
+
19
+ CUSTOM_LABEL = "✎ Custom Sequence"
20
+ PROTEIN_CHOICES = [CUSTOM_LABEL] + [p["name"] for p in PROTEIN_DATASET]
21
+
22
+
23
+ class SERAPH(nn.Module):
24
+ def __init__(self, esm_model, conv_channels=256, kernel_size=7,
25
+ lstm_hidden=256, num_classes=3, dropout=0.3, freeze_esm=True):
26
+ super().__init__()
27
+ self.esm = esm_model
28
+ if freeze_esm:
29
+ for param in self.esm.encoder.layer[:-2].parameters():
30
+ param.requires_grad = False
31
+ esm_embed_dim = self.esm.config.hidden_size
32
+ self.conv = nn.Conv1d(esm_embed_dim, conv_channels, kernel_size=kernel_size, padding=kernel_size // 2)
33
+ self.bn = nn.BatchNorm1d(conv_channels)
34
+ self.dropout = nn.Dropout(dropout)
35
+ self.bilstm = nn.LSTM(conv_channels, lstm_hidden, num_layers=2, batch_first=True, bidirectional=True)
36
+ self.fc = nn.Linear(lstm_hidden * 2, num_classes)
37
+
38
+ def forward(self, input_ids, attention_mask=None):
39
+ x = self.esm(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
40
+ x = x.transpose(1, 2)
41
+ x = torch.relu(self.bn(self.conv(x)))
42
+ x = self.dropout(x)
43
+ x = x.transpose(1, 2)
44
+ x, _ = self.bilstm(x)
45
+ x = self.dropout(x)
46
+ return self.fc(x)
47
+
48
+
49
+ print("Loading ESM2 backbone...")
50
+ esm = EsmModel.from_pretrained(ESM_MODEL_ID)
51
+ tokenizer = EsmTokenizer.from_pretrained(ESM_MODEL_ID)
52
+
53
+ print("Downloading SERAPH weights...")
54
+ weights_path = hf_hub_download(repo_id=HF_REPO_ID, filename=WEIGHTS_FILE)
55
+ checkpoint = torch.load(weights_path, map_location="cpu")
56
+
57
+ model = SERAPH(esm_model=esm)
58
+ model.load_state_dict(checkpoint["model_state_dict"])
59
+ model.eval()
60
+ print("SERAPH ready.")
61
+
62
+
63
+ # ──────────────────────────────────────────────────────────────────────────
64
+ # Inference
65
+ # ──────────────────────────────────────────────────────────────────────────
66
+ @spaces.GPU
67
+ def run_model(sequence: str) -> str:
68
+ tokens = tokenizer(sequence, return_tensors="pt", truncation=True, max_length=512)
69
+ with torch.no_grad():
70
+ output = model(input_ids=tokens["input_ids"], attention_mask=tokens["attention_mask"])
71
+ preds = output.argmax(dim=-1)[0]
72
+ labels = [IDX_TO_LABEL[p.item()] for p in preds[1:-1]]
73
+ return "".join(labels)
74
+
75
+
76
+ def render_alignment(sequence: str, prediction: str, true_ss: str = None) -> str:
77
+ """Builds the responsive residue-by-residue alignment strip."""
78
+ cols = []
79
+ for i, aa in enumerate(sequence):
80
+ pred_cls = f"ss-{prediction[i].lower()}"
81
+ mismatch = " mismatch" if true_ss and i < len(true_ss) and prediction[i] != true_ss[i] else ""
82
+ true_block = f'<div class="ss-block ss-{true_ss[i].lower()} true-row"></div>' if true_ss else ""
83
+ cols.append(
84
+ f'<div class="residue-col">'
85
+ f'<span class="aa">{aa}</span>'
86
+ f'<div class="ss-block {pred_cls}{mismatch}"></div>'
87
+ f'{true_block}'
88
+ f'</div>'
89
+ )
90
+ return f'<div class="alignment-strip">{"".join(cols)}</div>'
91
+
92
+
93
+ def render_legend(show_true: bool) -> str:
94
+ rows = f"""
95
+ <div class="legend">
96
+ <span class="legend-item"><span class="swatch ss-h"></span>Helix (H)</span>
97
+ <span class="legend-item"><span class="swatch ss-e"></span>Sheet (E)</span>
98
+ <span class="legend-item"><span class="swatch ss-c"></span>Coil (C)</span>
99
+ </div>
100
+ """
101
+ if show_true:
102
+ rows += '<div class="legend-note">Top block = predicted Β· bottom block = ground truth Β· red outline = mismatch</div>'
103
+ return rows
104
+
105
+
106
+ def render_stats(prediction: str, true_ss: str = None) -> str:
107
+ n = len(prediction)
108
+ h, e, c = prediction.count("H"), prediction.count("E"), prediction.count("C")
109
+ bars = f"""
110
+ <div class="stat-bars">
111
+ <div class="stat-row"><span class="stat-label">Helix (H)</span><div class="bar-track"><div class="bar-fill ss-h" style="width:{h/n*100:.1f}%"></div></div><span class="stat-pct">{h/n*100:.1f}%</span></div>
112
+ <div class="stat-row"><span class="stat-label">Sheet (E)</span><div class="bar-track"><div class="bar-fill ss-e" style="width:{e/n*100:.1f}%"></div></div><span class="stat-pct">{e/n*100:.1f}%</span></div>
113
+ <div class="stat-row"><span class="stat-label">Coil (C)</span><div class="bar-track"><div class="bar-fill ss-c" style="width:{c/n*100:.1f}%"></div></div><span class="stat-pct">{c/n*100:.1f}%</span></div>
114
+ </div>
115
+ """
116
+ accuracy_html = ""
117
+ if true_ss and len(true_ss) == n:
118
+ matches = sum(1 for a, b in zip(prediction, true_ss) if a == b)
119
+ acc = matches / n * 100
120
+ accuracy_html = f"""
121
+ <div class="accuracy-badge">
122
+ <span class="accuracy-label">Q3 Accuracy vs. known structure</span>
123
+ <span class="accuracy-value">{acc:.1f}%</span>
124
  </div>
125
+ """
126
+ return f'<div class="stats-panel">{bars}{accuracy_html}</div>'
127
+
128
+
129
+ def predict(sequence: str, selected_name: str):
130
+ sequence = (sequence or "").upper().strip()
131
+ if not sequence:
132
+ return '<div class="placeholder-msg">Enter or select a sequence, then hit Predict.</div>'
133
+ if len(sequence) > 512:
134
+ sequence = sequence[:512]
135
+
136
+ prediction = run_model(sequence)
137
+ true_ss = None
138
+ if selected_name and selected_name != CUSTOM_LABEL:
139
+ entry = PROTEIN_MAP.get(selected_name)
140
+ if entry and len(entry["true_ss"]) == len(prediction):
141
+ true_ss = entry["true_ss"]
142
+
143
+ alignment = render_alignment(sequence, prediction, true_ss)
144
+ legend = render_legend(true_ss is not None)
145
+ stats = render_stats(prediction, true_ss)
146
+
147
+ return f"""
148
+ <div class="result-card">
149
+ <div class="result-header">
150
+ <span>Predicted Structure</span>
151
+ <span class="result-length">{len(sequence)} residues</span>
152
+ </div>
153
+ {alignment}
154
+ {legend}
155
+ {stats}
156
  </div>
157
+ """
158
 
 
 
 
 
159
 
160
+ def load_preset(selected_name: str):
161
+ if not selected_name or selected_name == CUSTOM_LABEL:
162
+ return "", '<div class="info-card empty">Pick a preset above to see its background, or paste your own sequence.</div>'
163
+ p = PROTEIN_MAP[selected_name]
164
+ info_html = f"""
165
+ <div class="info-card">
166
+ <div class="info-title">{p['name']}</div>
167
+ <p class="info-desc">{p['description']}</p>
168
+ <div class="fun-fact">πŸ’‘ {p['fun_fact']}</div>
169
+ </div>
170
+ """
171
+ return p["sequence"], info_html
172
 
 
 
 
 
 
 
 
173
 
174
+ # ──────────────────────────────────────────────────────────────────────────
175
+ # Theme + CSS (matches playground.html design language)
176
+ # ──────────────────────────────────────────────────────────────────────────
177
+ THEME = gr.themes.Base(
178
+ font=[gr.themes.GoogleFont("Plus Jakarta Sans"), "sans-serif"],
179
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
180
+ ).set(
181
+ body_background_fill="#060608",
182
+ body_background_fill_dark="#060608",
183
+ body_text_color="#f5f5f7",
184
+ body_text_color_dark="#f5f5f7",
185
+ background_fill_primary="rgba(255,255,255,0.025)",
186
+ background_fill_primary_dark="rgba(255,255,255,0.025)",
187
+ background_fill_secondary="rgba(255,255,255,0.025)",
188
+ block_background_fill="rgba(255,255,255,0.025)",
189
+ block_background_fill_dark="rgba(255,255,255,0.025)",
190
+ block_border_color="rgba(255,255,255,0.08)",
191
+ block_border_color_dark="rgba(255,255,255,0.08)",
192
+ block_label_text_color="#8e8e93",
193
+ block_label_text_color_dark="#8e8e93",
194
+ block_title_text_color="#f5f5f7",
195
+ body_text_color_subdued="#8e8e93",
196
+ input_background_fill="rgba(255,255,255,0.03)",
197
+ input_background_fill_dark="rgba(255,255,255,0.03)",
198
+ input_border_color="rgba(255,255,255,0.08)",
199
+ input_border_color_dark="rgba(255,255,255,0.08)",
200
+ button_primary_background_fill="#f5f5f7",
201
+ button_primary_background_fill_hover="#ffffff",
202
+ button_primary_text_color="#000000",
203
+ button_secondary_background_fill="rgba(255,255,255,0.05)",
204
+ button_secondary_background_fill_hover="rgba(255,255,255,0.09)",
205
+ button_secondary_text_color="#f5f5f7",
206
+ button_secondary_border_color="rgba(255,255,255,0.08)",
207
+ border_color_primary="rgba(255,255,255,0.08)",
208
+ color_accent_soft="rgba(255,255,255,0.05)",
209
+ )
210
 
211
+ CSS = """
212
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@600;700&family=JetBrains+Mono:wght@400;500&display=swap');
213
+
214
+ :root{
215
+ --struct-helix:#7c9eff;
216
+ --struct-sheet:#f59e0b;
217
+ --struct-coil:rgba(255,255,255,0.18);
218
+ --text-muted:#8e8e93;
219
+ --text-dim:#55555a;
220
+ --border-subtle:rgba(255,255,255,0.08);
221
+ }
222
+
223
+ .gradio-container{ max-width: 1020px !important; margin: 0 auto !important; }
224
+
225
+ #header-row{ display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px; margin-bottom: 6px; }
226
+ #back-btn{
227
+ display:inline-flex; align-items:center; gap:8px; text-decoration:none;
228
+ background: rgba(255,255,255,0.05); color:#f5f5f7; border:1px solid var(--border-subtle);
229
+ padding:8px 16px; border-radius:99px; font-size:0.82rem; font-weight:500;
230
+ transition: all 0.2s ease; white-space:nowrap;
231
+ }
232
+ #back-btn:hover{ background: rgba(255,255,255,0.09); border-color: rgba(255,255,255,0.22); }
233
+
234
+ .hero-title{
235
+ font-family:'Space Grotesk', sans-serif; font-weight:700;
236
+ font-size: clamp(1.9rem, 4.5vw, 2.8rem); letter-spacing:-0.02em; line-height:1.1;
237
+ background: linear-gradient(180deg,#ffffff 0%, rgba(255,255,255,0.7) 100%);
238
+ -webkit-background-clip:text; -webkit-text-fill-color:transparent; margin: 4px 0 2px 0;
239
+ }
240
+ .hero-subtitle{ color: var(--text-muted); font-size:0.95rem; max-width:640px; margin-bottom: 8px; }
241
+
242
+ .info-card{
243
+ background: rgba(255,255,255,0.025); border:1px solid var(--border-subtle); border-radius:14px;
244
+ padding:16px 18px; height:100%;
245
+ }
246
+ .info-card.empty{ display:flex; align-items:center; color: var(--text-dim); font-size:0.85rem; }
247
+ .info-title{ font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:1.02rem; margin-bottom:6px; }
248
+ .info-desc{ color: var(--text-muted); font-size:0.85rem; line-height:1.5; margin-bottom:10px; }
249
+ .fun-fact{
250
+ font-size:0.82rem; color:#f5f5f7; background: rgba(255,255,255,0.04);
251
+ border-left:2px solid var(--struct-sheet); padding:8px 10px; border-radius:6px; line-height:1.5;
252
+ }
253
+
254
+ .placeholder-msg{ color: var(--text-dim); font-size:0.85rem; padding: 24px 8px; text-align:center; }
255
 
256
+ .result-card{ border:1px solid var(--border-subtle); border-radius:14px; padding:18px; background: rgba(255,255,255,0.02); }
257
+ .result-header{
258
+ display:flex; justify-content:space-between; align-items:baseline; font-family:'Space Grotesk', sans-serif;
259
+ font-weight:700; font-size:1rem; margin-bottom:14px;
260
+ }
261
+ .result-length{ font-family:'JetBrains Mono', monospace; font-weight:400; font-size:0.75rem; color: var(--text-muted); }
262
+
263
+ .alignment-strip{ display:flex; flex-wrap:wrap; gap:2px; margin-bottom:14px; max-height: 320px; overflow-y:auto; padding-right:4px; }
264
+ .residue-col{ display:inline-flex; flex-direction:column; align-items:center; width:15px; font-family:'JetBrains Mono', monospace; }
265
+ .residue-col .aa{ font-size:10px; color: var(--text-muted); line-height:1.4; }
266
+ .ss-block{ width:100%; height:12px; border-radius:2px; margin-top:2px; }
267
+ .ss-block.true-row{ margin-top:1px; opacity:0.55; }
268
+ .ss-block.mismatch{ outline:1.5px solid #ef4444; outline-offset:-1px; }
269
+ .ss-h{ background: var(--struct-helix); }
270
+ .ss-e{ background: var(--struct-sheet); }
271
+ .ss-c{ background: var(--struct-coil); }
272
+
273
+ .legend{ display:flex; gap:16px; flex-wrap:wrap; margin-bottom:4px; }
274
+ .legend-item{ display:flex; align-items:center; gap:6px; font-size:0.75rem; color: var(--text-muted); }
275
+ .swatch{ width:10px; height:10px; border-radius:2px; display:inline-block; }
276
+ .legend-note{ font-size:0.72rem; color: var(--text-dim); margin-bottom:14px; }
277
+
278
+ .stats-panel{ margin-top:16px; padding-top:14px; border-top:1px solid var(--border-subtle); }
279
+ .stat-row{ display:flex; align-items:center; gap:10px; margin-bottom:8px; }
280
+ .stat-label{ width:70px; font-size:0.75rem; color: var(--text-muted); font-family:'JetBrains Mono', monospace; }
281
+ .bar-track{ flex:1; background: rgba(255,255,255,0.06); border-radius:99px; height:7px; overflow:hidden; }
282
+ .bar-fill{ height:100%; border-radius:99px; }
283
+ .stat-pct{ width:44px; text-align:right; font-size:0.75rem; font-family:'JetBrains Mono', monospace; color: var(--text-muted); }
284
+
285
+ .accuracy-badge{
286
+ display:flex; justify-content:space-between; align-items:center; margin-top:14px;
287
+ background: rgba(124,158,255,0.08); border:1px solid rgba(124,158,255,0.25);
288
+ border-radius:10px; padding:10px 14px;
289
+ }
290
+ .accuracy-label{ font-size:0.8rem; color: var(--text-muted); }
291
+ .accuracy-value{ font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:1.1rem; color:#7c9eff; }
292
+
293
+ .footer-note{ text-align:center; color: var(--text-dim); font-size:0.75rem; margin-top: 24px; padding-top:18px; border-top:1px solid var(--border-subtle); }
294
+
295
+ @media (max-width: 640px){
296
+ .residue-col{ width:13px; }
297
+ .hero-title{ font-size: 1.7rem; }
298
+ }
299
+ """
300
+
301
+ # ──────────────────────────────────────────────────────────────────────────
302
+ # UI
303
+ # ──────────────────────────────────────────────────────────────────────────
304
+ with gr.Blocks(theme=THEME, css=CSS, title="SERAPH β€” Protein Structure Prediction") as demo:
305
+
306
+ gr.HTML(
307
+ '<div id="header-row">'
308
+ '<a id="back-btn" href="https://muhammad-asad-ullah.vercel.app/" target="_self">'
309
+ '← Back to Portfolio</a>'
310
+ '</div>'
311
+ '<div class="hero-title">SERAPH β€” Protein Secondary Structure Prediction</div>'
312
+ '<div class="hero-subtitle">An ESM2 + Conv-BiLSTM model that predicts helix, sheet, and coil '
313
+ 'structure directly from an amino acid sequence. Pick one of 50 preloaded proteins or paste your own.</div>'
314
  )
315
 
316
+ with gr.Row():
317
+ with gr.Column(scale=3):
318
+ protein_dropdown = gr.Dropdown(
319
+ choices=PROTEIN_CHOICES, value=CUSTOM_LABEL,
320
+ label="Preset Protein", elem_id="protein-select",
321
+ )
322
+ sequence_box = gr.Textbox(
323
+ label="Amino Acid Sequence", placeholder="e.g. GLSDGEWQLVLNVWGKV...",
324
+ lines=4, elem_id="sequence-input",
325
+ )
326
+ predict_btn = gr.Button("Predict Structure", variant="primary")
327
+ with gr.Column(scale=2):
328
+ info_box = gr.HTML('<div class="info-card empty">Pick a preset above to see its background, or paste your own sequence.</div>')
329
+
330
+ result_box = gr.HTML('<div class="placeholder-msg">Enter or select a sequence, then hit Predict.</div>')
331
+
332
+ gr.HTML('<div class="footer-note">SERAPH Β· Built by Muhammad Asad Ullah (PypCoder)</div>')
333
+
334
+ protein_dropdown.change(fn=load_preset, inputs=protein_dropdown, outputs=[sequence_box, info_box])
335
+ predict_btn.click(fn=predict, inputs=[sequence_box, protein_dropdown], outputs=result_box)
336
 
337
+ demo.launch()