MogensR commited on
Commit
e74531f
·
verified ·
1 Parent(s): 1582a90

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +201 -126
streamlit_app.py CHANGED
@@ -14,6 +14,7 @@
14
  import io
15
  import torch
16
  import traceback
 
17
 
18
  # --- Project Setup ---
19
  sys.path.append(str(Path(__file__).parent.absolute()))
@@ -88,20 +89,19 @@ def initialize_session_state():
88
  defaults = {
89
  'uploaded_video': None,
90
  'video_bytes_cache': None,
91
- 'video_preview_placeholder': None,
92
  'bg_image': None,
93
  'bg_image_cache': None,
94
  'bg_image_name': None,
95
- 'bg_preview_placeholder': None,
96
  'bg_color': "#00FF00",
97
  'cached_color': None,
98
  'color_display_cache': None,
99
- 'processed_video_path': None,
100
  'processing': False,
101
  'progress': 0,
102
  'progress_text': "Ready",
103
  'last_video_id': None,
104
- 'last_bg_image_id': None
 
105
  }
106
  for key, value in defaults.items():
107
  if key not in st.session_state:
@@ -111,93 +111,148 @@ def initialize_session_state():
111
  def process_video(input_file, background, bg_type="image"):
112
  """
113
  Process video with the selected background using SAM2 and MatAnyone pipeline.
114
- Returns the path to the processed video file.
115
  """
 
 
 
 
116
  try:
117
- logger.info("==== Starting process_video ====")
118
- # Create a temporary directory for processing
119
- with tempfile.TemporaryDirectory() as temp_dir:
120
- temp_dir = Path(temp_dir)
121
- logger.info(f"Initialized temp_dir: {temp_dir}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
- # Save the uploaded video to a temporary file
124
- input_path = str(temp_dir / "input.mp4")
125
- logger.info(f"Writing video to {input_path}")
126
- with open(input_path, "wb") as f:
127
- written = f.write(input_file.getvalue())
128
- logger.info(f"Wrote {written/1e6:.2f}MB")
129
- if not os.path.exists(input_path):
130
- raise FileNotFoundError(f"Input video not saved: {input_path}")
131
 
132
- # Prepare background
133
- bg_path = None
134
- if bg_type == "image" and background is not None:
135
- bg_cv = cv2.cvtColor(np.array(background), cv2.COLOR_RGB2BGR)
136
- bg_path = str(temp_dir / "background.jpg")
137
- cv2.imwrite(bg_path, bg_cv)
138
- logger.info(f"Background image written to {bg_path}")
139
- elif bg_type == "color" and hasattr(st.session_state, 'bg_color'):
140
- color_hex = st.session_state.bg_color.lstrip('#')
141
- color_rgb = tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
142
- bg_path = str(temp_dir / "background.jpg")
143
- cv2.imwrite(bg_path, np.ones((100, 100, 3), dtype=np.uint8) * color_rgb[::-1])
144
- logger.info(f"Background color image written to {bg_path}")
145
 
146
- logger.info(f"Disk free before processing: {os.popen('df -h / | tail -1').read().strip()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- # Set up progress placeholders
149
- progress_placeholder = st.empty()
150
- status_placeholder = st.empty()
151
 
152
- def progress_callback(progress, message):
153
- progress = max(0, min(1, float(progress)))
154
- progress_placeholder.progress(progress)
155
- status_placeholder.text(f"Status: {message}")
156
-
157
- # Log GPU state
158
- if torch.cuda.is_available():
159
- logger.info(f"CUDA Available: True, Device: {torch.cuda.get_device_name(0)}")
160
- logger.info(f"GPU Mem Before: {torch.cuda.memory_allocated()/1e9:.2f}GB")
161
- else:
162
- logger.info("CUDA Available: False")
163
 
164
- # Process the video
165
- output_path = str(temp_dir / "output.mp4")
166
- click_points = [[0.5, 0.5]]
167
- from pipeline.integrated_pipeline import TwoStageProcessor
168
 
169
- # Cache processor
170
- @st.cache_resource
171
- def load_processor(temp_dir):
172
- return TwoStageProcessor(temp_dir=temp_dir)
173
- processor = load_processor(str(temp_dir))
174
 
175
- try:
176
- logger.info("Calling TwoStageProcessor.process_video...")
177
- success = processor.process_video(
178
- input_video=input_path,
179
- background_video=bg_path if bg_type == "image" else "",
180
- click_points=click_points,
181
- output_path=output_path,
182
- use_matanyone=True,
183
- progress_callback=progress_callback
184
- )
185
- except Exception as e:
186
- logger.error(f"Pipeline processing failed: {traceback.format_exc()}", exc_info=True)
187
- raise
188
 
189
- if torch.cuda.is_available():
190
- logger.info(f"GPU Mem After: {torch.cuda.memory_allocated()/1e9:.2f}GB")
191
- torch.cuda.empty_cache()
 
 
 
192
 
193
- if not success:
194
- raise RuntimeError("Video processing failed")
 
 
 
 
 
195
 
196
- logger.info("==== process_video completed successfully ====")
197
- return output_path
 
 
 
198
 
199
  except Exception as e:
200
- logger.error(f"Error in video processing: {str(e)}", exc_info=True)
 
 
 
201
  st.error(f"An error occurred during processing: {str(e)}")
202
  return None
203
 
@@ -209,6 +264,8 @@ def main():
209
  # Initialize session state
210
  initialize_session_state()
211
 
 
 
212
  # Main layout
213
  col1, col2 = st.columns([1, 1], gap="large")
214
 
@@ -224,24 +281,28 @@ def main():
224
  # Check if video actually changed using id()
225
  current_video_id = id(uploaded)
226
  if current_video_id != st.session_state.last_video_id:
 
227
  st.session_state.uploaded_video = uploaded
228
  st.session_state.last_video_id = current_video_id
229
  st.session_state.video_bytes_cache = None
 
 
230
 
231
  # Video preview section
232
  st.markdown("### Video Preview")
233
- if st.session_state.video_preview_placeholder is None:
234
- st.session_state.video_preview_placeholder = st.empty()
235
 
236
- if st.session_state.uploaded_video is not None:
237
- if st.session_state.video_bytes_cache is None:
238
- st.session_state.video_bytes_cache = st.session_state.uploaded_video.getvalue()
239
- st.session_state.uploaded_video.seek(0)
240
-
241
- with st.session_state.video_preview_placeholder.container():
 
 
242
  st.video(st.session_state.video_bytes_cache)
243
- else:
244
- st.session_state.video_preview_placeholder.empty()
245
 
246
  with col2:
247
  st.header("2. Background Settings")
@@ -264,26 +325,26 @@ def main():
264
  # Check if image actually changed using id()
265
  current_bg_id = id(bg_image)
266
  if current_bg_id != st.session_state.last_bg_image_id:
 
267
  st.session_state.last_bg_image_id = current_bg_id
268
  if bg_image is not None:
269
  st.session_state.bg_image_cache = Image.open(bg_image)
270
  st.session_state.bg_image_name = bg_image.name
 
271
  else:
272
  st.session_state.bg_image_cache = None
273
 
274
- # Background preview section
275
- if st.session_state.bg_preview_placeholder is None:
276
- st.session_state.bg_preview_placeholder = st.empty()
277
-
278
- if st.session_state.bg_image_cache is not None:
279
- with st.session_state.bg_preview_placeholder.container():
280
  st.image(
281
  st.session_state.bg_image_cache,
282
  caption="Selected Background",
283
  use_container_width=True
284
  )
285
- else:
286
- st.session_state.bg_preview_placeholder.empty()
287
 
288
  elif bg_type == "Color":
289
  selected_color = st.color_picker(
@@ -294,6 +355,7 @@ def main():
294
 
295
  # Update only if color actually changed
296
  if selected_color != st.session_state.cached_color:
 
297
  st.session_state.bg_color = selected_color
298
  st.session_state.cached_color = selected_color
299
 
@@ -302,15 +364,11 @@ def main():
302
  color_display[:, :] = color_rgb[::-1]
303
  st.session_state.color_display_cache = color_display
304
 
305
- # Color preview section
306
- if st.session_state.bg_preview_placeholder is None:
307
- st.session_state.bg_preview_placeholder = st.empty()
308
-
309
- if st.session_state.color_display_cache is not None:
310
- with st.session_state.bg_preview_placeholder.container():
311
  st.image(st.session_state.color_display_cache, caption="Selected Color", width=200)
312
- else:
313
- st.session_state.bg_preview_placeholder.empty()
314
 
315
  st.header("3. Process & Download")
316
 
@@ -319,52 +377,69 @@ def main():
319
  "🚀 Process Video",
320
  disabled=not st.session_state.uploaded_video or st.session_state.processing
321
  )
322
- process_status = st.empty()
323
 
324
  if submitted and not st.session_state.processing:
 
325
  st.session_state.processing = True
 
 
 
326
  with st.spinner("Processing video (this may take a few minutes)..."):
327
  try:
328
  background = None
329
- if bg_type == "image" and st.session_state.bg_image_cache is not None:
330
  background = st.session_state.bg_image_cache
331
- elif bg_type == "color" and 'bg_color' in st.session_state:
 
332
  background = st.session_state.bg_color
 
333
 
334
- output_path = process_video(
335
  st.session_state.uploaded_video,
336
  background,
337
  bg_type=bg_type.lower()
338
  )
339
 
340
- if output_path and os.path.exists(output_path):
341
- st.session_state.processed_video_path = output_path
342
- process_status.success("✅ Video processing complete!")
 
 
343
  else:
344
- process_status.error("❌ Failed to process video. Please check the logs for details.")
 
 
345
  except Exception as e:
346
- process_status.error(f"❌ An error occurred: {str(e)}")
347
- logger.exception("Video processing failed")
 
348
  finally:
349
  st.session_state.processing = False
 
350
 
351
- if st.session_state.processed_video_path:
352
- st.markdown("### Processed Video")
 
 
 
353
  try:
354
- with open(st.session_state.processed_video_path, 'rb') as f:
355
- video_bytes = f.read()
356
- st.video(video_bytes)
357
- st.download_button(
358
- label="💾 Download Processed Video",
359
- data=video_bytes,
360
- file_name="processed_video.mp4",
361
- mime="video/mp4",
362
- use_container_width=True,
363
- key="download_button"
364
- )
 
 
365
  except Exception as e:
 
 
366
  st.error(f"Error displaying video: {str(e)}")
367
- logger.error(f"Error displaying video: {str(e)}", exc_info=True)
368
 
369
  if __name__ == "__main__":
370
  main()
 
14
  import io
15
  import torch
16
  import traceback
17
+ import shutil
18
 
19
  # --- Project Setup ---
20
  sys.path.append(str(Path(__file__).parent.absolute()))
 
89
  defaults = {
90
  'uploaded_video': None,
91
  'video_bytes_cache': None,
 
92
  'bg_image': None,
93
  'bg_image_cache': None,
94
  'bg_image_name': None,
 
95
  'bg_color': "#00FF00",
96
  'cached_color': None,
97
  'color_display_cache': None,
98
+ 'processed_video_bytes': None, # Store bytes instead of path
99
  'processing': False,
100
  'progress': 0,
101
  'progress_text': "Ready",
102
  'last_video_id': None,
103
+ 'last_bg_image_id': None,
104
+ 'process_complete': False
105
  }
106
  for key, value in defaults.items():
107
  if key not in st.session_state:
 
111
  def process_video(input_file, background, bg_type="image"):
112
  """
113
  Process video with the selected background using SAM2 and MatAnyone pipeline.
114
+ Returns the video bytes.
115
  """
116
+ logger.info("=" * 60)
117
+ logger.info("🎬 STARTING VIDEO PROCESSING")
118
+ logger.info("=" * 60)
119
+
120
  try:
121
+ # Create a persistent temporary directory
122
+ temp_base = Path(tempfile.gettempdir()) / "video_processing"
123
+ temp_base.mkdir(exist_ok=True)
124
+ temp_dir = temp_base / f"session_{int(time.time())}"
125
+ temp_dir.mkdir(exist_ok=True)
126
+
127
+ logger.info(f"📁 Created temp directory: {temp_dir}")
128
+
129
+ # Save the uploaded video to a temporary file
130
+ input_path = str(temp_dir / "input.mp4")
131
+ logger.info(f"💾 Writing video to {input_path}")
132
+
133
+ with open(input_path, "wb") as f:
134
+ written = f.write(input_file.getvalue())
135
+ logger.info(f"✅ Wrote {written/1e6:.2f}MB")
136
+
137
+ if not os.path.exists(input_path):
138
+ raise FileNotFoundError(f"❌ Input video not saved: {input_path}")
139
+
140
+ logger.info(f"✅ Video file exists: {os.path.getsize(input_path)} bytes")
141
+
142
+ # Prepare background
143
+ bg_path = None
144
+ if bg_type == "image" and background is not None:
145
+ logger.info("🖼️ Processing background IMAGE")
146
+ bg_cv = cv2.cvtColor(np.array(background), cv2.COLOR_RGB2BGR)
147
+ bg_path = str(temp_dir / "background.jpg")
148
+ cv2.imwrite(bg_path, bg_cv)
149
+ logger.info(f"✅ Background image written to {bg_path}")
150
+
151
+ elif bg_type == "color" and hasattr(st.session_state, 'bg_color'):
152
+ logger.info(f"🎨 Processing background COLOR: {st.session_state.bg_color}")
153
+ color_hex = st.session_state.bg_color.lstrip('#')
154
+ color_rgb = tuple(int(color_hex[i:i+2], 16) for i in (0, 2, 4))
155
+ bg_path = str(temp_dir / "background.jpg")
156
+ cv2.imwrite(bg_path, np.ones((100, 100, 3), dtype=np.uint8) * color_rgb[::-1])
157
+ logger.info(f"✅ Background color image written to {bg_path}")
158
+
159
+ # Set up progress placeholders
160
+ progress_placeholder = st.empty()
161
+ status_placeholder = st.empty()
162
+
163
+ def progress_callback(progress, message):
164
+ progress = max(0, min(1, float(progress)))
165
+ logger.info(f"📊 Progress: {progress*100:.1f}% - {message}")
166
+ progress_placeholder.progress(progress)
167
+ status_placeholder.text(f"Status: {message}")
168
 
169
+ # Log GPU state
170
+ if torch.cuda.is_available():
171
+ logger.info(f"🎮 CUDA Available: True, Device: {torch.cuda.get_device_name(0)}")
172
+ logger.info(f"💾 GPU Mem Before: {torch.cuda.memory_allocated()/1e9:.2f}GB")
173
+ else:
174
+ logger.info("⚠️ CUDA Available: False (using CPU)")
 
 
175
 
176
+ # Process the video
177
+ output_path = str(temp_dir / "output.mp4")
178
+ click_points = [[0.5, 0.5]]
179
+
180
+ logger.info("🔧 Importing TwoStageProcessor...")
181
+ from pipeline.integrated_pipeline import TwoStageProcessor
 
 
 
 
 
 
 
182
 
183
+ # Cache processor
184
+ @st.cache_resource
185
+ def load_processor(temp_dir_str):
186
+ logger.info(f"🏗️ Loading processor with temp_dir: {temp_dir_str}")
187
+ return TwoStageProcessor(temp_dir=temp_dir_str)
188
+
189
+ processor = load_processor(str(temp_dir))
190
+ logger.info("✅ Processor loaded")
191
+
192
+ try:
193
+ logger.info("🎬 Calling TwoStageProcessor.process_video...")
194
+ logger.info(f" - Input: {input_path}")
195
+ logger.info(f" - Background: {bg_path}")
196
+ logger.info(f" - Output: {output_path}")
197
+ logger.info(f" - Click points: {click_points}")
198
+
199
+ success = processor.process_video(
200
+ input_video=input_path,
201
+ background_video=bg_path if bg_type == "image" else "",
202
+ click_points=click_points,
203
+ output_path=output_path,
204
+ use_matanyone=True,
205
+ progress_callback=progress_callback
206
+ )
207
 
208
+ logger.info(f"📊 Processing returned: {success}")
 
 
209
 
210
+ except Exception as e:
211
+ logger.error(f"❌ Pipeline processing failed: {traceback.format_exc()}", exc_info=True)
212
+ raise
 
 
 
 
 
 
 
 
213
 
214
+ if torch.cuda.is_available():
215
+ logger.info(f"💾 GPU Mem After: {torch.cuda.memory_allocated()/1e9:.2f}GB")
216
+ torch.cuda.empty_cache()
217
+ logger.info("🧹 GPU cache cleared")
218
 
219
+ if not success:
220
+ raise RuntimeError("❌ Video processing returned False")
 
 
 
221
 
222
+ # Check if output file exists
223
+ if not os.path.exists(output_path):
224
+ logger.error(f"❌ Output file does not exist: {output_path}")
225
+ raise FileNotFoundError(f"Output video not created: {output_path}")
226
+
227
+ output_size = os.path.getsize(output_path)
228
+ logger.info(f"✅ Output file exists: {output_size} bytes ({output_size/1e6:.2f}MB)")
 
 
 
 
 
 
229
 
230
+ # Read the output video into memory
231
+ logger.info("📖 Reading output video into memory...")
232
+ with open(output_path, 'rb') as f:
233
+ video_bytes = f.read()
234
+
235
+ logger.info(f"✅ Read {len(video_bytes)/1e6:.2f}MB into memory")
236
 
237
+ # Clean up temp directory
238
+ try:
239
+ logger.info(f"🧹 Cleaning up temp directory: {temp_dir}")
240
+ shutil.rmtree(temp_dir)
241
+ logger.info("✅ Temp directory cleaned")
242
+ except Exception as e:
243
+ logger.warning(f"⚠️ Could not clean temp directory: {e}")
244
 
245
+ logger.info("=" * 60)
246
+ logger.info("✅ VIDEO PROCESSING COMPLETED SUCCESSFULLY")
247
+ logger.info("=" * 60)
248
+
249
+ return video_bytes
250
 
251
  except Exception as e:
252
+ logger.error("=" * 60)
253
+ logger.error(f"❌ ERROR IN VIDEO PROCESSING: {str(e)}")
254
+ logger.error(traceback.format_exc())
255
+ logger.error("=" * 60)
256
  st.error(f"An error occurred during processing: {str(e)}")
257
  return None
258
 
 
264
  # Initialize session state
265
  initialize_session_state()
266
 
267
+ logger.info(f"🔄 App rerun - Processing: {st.session_state.processing}, Complete: {st.session_state.process_complete}")
268
+
269
  # Main layout
270
  col1, col2 = st.columns([1, 1], gap="large")
271
 
 
281
  # Check if video actually changed using id()
282
  current_video_id = id(uploaded)
283
  if current_video_id != st.session_state.last_video_id:
284
+ logger.info(f"📹 New video uploaded: {uploaded.name if uploaded else 'None'}")
285
  st.session_state.uploaded_video = uploaded
286
  st.session_state.last_video_id = current_video_id
287
  st.session_state.video_bytes_cache = None
288
+ st.session_state.processed_video_bytes = None # Clear processed video
289
+ st.session_state.process_complete = False
290
 
291
  # Video preview section
292
  st.markdown("### Video Preview")
293
+ video_preview_container = st.container()
 
294
 
295
+ with video_preview_container:
296
+ if st.session_state.uploaded_video is not None:
297
+ if st.session_state.video_bytes_cache is None:
298
+ logger.info("📖 Caching video bytes...")
299
+ st.session_state.video_bytes_cache = st.session_state.uploaded_video.getvalue()
300
+ st.session_state.uploaded_video.seek(0)
301
+ logger.info(f"✅ Cached {len(st.session_state.video_bytes_cache)/1e6:.2f}MB")
302
+
303
  st.video(st.session_state.video_bytes_cache)
304
+ else:
305
+ st.info("No video uploaded yet")
306
 
307
  with col2:
308
  st.header("2. Background Settings")
 
325
  # Check if image actually changed using id()
326
  current_bg_id = id(bg_image)
327
  if current_bg_id != st.session_state.last_bg_image_id:
328
+ logger.info(f"🖼️ New background image: {bg_image.name if bg_image else 'None'}")
329
  st.session_state.last_bg_image_id = current_bg_id
330
  if bg_image is not None:
331
  st.session_state.bg_image_cache = Image.open(bg_image)
332
  st.session_state.bg_image_name = bg_image.name
333
+ logger.info(f"✅ Background image cached: {bg_image.name}")
334
  else:
335
  st.session_state.bg_image_cache = None
336
 
337
+ # Background preview
338
+ bg_preview_container = st.container()
339
+ with bg_preview_container:
340
+ if st.session_state.bg_image_cache is not None:
 
 
341
  st.image(
342
  st.session_state.bg_image_cache,
343
  caption="Selected Background",
344
  use_container_width=True
345
  )
346
+ else:
347
+ st.info("No background image uploaded yet")
348
 
349
  elif bg_type == "Color":
350
  selected_color = st.color_picker(
 
355
 
356
  # Update only if color actually changed
357
  if selected_color != st.session_state.cached_color:
358
+ logger.info(f"🎨 Color changed to: {selected_color}")
359
  st.session_state.bg_color = selected_color
360
  st.session_state.cached_color = selected_color
361
 
 
364
  color_display[:, :] = color_rgb[::-1]
365
  st.session_state.color_display_cache = color_display
366
 
367
+ # Color preview
368
+ color_preview_container = st.container()
369
+ with color_preview_container:
370
+ if st.session_state.color_display_cache is not None:
 
 
371
  st.image(st.session_state.color_display_cache, caption="Selected Color", width=200)
 
 
372
 
373
  st.header("3. Process & Download")
374
 
 
377
  "🚀 Process Video",
378
  disabled=not st.session_state.uploaded_video or st.session_state.processing
379
  )
 
380
 
381
  if submitted and not st.session_state.processing:
382
+ logger.info("🚀 PROCESS BUTTON CLICKED")
383
  st.session_state.processing = True
384
+ st.session_state.process_complete = False
385
+ st.session_state.processed_video_bytes = None
386
+
387
  with st.spinner("Processing video (this may take a few minutes)..."):
388
  try:
389
  background = None
390
+ if bg_type == "Image" and st.session_state.bg_image_cache is not None:
391
  background = st.session_state.bg_image_cache
392
+ logger.info("📦 Using background IMAGE")
393
+ elif bg_type == "Color" and 'bg_color' in st.session_state:
394
  background = st.session_state.bg_color
395
+ logger.info(f"📦 Using background COLOR: {background}")
396
 
397
+ video_bytes = process_video(
398
  st.session_state.uploaded_video,
399
  background,
400
  bg_type=bg_type.lower()
401
  )
402
 
403
+ if video_bytes and len(video_bytes) > 0:
404
+ st.session_state.processed_video_bytes = video_bytes
405
+ st.session_state.process_complete = True
406
+ logger.info(f"✅ Processing complete! Video size: {len(video_bytes)/1e6:.2f}MB")
407
+ st.success("✅ Video processing complete!")
408
  else:
409
+ logger.error("❌ Processing returned empty or None")
410
+ st.error("❌ Failed to process video. Please check the logs for details.")
411
+
412
  except Exception as e:
413
+ logger.error(f"❌ Exception during processing: {str(e)}")
414
+ logger.error(traceback.format_exc())
415
+ st.error(f"❌ An error occurred: {str(e)}")
416
  finally:
417
  st.session_state.processing = False
418
+ logger.info(f"🏁 Processing finished. Success: {st.session_state.process_complete}")
419
 
420
+ # Show processed video if available (OUTSIDE the form)
421
+ if st.session_state.processed_video_bytes is not None and len(st.session_state.processed_video_bytes) > 0:
422
+ st.markdown("---")
423
+ st.markdown("### ✅ Processed Video")
424
+
425
  try:
426
+ logger.info(f"📺 Displaying processed video: {len(st.session_state.processed_video_bytes)/1e6:.2f}MB")
427
+ st.video(st.session_state.processed_video_bytes)
428
+
429
+ st.download_button(
430
+ label="💾 Download Processed Video",
431
+ data=st.session_state.processed_video_bytes,
432
+ file_name="processed_video.mp4",
433
+ mime="video/mp4",
434
+ use_container_width=True,
435
+ key="download_button"
436
+ )
437
+ logger.info("✅ Video displayed successfully")
438
+
439
  except Exception as e:
440
+ logger.error(f"❌ Error displaying video: {str(e)}")
441
+ logger.error(traceback.format_exc())
442
  st.error(f"Error displaying video: {str(e)}")
 
443
 
444
  if __name__ == "__main__":
445
  main()