kevinky commited on
Commit
ffeeefa
·
verified ·
1 Parent(s): 1734936

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +753 -0
  2. packages.txt +1 -0
  3. requirements.txt +8 -0
app.py ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import threading
7
+ from itertools import groupby
8
+ from pathlib import Path
9
+ from tempfile import NamedTemporaryFile
10
+
11
+ import gradio as gr
12
+ import matplotlib.pyplot as plt
13
+ import numpy as np
14
+ import pandas as pd
15
+ import spaces
16
+ import torch
17
+ from transformers import T5EncoderModel, T5Tokenizer
18
+
19
+ # -----------------------------------------------------------------------------
20
+ # Reproducible upstream sources: these match the working TREAD Colab demo.
21
+ # -----------------------------------------------------------------------------
22
+ TREAD_REPO = "https://github.com/KYQiu21/TREAD.git"
23
+ TREAD_COMMIT = "7a7b3f571778cb89035c798c88df7eacb63ed2c1"
24
+ PROTT5_MODEL = "Rostlab/prot_t5_xl_half_uniref50-enc"
25
+
26
+ APP_DIR = Path(__file__).resolve().parent
27
+ TREAD_DIR = APP_DIR / ".tread_source"
28
+
29
+ # Web-demo guardrails only; these are not biological/model validity limits.
30
+ MAX_GPU_SEQUENCE_LENGTH = int(os.getenv("TREAD_MAX_GPU_SEQUENCE_LENGTH", "1200"))
31
+ MAX_CPU_SEQUENCE_LENGTH = int(os.getenv("TREAD_MAX_CPU_SEQUENCE_LENGTH", "600"))
32
+
33
+ REPEAT_HEADS = [
34
+ "Any repeat (segmentation head)",
35
+ "Alpha solenoid",
36
+ "TIM-barrel",
37
+ "Beta-propeller",
38
+ "Beta-barrel",
39
+ "Beta-solenoid",
40
+ "Alpha/beta solenoid",
41
+ ]
42
+ TYPE_HEAD_INDEX = {
43
+ "Alpha solenoid": 0,
44
+ "TIM-barrel": 1,
45
+ "Beta-propeller": 2,
46
+ "Beta-barrel": 3,
47
+ "Beta-solenoid": 4,
48
+ "Alpha/beta solenoid": 5,
49
+ }
50
+
51
+ EXAMPLE_REPEAT = (
52
+ "MMKRNILAVIVPALLVAGTANAAEIYNKDGNKVDLYGKAVGLHYFSKGNGENSYGGNGDMTYARLGFKGETQINSDLTGYGQWEYNFQGNNSEGADAQTGNKTRLAFAGLKYADVGSFDYGRNYGVVYDALGYTDMLPEFGGDTAYSDDFFVGRVGGVATYRNSNFFGLVDGLNFAVQYLGKNERDTARRSNGDGVGGSISYEYEGFGIVGAYGAADRTNLQEAQPLGNGKKAEQWATGLKYDANNIYLAANYGETRNATPITNKFTNTSGFANKTQDVLLVAQYQFDFGLRPSIAYTKSKAKDVEGIGDVDLVNYFEVGATYYFNKNMSTYVDYIINQIDSDNKLGVGSDDTVAVGIVYQF"
53
+ )
54
+ EXAMPLE_PROPELLER = (
55
+ "MEQVLYLGSYTKRESKGVHQIILDTDKKELRDYRLIAEVDSPTYLDLSADKGTLYSISKTDEGGGITSFKKNENGTYDKVAEISAEGSAPCYIHYDEDKKLIFTANYHGGYLTVYKENADGSFTMSDRAQHEGSSIHENQTIPHVHYSALSPDKKFLLACDLGTDEVYTYTVSDEGKLTEAARYKATPGTGPRHLVFHPNGKVAYLFGELSSDVEVLAYEAATGTFSLLQVITTIPAEHTGFNGGAAIRISADGKFVYASNRGHDSLVVYAVSEDGETLSLVEYVPTEGNTPRDFNLDPSGQFVIVAHQDSDNLTLFERDATTGKLTLVQKDVYAPECVCVFY"
56
+ )
57
+
58
+ # -----------------------------------------------------------------------------
59
+ # Model state
60
+ #
61
+ # ZeroGPU path:
62
+ # The GPU models are placed on "cuda" during app startup. In a ZeroGPU Space,
63
+ # Hugging Face's CUDA emulation handles this even though a real GPU is only
64
+ # assigned while a @spaces.GPU function is executing.
65
+ #
66
+ # CPU path:
67
+ # A separate float32 copy is loaded lazily only if somebody presses the CPU
68
+ # button. This avoids paying the RAM cost unless CPU fallback is actually used.
69
+ # -----------------------------------------------------------------------------
70
+ _gpu_tokenizer = None
71
+ _gpu_encoder = None
72
+ _gpu_repeat_model = None
73
+ _gpu_propeller_model = None
74
+ _gpu_init_error = None
75
+
76
+ _cpu_lock = threading.Lock()
77
+ _cpu_tokenizer = None
78
+ _cpu_encoder = None
79
+ _cpu_repeat_model = None
80
+ _cpu_propeller_model = None
81
+ _cpu_init_error = None
82
+
83
+
84
+ def _ensure_tread_source():
85
+ """Fetch exactly the same TREAD commit used by the public Colab demo."""
86
+ package_init = TREAD_DIR / "tread" / "__init__.py"
87
+ if package_init.is_file():
88
+ if str(TREAD_DIR) not in sys.path:
89
+ sys.path.insert(0, str(TREAD_DIR))
90
+ return
91
+
92
+ shutil.rmtree(TREAD_DIR, ignore_errors=True)
93
+ subprocess.run(["git", "init", str(TREAD_DIR)], check=True)
94
+ subprocess.run(
95
+ ["git", "-C", str(TREAD_DIR), "remote", "add", "origin", TREAD_REPO],
96
+ check=True,
97
+ )
98
+ subprocess.run(
99
+ [
100
+ "git",
101
+ "-C",
102
+ str(TREAD_DIR),
103
+ "fetch",
104
+ "--depth",
105
+ "1",
106
+ "origin",
107
+ TREAD_COMMIT,
108
+ ],
109
+ check=True,
110
+ )
111
+ subprocess.run(
112
+ ["git", "-C", str(TREAD_DIR), "checkout", "--detach", "FETCH_HEAD"],
113
+ check=True,
114
+ )
115
+
116
+ if not package_init.is_file():
117
+ raise FileNotFoundError(
118
+ f"TREAD installation incomplete: {package_init} not found"
119
+ )
120
+
121
+ if str(TREAD_DIR) not in sys.path:
122
+ sys.path.insert(0, str(TREAD_DIR))
123
+
124
+
125
+ def _build_repeat_model(DMDModel, device):
126
+ model = DMDModel(
127
+ per_resi_emb_dim=1024,
128
+ out_channel=64,
129
+ hidden_dim=64,
130
+ num_block=2,
131
+ dropout=0.2,
132
+ bilstm=True,
133
+ kernel_size_conv1=3,
134
+ kernel_size_block=7,
135
+ multi=True,
136
+ num_types=6,
137
+ device=device,
138
+ )
139
+ state = torch.load(
140
+ TREAD_DIR / "trained_model" / "linear-edge_model_repeatsdb.pt",
141
+ map_location="cpu",
142
+ weights_only=True,
143
+ )
144
+ model.load_state_dict(state)
145
+ model = model.to(device).eval()
146
+ return model
147
+
148
+
149
+ def _build_propeller_model(DMDModel, device):
150
+ model = DMDModel(
151
+ per_resi_emb_dim=1024,
152
+ out_channel=128,
153
+ hidden_dim=64,
154
+ num_block=1,
155
+ dropout=0.2,
156
+ bilstm=True,
157
+ kernel_size_conv1=11,
158
+ kernel_size_block=7,
159
+ device=device,
160
+ )
161
+ state = torch.load(
162
+ TREAD_DIR / "trained_model" / "linear-edge_model_propeller_blade.pt",
163
+ map_location="cpu",
164
+ weights_only=True,
165
+ )
166
+ model.load_state_dict(state)
167
+ model = model.to(device).eval()
168
+ return model
169
+
170
+
171
+ def initialize_gpu_models_at_startup():
172
+ """Prepare the ZeroGPU model copy on emulated CUDA during app startup."""
173
+ global _gpu_tokenizer, _gpu_encoder, _gpu_repeat_model
174
+ global _gpu_propeller_model, _gpu_init_error
175
+
176
+ try:
177
+ _ensure_tread_source()
178
+ from tread.model import DMDModel
179
+
180
+ _gpu_tokenizer = T5Tokenizer.from_pretrained(
181
+ PROTT5_MODEL,
182
+ do_lower_case=False,
183
+ legacy=True,
184
+ )
185
+ _gpu_encoder = T5EncoderModel.from_pretrained(PROTT5_MODEL)
186
+ _gpu_encoder = _gpu_encoder.to("cuda").eval()
187
+
188
+ _gpu_repeat_model = _build_repeat_model(DMDModel, torch.device("cuda"))
189
+ _gpu_propeller_model = _build_propeller_model(
190
+ DMDModel, torch.device("cuda")
191
+ )
192
+
193
+ print("ZeroGPU model copy initialized successfully.")
194
+
195
+ except Exception as exc:
196
+ # Do not kill the whole web app: CPU fallback may still work and the
197
+ # error will be shown clearly if the ZeroGPU button is pressed.
198
+ _gpu_init_error = (
199
+ f"{type(exc).__name__}: {exc}"
200
+ )
201
+ print("ZeroGPU initialization failed:", _gpu_init_error)
202
+
203
+
204
+ def initialize_cpu_models():
205
+ """Load a separate CPU copy only when the CPU fallback is requested."""
206
+ global _cpu_tokenizer, _cpu_encoder, _cpu_repeat_model
207
+ global _cpu_propeller_model, _cpu_init_error
208
+
209
+ if all(
210
+ x is not None
211
+ for x in (
212
+ _cpu_tokenizer,
213
+ _cpu_encoder,
214
+ _cpu_repeat_model,
215
+ _cpu_propeller_model,
216
+ )
217
+ ):
218
+ return
219
+
220
+ if _cpu_init_error is not None:
221
+ raise RuntimeError(_cpu_init_error)
222
+
223
+ with _cpu_lock:
224
+ if all(
225
+ x is not None
226
+ for x in (
227
+ _cpu_tokenizer,
228
+ _cpu_encoder,
229
+ _cpu_repeat_model,
230
+ _cpu_propeller_model,
231
+ )
232
+ ):
233
+ return
234
+
235
+ try:
236
+ _ensure_tread_source()
237
+ from tread.model import DMDModel
238
+
239
+ # Tokenizers are device independent. Reuse the one already loaded
240
+ # for ZeroGPU if available.
241
+ _cpu_tokenizer = _gpu_tokenizer
242
+ if _cpu_tokenizer is None:
243
+ _cpu_tokenizer = T5Tokenizer.from_pretrained(
244
+ PROTT5_MODEL,
245
+ do_lower_case=False,
246
+ legacy=True,
247
+ )
248
+
249
+ # CPU inference uses float32 for broad PyTorch CPU compatibility.
250
+ # low_cpu_mem_usage reduces peak RAM while loading this large model.
251
+ _cpu_encoder = T5EncoderModel.from_pretrained(
252
+ PROTT5_MODEL,
253
+ torch_dtype=torch.float32,
254
+ low_cpu_mem_usage=True,
255
+ )
256
+ _cpu_encoder = _cpu_encoder.to("cpu").eval()
257
+
258
+ _cpu_repeat_model = _build_repeat_model(
259
+ DMDModel, torch.device("cpu")
260
+ )
261
+ _cpu_propeller_model = _build_propeller_model(
262
+ DMDModel, torch.device("cpu")
263
+ )
264
+
265
+ print("CPU fallback model copy initialized successfully.")
266
+
267
+ except Exception as exc:
268
+ _cpu_init_error = f"{type(exc).__name__}: {exc}"
269
+ raise RuntimeError(_cpu_init_error) from exc
270
+
271
+
272
+ def clean_sequence(raw_sequence: str, max_length: int) -> str:
273
+ if raw_sequence is None:
274
+ raise gr.Error("Please paste a protein sequence.")
275
+
276
+ text = raw_sequence.strip()
277
+ if not text:
278
+ raise gr.Error("Please paste a protein sequence.")
279
+
280
+ # Accept either a raw sequence or a single FASTA record.
281
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
282
+ if lines and lines[0].startswith(">"):
283
+ lines = [line for line in lines[1:] if not line.startswith(">")]
284
+
285
+ sequence = "".join(lines)
286
+ sequence = re.sub(r"\s+", "", sequence).upper()
287
+
288
+ if not sequence:
289
+ raise gr.Error("No amino-acid sequence was found.")
290
+
291
+ # Standard 20 amino acids plus the symbols handled by the notebook's
292
+ # ProtT5 preprocessing (U, Z, O, B -> X) and X itself.
293
+ allowed = set("ACDEFGHIKLMNPQRSTVWYUZOBX")
294
+ invalid = sorted(set(sequence) - allowed)
295
+ if invalid:
296
+ raise gr.Error(
297
+ "Invalid sequence characters: "
298
+ + ", ".join(invalid)
299
+ + ". Paste amino-acid letters only (a FASTA header is allowed)."
300
+ )
301
+
302
+ if len(sequence) > max_length:
303
+ raise gr.Error(
304
+ f"This backend currently accepts up to {max_length} residues; "
305
+ f"your sequence has {len(sequence)} residues."
306
+ )
307
+
308
+ return sequence
309
+
310
+
311
+ def get_prott5_embedding(sequence, tokenizer, encoder, device):
312
+ processed = " ".join(list(re.sub(r"[UZOB]", "X", sequence)))
313
+ ids = tokenizer(
314
+ [processed],
315
+ add_special_tokens=True,
316
+ padding="longest",
317
+ truncation=False,
318
+ return_attention_mask=True,
319
+ )
320
+
321
+ input_ids = torch.tensor(ids["input_ids"], device=device)
322
+ attention_mask = torch.tensor(ids["attention_mask"], device=device)
323
+
324
+ with torch.inference_mode():
325
+ embedding_repr = encoder(
326
+ input_ids=input_ids,
327
+ attention_mask=attention_mask,
328
+ )
329
+
330
+ # Drop the terminal special token, exactly as in the working Colab demo.
331
+ emb = embedding_repr.last_hidden_state[0, :-1]
332
+
333
+ if emb.shape[0] != len(sequence):
334
+ raise RuntimeError(
335
+ f"ProtT5 returned {emb.shape[0]} residue embeddings for a "
336
+ f"{len(sequence)}-residue sequence."
337
+ )
338
+
339
+ return emb
340
+
341
+
342
+ def get_ranges(preds, cutoff1=0.5, min_len=15, cutoff2=0.5, frac2=0.5):
343
+ """Same motif-range logic as tread.utils.get_ranges."""
344
+ preds = np.asarray(preds).flatten()
345
+ above_threshold = preds > cutoff1
346
+ peaks = []
347
+
348
+ for key, group in groupby(enumerate(above_threshold), key=lambda x: x[1]):
349
+ if key:
350
+ group = list(group)
351
+ if len(group) >= min_len:
352
+ beg = group[0][0]
353
+ end = beg + len(group)
354
+ if (
355
+ len(np.where(preds[beg:end] > cutoff2)[0]) / len(group)
356
+ >= frac2
357
+ ):
358
+ peaks.append((beg, end))
359
+
360
+ return peaks
361
+
362
+
363
+ def make_plot(scores, ranges, title, cutoff):
364
+ scores = np.asarray(scores).flatten()
365
+ x = np.arange(1, len(scores) + 1)
366
+
367
+ fig, ax = plt.subplots(figsize=(10, 4.5), dpi=150)
368
+ ax.plot(x, scores, linewidth=1.5)
369
+ ax.axhline(
370
+ cutoff,
371
+ linestyle="--",
372
+ linewidth=1.0,
373
+ alpha=0.7,
374
+ label=f"cutoff = {cutoff:g}",
375
+ )
376
+ for start0, end0 in ranges:
377
+ ax.axvspan(start0 + 1, end0, alpha=0.18)
378
+
379
+ ax.set_xlim(1, max(1, len(scores)))
380
+ ax.set_ylim(0, 1)
381
+ ax.set_xlabel("Residue")
382
+ ax.set_ylabel("Residue score")
383
+ ax.set_title(title)
384
+ ax.legend(loc="upper right")
385
+ fig.tight_layout()
386
+
387
+ return fig
388
+
389
+
390
+ def make_download_table(sequence, scores, ranges):
391
+ scores = np.asarray(scores).flatten()
392
+ predicted = np.zeros(len(sequence), dtype=bool)
393
+
394
+ for start0, end0 in ranges:
395
+ predicted[start0:end0] = True
396
+
397
+ return pd.DataFrame(
398
+ {
399
+ "residue": np.arange(1, len(sequence) + 1),
400
+ "amino_acid": list(sequence),
401
+ "score": scores,
402
+ "predicted_motif": predicted,
403
+ }
404
+ )
405
+
406
+
407
+ def save_csv(df: pd.DataFrame):
408
+ temp = NamedTemporaryFile(
409
+ prefix="tread_prediction_",
410
+ suffix=".csv",
411
+ delete=False,
412
+ )
413
+ temp.close()
414
+ df.to_csv(temp.name, index=False)
415
+ return temp.name
416
+
417
+
418
+ def _predict_core(
419
+ sequence_text,
420
+ model_choice,
421
+ repeat_head,
422
+ cutoff,
423
+ min_len,
424
+ *,
425
+ tokenizer,
426
+ encoder,
427
+ repeat_model,
428
+ propeller_model,
429
+ device,
430
+ backend_label,
431
+ max_length,
432
+ ):
433
+ sequence = clean_sequence(sequence_text, max_length=max_length)
434
+
435
+ embedding = get_prott5_embedding(
436
+ sequence,
437
+ tokenizer=tokenizer,
438
+ encoder=encoder,
439
+ device=device,
440
+ )
441
+
442
+ with torch.inference_mode():
443
+ if model_choice == "RepeatsDB repeat annotation":
444
+ seg_prediction, type_prediction = repeat_model.predict_single(
445
+ embedding
446
+ )
447
+
448
+ if repeat_head == "Any repeat (segmentation head)":
449
+ scores = seg_prediction
450
+ else:
451
+ scores = type_prediction[TYPE_HEAD_INDEX[repeat_head]]
452
+
453
+ profile_name = repeat_head
454
+ title = f"TREAD RepeatsDB — {profile_name}"
455
+
456
+ else:
457
+ scores = propeller_model.predict_single(embedding)
458
+ profile_name = "Beta-propeller blade"
459
+ title = "TREAD — beta-propeller blade annotation"
460
+
461
+ ranges = get_ranges(
462
+ scores,
463
+ cutoff1=float(cutoff),
464
+ min_len=int(min_len),
465
+ )
466
+ fig = make_plot(scores, ranges, title, float(cutoff))
467
+
468
+ # Convert notebook's 0-based, end-exclusive ranges to user-facing
469
+ # 1-based inclusive residue coordinates.
470
+ if ranges:
471
+ ranges_df = pd.DataFrame(
472
+ [
473
+ {
474
+ "Start (1-based)": start0 + 1,
475
+ "End (1-based)": end0,
476
+ "Length": end0 - start0,
477
+ }
478
+ for start0, end0 in ranges
479
+ ]
480
+ )
481
+ range_text = ", ".join(f"{s + 1}–{e}" for s, e in ranges)
482
+ else:
483
+ ranges_df = pd.DataFrame(
484
+ columns=["Start (1-based)", "End (1-based)", "Length"]
485
+ )
486
+ range_text = "None at the current thresholds"
487
+
488
+ profile_df = make_download_table(sequence, scores, ranges)
489
+ csv_path = save_csv(profile_df)
490
+
491
+ status = (
492
+ f"**Sequence length:** {len(sequence)} aa \n"
493
+ f"**Model:** {model_choice} \n"
494
+ f"**Profile:** {profile_name} \n"
495
+ f"**Predicted motif ranges:** {range_text} \n"
496
+ f"**Compute backend:** {backend_label}"
497
+ )
498
+
499
+ return status, fig, ranges_df, csv_path
500
+
501
+
502
+ @spaces.GPU(duration=120)
503
+ def predict_gpu(sequence_text, model_choice, repeat_head, cutoff, min_len):
504
+ """ZeroGPU path. A real GPU is allocated only for this function call."""
505
+ if _gpu_init_error is not None:
506
+ raise gr.Error(
507
+ "The ZeroGPU model copy could not be initialized. "
508
+ f"Startup error: {_gpu_init_error}"
509
+ )
510
+
511
+ if any(
512
+ x is None
513
+ for x in (
514
+ _gpu_tokenizer,
515
+ _gpu_encoder,
516
+ _gpu_repeat_model,
517
+ _gpu_propeller_model,
518
+ )
519
+ ):
520
+ raise gr.Error(
521
+ "ZeroGPU models are not initialized. Check the Space runtime log."
522
+ )
523
+
524
+ device_name = "ZeroGPU"
525
+ try:
526
+ device_name = f"ZeroGPU — {torch.cuda.get_device_name(0)}"
527
+ except Exception:
528
+ pass
529
+
530
+ return _predict_core(
531
+ sequence_text,
532
+ model_choice,
533
+ repeat_head,
534
+ cutoff,
535
+ min_len,
536
+ tokenizer=_gpu_tokenizer,
537
+ encoder=_gpu_encoder,
538
+ repeat_model=_gpu_repeat_model,
539
+ propeller_model=_gpu_propeller_model,
540
+ device=torch.device("cuda"),
541
+ backend_label=device_name,
542
+ max_length=MAX_GPU_SEQUENCE_LENGTH,
543
+ )
544
+
545
+
546
+ def predict_cpu(sequence_text, model_choice, repeat_head, cutoff, min_len):
547
+ """CPU fallback. Does not consume ZeroGPU quota, but is much slower."""
548
+ try:
549
+ initialize_cpu_models()
550
+ except Exception as exc:
551
+ raise gr.Error(f"CPU model initialization failed: {exc}") from exc
552
+
553
+ return _predict_core(
554
+ sequence_text,
555
+ model_choice,
556
+ repeat_head,
557
+ cutoff,
558
+ min_len,
559
+ tokenizer=_cpu_tokenizer,
560
+ encoder=_cpu_encoder,
561
+ repeat_model=_cpu_repeat_model,
562
+ propeller_model=_cpu_propeller_model,
563
+ device=torch.device("cpu"),
564
+ backend_label="CPU fallback",
565
+ max_length=MAX_CPU_SEQUENCE_LENGTH,
566
+ )
567
+
568
+
569
+ def load_repeat_example():
570
+ return (
571
+ EXAMPLE_REPEAT,
572
+ "RepeatsDB repeat annotation",
573
+ "Beta-barrel",
574
+ 0.8,
575
+ 20,
576
+ )
577
+
578
+
579
+ def load_propeller_example():
580
+ return (
581
+ EXAMPLE_PROPELLER,
582
+ "Beta-propeller blade annotation",
583
+ "Beta-propeller",
584
+ 0.8,
585
+ 20,
586
+ )
587
+
588
+
589
+ # IMPORTANT for ZeroGPU:
590
+ # Prepare the CUDA/emulated-CUDA model copy at module level, before requests.
591
+ initialize_gpu_models_at_startup()
592
+
593
+
594
+ with gr.Blocks(title="TREAD — Protein Repeat Annotation") as demo:
595
+ gr.Markdown(
596
+ """
597
+ # TREAD — Protein Repeat Annotation
598
+
599
+ Paste a protein sequence and run one of the two pretrained TREAD models.
600
+
601
+ - **RepeatsDB repeat annotation:** residue-wise repeat segmentation plus six repeat-fold heads.
602
+ - **Beta-propeller blade annotation:** residue-wise blade prediction.
603
+
604
+ **Recommended:** use **Run with ZeroGPU**.
605
+ **Fallback:** use **Run on CPU** if GPU quota/availability is a problem; CPU inference is substantially slower.
606
+
607
+ ProtT5 embeddings are generated on the fly with `Rostlab/prot_t5_xl_half_uniref50-enc`.
608
+ """
609
+ )
610
+
611
+ with gr.Row():
612
+ with gr.Column(scale=3):
613
+ sequence_input = gr.Textbox(
614
+ label="Protein sequence",
615
+ lines=10,
616
+ placeholder=(
617
+ "Paste a raw amino-acid sequence or a single FASTA record..."
618
+ ),
619
+ )
620
+ model_choice = gr.Radio(
621
+ choices=[
622
+ "RepeatsDB repeat annotation",
623
+ "Beta-propeller blade annotation",
624
+ ],
625
+ value="RepeatsDB repeat annotation",
626
+ label="Model",
627
+ )
628
+ repeat_head = gr.Dropdown(
629
+ choices=REPEAT_HEADS,
630
+ value="Beta-barrel",
631
+ label="RepeatsDB profile to display",
632
+ info=(
633
+ "Used only for the RepeatsDB model. "
634
+ "The default matches the public Colab example."
635
+ ),
636
+ )
637
+
638
+ with gr.Column(scale=2):
639
+ cutoff = gr.Slider(
640
+ minimum=0.0,
641
+ maximum=1.0,
642
+ value=0.8,
643
+ step=0.01,
644
+ label="Residue score threshold",
645
+ )
646
+ min_len = gr.Slider(
647
+ minimum=1,
648
+ maximum=100,
649
+ value=20,
650
+ step=1,
651
+ label="Minimum motif length",
652
+ )
653
+
654
+ gpu_button = gr.Button(
655
+ "Run with ZeroGPU (recommended)",
656
+ variant="primary",
657
+ )
658
+ cpu_button = gr.Button(
659
+ "Run on CPU (slow fallback)",
660
+ variant="secondary",
661
+ )
662
+
663
+ with gr.Row():
664
+ repeat_example_button = gr.Button("Load RepeatsDB example")
665
+ propeller_example_button = gr.Button("Load propeller example")
666
+
667
+ gr.Markdown("## Results")
668
+ status_output = gr.Markdown()
669
+ plot_output = gr.Plot(label="Residue-wise score profile")
670
+ ranges_output = gr.Dataframe(
671
+ headers=["Start (1-based)", "End (1-based)", "Length"],
672
+ label="Predicted motif ranges",
673
+ interactive=False,
674
+ )
675
+ download_output = gr.File(label="Download per-residue CSV")
676
+
677
+ gr.Markdown(
678
+ f"""
679
+ ### Notes
680
+
681
+ - ZeroGPU accepts sequences up to **{MAX_GPU_SEQUENCE_LENGTH} aa** in this web demo.
682
+ - CPU fallback accepts sequences up to **{MAX_CPU_SEQUENCE_LENGTH} aa** by default because ProtT5-XL is very slow on the free CPU backend.
683
+ - The range-calling logic uses the same thresholding rule as the public Colab/TREAD utility.
684
+ - The web table reports **1-based inclusive** residue coordinates for readability.
685
+ - Rare/ambiguous residues `U`, `Z`, `O`, and `B` are mapped to `X` for ProtT5 embedding, matching the Colab demo.
686
+
687
+ Source code and pretrained TREAD checkpoints: [KYQiu21/TREAD](https://github.com/KYQiu21/TREAD)
688
+ """
689
+ )
690
+
691
+ gpu_button.click(
692
+ fn=predict_gpu,
693
+ inputs=[
694
+ sequence_input,
695
+ model_choice,
696
+ repeat_head,
697
+ cutoff,
698
+ min_len,
699
+ ],
700
+ outputs=[
701
+ status_output,
702
+ plot_output,
703
+ ranges_output,
704
+ download_output,
705
+ ],
706
+ )
707
+
708
+ cpu_button.click(
709
+ fn=predict_cpu,
710
+ inputs=[
711
+ sequence_input,
712
+ model_choice,
713
+ repeat_head,
714
+ cutoff,
715
+ min_len,
716
+ ],
717
+ outputs=[
718
+ status_output,
719
+ plot_output,
720
+ ranges_output,
721
+ download_output,
722
+ ],
723
+ )
724
+
725
+ repeat_example_button.click(
726
+ fn=load_repeat_example,
727
+ inputs=[],
728
+ outputs=[
729
+ sequence_input,
730
+ model_choice,
731
+ repeat_head,
732
+ cutoff,
733
+ min_len,
734
+ ],
735
+ )
736
+
737
+ propeller_example_button.click(
738
+ fn=load_propeller_example,
739
+ inputs=[],
740
+ outputs=[
741
+ sequence_input,
742
+ model_choice,
743
+ repeat_head,
744
+ cutoff,
745
+ min_len,
746
+ ],
747
+ )
748
+
749
+
750
+ demo.queue()
751
+
752
+ if __name__ == "__main__":
753
+ demo.launch()
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ git
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch==2.9.1
2
+ transformers==4.57.6
3
+ huggingface-hub==0.36.2
4
+ accelerate>=1,<2
5
+ sentencepiece>=0.2
6
+ numpy>=1.26,<3
7
+ pandas>=2.0,<3
8
+ matplotlib>=3.8