MassimoGregorioTotaro commited on
Commit
2dd6312
1 Parent(s): 5543c12

general reorganisation

Browse files
Files changed (4) hide show
  1. .gitignore +1 -0
  2. app.py +162 -202
  3. instructions.md +13 -0
  4. requirements.txt +0 -1
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ */
app.py CHANGED
@@ -1,218 +1,178 @@
 
1
  from huggingface_hub import HfApi, ModelFilter
2
- from transformers import AutoTokenizer, AutoModelForMaskedLM
3
  import pandas as pd
4
- import re
5
- from tqdm import tqdm
6
  import torch
7
- import gradio as gr
8
- import warnings
9
- warnings.filterwarnings('ignore')
10
 
11
- MODEL, MODEL_NAME, BATCH_CONVERTER, ALPHABET = None, None, None, None
12
- OFFSET = 1
13
  MODELS = [m.modelId for m in HfApi().list_models(filter=ModelFilter(author="facebook", model_name="esm", task="fill-mask"), sort="lastModified", direction=-1)]
14
- SCORING = ["masked-marginals (more accurate)", "wt-marginals (faster)"]
15
-
16
- def label_row(row, sequence, token_probs):
17
- wt, idx, mt = row[0], int(row[1:-1]) - OFFSET, row[-1]
18
- assert sequence[idx] == wt, "The listed wildtype does not match the provided sequence"
19
-
20
- wt_encoded, mt_encoded = ALPHABET[wt], ALPHABET[mt]
21
-
22
- score = token_probs[0, 1 + idx, mt_encoded] - token_probs[0, 1 + idx, wt_encoded]
23
- return score.item()
24
 
25
- def initialise_model(model_name):
26
- global MODEL, MODEL_NAME, BATCH_CONVERTER, ALPHABET
27
- MODEL_NAME = model_name
28
- MODEL = AutoModelForMaskedLM.from_pretrained(model_name)
29
- BATCH_CONVERTER = AutoTokenizer.from_pretrained(model_name)
30
- ALPHABET = BATCH_CONVERTER.get_vocab()
31
- if torch.cuda.is_available():
32
- MODEL = MODEL.cuda()
33
-
34
- def parse_input(seq, sub):
35
- assert seq.isalpha(), "Sequence must be alphabetic"
36
- substitutions, mode = list(), None
37
 
38
- if len(sub.split()) == 1 and len(sub.split()[0]) == len(seq):
39
- mode = 'seq vs seq'
40
- for resi,(src,trg) in enumerate(zip(seq,sub), OFFSET):
41
- if src != trg:
42
- substitutions.append(f"{src}{resi}{trg}")
43
- elif len(targets := sub.split()) > 1:
44
- if all(re.match(r'\d+', x) for x in targets):
45
- mode = 'deep mutational scan'
46
- for resi in map(int, sub.split()):
47
- src = seq[resi-OFFSET]
48
- for trg in "ACDEFGHIKLMNPQRSTVWY".replace(src,''):
49
- substitutions.append(f"{src}{resi}{trg}")
50
- elif all(re.match(r'[A-Z]\d+[A-Z]', x) for x in targets):
51
- mode = 'aa substitutions'
52
- substitutions = targets
53
 
54
- if not mode:
55
- raise RuntimeError("Unrecognised running mode")
56
-
57
- return mode, pd.DataFrame(substitutions, columns=['0'])
58
 
59
- def run_model(sequence, substitutions, batch_tokens, scoring_strategy):
60
- if scoring_strategy.startswith("wt-marginals"):
61
- with torch.no_grad():
62
- token_probs = torch.log_softmax(MODEL(batch_tokens)["logits"], dim=-1)
63
- substitutions[MODEL_NAME] = substitutions.apply(
64
- lambda row: label_row(
65
- row['0'],
66
- sequence,
67
- token_probs,
68
- ),
69
- axis=1,
70
- )
71
- elif scoring_strategy.startswith("masked-marginals"):
72
- all_token_probs = []
73
- for i in tqdm(range(batch_tokens.size()[1])):
74
- batch_tokens_masked = batch_tokens.clone()
75
- batch_tokens_masked[0, i] = ALPHABET['<mask>']
76
  with torch.no_grad():
77
- token_probs = torch.log_softmax(
78
- MODEL(batch_tokens_masked)["logits"], dim=-1
79
- )
80
- all_token_probs.append(token_probs[:, i])
81
- token_probs = torch.cat(all_token_probs, dim=0).unsqueeze(0)
82
- substitutions[MODEL_NAME] = substitutions.apply(
83
- lambda row: label_row(
84
- row['0'],
85
- sequence,
86
- token_probs,
87
- ),
88
- axis=1,
89
- )
90
-
91
- return substitutions
92
-
93
- def parse_output(output, mode):
94
- if mode == 'aa substitutions':
95
- output = output.sort_values(MODEL_NAME, ascending=False)
96
- elif mode == 'deep mutational scan':
97
- output = pd.concat([(output.assign(resi=output['0'].str.extract(r'(\d+)', expand=False).astype(int))
98
- .sort_values(['resi', MODEL_NAME], ascending=[True,False])
99
- .groupby(['resi'])
100
- .head(19)
101
- .drop(['resi'], axis=1)).iloc[19*x:19*(x+1)].reset_index(drop=True) for x in range(output.shape[0]//19)]
102
- , axis=1).set_axis(range(output.shape[0]//19*2), axis='columns')
103
-
104
- return output.style.format(lambda x: f'{x:.2f}' if isinstance(x, float) else x).hide_index().hide_columns().background_gradient(cmap="RdYlGn", vmax=8, vmin=-8).to_html()
105
-
106
 
107
- # mode = 'deep mutational scan' #@param ['seq vs seq', 'deep mutational scan', 'aa substitutions']
108
- # sequence = "MVEQYLLEAIVRDARDGITISDCSRPDNPLVFVNDAFTRMTGYDAEEVIGKNCRFLQRGDINLSAVHTIKIAMLTHEPCLVTLKNYRKDGTIFWNELSLTPIINKNGLITHYLGIQKDVSAQVILNQTLHEENHLLKSNKEMLEYLVNIDALTGLHNRRFLEDQLVIQWKLASRHINTITIFMIDIDYFKAFNDTYGHTAGDEALRTIAKTLNNCFMRGSDFVARYGGEEFTILAIGMTELQAHEYSTKLVQKIENLNIHHKGSPLGHLTISLGYSQANPQYHNDQNLVIEQADRALYSAKVEGKNRAVAYREQ" #@param {type:"string"}
109
- # target = "61 214 19 30 122 140" #@param {type:"string"}
110
- # substitutions = list()
111
- # scoring_strategy = "masked-marginals"
112
-
113
- # if mode == 'seq vs seq':
114
- # for resi,(seq,trg) in enumerate(zip(sequence,target), OFFSET):
115
- # if seq != trg:
116
- # substitutions.append(f"{seq}{resi}{trg}")
117
- # elif mode == 'deep mutational scan':
118
- # for resi in map(int, target.split()):
119
- # seq = sequence[resi-OFFSET]
120
- # for trg in "ACDEFGHIKLMNPQRSTVWY".replace(seq,''):
121
- # substitutions.append(f"{seq}{resi}{trg}")
122
- # elif mode == 'aa substitutions':
123
- # substitutions = target.split()
124
- # else:
125
- # raise RuntimeError("Unrecognised running mode")
126
-
127
- # df = pd.DataFrame(substitutions, columns=['0'])
128
- # mutation_col = df.columns[0]
129
-
130
- # batch_tokens = batch_converter(sequence, return_tensors='pt')['input_ids']
131
-
132
- # if scoring_strategy == "wt-marginals":
133
- # with torch.no_grad():
134
- # token_probs = torch.log_softmax(model(batch_tokens)["logits"], dim=-1)
135
- # df[model_name] = df.apply(
136
- # lambda row: label_row(
137
- # row[mutation_col],
138
- # sequence,
139
- # token_probs,
140
- # alphabet,
141
- # OFFSET,
142
- # ),
143
- # axis=1,
144
- # )
145
- # elif scoring_strategy == "masked-marginals":
146
- # all_token_probs = []
147
- # for i in tqdm(range(batch_tokens.size()[1])):
148
- # batch_tokens_masked = batch_tokens.clone()
149
- # batch_tokens_masked[0, i] = alphabet['<mask>']
150
- # with torch.no_grad():
151
- # token_probs = torch.log_softmax(
152
- # model(batch_tokens_masked)["logits"], dim=-1
153
- # )
154
- # all_token_probs.append(token_probs[:, i]) # vocab size
155
- # token_probs = torch.cat(all_token_probs, dim=0).unsqueeze(0)
156
- # df[model_name] = df.apply(
157
- # lambda row: label_row(
158
- # row[mutation_col],
159
- # sequence,
160
- # token_probs,
161
- # alphabet,
162
- # OFFSET,
163
- # ),
164
- # axis=1,
165
- # )
166
-
167
- # if mode == 'aa substitutions':
168
- # df = df.sort_values(model_name, ascending=False)
169
- # elif mode == 'deep mutational scan':
170
- # df = pd.concat([(df.assign(resi=df['0'].str.extract(f'(\d+)', expand=False).astype(int))
171
- # .sort_values(['resi', model_name], ascending=[True,False])
172
- # .groupby(['resi'])
173
- # .head(19)
174
- # .drop(['resi'], axis=1)).iloc[19*x:19*(x+1)].reset_index(drop=True) for x in range(df.shape[0]//19)]
175
- # , axis=1).set_axis(range(df.shape[0]//19*2), axis='columns')
176
-
177
- # df.style.hide_index().hide_columns().background_gradient(cmap="RdYlGn", vmax=8, vmin=-8)
 
 
 
 
 
178
 
179
  def app(*argv):
180
- seq, trg, model_name, scoring_strategy, *_ = argv
181
-
182
- mode, substitutions = parse_input(seq, trg)
183
-
184
- if model_name != MODEL_NAME:
185
- initialise_model(model_name)
186
-
187
- batch_tokens = BATCH_CONVERTER(seq, return_tensors='pt')['input_ids']
188
-
189
- df = run_model(seq, substitutions, batch_tokens, scoring_strategy)
190
-
191
- return parse_output(df, mode)
192
-
193
- # demo = gr.Interface(
194
- # theme=gr.themes.Base(),
195
- # title="Protein Sequence Mutagenesis",
196
- # description="Predict the effect of mutations on protein stability",
197
- # fn=app,
198
- # inputs=[gr.Textbox(lines=2, label="Sequence", placeholder="Sequence here...", required=True, value='MVEQYLLEAIVRDARDGITISDCSRPDNPLVFVNDAFTRMTGYDAEEVIGKNCRFLQRGDINLSAVHTIKIAMLTHEPCLVTLKNYRKDGTIFWNELSLTPIINKNGLITHYLGIQKDVSAQVILNQTLHEENHLLKSNKEMLEYLVNIDALTGLHNRRFLEDQLVIQWKLASRHINTITIFMIDIDYFKAFNDTYGHTAGDEALRTIAKTLNNCFMRGSDFVARYGGEEFTILAIGMTELQAHEYSTKLVQKIENLNIHHKGSPLGHLTISLGYSQANPQYHNDQNLVIEQADRALYSAKVEGKNRAVAYREQ'),
199
- # gr.Textbox(lines=2, label="Substitutions", placeholder="Substitutions here...", required=True, value="61 214 19 30 122 140"),
200
- # gr.Dropdown(MODELS, label="Model", value=MODELS[1]),
201
- # gr.Dropdown(["masked-marginals (more accurate)", "wt-marginals (faster)"], label="Scoring strategy", value="wt-marginals (faster)"),
202
- # ],
203
- # outputs=gr.HTML(formatter="html", label="Output"),
204
- # )
205
-
206
- with gr.Blocks() as demo:
207
- gr.Markdown("""Protein Sequence Mutagenesis""", name="title")
208
- gr.Markdown("""Predict the effect of mutations on protein stability""", name="description")
209
- seq = gr.Textbox(lines=2, label="Sequence", placeholder="Sequence here...", required=True, value='MVEQYLLEAIVRDARDGITISDCSRPDNPLVFVNDAFTRMTGYDAEEVIGKNCRFLQRGDINLSAVHTIKIAMLTHEPCLVTLKNYRKDGTIFWNELSLTPIINKNGLITHYLGIQKDVSAQVILNQTLHEENHLLKSNKEMLEYLVNIDALTGLHNRRFLEDQLVIQWKLASRHINTITIFMIDIDYFKAFNDTYGHTAGDEALRTIAKTLNNCFMRGSDFVARYGGEEFTILAIGMTELQAHEYSTKLVQKIENLNIHHKGSPLGHLTISLGYSQANPQYHNDQNLVIEQADRALYSAKVEGKNRAVAYREQ')
210
- trg = gr.Textbox(lines=1, label="Substitutions", placeholder="Substitutions here...", required=True, value="61 214 19 30 122 140")
211
  model_name = gr.Dropdown(MODELS, label="Model", value=MODELS[1])
212
  scoring_strategy = gr.Dropdown(SCORING, label="Scoring strategy", value=SCORING[1])
213
- btn = gr.Button(label="Submit", type="submit")
214
- btn.click(fn=app, inputs=[seq, trg, model_name, scoring_strategy], outputs=[gr.HTML()])
 
 
215
 
216
- if __name__ == '__main__':
217
- demo.launch()
218
- # demo.launch(share=True, server_name="0.0.0.0", server_port=7878)
 
1
+ import gradio as gr
2
  from huggingface_hub import HfApi, ModelFilter
 
3
  import pandas as pd
4
+ from re import match
5
+ from tempfile import NamedTemporaryFile
6
  import torch
7
+ from transformers import AutoTokenizer, AutoModelForMaskedLM
 
 
8
 
9
+ # fetch suitable ESM models from HuggingFace Hub
 
10
  MODELS = [m.modelId for m in HfApi().list_models(filter=ModelFilter(author="facebook", model_name="esm", task="fill-mask"), sort="lastModified", direction=-1)]
11
+ if not any(MODELS):
12
+ raise RuntimeError("Error while retrieving models from HuggingFace Hub")
 
 
 
 
 
 
 
 
13
 
14
+ # scoring strategies
15
+ SCORING = ["masked-marginals (more accurate)", "wt-marginals (faster)"]
 
 
 
 
 
 
 
 
 
 
16
 
17
+ class Model:
18
+ """Wrapper for ESM models"""
19
+ def __init__(self, model_name:str=""):
20
+ "load selected model and tokenizer"
21
+ self.model_name = model_name
22
+ if model_name:
23
+ self.model = AutoModelForMaskedLM.from_pretrained(model_name)
24
+ self.batch_converter = AutoTokenizer.from_pretrained(model_name)
25
+ self.alphabet = self.batch_converter.get_vocab()
26
+ if torch.cuda.is_available():
27
+ self.model = self.model.cuda()
28
+
29
+ def __rshift__(self, batch_tokens:torch.Tensor) -> torch.Tensor:
30
+ "run model on batch of tokens"
31
+ return self.model(batch_tokens)["logits"]
32
 
33
+ def __lshift__(self, input:str) -> torch.Tensor:
34
+ "convert input string to batch of tokens"
35
+ return self.batch_converter(input, return_tensors="pt")["input_ids"]
 
36
 
37
+ def __getitem__(self, key:str) -> int:
38
+ "get token ID from character"
39
+ return self.alphabet[key]
40
+
41
+ def run_model(self, data):
42
+ "run model on data"
43
+ def label_row(row, token_probs):
44
+ "label row with score"
45
+ wt, idx, mt = row[0], int(row[1:-1])-1, row[-1]
46
+ score = token_probs[0, 1+idx, self[mt]] - token_probs[0, 1+idx, self[wt]]
47
+ return score.item()
48
+
49
+ batch_tokens = self<<data.seq
50
+
51
+ # run model with selected scoring strategy (info thereof available in the original ESM paper)
52
+ if data.scoring_strategy.startswith("wt-marginals"):
 
53
  with torch.no_grad():
54
+ token_probs = torch.log_softmax(self>>batch_tokens, dim=-1)
55
+ data.out[self.model_name] = data.sub.apply(
56
+ lambda row: label_row(
57
+ row['0'],
58
+ token_probs,
59
+ ),
60
+ axis=1,
61
+ )
62
+ elif data.scoring_strategy.startswith("masked-marginals"):
63
+ all_token_probs = []
64
+ for i in range(batch_tokens.size()[1]):
65
+ batch_tokens_masked = batch_tokens.clone()
66
+ batch_tokens_masked[0, i] = self['<mask>']
67
+ with torch.no_grad():
68
+ token_probs = torch.log_softmax(
69
+ self>>batch_tokens_masked, dim=-1
70
+ )
71
+ all_token_probs.append(token_probs[:, i])
72
+ token_probs = torch.cat(all_token_probs, dim=0).unsqueeze(0)
73
+ data.out[self.model_name] = data.sub.apply(
74
+ lambda row: label_row(
75
+ row['0'],
76
+ token_probs,
77
+ ),
78
+ axis=1,
79
+ )
 
 
 
80
 
81
+ class Data:
82
+ """Container for input and output data"""
83
+ # initialise empty model as static class member for efficiency
84
+ model = Model()
85
+
86
+ def parse_seq(self, src:str):
87
+ "parse input sequence"
88
+ self.seq = src.strip().upper()
89
+ if not all(x in self.model.alphabet for x in src):
90
+ raise RuntimeError("Unrecognised characters in sequence")
91
+
92
+ def parse_sub(self, trg:str):
93
+ "parse input substitutions"
94
+ self.mode = None
95
+ self.sub = list()
96
+ self.trg = trg.strip().upper()
97
+
98
+ # identify running mode
99
+ if len(self.trg.split()) == 1 and len(self.trg.split()[0]) == len(self.seq): # if single string of same length as sequence, seq vs seq mode
100
+ self.mode = 'SVS'
101
+ for resi,(src,trg) in enumerate(zip(self.seq, self.trg), 1):
102
+ if src != trg:
103
+ self.sub.append(f"{src}{resi}{trg}")
104
+ else:
105
+ self.trg = self.trg.split()
106
+ if all(match(r'\d+', x) for x in self.trg): # if all strings are numbers, deep mutational scanning mode
107
+ self.mode = 'DMS'
108
+ for resi in map(int, self.trg):
109
+ src = self.seq[resi-1]
110
+ for trg in "ACDEFGHIKLMNPQRSTVWY".replace(src,''):
111
+ self.sub.append(f"{src}{resi}{trg}")
112
+ elif all(match(r'[A-Z]\d+[A-Z]', x) for x in self.trg): # if all strings are of the form X#Y, single substitution mode
113
+ self.mode = 'MUT'
114
+ self.sub = self.trg
115
+ else:
116
+ raise RuntimeError("Unrecognised running mode; wrong inputs?")
117
+
118
+ self.sub = pd.DataFrame(self.sub, columns=['0'])
119
+
120
+ def __init__(self, src:str, trg:str, model_name:str, scoring_strategy:str, out_file):
121
+ "initialise data"
122
+ # if model has changed, load new model
123
+ if self.model.model_name != model_name:
124
+ self.model_name = model_name
125
+ self.model = Model(model_name)
126
+ self.parse_seq(src)
127
+ self.parse_sub(trg)
128
+ self.scoring_strategy = scoring_strategy
129
+ self.out = pd.DataFrame(self.sub, columns=['0', self.model_name])
130
+ self.out_buffer = out_file.name
131
+
132
+ def parse_output(self) -> str:
133
+ "format output data for visualisation"
134
+ if self.mode == 'MUT': # if single substitution mode, sort by score
135
+ self.out = self.out.sort_values(self.model_name, ascending=False)
136
+ elif self.mode == 'DMS': # if deep mutational scanning mode, sort by residue and score
137
+ self.out = pd.concat([(self.out.assign(resi=self.out['0'].str.extract(r'(\d+)', expand=False).astype(int)) # FIX: this doesn't work if there's jolly characters in the input sequence
138
+ .sort_values(['resi', self.model_name], ascending=[True,False])
139
+ .groupby(['resi'])
140
+ .head(19)
141
+ .drop(['resi'], axis=1)).iloc[19*x:19*(x+1)]
142
+ .reset_index(drop=True) for x in range(self.out.shape[0]//19)]
143
+ , axis=1).set_axis(range(self.out.shape[0]//19*2), axis='columns')
144
+ # save to temporary file to be downloaded
145
+ self.out.round(2).to_csv(self.out_buffer, index=False)
146
+ return (self.out.style
147
+ .format(lambda x: f'{x:.2f}' if isinstance(x, float) else x)
148
+ .hide(axis=0)
149
+ .hide(axis=1)
150
+ .background_gradient(cmap="RdYlGn", vmax=8, vmin=-8)
151
+ .to_html())
152
+
153
+ def calculate(self):
154
+ "run model and parse output"
155
+ self.model.run_model(self)
156
+ return self, self.parse_output()
157
 
158
  def app(*argv):
159
+ "run app"
160
+ seq, trg, model_name, scoring_strategy, out_file, *_ = argv
161
+ data, html = Data(seq, trg, model_name, scoring_strategy, out_file).calculate()
162
+ return html, gr.File.update(value=out_file.name, visible=True)
163
+ # df = pd.DataFrame((pd.np.random.random((10, 5))-0.5)*10, columns=list('ABCDE'))
164
+ # df.to_csv(out_file.name, index=False)
165
+ # return df.to_html(), gr.File.update(value=out_file.name, visible=True)
166
+
167
+ with gr.Blocks() as demo, NamedTemporaryFile(mode='w+', prefix='out_', suffix='.csv') as out_file, open("instructions.md", "r") as md:
168
+ gr.Markdown(md.read())
169
+ seq = gr.Textbox(lines=2, label="Sequence", placeholder="Sequence here...", value='MVEQYLLEAIVRDARDGITISDCSRPDNPLVFVNDAFTRMTGYDAEEVIGKNCRFLQRGDINLSAVHTIKIAMLTHEPCLVTLKNYRKDGTIFWNELSLTPIINKNGLITHYLGIQKDVSAQVILNQTLHEENHLLKSNKEMLEYLVNIDALTGLHNRRFLEDQLVIQWKLASRHINTITIFMIDIDYFKAFNDTYGHTAGDEALRTIAKTLNNCFMRGSDFVARYGGEEFTILAIGMTELQAHEYSTKLVQKIENLNIHHKGSPLGHLTISLGYSQANPQYHNDQNLVIEQADRALYSAKVEGKNRAVAYREQ')
170
+ trg = gr.Textbox(lines=1, label="Substitutions", placeholder="Substitutions here...", value="61 214 19 30 122 140")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  model_name = gr.Dropdown(MODELS, label="Model", value=MODELS[1])
172
  scoring_strategy = gr.Dropdown(SCORING, label="Scoring strategy", value=SCORING[1])
173
+ btn = gr.Button(value="Submit")
174
+ out = gr.HTML()
175
+ bto = gr.File(value=out_file.name, visible=False, label="Download", file_count='single', interactive=False)
176
+ btn.click(fn=app, inputs=[seq, trg, model_name, scoring_strategy, bto], outputs=[out, bto])
177
 
178
+ # demo.launch(share=True, server_name="0.0.0.0", server_port=7878)
 
 
instructions.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # **ESM zero-shot variant prediction**
2
+ this was inspired from this [paper](https://doi.org/10.1101/2021.07.09.450648) and adaptated from [this repo](https://github.com/facebookresearch/esm/tree/main/esm)
3
+
4
+ #### **Instructions**
5
+ - in the 'sequence' text box the protein full amino acid sequence that is to be analysed must be given, jolly charachters (e.g. -X.B) are supported (but at the moment the visualisation does not show the correct results)
6
+ - there's three running modes that can be chosen, depending on the input in the 'substitution' box:
7
+ - if another sequence is given, the positions that are different between the two will be evaluated (NB the sequences must be of the same length) and their score returned
8
+ - if a list of integers is given, a deep mutational scan will be performed at those positions in the input sequence and the scores for the amino acids, different from the original one, will be returned
9
+ - if a single substitution or a list thereof is given (in the form of **B008S**), the single substitution score is returned
10
+ - you can choose which ESM model to use for the calculations, these models are the ones that are available at runtime on Hugging Face Model Hub
11
+ - there's 2 scoring strategies available: wt-marginals and masked marginals; the first one is faster, but less accurate, the second one considers the sequence context more thoroughly, but is sensibly slower (the run time scales linearly with sequence length)
12
+ - the results will be shown in a table, with color coding and sorted by fitness (if performing a deep mutational scan)
13
+ - the output data is available for download from the box at the bottom as a CSV file
requirements.txt CHANGED
@@ -2,5 +2,4 @@ gradio
2
  huggingface_hub
3
  pandas
4
  torch
5
- tqdm
6
  transformers
 
2
  huggingface_hub
3
  pandas
4
  torch
 
5
  transformers