vijayvizag commited on
Commit
bcb80f2
·
1 Parent(s): adf8249

readme update

Browse files
Files changed (4) hide show
  1. README.md +20 -6
  2. app.py +345 -121
  3. code_analyzer.py → code_analyzer2.py +20 -19
  4. requirements.txt +3 -8
README.md CHANGED
@@ -9,12 +9,26 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- # Code to Doc Streamlit App
13
 
14
- A Streamlit-based application that converts code to documentation.
15
 
16
  ## Features
17
- - Code analysis
18
- - Documentation generation
19
- - Interactive interface
20
- - Real-time updates
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  pinned: false
10
  ---
11
 
12
+ # Python Code Summarizer
13
 
14
+ This Streamlit app utilizes the CodeT5 model to generate summaries of Python code files, leveraging the Hugging Face Transformers library.
15
 
16
  ## Features
17
+ - Upload Python files or paste code directly
18
+ - Generate file-level summaries
19
+ - Generate function-level summaries
20
+ - Generate class-level summaries
21
+
22
+ ## Usage
23
+ 1. Upload a Python file or paste your code
24
+ 2. Select the types of summaries you want to generate
25
+ 3. Click "Summarize Code"
26
+ 4. View the generated summaries
27
+
28
+ ## Model Information
29
+ This app employs CodeT5, a pretrained model available on Hugging Face, developed by Salesforce Research for code understanding and generation tasks. It is trained on a vast corpus of code and documentation.
30
+
31
+ ## Limitations
32
+ - Summaries may not always be accurate
33
+ - Long files may be truncated due to model context limits
34
+ - Complex code structures might not be properly understood
app.py CHANGED
@@ -1,136 +1,360 @@
1
  import streamlit as st
2
- import os
3
- import tempfile
4
- import shutil
5
- from code_analyzer import CodeAnalyzer
6
- import plotly.express as px
7
- import pandas as pd
8
 
9
- st.set_page_config(
10
- page_title="Code Analyzer",
11
- page_icon="🔍",
12
- layout="wide"
13
- )
14
 
15
- st.title("🔍 Code Project Analyzer")
16
- st.write("Upload your code files and analyze them with AI-powered insights")
17
-
18
- def create_metrics_chart(metrics):
19
- """Create a bar chart for code metrics"""
20
- df = pd.DataFrame({
21
- 'Metric': list(metrics.keys()),
22
- 'Value': list(metrics.values())
23
- })
24
- fig = px.bar(df, x='Metric', y='Value', title='Code Metrics')
25
- return fig
26
-
27
- def display_tech_stack(tech_stack):
28
- """Display technology stack in an organized way"""
29
- st.subheader("🛠️ Technology Stack")
30
- cols = st.columns(3)
31
 
32
- with cols[0]:
33
- st.write("**Languages**")
34
- if tech_stack["languages"]:
35
- for lang in tech_stack["languages"]:
36
- st.write(f"- {lang}")
37
- else:
38
- st.write("No languages detected")
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- with cols[1]:
41
- st.write("**Frameworks**")
42
- if tech_stack["frameworks"]:
43
- for framework in tech_stack["frameworks"]:
44
- st.write(f"- {framework}")
45
- else:
46
- st.write("No frameworks detected")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- with cols[2]:
49
- st.write("**Dependencies**")
50
- if tech_stack["dependencies"]:
51
- for dep in tech_stack["dependencies"]:
52
- st.write(f"- {dep}")
53
- else:
54
- st.write("No dependencies detected")
55
-
56
- def save_uploaded_files(uploaded_files):
57
- """Save uploaded files to a temporary directory"""
58
- temp_dir = tempfile.mkdtemp()
59
- for uploaded_file in uploaded_files:
60
- file_path = os.path.join(temp_dir, uploaded_file.name)
61
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
62
- with open(file_path, "wb") as f:
63
- f.write(uploaded_file.getbuffer())
64
- return temp_dir
65
-
66
- # File upload section
67
- uploaded_files = st.file_uploader(
68
- "Upload your code files",
69
- accept_multiple_files=True,
70
- type=['py', 'java', 'js', 'jsx', 'ts', 'tsx']
71
- )
72
-
73
- # Questions input
74
- st.subheader("📝 Analysis Questions")
75
- default_questions = """What is the project's abstract?
76
- What is the system architecture?
77
- What are the software requirements?
78
- What are the hardware requirements?"""
79
-
80
- questions = st.text_area(
81
- "Enter your questions (one per line)",
82
- value=default_questions,
83
- height=150
84
- )
85
-
86
- analyze_button = st.button("🔍 Analyze Code")
87
-
88
- if analyze_button and uploaded_files:
89
- with st.spinner("Analyzing your code..."):
90
- # Save uploaded files
91
- temp_dir = save_uploaded_files(uploaded_files)
92
 
93
- # Save questions to a temporary file
94
- questions_file = os.path.join(temp_dir, "questions.txt")
95
- with open(questions_file, "w") as f:
96
- f.write(questions)
97
 
98
- try:
99
- # Run analysis
100
- analyzer = CodeAnalyzer()
101
- results = analyzer.analyze_project(temp_dir, questions_file)
102
 
103
- # Display results in tabs
104
- tab1, tab2, tab3 = st.tabs(["📊 Overview", "💻 Code Metrics", "❓ Q&A"])
 
105
 
106
- with tab1:
107
- st.subheader("🎯 Project Objective")
108
- st.write(results["objective"])
 
 
109
 
110
- display_tech_stack(results["tech_stack"])
111
-
112
- with tab2:
113
- st.subheader("📊 Code Metrics")
114
- metrics_chart = create_metrics_chart(results["metrics"])
115
- st.plotly_chart(metrics_chart, use_container_width=True)
116
 
117
- # Complexity assessment
118
- complexity = "Low" if results["metrics"]["complexity_score"] < 10 else \
119
- "Medium" if results["metrics"]["complexity_score"] < 30 else "High"
120
- st.info(f"Project Complexity: {complexity}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- with tab3:
123
- st.subheader("❓ Analysis Results")
124
- for question, answer in results["answers"].items():
125
- with st.expander(question):
126
- st.write(answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
 
 
 
 
128
  except Exception as e:
129
- st.error(f"An error occurred during analysis: {str(e)}")
130
-
131
- finally:
132
- # Cleanup
133
- shutil.rmtree(temp_dir)
134
- else:
135
- if analyze_button:
136
- st.warning("Please upload some code files first!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import torch
3
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
4
+ import re
5
+ import time
 
 
6
 
7
+ # Model constants
8
+ CODET5_MODEL = "Salesforce/codet5-base-multi-sum"
 
 
 
9
 
10
+ class CodeT5Summarizer:
11
+ def __init__(self, device=None):
12
+ """Initialize CodeT5 summarization model."""
13
+ self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu')
14
+
15
+ # Initialize model and tokenizer
16
+ with st.spinner("Loading CodeT5 model... this may take a minute..."):
17
+ self.tokenizer = AutoTokenizer.from_pretrained(CODET5_MODEL)
18
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(CODET5_MODEL).to(self.device)
 
 
 
 
 
 
 
19
 
20
+ def preprocess_code(self, code):
21
+ """Clean and preprocess the Python code."""
22
+ # Remove empty lines
23
+ code = re.sub(r'\n\s*\n', '\n', code)
24
+
25
+ # Remove excessive comments (keeping docstrings)
26
+ code_lines = []
27
+ in_docstring = False
28
+ docstring_delimiter = None
29
+
30
+ for line in code.split('\n'):
31
+ # Check for docstring delimiters
32
+ if '"""' in line or "'''" in line:
33
+ delimiter = '"""' if '"""' in line else "'''"
34
+ if not in_docstring:
35
+ in_docstring = True
36
+ docstring_delimiter = delimiter
37
+ elif docstring_delimiter == delimiter:
38
+ in_docstring = False
39
+ docstring_delimiter = None
40
 
41
+ # Keep docstrings and non-comment lines
42
+ if in_docstring or not line.strip().startswith('#'):
43
+ code_lines.append(line)
44
+
45
+ processed_code = '\n'.join(code_lines)
46
+
47
+ # Normalize whitespace
48
+ processed_code = re.sub(r' +', ' ', processed_code)
49
+
50
+ return processed_code
51
+
52
+ def extract_functions(self, code):
53
+ """Extract individual functions for summarization"""
54
+ # Simple regex to find function definitions
55
+ function_pattern = r'def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(.*?\).*?:'
56
+ function_matches = re.finditer(function_pattern, code, re.DOTALL)
57
+
58
+ functions = []
59
+ for match in function_matches:
60
+ start_pos = match.start()
61
+ # Find the function body
62
+ function_name = match.group(1)
63
+ lines = code[start_pos:].split('\n')
64
 
65
+ # Skip the function definition line
66
+ body_start = 1
67
+ while body_start < len(lines) and not lines[body_start].strip():
68
+ body_start += 1
69
+
70
+ if body_start < len(lines):
71
+ # Get the indentation of the function body
72
+ body_indent = len(lines[body_start]) - len(lines[body_start].lstrip())
73
+
74
+ # Gather all lines with at least this indentation
75
+ function_body = [lines[0]] # The function definition
76
+ i = 1
77
+ while i < len(lines):
78
+ line = lines[i]
79
+ if line.strip() and (len(line) - len(line.lstrip())) < body_indent and not line.strip().startswith('#'):
80
+ break
81
+ function_body.append(line)
82
+ i += 1
83
+
84
+ function_code = '\n'.join(function_body)
85
+ functions.append((function_name, function_code))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ # Simple regex to find class methods
88
+ class_pattern = r'class\s+([a-zA-Z_][a-zA-Z0-9_]*)'
89
+ class_matches = re.finditer(class_pattern, code, re.DOTALL)
 
90
 
91
+ for match in class_matches:
92
+ class_name = match.group(1)
93
+ start_pos = match.start()
 
94
 
95
+ # Find class methods using the function pattern
96
+ class_code = code[start_pos:]
97
+ method_matches = re.finditer(function_pattern, class_code, re.DOTALL)
98
 
99
+ for method_match in method_matches:
100
+ method_name = method_match.group(1)
101
+ # Skip if this is not a method (i.e., it's a function outside the class)
102
+ if method_match.start() > 200: # Simple heuristic to check if method is within class scope
103
+ break
104
 
105
+ # Get the full method code
106
+ method_start = method_match.start()
107
+ method_lines = class_code[method_start:].split('\n')
 
 
 
108
 
109
+ # Skip the method definition line
110
+ body_start = 1
111
+ while body_start < len(method_lines) and not method_lines[body_start].strip():
112
+ body_start += 1
113
+
114
+ if body_start < len(method_lines):
115
+ # Get the indentation of the method body
116
+ body_indent = len(method_lines[body_start]) - len(method_lines[body_start].lstrip())
117
+
118
+ # Gather all lines with at least this indentation
119
+ method_body = [method_lines[0]] # The method definition
120
+ i = 1
121
+ while i < len(method_lines):
122
+ line = method_lines[i]
123
+ if line.strip() and (len(line) - len(line.lstrip())) < body_indent and not line.strip().startswith('#'):
124
+ break
125
+ method_body.append(line)
126
+ i += 1
127
+
128
+ method_code = '\n'.join(method_body)
129
+ functions.append((f"{class_name}.{method_name}", method_code))
130
+
131
+ return functions
132
+
133
+ def extract_classes(self, code):
134
+ """Extract class definitions for summarization"""
135
+ class_pattern = r'class\s+([a-zA-Z_][a-zA-Z0-9_]*)'
136
+ class_matches = re.finditer(class_pattern, code, re.DOTALL)
137
+
138
+ classes = []
139
+ for match in class_matches:
140
+ class_name = match.group(1)
141
+ start_pos = match.start()
142
 
143
+ # Extract class body
144
+ class_lines = code[start_pos:].split('\n')
145
+
146
+ # Skip the class definition line
147
+ body_start = 1
148
+ while body_start < len(class_lines) and not class_lines[body_start].strip():
149
+ body_start += 1
150
+
151
+ if body_start < len(class_lines):
152
+ # Get the indentation of the class body
153
+ body_indent = len(class_lines[body_start]) - len(class_lines[body_start].lstrip())
154
+
155
+ # Gather all lines with at least this indentation
156
+ class_body = [class_lines[0]] # The class definition
157
+ i = 1
158
+ while i < len(class_lines):
159
+ line = class_lines[i]
160
+ if line.strip() and (len(line) - len(line.lstrip())) < body_indent:
161
+ break
162
+ class_body.append(line)
163
+ i += 1
164
+
165
+ class_code = '\n'.join(class_body)
166
+ classes.append((class_name, class_code))
167
+
168
+ return classes
169
+
170
+ def summarize(self, code, max_length=50):
171
+ """Generate summary using CodeT5."""
172
+ # Truncate input if needed
173
+ max_input_length = 512 # CodeT5 typically accepts up to 512 tokens
174
+ tokenized_code = self.tokenizer(code, truncation=True, max_length=max_input_length, return_tensors="pt").to(self.device)
175
+
176
+ with torch.no_grad():
177
+ generated_ids = self.model.generate(
178
+ tokenized_code["input_ids"],
179
+ max_length=max_length,
180
+ num_beams=4,
181
+ early_stopping=True
182
+ )
183
+
184
+ summary = self.tokenizer.decode(generated_ids[0], skip_special_tokens=True)
185
+ return summary
186
+
187
+ def summarize_code(self, code, summarize_functions=True, summarize_classes=True):
188
+ """
189
+ Generate full file summary and optionally function/class level summaries.
190
+ Returns a dictionary with summaries.
191
+ """
192
+ preprocessed_code = self.preprocess_code(code)
193
+
194
+ results = {
195
+ "file_summary": None,
196
+ "function_summaries": {},
197
+ "class_summaries": {}
198
+ }
199
 
200
+ # Generate file-level summary
201
+ try:
202
+ file_summary = self.summarize(preprocessed_code)
203
+ results["file_summary"] = file_summary
204
  except Exception as e:
205
+ results["file_summary"] = f"Error generating file summary: {str(e)}"
206
+
207
+ # Generate function-level summaries if requested
208
+ if summarize_functions:
209
+ functions = self.extract_functions(preprocessed_code)
210
+
211
+ for function_name, function_code in functions:
212
+ try:
213
+ summary = self.summarize(function_code)
214
+ results["function_summaries"][function_name] = summary
215
+ except Exception as e:
216
+ results["function_summaries"][function_name] = f"Error: {str(e)}"
217
+
218
+ # Generate class-level summaries if requested
219
+ if summarize_classes:
220
+ classes = self.extract_classes(preprocessed_code)
221
+
222
+ for class_name, class_code in classes:
223
+ try:
224
+ summary = self.summarize(class_code)
225
+ results["class_summaries"][class_name] = summary
226
+ except Exception as e:
227
+ results["class_summaries"][class_name] = f"Error: {str(e)}"
228
+
229
+ return results
230
+
231
+ def main():
232
+ st.set_page_config(
233
+ page_title="Python Code Summarizer",
234
+ page_icon="📝",
235
+ layout="wide"
236
+ )
237
+
238
+ st.title("📝 Python Code Summarizer using CodeT5")
239
+ st.markdown("""
240
+ Upload a Python file or paste code directly to generate summaries.
241
+ This app uses CodeT5, a pretrained model for code understanding and generation.
242
+ """)
243
+
244
+ # Initialize session state
245
+ if 'summarizer' not in st.session_state:
246
+ st.session_state.summarizer = None
247
+
248
+ # Load model if not already loaded
249
+ if st.session_state.summarizer is None:
250
+ st.session_state.summarizer = CodeT5Summarizer()
251
+
252
+ # Create tabs for different input methods
253
+ tab1, tab2 = st.tabs(["Upload Python File", "Paste Code"])
254
+
255
+ with tab1:
256
+ uploaded_file = st.file_uploader("Choose a Python file", type=['py'])
257
+ if uploaded_file is not None:
258
+ code = uploaded_file.getvalue().decode('utf-8')
259
+ with st.expander("View Uploaded Code", expanded=False):
260
+ st.code(code, language='python')
261
+
262
+ # Add summarization options
263
+ st.subheader("Summarization Options")
264
+ col1, col2 = st.columns(2)
265
+ with col1:
266
+ summarize_functions = st.checkbox("Generate function summaries", value=True)
267
+ with col2:
268
+ summarize_classes = st.checkbox("Generate class summaries", value=True)
269
+
270
+ if st.button("Summarize Code", key="summarize_file"):
271
+ with st.spinner("Generating summaries..."):
272
+ start_time = time.time()
273
+ summaries = st.session_state.summarizer.summarize_code(
274
+ code,
275
+ summarize_functions=summarize_functions,
276
+ summarize_classes=summarize_classes
277
+ )
278
+ end_time = time.time()
279
+
280
+ # Display summaries
281
+ st.success(f"Summarization completed in {end_time - start_time:.2f} seconds!")
282
+
283
+ # File summary
284
+ st.subheader("File Summary")
285
+ st.write(summaries["file_summary"])
286
+
287
+ # Function summaries
288
+ if summarize_functions and summaries["function_summaries"]:
289
+ st.subheader("Function Summaries")
290
+ for func_name, summary in summaries["function_summaries"].items():
291
+ with st.expander(f"Function: {func_name}"):
292
+ st.write(summary)
293
+
294
+ # Class summaries
295
+ if summarize_classes and summaries["class_summaries"]:
296
+ st.subheader("Class Summaries")
297
+ for class_name, summary in summaries["class_summaries"].items():
298
+ with st.expander(f"Class: {class_name}"):
299
+ st.write(summary)
300
+
301
+ with tab2:
302
+ code = st.text_area("Paste Python code here", height=300)
303
+ if code:
304
+ # Add summarization options
305
+ st.subheader("Summarization Options")
306
+ col1, col2 = st.columns(2)
307
+ with col1:
308
+ summarize_functions = st.checkbox("Generate function summaries", value=True, key="func_paste")
309
+ with col2:
310
+ summarize_classes = st.checkbox("Generate class summaries", value=True, key="class_paste")
311
+
312
+ if st.button("Summarize Code", key="summarize_paste"):
313
+ with st.spinner("Generating summaries..."):
314
+ start_time = time.time()
315
+ summaries = st.session_state.summarizer.summarize_code(
316
+ code,
317
+ summarize_functions=summarize_functions,
318
+ summarize_classes=summarize_classes
319
+ )
320
+ end_time = time.time()
321
+
322
+ # Display summaries
323
+ st.success(f"Summarization completed in {end_time - start_time:.2f} seconds!")
324
+
325
+ # File summary
326
+ st.subheader("File Summary")
327
+ st.write(summaries["file_summary"])
328
+
329
+ # Function summaries
330
+ if summarize_functions and summaries["function_summaries"]:
331
+ st.subheader("Function Summaries")
332
+ for func_name, summary in summaries["function_summaries"].items():
333
+ with st.expander(f"Function: {func_name}"):
334
+ st.write(summary)
335
+
336
+ # Class summaries
337
+ if summarize_classes and summaries["class_summaries"]:
338
+ st.subheader("Class Summaries")
339
+ for class_name, summary in summaries["class_summaries"].items():
340
+ with st.expander(f"Class: {class_name}"):
341
+ st.write(summary)
342
+
343
+ st.markdown("---")
344
+ st.markdown("### About")
345
+ st.markdown("""
346
+ This app uses the CodeT5 model to generate summaries of Python code. The model is trained on a large corpus of code and documentation.
347
+
348
+ **Features:**
349
+ - File-level summaries
350
+ - Function-level summaries
351
+ - Class-level summaries
352
+
353
+ **Limitations:**
354
+ - Summaries may not always be accurate
355
+ - Long files may be truncated
356
+ - Complex code structures might not be properly understood
357
+ """)
358
+
359
+ if __name__ == "__main__":
360
+ main()
code_analyzer.py → code_analyzer2.py RENAMED
@@ -7,11 +7,12 @@ from typing import List, Dict, Set, Any
7
  import pkg_resources
8
  import importlib.util
9
  from collections import defaultdict
 
10
 
11
- class CodeAnalyzer:
12
  def __init__(self):
13
  # Using different models for different types of analysis
14
- self.summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
15
 
16
  def detect_technologies(self, code_files: Dict[str, str]) -> Dict[str, Any]:
17
  """Detect technologies used in the project"""
@@ -210,22 +211,22 @@ class CodeAnalyzer:
210
  "answers": answers
211
  }
212
 
213
- if __name__ == "__main__":
214
- analyzer = CodeAnalyzer()
215
- # Example usage
216
- results = analyzer.analyze_project(
217
- "./example_project",
218
- "./questions.txt"
219
- )
220
- print("\nProject Objective:", results["objective"])
221
- print("\nTechnology Stack:")
222
- for category, items in results["tech_stack"].items():
223
- print(f"- {category.title()}: {', '.join(items)}")
224
 
225
- print("\nCode Metrics:")
226
- for metric, value in results["metrics"].items():
227
- print(f"- {metric.replace('_', ' ').title()}: {value}")
228
 
229
- print("\nAnswers to Questions:")
230
- for q, a in results["answers"].items():
231
- print(f"\n{q}:\n{a}")
 
7
  import pkg_resources
8
  import importlib.util
9
  from collections import defaultdict
10
+ import huggingface_hub
11
 
12
+ class CodeAnalyzer2:
13
  def __init__(self):
14
  # Using different models for different types of analysis
15
+ self.summarizer = pipeline("summarization", model="Graverman/t5-code-summary")
16
 
17
  def detect_technologies(self, code_files: Dict[str, str]) -> Dict[str, Any]:
18
  """Detect technologies used in the project"""
 
211
  "answers": answers
212
  }
213
 
214
+ # if __name__ == "__main__":
215
+ # analyzer = CodeAnalyzer()
216
+ # # Example usage
217
+ # results = analyzer.analyze_project(
218
+ # "./example_project",
219
+ # "./questions.txt"
220
+ # )
221
+ # print("\nProject Objective:", results["objective"])
222
+ # print("\nTechnology Stack:")
223
+ # for category, items in results["tech_stack"].items():
224
+ # print(f"- {category.title()}: {', '.join(items)}")
225
 
226
+ # print("\nCode Metrics:")
227
+ # for metric, value in results["metrics"].items():
228
+ # print(f"- {metric.replace('_', ' ').title()}: {value}")
229
 
230
+ # print("\nAnswers to Questions:")
231
+ # for q, a in results["answers"].items():
232
+ # print(f"\n{q}:\n{a}")
requirements.txt CHANGED
@@ -1,8 +1,3 @@
1
- transformers[torch]==4.35.0
2
- --extra-index-url https://download.pytorch.org/whl/cpu
3
- torch>=2.0.0
4
- numpy>=1.24.0
5
- pandas>=2.0.0
6
- streamlit>=1.30.0
7
- plotly>=5.18.0
8
- altair>=5.2.0
 
1
+ streamlit>=1.22.0
2
+ torch>=1.13.0
3
+ transformers>=4.28.0