Michele Garibbo commited on
Commit
f51792f
·
1 Parent(s): 1086db0
Files changed (1) hide show
  1. app.py +138 -37
app.py CHANGED
@@ -5,11 +5,6 @@ Upload a FASTA of homologous sequences. If more than 15 are given, 15 are
5
  randomly sampled and used to prompt AI4PD/ProtGPT3-MSA, which then generates
6
  N new family-consistent sequences in parallel. Generation stops at EOS or the
7
  <s> separator, so each output is exactly one new sequence.
8
-
9
- DEPLOY:
10
- 1. Create a new Space (SDK = Gradio) at huggingface.co/new-space.
11
- 2. Add this file as `app.py` and requirements.txt next to it.
12
- 3. It builds and goes live. Free CPU is enough; ZeroGPU/GPU is faster.
13
  """
14
 
15
  import random
@@ -26,7 +21,6 @@ MODEL_ID = "AI4PD/ProtGPT3-MSA"
26
  MAX_CONTEXT = 15 # the model is prompted with at most 15 homologs
27
  DIRECTION = "1" # "1" = N-to-C (fixed; "2" would need reversed seqs)
28
 
29
- # bf16 on GPU; fall back to fp32 on the free CPU tier (bf16 CPU ops are spotty)
30
  DTYPE = torch.bfloat16 if torch.cuda.is_available() else torch.float32
31
 
32
  tokenizer = AutoTokenizer.from_pretrained(
@@ -44,7 +38,6 @@ model = AutoModelForCausalLM.from_pretrained(
44
  )
45
  model.eval()
46
 
47
- # Stop on EOS *or* the sequence separator -> only one sequence is generated
48
  S_TOKEN_ID = tokenizer.convert_tokens_to_ids("<s>")
49
  EOS_IDS = [i for i in [tokenizer.eos_token_id, S_TOKEN_ID] if i is not None]
50
  PAD_ID = tokenizer.pad_token_id or tokenizer.eos_token_id or S_TOKEN_ID
@@ -75,9 +68,9 @@ def build_prompt(sequences: list, gap: bool = False, direction: str = "1") -> st
75
 
76
  tokens = ["<|bos|>", direction, gap_token]
77
  for seq in sequences:
78
- tokens.append("<s>") # separator before each seq
79
  tokens.extend(list(process_style(seq, gap=gap)))
80
- tokens.append("<s>") # train-time separator before continuation
81
  return " ".join(tokens)
82
 
83
 
@@ -120,7 +113,6 @@ def generate(fasta_file, n_sequences, max_new_tokens, temperature, top_p,
120
  random.seed(int(seed))
121
  torch.manual_seed(int(seed))
122
 
123
- # Randomly sample up to 15 homologs for the prompt
124
  context = random.sample(seqs, MAX_CONTEXT) if len(seqs) > MAX_CONTEXT else seqs
125
 
126
  try:
@@ -137,12 +129,11 @@ def generate(fasta_file, n_sequences, max_new_tokens, temperature, top_p,
137
  do_sample=True,
138
  temperature=float(temperature),
139
  top_p=float(top_p),
140
- eos_token_id=EOS_IDS, # stop at EOS or <s> -> one sequence
141
  pad_token_id=PAD_ID,
142
  num_return_sequences=int(n_sequences),
143
  )
144
 
145
- # Keep only the newly generated continuation (strip the prompt prefix)
146
  generated = output_ids[:, prompt_len:]
147
  records_out = []
148
  for i, row in enumerate(generated, 1):
@@ -164,34 +155,144 @@ def generate(fasta_file, n_sequences, max_new_tokens, temperature, top_p,
164
  return fasta_text, out_path
165
 
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  # --------------------------------------------------------------------------- #
168
  # UI
169
  # --------------------------------------------------------------------------- #
170
- demo = gr.Interface(
171
- fn=generate,
172
- inputs=[
173
- gr.File(label="Homologs (FASTA)", file_types=[".fasta", ".fa", ".txt"],
174
- type="filepath"),
175
- gr.Slider(1, 100, value=20, step=1, label="N sequences to generate"),
176
- gr.Slider(16, 1024, value=512, step=16, label="Max new tokens (per sequence)"),
177
- gr.Slider(0.1, 2.0, value=0.8, step=0.05, label="Temperature"),
178
- gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p"),
179
- gr.Checkbox(value=False, label="use aligned sequences"),
180
- gr.Number(value=-1, label="Random seed (-1 = random)"),
181
- ],
182
- outputs=[
183
- gr.Textbox(label="Generated sequences (FASTA)", lines=16),
184
- gr.File(label="Download FASTA"),
185
- ],
186
- title="ProtGPT3-MSA — homolog-conditioned protein generator",
187
- description=(
188
- "Upload homologous protein sequences. Up to 15 are used to prompt "
189
- "ProtGPT3-MSA, which generates N new family-consistent sequences. "
190
- "More than 15 → a random subset is sampled. Tick **use aligned "
191
- "sequences** to run in <gap> mode (requires equal-length, aligned input)."
192
- ),
193
- flagging_mode="never",
194
- )
 
 
195
 
196
  if __name__ == "__main__":
197
  demo.launch()
 
5
  randomly sampled and used to prompt AI4PD/ProtGPT3-MSA, which then generates
6
  N new family-consistent sequences in parallel. Generation stops at EOS or the
7
  <s> separator, so each output is exactly one new sequence.
 
 
 
 
 
8
  """
9
 
10
  import random
 
21
  MAX_CONTEXT = 15 # the model is prompted with at most 15 homologs
22
  DIRECTION = "1" # "1" = N-to-C (fixed; "2" would need reversed seqs)
23
 
 
24
  DTYPE = torch.bfloat16 if torch.cuda.is_available() else torch.float32
25
 
26
  tokenizer = AutoTokenizer.from_pretrained(
 
38
  )
39
  model.eval()
40
 
 
41
  S_TOKEN_ID = tokenizer.convert_tokens_to_ids("<s>")
42
  EOS_IDS = [i for i in [tokenizer.eos_token_id, S_TOKEN_ID] if i is not None]
43
  PAD_ID = tokenizer.pad_token_id or tokenizer.eos_token_id or S_TOKEN_ID
 
68
 
69
  tokens = ["<|bos|>", direction, gap_token]
70
  for seq in sequences:
71
+ tokens.append("<s>")
72
  tokens.extend(list(process_style(seq, gap=gap)))
73
+ tokens.append("<s>")
74
  return " ".join(tokens)
75
 
76
 
 
113
  random.seed(int(seed))
114
  torch.manual_seed(int(seed))
115
 
 
116
  context = random.sample(seqs, MAX_CONTEXT) if len(seqs) > MAX_CONTEXT else seqs
117
 
118
  try:
 
129
  do_sample=True,
130
  temperature=float(temperature),
131
  top_p=float(top_p),
132
+ eos_token_id=EOS_IDS,
133
  pad_token_id=PAD_ID,
134
  num_return_sequences=int(n_sequences),
135
  )
136
 
 
137
  generated = output_ids[:, prompt_len:]
138
  records_out = []
139
  for i, row in enumerate(generated, 1):
 
155
  return fasta_text, out_path
156
 
157
 
158
+ # --------------------------------------------------------------------------- #
159
+ # Header banner (inline SVG — ships inside the app, works in light/dark mode)
160
+ # --------------------------------------------------------------------------- #
161
+ def _banner_svg() -> str:
162
+ rng = random.Random(11)
163
+ bars = []
164
+ for i in range(6):
165
+ w = rng.randint(150, 225)
166
+ y = 100 + i * 20
167
+ op = 0.5 + i * 0.08
168
+ bars.append(
169
+ f'<rect x="60" y="{y}" width="{w}" height="11" rx="5.5" '
170
+ f'fill="url(#homo)" opacity="{op:.2f}"/>'
171
+ )
172
+ bars_svg = "\n".join(bars)
173
+ return f'''
174
+ <div style="width:100%;max-width:980px;margin:0 auto 4px;">
175
+ <svg viewBox="0 0 980 250" xmlns="http://www.w3.org/2000/svg"
176
+ style="width:100%;height:auto;display:block;border-radius:20px;">
177
+ <defs>
178
+ <linearGradient id="panel" x1="0" y1="0" x2="1" y2="1">
179
+ <stop offset="0" stop-color="#0f2742"/>
180
+ <stop offset="1" stop-color="#1c3a5e"/>
181
+ </linearGradient>
182
+ <linearGradient id="homo" x1="0" y1="0" x2="1" y2="0">
183
+ <stop offset="0" stop-color="#5b8fc7"/>
184
+ <stop offset="1" stop-color="#83b4dc"/>
185
+ </linearGradient>
186
+ <linearGradient id="gen" x1="0" y1="0" x2="1" y2="0">
187
+ <stop offset="0" stop-color="#34d29c"/>
188
+ <stop offset="1" stop-color="#7ff0cb"/>
189
+ </linearGradient>
190
+ <filter id="glow" x="-40%" y="-80%" width="180%" height="260%">
191
+ <feGaussianBlur stdDeviation="4" result="b"/>
192
+ <feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
193
+ </filter>
194
+ <marker id="arr" markerWidth="9" markerHeight="9" refX="5.5" refY="3" orient="auto">
195
+ <path d="M0,0 L6,3 L0,6 Z" fill="#86a5cb"/>
196
+ </marker>
197
+ </defs>
198
+
199
+ <rect x="0" y="0" width="980" height="250" rx="20" fill="url(#panel)"/>
200
+
201
+ <text x="60" y="46" font-family="system-ui,Segoe UI,Helvetica,Arial"
202
+ font-size="31" font-weight="700" fill="#eef4fb">ProtGPT3-MSA</text>
203
+ <text x="60" y="72" font-family="system-ui,Segoe UI,Helvetica,Arial"
204
+ font-size="14" fill="#9fbbd8">homolog-conditioned protein sequence generation</text>
205
+
206
+ {bars_svg}
207
+ <text x="60" y="236" font-family="system-ui,Segoe UI,Helvetica,Arial"
208
+ font-size="12" fill="#9fbbd8">up to 15 homologs</text>
209
+
210
+ <line x1="300" y1="151" x2="360" y2="151" stroke="#86a5cb"
211
+ stroke-width="2.5" marker-end="url(#arr)"/>
212
+
213
+ <rect x="384" y="119" width="64" height="64" rx="15"
214
+ fill="rgba(255,255,255,0.05)" stroke="#466a99" stroke-width="1.5"/>
215
+ <g stroke="#83b4dc" stroke-width="1.4" fill="#83b4dc">
216
+ <line x1="402" y1="135" x2="430" y2="151"/><line x1="402" y1="151" x2="430" y2="151"/>
217
+ <line x1="402" y1="167" x2="430" y2="151"/><line x1="402" y1="135" x2="430" y2="167"/>
218
+ <line x1="402" y1="167" x2="430" y2="135"/>
219
+ <circle cx="402" cy="135" r="3.4"/><circle cx="402" cy="151" r="3.4"/>
220
+ <circle cx="402" cy="167" r="3.4"/><circle cx="430" cy="151" r="3.8"/>
221
+ </g>
222
+ <text x="416" y="200" text-anchor="middle"
223
+ font-family="system-ui,Segoe UI,Helvetica,Arial"
224
+ font-size="12" fill="#9fbbd8">model</text>
225
+
226
+ <line x1="460" y1="151" x2="524" y2="151" stroke="#86a5cb"
227
+ stroke-width="2.5" marker-end="url(#arr)"/>
228
+
229
+ <rect x="552" y="130" width="222" height="13" rx="6.5" fill="url(#gen)" opacity="0.25"/>
230
+ <rect x="548" y="138" width="228" height="14" rx="7" fill="url(#gen)" opacity="0.45"/>
231
+ <rect x="544" y="147" width="234" height="17" rx="8.5" fill="url(#gen)" filter="url(#glow)"/>
232
+ <text x="544" y="186" font-family="system-ui,Segoe UI,Helvetica,Arial"
233
+ font-size="12" fill="#9fbbd8">N new sequences</text>
234
+ </svg>
235
+ </div>'''
236
+
237
+
238
+ DESCRIPTION = """
239
+ **ProtGPT3-MSA** is an autoregressive protein language model that can be prompted \
240
+ with up to 15 homologous protein sequences to generate new, family-consistent \
241
+ sequences. ([read the paper](https://www.biorxiv.org/content/10.64898/2026.06.04.730041v1))
242
+
243
+ It operates in two modalities. In **unaligned** mode, homologs are supplied as raw \
244
+ sequences and the model returns a plain sequence. In **aligned** mode, homologs are \
245
+ supplied as a gapped multiple-sequence alignment and the model returns a new aligned \
246
+ (gapped) sequence consistent with that alignment.
247
+
248
+ **Settings**
249
+ - **Upload a FASTA file** of homologous sequences. If it contains more than 15 \
250
+ sequences, a random subset of 15 is sampled to build the prompt.
251
+ - **Temperature** sets how stochastic generation is: raise it (e.g. > 1.0) for more \
252
+ diverse, exploratory sequences, or lower it (e.g. < 0.7) to keep generation \
253
+ conservative and close to the input family.
254
+ - **Use aligned sequences** — tick this box to run in aligned mode, in which the model \
255
+ generates aligned (gapped) sequences. Note that your uploaded homologs must then \
256
+ already be aligned (equal length, with gap characters).
257
+
258
+ **Recommendation.** ProtGPT3-MSA performs best when several homologs are provided. \
259
+ With very few sequences (fewer than 5), generation quality may be limited.
260
+
261
+ For full functionality and programmatic use, ProtGPT3-MSA is available on the \
262
+ [Hugging Face Hub](https://huggingface.co/AI4PD/ProtGPT3-MSA).
263
+ """
264
+
265
+
266
  # --------------------------------------------------------------------------- #
267
  # UI
268
  # --------------------------------------------------------------------------- #
269
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="teal"),
270
+ title="ProtGPT3-MSA") as demo:
271
+ gr.HTML(_banner_svg())
272
+ gr.Markdown(DESCRIPTION)
273
+
274
+ with gr.Row():
275
+ with gr.Column(scale=1):
276
+ fasta = gr.File(label="Homologs (FASTA)",
277
+ file_types=[".fasta", ".fa", ".txt"], type="filepath")
278
+ n_seq = gr.Slider(1, 100, value=20, step=1, label="N sequences to generate")
279
+ max_tok = gr.Slider(16, 1024, value=512, step=16,
280
+ label="Max new tokens (per sequence)")
281
+ temp = gr.Slider(0.1, 2.0, value=0.8, step=0.05, label="Temperature")
282
+ top_p = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p")
283
+ aligned = gr.Checkbox(value=False, label="use aligned sequences")
284
+ seed = gr.Number(value=-1, label="Random seed (-1 = random)")
285
+ btn = gr.Button("Generate", variant="primary")
286
+ with gr.Column(scale=1):
287
+ out_text = gr.Textbox(label="Generated sequences (FASTA)", lines=20)
288
+ out_file = gr.File(label="Download FASTA")
289
+
290
+ btn.click(
291
+ generate,
292
+ inputs=[fasta, n_seq, max_tok, temp, top_p, aligned, seed],
293
+ outputs=[out_text, out_file],
294
+ api_name="predict",
295
+ )
296
 
297
  if __name__ == "__main__":
298
  demo.launch()