andevs commited on
Commit
d4bd155
·
verified ·
1 Parent(s): 06dcf85

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +9 -130
app.py CHANGED
@@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
45
  os.environ["U2NET_HOME"] = "/tmp/u2net_models"
46
  os.environ["OMP_NUM_THREADS"] = "1"
47
  os.environ["CUDA_VISIBLE_DEVICES"] = ""
48
- os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # Suppress TF warnings
49
  os.environ["PYTHONUNBUFFERED"] = "1"
50
 
51
  # Create necessary directories
@@ -86,10 +86,8 @@ def download_model_with_retry(model_name: str, max_retries: int = 3) -> Optional
86
  try:
87
  logger.info("🚀 Initializing AI models...")
88
 
89
- # Import rembg only when needed
90
  from rembg import remove, new_session
91
 
92
- # Model configurations with priorities
93
  model_configs = [
94
  {"name": "u2net", "description": "Best quality - general purpose", "size": "176MB", "priority": 1},
95
  {"name": "u2netp", "description": "Fast processing - lightweight", "size": "4.7MB", "priority": 2},
@@ -97,10 +95,8 @@ try:
97
  {"name": "u2net_human_seg", "description": "Specialized for portraits", "size": "176MB", "priority": 4},
98
  ]
99
 
100
- # Sort by priority
101
  model_configs.sort(key=lambda x: x["priority"])
102
 
103
- # Load models
104
  for config in model_configs:
105
  session = download_model_with_retry(config["name"])
106
  if session:
@@ -134,7 +130,6 @@ app = FastAPI(
134
  version="8.2.0"
135
  )
136
 
137
- # CORS configuration
138
  app.add_middleware(
139
  CORSMiddleware,
140
  allow_origins=["*"],
@@ -146,29 +141,24 @@ app.add_middleware(
146
  # ========== UTILITY FUNCTIONS ==========
147
 
148
  def validate_image_file(file: UploadFile) -> None:
149
- """Validate image file upload"""
150
  if not file.content_type or not file.content_type.startswith('image/'):
151
  raise HTTPException(400, f"File must be an image, got {file.content_type}")
152
 
153
- # Check file extension
154
  allowed_extensions = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.tif'}
155
  file_ext = Path(file.filename).suffix.lower()
156
  if file_ext not in allowed_extensions:
157
  raise HTTPException(400, f"Unsupported image format: {file_ext}")
158
 
159
  def validate_video_file(file: UploadFile) -> None:
160
- """Validate video file upload"""
161
  if not file.content_type or not file.content_type.startswith('video/'):
162
  raise HTTPException(400, f"File must be a video, got {file.content_type}")
163
 
164
- # Check file extension
165
  allowed_extensions = {'.mp4', '.avi', '.mov', '.webm', '.mkv', '.flv', '.wmv'}
166
  file_ext = Path(file.filename).suffix.lower()
167
  if file_ext not in allowed_extensions:
168
  raise HTTPException(400, f"Unsupported video format: {file_ext}")
169
 
170
  def cleanup_temp_files(*file_paths: str) -> None:
171
- """Clean up temporary files"""
172
  for path in file_paths:
173
  if path and os.path.exists(path):
174
  try:
@@ -178,7 +168,6 @@ def cleanup_temp_files(*file_paths: str) -> None:
178
  logger.warning(f"Failed to cleanup {path}: {e}")
179
 
180
  def get_file_size_mb(file_path: str) -> float:
181
- """Get file size in megabytes"""
182
  if os.path.exists(file_path):
183
  return os.path.getsize(file_path) / (1024 * 1024)
184
  return 0.0
@@ -192,16 +181,12 @@ def process_image_sync(
192
  max_size: int = 1024,
193
  quality: int = 95
194
  ) -> Dict:
195
- """Process single image synchronously"""
196
  try:
197
- # Open and validate image
198
  image = Image.open(io.BytesIO(image_data))
199
 
200
- # Convert to RGB if necessary
201
  if image.mode not in ['RGB', 'RGBA']:
202
  image = image.convert('RGB')
203
 
204
- # Select model (fallback to first available)
205
  if model_name not in models:
206
  available_models = list(models.keys())
207
  if not available_models:
@@ -211,30 +196,24 @@ def process_image_sync(
211
 
212
  session = models[model_name]["session"]
213
 
214
- # Resize if too large
215
  original_size = image.size
216
  if max(image.size) > max_size:
217
  image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
218
  logger.info(f"Resized from {original_size} to {image.size}")
219
 
220
- # Ensure RGBA for transparency support
221
  if image.mode != 'RGBA':
222
  image = image.convert('RGBA')
223
 
224
- # Remove background
225
  result = remove_func(image, session=session)
226
 
227
- # Handle transparency
228
  if not transparent:
229
  if result.mode != 'RGBA':
230
  result = result.convert('RGBA')
231
  white_bg = Image.new('RGBA', result.size, (255, 255, 255, 255))
232
  result = Image.alpha_composite(white_bg, result)
233
 
234
- # Optimize and save
235
  output_buffer = io.BytesIO()
236
 
237
- # Convert to RGB for JPEG if not transparent
238
  if not transparent:
239
  result_rgb = result.convert('RGB')
240
  result_rgb.save(output_buffer, format="JPEG", quality=quality, optimize=True)
@@ -268,9 +247,7 @@ def process_frame_with_fallback(
268
  model_name: str = "silueta",
269
  transparent: bool = True
270
  ) -> Optional[np.ndarray]:
271
- """Process a single frame with robust error handling"""
272
  try:
273
- # Select available model
274
  if model_name not in models:
275
  available_models = list(models.keys())
276
  if not available_models:
@@ -279,7 +256,6 @@ def process_frame_with_fallback(
279
 
280
  session = models[model_name]["session"]
281
 
282
- # Convert BGR to RGB for PIL
283
  if len(frame.shape) == 3:
284
  if frame.shape[2] == 3:
285
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
@@ -288,33 +264,24 @@ def process_frame_with_fallback(
288
  else:
289
  return None
290
  else:
291
- # Grayscale to RGB
292
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB)
293
 
294
- # Convert to PIL Image
295
  img = Image.fromarray(frame_rgb)
296
  if img.mode != 'RGBA':
297
  img = img.convert('RGBA')
298
 
299
- # Remove background
300
  result = remove_func(img, session=session)
301
-
302
- # Convert back to numpy
303
  result_array = np.array(result)
304
 
305
  if transparent:
306
  if result_array.shape[2] == 4:
307
- # RGBA to BGRA for OpenCV
308
  return cv2.cvtColor(result_array, cv2.COLOR_RGBA2BGRA)
309
  else:
310
- # Add alpha channel
311
  bgr = cv2.cvtColor(result_array, cv2.COLOR_RGB2BGR)
312
  alpha = np.ones((bgr.shape[0], bgr.shape[1], 1), dtype=np.uint8) * 255
313
  return np.concatenate([bgr, alpha], axis=2)
314
  else:
315
- # Add white background
316
  if result_array.shape[2] == 4:
317
- # Create white background
318
  alpha = result_array[:, :, 3:4] / 255.0
319
  white_bg = np.ones_like(result_array[:, :, :3]) * 255
320
  blended = (1 - alpha) * white_bg + alpha * result_array[:, :, :3]
@@ -324,7 +291,6 @@ def process_frame_with_fallback(
324
 
325
  except Exception as e:
326
  logger.error(f"Frame processing failed: {e}")
327
- # Return original frame with alpha if needed
328
  if transparent and len(frame.shape) == 3 and frame.shape[2] == 3:
329
  return cv2.cvtColor(frame, cv2.COLOR_BGR2BGRA)
330
  return frame
@@ -338,9 +304,7 @@ def create_video_with_ffmpeg(
338
  height: int = 360,
339
  transparent: bool = True
340
  ) -> Tuple[bool, str]:
341
- """Create video using ffmpeg with specified parameters"""
342
  try:
343
- # Check if ffmpeg is available
344
  result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True)
345
  if result.returncode != 0:
346
  logger.warning("ffmpeg not found, using OpenCV fallback")
@@ -348,10 +312,8 @@ def create_video_with_ffmpeg(
348
 
349
  actual_output_path = output_path
350
 
351
- # Build ffmpeg command based on format and transparency
352
  if format.lower() in ['mp4', 'mov']:
353
  if transparent:
354
- # MP4 doesn't support transparency well, use WebM instead
355
  logger.info("MP4 doesn't support alpha channel, switching to WebM format")
356
  actual_output_path = output_path.replace('.mp4', '.webm').replace('.mov', '.webm')
357
  cmd = [
@@ -438,31 +400,25 @@ def process_video_reliable(
438
  transparent: bool = True,
439
  format: str = "mp4"
440
  ) -> Dict:
441
- """Process video with reliable output"""
442
  try:
443
  logger.info(f"Processing video: {input_path} -> {output_path}")
444
  logger.info(f"Settings: model={model_name}, size={max_size}, fps={fps}, format={format}")
445
 
446
- # Open video
447
  cap = cv2.VideoCapture(input_path)
448
  if not cap.isOpened():
449
  raise ValueError(f"Cannot open video: {input_path}")
450
 
451
- # Get video properties
452
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
453
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
454
  orig_fps = cap.get(cv2.CAP_PROP_FPS)
455
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
456
 
457
- # Calculate FPS
458
  target_fps = min(orig_fps if orig_fps > 0 else fps, 15)
459
 
460
- # Calculate dimensions
461
  scale = min(max_size / max(width, height), 1.0)
462
  new_width = int(width * scale)
463
  new_height = int(height * scale)
464
 
465
- # Ensure even dimensions
466
  if new_width % 2 != 0:
467
  new_width += 1
468
  if new_height % 2 != 0:
@@ -471,15 +427,13 @@ def process_video_reliable(
471
  logger.info(f"Video: {width}x{height} -> {new_width}x{new_height}")
472
  logger.info(f"FPS: {target_fps}, Total frames: {total_frames}")
473
 
474
- # Create temp directory for frames
475
  frames_dir = tempfile.mkdtemp(prefix="video_frames_")
476
  logger.info(f"Frames directory: {frames_dir}")
477
 
478
- # Process frames
479
  frame_count = 0
480
  processed_count = 0
481
  start_time = time.time()
482
- max_processing_time = 300 # 5 minutes timeout
483
 
484
  while True:
485
  ret, frame = cap.read()
@@ -488,36 +442,28 @@ def process_video_reliable(
488
 
489
  frame_count += 1
490
 
491
- # Skip frames if configured
492
  if frame_skip > 0 and frame_count % (frame_skip + 1) != 0:
493
  continue
494
 
495
- # Check timeout
496
  if time.time() - start_time > max_processing_time:
497
  logger.warning(f"Processing timeout reached after {processed_count} frames")
498
  break
499
 
500
  try:
501
- # Resize frame
502
  frame_resized = cv2.resize(frame, (new_width, new_height))
503
-
504
- # Process frame
505
  processed_frame = process_frame_with_fallback(frame_resized, model_name, transparent)
506
 
507
  if processed_frame is not None:
508
- # Save frame
509
  frame_path = os.path.join(frames_dir, f"frame_{processed_count:06d}.png")
510
  cv2.imwrite(frame_path, processed_frame)
511
  processed_count += 1
512
 
513
- # Log progress
514
  if processed_count % 10 == 0:
515
  elapsed = time.time() - start_time
516
  logger.info(f"Processed {processed_count} frames ({elapsed:.1f}s)")
517
 
518
  except Exception as e:
519
  logger.error(f"Frame {frame_count} failed: {e}")
520
- # Save original frame as fallback
521
  try:
522
  frame_path = os.path.join(frames_dir, f"frame_{processed_count:06d}.png")
523
  frame_resized = cv2.resize(frame, (new_width, new_height))
@@ -535,7 +481,6 @@ def process_video_reliable(
535
  if processed_count == 0:
536
  raise ValueError("No frames processed")
537
 
538
- # Create video
539
  actual_format = format
540
  if transparent and format.lower() in ['mp4', 'mov']:
541
  actual_format = 'webm'
@@ -547,7 +492,6 @@ def process_video_reliable(
547
  )
548
 
549
  if not ffmpeg_success:
550
- # Fallback to OpenCV
551
  logger.info("Using OpenCV fallback")
552
  if transparent and actual_format.lower() == "avi":
553
  fourcc = cv2.VideoWriter_fourcc(*'FFV1')
@@ -559,11 +503,9 @@ def process_video_reliable(
559
  out = cv2.VideoWriter(actual_output_path, fourcc, target_fps, (new_width, new_height), True)
560
 
561
  if not out.isOpened():
562
- # Last resort fallback
563
  fourcc = cv2.VideoWriter_fourcc(*'MJPG')
564
  out = cv2.VideoWriter(actual_output_path, fourcc, target_fps, (new_width, new_height))
565
 
566
- # Write frames
567
  for i in range(processed_count):
568
  frame_path = os.path.join(frames_dir, f"frame_{i:06d}.png")
569
  if os.path.exists(frame_path):
@@ -573,7 +515,6 @@ def process_video_reliable(
573
 
574
  out.release()
575
 
576
- # Cleanup frames directory
577
  try:
578
  shutil.rmtree(frames_dir)
579
  except:
@@ -581,7 +522,6 @@ def process_video_reliable(
581
 
582
  processing_time = time.time() - start_time
583
 
584
- # Verify output
585
  if not os.path.exists(actual_output_path) or os.path.getsize(actual_output_path) < 1024:
586
  logger.error(f"Output file too small or missing: {actual_output_path}")
587
  create_fallback_video(actual_output_path, new_width, new_height, target_fps)
@@ -608,20 +548,17 @@ def process_video_reliable(
608
  raise
609
 
610
  def create_fallback_video(output_path: str, width: int, height: int, fps: int):
611
- """Create a simple video with error message"""
612
  try:
613
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
614
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
615
 
616
- for i in range(fps * 5): # 5 second video
617
  frame = np.zeros((height, width, 3), dtype=np.uint8)
618
 
619
- # Gradient background
620
  for y in range(height):
621
  color_value = int(40 + (y / height) * 40)
622
  cv2.rectangle(frame, (0, y), (width, y+1), (color_value, color_value, color_value+20), -1)
623
 
624
- # Add text
625
  if i < fps * 2:
626
  text = "Processing Video..."
627
  else:
@@ -647,7 +584,6 @@ def create_fallback_video(output_path: str, width: int, height: int, fps: int):
647
 
648
  @app.get("/")
649
  async def home():
650
- """Home page with API documentation"""
651
  html = """
652
  <!DOCTYPE html>
653
  <html lang="en">
@@ -656,12 +592,7 @@ async def home():
656
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
657
  <title>🎬 Background Remover Pro</title>
658
  <style>
659
- * {
660
- margin: 0;
661
- padding: 0;
662
- box-sizing: border-box;
663
- }
664
-
665
  body {
666
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
667
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@@ -673,7 +604,6 @@ async def home():
673
  align-items: center;
674
  justify-content: center;
675
  }
676
-
677
  .container {
678
  max-width: 900px;
679
  margin: 0 auto;
@@ -684,7 +614,6 @@ async def home():
684
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
685
  border: 1px solid rgba(255, 255, 255, 0.2);
686
  }
687
-
688
  h1 {
689
  font-size: 2.5em;
690
  margin-bottom: 10px;
@@ -693,14 +622,12 @@ async def home():
693
  -webkit-text-fill-color: transparent;
694
  text-align: center;
695
  }
696
-
697
  .subtitle {
698
  text-align: center;
699
  color: #cbd5e0;
700
  margin-bottom: 30px;
701
  font-size: 1.1em;
702
  }
703
-
704
  .status {
705
  background: linear-gradient(135deg, rgba(72, 187, 120, 0.2), rgba(72, 187, 120, 0.1));
706
  padding: 15px 20px;
@@ -709,25 +636,17 @@ async def home():
709
  margin: 20px 0;
710
  border: 1px solid rgba(72, 187, 120, 0.3);
711
  }
712
-
713
- .status.warning {
714
- background: linear-gradient(135deg, rgba(237, 137, 54, 0.2), rgba(237, 137, 54, 0.1));
715
- border: 1px solid rgba(237, 137, 54, 0.3);
716
- }
717
-
718
  h2 {
719
  margin: 25px 0 15px 0;
720
  color: #e0e7ff;
721
  font-size: 1.5em;
722
  }
723
-
724
  .endpoint-grid {
725
  display: grid;
726
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
727
  gap: 15px;
728
  margin: 20px 0;
729
  }
730
-
731
  .endpoint {
732
  background: rgba(255, 255, 255, 0.05);
733
  padding: 20px;
@@ -735,19 +654,16 @@ async def home():
735
  border-left: 4px solid #667eea;
736
  transition: transform 0.2s, background 0.2s;
737
  }
738
-
739
  .endpoint:hover {
740
  background: rgba(255, 255, 255, 0.1);
741
  transform: translateY(-2px);
742
  }
743
-
744
  .endpoint strong {
745
  display: block;
746
  font-size: 1.1em;
747
  margin-bottom: 8px;
748
  color: #a78bfa;
749
  }
750
-
751
  .endpoint code {
752
  background: rgba(0, 0, 0, 0.3);
753
  padding: 4px 8px;
@@ -756,21 +672,18 @@ async def home():
756
  display: inline-block;
757
  margin: 5px 0;
758
  }
759
-
760
  .feature-list {
761
  display: grid;
762
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
763
  gap: 10px;
764
  margin: 20px 0;
765
  }
766
-
767
  .feature {
768
  background: rgba(255, 255, 255, 0.05);
769
  padding: 12px;
770
  border-radius: 8px;
771
  text-align: center;
772
  }
773
-
774
  .footer {
775
  text-align: center;
776
  margin-top: 30px;
@@ -778,7 +691,6 @@ async def home():
778
  border-top: 1px solid rgba(255, 255, 255, 0.1);
779
  color: #a0aec0;
780
  }
781
-
782
  .badge {
783
  display: inline-block;
784
  padding: 4px 10px;
@@ -787,15 +699,9 @@ async def home():
787
  font-size: 0.85em;
788
  margin: 5px 5px 0 0;
789
  }
790
-
791
  @media (max-width: 768px) {
792
- .container {
793
- padding: 20px;
794
- }
795
-
796
- h1 {
797
- font-size: 2em;
798
- }
799
  }
800
  </style>
801
  </head>
@@ -879,7 +785,6 @@ async def home():
879
 
880
  @app.get("/api/health")
881
  async def health_check():
882
- """Health check endpoint"""
883
  return {
884
  "status": "healthy" if AI_AVAILABLE else "degraded",
885
  "ai_available": AI_AVAILABLE,
@@ -895,7 +800,6 @@ async def health_check():
895
 
896
  @app.get("/api/models")
897
  async def list_models():
898
- """List available AI models"""
899
  model_info = {}
900
  for name, info in models.items():
901
  model_info[name] = {
@@ -919,18 +823,15 @@ async def process_image_api(
919
  max_size: int = Query(1024, ge=256, le=2048, description="Maximum dimension"),
920
  quality: int = Query(95, ge=50, le=100, description="Output quality (JPEG only)")
921
  ):
922
- """Remove background from an image"""
923
  start_time = time.time()
924
 
925
  try:
926
- # Validate
927
  validate_image_file(file)
928
 
929
- # Read file
930
  contents = await file.read()
931
  if len(contents) == 0:
932
  raise HTTPException(400, "Empty file")
933
- if len(contents) > 20 * 1024 * 1024: # 20MB limit
934
  raise HTTPException(400, "File too large (max 20MB)")
935
 
936
  logger.info(f"Processing image: {file.filename} ({len(contents)/1024:.1f}KB)")
@@ -938,7 +839,6 @@ async def process_image_api(
938
  if not AI_AVAILABLE:
939
  raise HTTPException(503, "AI service temporarily unavailable")
940
 
941
- # Process image
942
  loop = asyncio.get_event_loop()
943
  process_func = functools.partial(
944
  process_image_sync,
@@ -953,7 +853,6 @@ async def process_image_api(
953
 
954
  processing_time = time.time() - start_time
955
 
956
- # Prepare response headers
957
  filename = Path(file.filename).stem
958
  headers = {
959
  "X-Processing-Time": f"{processing_time:.3f}",
@@ -989,18 +888,15 @@ async def process_video_api(
989
  frame_skip: int = Query(1, ge=0, le=5, description="Skip frames (0=none)"),
990
  format: str = Query("mp4", description="Output format (mp4, avi, webm, mov)")
991
  ):
992
- """Remove background from a video"""
993
  start_time = time.time()
994
 
995
  try:
996
- # Validate
997
  validate_video_file(file)
998
 
999
- # Read file
1000
  contents = await file.read()
1001
  if len(contents) == 0:
1002
  raise HTTPException(400, "Empty file")
1003
- if len(contents) > 100 * 1024 * 1024: # 100MB limit
1004
  raise HTTPException(400, "File too large (max 100MB)")
1005
 
1006
  logger.info(f"Processing video: {file.filename} ({len(contents)/1024/1024:.1f}MB)")
@@ -1008,7 +904,6 @@ async def process_video_api(
1008
  if not AI_AVAILABLE:
1009
  raise HTTPException(503, "AI service temporarily unavailable")
1010
 
1011
- # Save uploaded file
1012
  input_suffix = Path(file.filename).suffix or '.mp4'
1013
  input_path = tempfile.mktemp(suffix=input_suffix, dir="/tmp/uploads")
1014
 
@@ -1017,16 +912,14 @@ async def process_video_api(
1017
 
1018
  output_path = None
1019
  try:
1020
- # Create output file
1021
  output_ext = format.lower()
1022
  if transparent and output_ext in ['mp4', 'mov']:
1023
- output_ext = 'webm' # WebM supports transparency better
1024
  format = 'webm'
1025
  logger.info("Switched to WebM format for transparency support")
1026
 
1027
  output_path = tempfile.mktemp(suffix=f'.{output_ext}', dir="/tmp/outputs")
1028
 
1029
- # Process video
1030
  loop = asyncio.get_event_loop()
1031
  process_func = functools.partial(
1032
  process_video_reliable,
@@ -1044,10 +937,8 @@ async def process_video_api(
1044
 
1045
  processing_time = time.time() - start_time
1046
 
1047
- # Schedule cleanup
1048
  background_tasks.add_task(cleanup_temp_files, input_path)
1049
 
1050
- # Determine media type
1051
  media_types = {
1052
  "mp4": "video/mp4",
1053
  "avi": "video/x-msvideo",
@@ -1056,7 +947,6 @@ async def process_video_api(
1056
  }
1057
  media_type = media_types.get(result["format"].lower(), "video/mp4")
1058
 
1059
- # Prepare headers
1060
  filename = Path(file.filename).stem
1061
  headers = {
1062
  "X-Processing-Time": f"{processing_time:.3f}",
@@ -1093,11 +983,8 @@ async def process_video_api(
1093
  logger.error(f"Video API error: {str(e)}")
1094
  raise HTTPException(500, f"Video processing failed: {str(e)[:100]}")
1095
 
1096
- # ========== ERROR HANDLERS ==========
1097
-
1098
  @app.exception_handler(HTTPException)
1099
  async def http_exception_handler(request, exc):
1100
- """Custom HTTP exception handler"""
1101
  return JSONResponse(
1102
  status_code=exc.status_code,
1103
  content={
@@ -1109,7 +996,6 @@ async def http_exception_handler(request, exc):
1109
 
1110
  @app.exception_handler(Exception)
1111
  async def general_exception_handler(request, exc):
1112
- """General exception handler"""
1113
  logger.error(f"Unhandled exception: {str(exc)}")
1114
  return JSONResponse(
1115
  status_code=500,
@@ -1121,11 +1007,8 @@ async def general_exception_handler(request, exc):
1121
  }
1122
  )
1123
 
1124
- # ========== STARTUP/SHUTDOWN ==========
1125
-
1126
  @app.on_event("startup")
1127
  async def startup_event():
1128
- """Startup tasks"""
1129
  logger.info("=" * 50)
1130
  logger.info("🚀 Background Remover Pro v8.2.0 Starting...")
1131
  logger.info(f"📊 AI Available: {AI_AVAILABLE}")
@@ -1135,11 +1018,9 @@ async def startup_event():
1135
 
1136
  @app.on_event("shutdown")
1137
  async def shutdown_event():
1138
- """Cleanup on shutdown"""
1139
  logger.info("Shutting down...")
1140
  cpu_executor.shutdown(wait=True)
1141
 
1142
- # Cleanup temp directories
1143
  for dir_path in ["/tmp/uploads", "/tmp/outputs"]:
1144
  try:
1145
  if os.path.exists(dir_path):
@@ -1147,8 +1028,6 @@ async def shutdown_event():
1147
  except:
1148
  pass
1149
 
1150
- # ========== MAIN ==========
1151
-
1152
  if __name__ == "__main__":
1153
  import uvicorn
1154
 
 
45
  os.environ["U2NET_HOME"] = "/tmp/u2net_models"
46
  os.environ["OMP_NUM_THREADS"] = "1"
47
  os.environ["CUDA_VISIBLE_DEVICES"] = ""
48
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
49
  os.environ["PYTHONUNBUFFERED"] = "1"
50
 
51
  # Create necessary directories
 
86
  try:
87
  logger.info("🚀 Initializing AI models...")
88
 
 
89
  from rembg import remove, new_session
90
 
 
91
  model_configs = [
92
  {"name": "u2net", "description": "Best quality - general purpose", "size": "176MB", "priority": 1},
93
  {"name": "u2netp", "description": "Fast processing - lightweight", "size": "4.7MB", "priority": 2},
 
95
  {"name": "u2net_human_seg", "description": "Specialized for portraits", "size": "176MB", "priority": 4},
96
  ]
97
 
 
98
  model_configs.sort(key=lambda x: x["priority"])
99
 
 
100
  for config in model_configs:
101
  session = download_model_with_retry(config["name"])
102
  if session:
 
130
  version="8.2.0"
131
  )
132
 
 
133
  app.add_middleware(
134
  CORSMiddleware,
135
  allow_origins=["*"],
 
141
  # ========== UTILITY FUNCTIONS ==========
142
 
143
  def validate_image_file(file: UploadFile) -> None:
 
144
  if not file.content_type or not file.content_type.startswith('image/'):
145
  raise HTTPException(400, f"File must be an image, got {file.content_type}")
146
 
 
147
  allowed_extensions = {'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.tif'}
148
  file_ext = Path(file.filename).suffix.lower()
149
  if file_ext not in allowed_extensions:
150
  raise HTTPException(400, f"Unsupported image format: {file_ext}")
151
 
152
  def validate_video_file(file: UploadFile) -> None:
 
153
  if not file.content_type or not file.content_type.startswith('video/'):
154
  raise HTTPException(400, f"File must be a video, got {file.content_type}")
155
 
 
156
  allowed_extensions = {'.mp4', '.avi', '.mov', '.webm', '.mkv', '.flv', '.wmv'}
157
  file_ext = Path(file.filename).suffix.lower()
158
  if file_ext not in allowed_extensions:
159
  raise HTTPException(400, f"Unsupported video format: {file_ext}")
160
 
161
  def cleanup_temp_files(*file_paths: str) -> None:
 
162
  for path in file_paths:
163
  if path and os.path.exists(path):
164
  try:
 
168
  logger.warning(f"Failed to cleanup {path}: {e}")
169
 
170
  def get_file_size_mb(file_path: str) -> float:
 
171
  if os.path.exists(file_path):
172
  return os.path.getsize(file_path) / (1024 * 1024)
173
  return 0.0
 
181
  max_size: int = 1024,
182
  quality: int = 95
183
  ) -> Dict:
 
184
  try:
 
185
  image = Image.open(io.BytesIO(image_data))
186
 
 
187
  if image.mode not in ['RGB', 'RGBA']:
188
  image = image.convert('RGB')
189
 
 
190
  if model_name not in models:
191
  available_models = list(models.keys())
192
  if not available_models:
 
196
 
197
  session = models[model_name]["session"]
198
 
 
199
  original_size = image.size
200
  if max(image.size) > max_size:
201
  image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
202
  logger.info(f"Resized from {original_size} to {image.size}")
203
 
 
204
  if image.mode != 'RGBA':
205
  image = image.convert('RGBA')
206
 
 
207
  result = remove_func(image, session=session)
208
 
 
209
  if not transparent:
210
  if result.mode != 'RGBA':
211
  result = result.convert('RGBA')
212
  white_bg = Image.new('RGBA', result.size, (255, 255, 255, 255))
213
  result = Image.alpha_composite(white_bg, result)
214
 
 
215
  output_buffer = io.BytesIO()
216
 
 
217
  if not transparent:
218
  result_rgb = result.convert('RGB')
219
  result_rgb.save(output_buffer, format="JPEG", quality=quality, optimize=True)
 
247
  model_name: str = "silueta",
248
  transparent: bool = True
249
  ) -> Optional[np.ndarray]:
 
250
  try:
 
251
  if model_name not in models:
252
  available_models = list(models.keys())
253
  if not available_models:
 
256
 
257
  session = models[model_name]["session"]
258
 
 
259
  if len(frame.shape) == 3:
260
  if frame.shape[2] == 3:
261
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
 
264
  else:
265
  return None
266
  else:
 
267
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB)
268
 
 
269
  img = Image.fromarray(frame_rgb)
270
  if img.mode != 'RGBA':
271
  img = img.convert('RGBA')
272
 
 
273
  result = remove_func(img, session=session)
 
 
274
  result_array = np.array(result)
275
 
276
  if transparent:
277
  if result_array.shape[2] == 4:
 
278
  return cv2.cvtColor(result_array, cv2.COLOR_RGBA2BGRA)
279
  else:
 
280
  bgr = cv2.cvtColor(result_array, cv2.COLOR_RGB2BGR)
281
  alpha = np.ones((bgr.shape[0], bgr.shape[1], 1), dtype=np.uint8) * 255
282
  return np.concatenate([bgr, alpha], axis=2)
283
  else:
 
284
  if result_array.shape[2] == 4:
 
285
  alpha = result_array[:, :, 3:4] / 255.0
286
  white_bg = np.ones_like(result_array[:, :, :3]) * 255
287
  blended = (1 - alpha) * white_bg + alpha * result_array[:, :, :3]
 
291
 
292
  except Exception as e:
293
  logger.error(f"Frame processing failed: {e}")
 
294
  if transparent and len(frame.shape) == 3 and frame.shape[2] == 3:
295
  return cv2.cvtColor(frame, cv2.COLOR_BGR2BGRA)
296
  return frame
 
304
  height: int = 360,
305
  transparent: bool = True
306
  ) -> Tuple[bool, str]:
 
307
  try:
 
308
  result = subprocess.run(['which', 'ffmpeg'], capture_output=True, text=True)
309
  if result.returncode != 0:
310
  logger.warning("ffmpeg not found, using OpenCV fallback")
 
312
 
313
  actual_output_path = output_path
314
 
 
315
  if format.lower() in ['mp4', 'mov']:
316
  if transparent:
 
317
  logger.info("MP4 doesn't support alpha channel, switching to WebM format")
318
  actual_output_path = output_path.replace('.mp4', '.webm').replace('.mov', '.webm')
319
  cmd = [
 
400
  transparent: bool = True,
401
  format: str = "mp4"
402
  ) -> Dict:
 
403
  try:
404
  logger.info(f"Processing video: {input_path} -> {output_path}")
405
  logger.info(f"Settings: model={model_name}, size={max_size}, fps={fps}, format={format}")
406
 
 
407
  cap = cv2.VideoCapture(input_path)
408
  if not cap.isOpened():
409
  raise ValueError(f"Cannot open video: {input_path}")
410
 
 
411
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
412
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
413
  orig_fps = cap.get(cv2.CAP_PROP_FPS)
414
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
415
 
 
416
  target_fps = min(orig_fps if orig_fps > 0 else fps, 15)
417
 
 
418
  scale = min(max_size / max(width, height), 1.0)
419
  new_width = int(width * scale)
420
  new_height = int(height * scale)
421
 
 
422
  if new_width % 2 != 0:
423
  new_width += 1
424
  if new_height % 2 != 0:
 
427
  logger.info(f"Video: {width}x{height} -> {new_width}x{new_height}")
428
  logger.info(f"FPS: {target_fps}, Total frames: {total_frames}")
429
 
 
430
  frames_dir = tempfile.mkdtemp(prefix="video_frames_")
431
  logger.info(f"Frames directory: {frames_dir}")
432
 
 
433
  frame_count = 0
434
  processed_count = 0
435
  start_time = time.time()
436
+ max_processing_time = 300
437
 
438
  while True:
439
  ret, frame = cap.read()
 
442
 
443
  frame_count += 1
444
 
 
445
  if frame_skip > 0 and frame_count % (frame_skip + 1) != 0:
446
  continue
447
 
 
448
  if time.time() - start_time > max_processing_time:
449
  logger.warning(f"Processing timeout reached after {processed_count} frames")
450
  break
451
 
452
  try:
 
453
  frame_resized = cv2.resize(frame, (new_width, new_height))
 
 
454
  processed_frame = process_frame_with_fallback(frame_resized, model_name, transparent)
455
 
456
  if processed_frame is not None:
 
457
  frame_path = os.path.join(frames_dir, f"frame_{processed_count:06d}.png")
458
  cv2.imwrite(frame_path, processed_frame)
459
  processed_count += 1
460
 
 
461
  if processed_count % 10 == 0:
462
  elapsed = time.time() - start_time
463
  logger.info(f"Processed {processed_count} frames ({elapsed:.1f}s)")
464
 
465
  except Exception as e:
466
  logger.error(f"Frame {frame_count} failed: {e}")
 
467
  try:
468
  frame_path = os.path.join(frames_dir, f"frame_{processed_count:06d}.png")
469
  frame_resized = cv2.resize(frame, (new_width, new_height))
 
481
  if processed_count == 0:
482
  raise ValueError("No frames processed")
483
 
 
484
  actual_format = format
485
  if transparent and format.lower() in ['mp4', 'mov']:
486
  actual_format = 'webm'
 
492
  )
493
 
494
  if not ffmpeg_success:
 
495
  logger.info("Using OpenCV fallback")
496
  if transparent and actual_format.lower() == "avi":
497
  fourcc = cv2.VideoWriter_fourcc(*'FFV1')
 
503
  out = cv2.VideoWriter(actual_output_path, fourcc, target_fps, (new_width, new_height), True)
504
 
505
  if not out.isOpened():
 
506
  fourcc = cv2.VideoWriter_fourcc(*'MJPG')
507
  out = cv2.VideoWriter(actual_output_path, fourcc, target_fps, (new_width, new_height))
508
 
 
509
  for i in range(processed_count):
510
  frame_path = os.path.join(frames_dir, f"frame_{i:06d}.png")
511
  if os.path.exists(frame_path):
 
515
 
516
  out.release()
517
 
 
518
  try:
519
  shutil.rmtree(frames_dir)
520
  except:
 
522
 
523
  processing_time = time.time() - start_time
524
 
 
525
  if not os.path.exists(actual_output_path) or os.path.getsize(actual_output_path) < 1024:
526
  logger.error(f"Output file too small or missing: {actual_output_path}")
527
  create_fallback_video(actual_output_path, new_width, new_height, target_fps)
 
548
  raise
549
 
550
  def create_fallback_video(output_path: str, width: int, height: int, fps: int):
 
551
  try:
552
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
553
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
554
 
555
+ for i in range(fps * 5):
556
  frame = np.zeros((height, width, 3), dtype=np.uint8)
557
 
 
558
  for y in range(height):
559
  color_value = int(40 + (y / height) * 40)
560
  cv2.rectangle(frame, (0, y), (width, y+1), (color_value, color_value, color_value+20), -1)
561
 
 
562
  if i < fps * 2:
563
  text = "Processing Video..."
564
  else:
 
584
 
585
  @app.get("/")
586
  async def home():
 
587
  html = """
588
  <!DOCTYPE html>
589
  <html lang="en">
 
592
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
593
  <title>🎬 Background Remover Pro</title>
594
  <style>
595
+ * { margin: 0; padding: 0; box-sizing: border-box; }
 
 
 
 
 
596
  body {
597
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
598
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
604
  align-items: center;
605
  justify-content: center;
606
  }
 
607
  .container {
608
  max-width: 900px;
609
  margin: 0 auto;
 
614
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
615
  border: 1px solid rgba(255, 255, 255, 0.2);
616
  }
 
617
  h1 {
618
  font-size: 2.5em;
619
  margin-bottom: 10px;
 
622
  -webkit-text-fill-color: transparent;
623
  text-align: center;
624
  }
 
625
  .subtitle {
626
  text-align: center;
627
  color: #cbd5e0;
628
  margin-bottom: 30px;
629
  font-size: 1.1em;
630
  }
 
631
  .status {
632
  background: linear-gradient(135deg, rgba(72, 187, 120, 0.2), rgba(72, 187, 120, 0.1));
633
  padding: 15px 20px;
 
636
  margin: 20px 0;
637
  border: 1px solid rgba(72, 187, 120, 0.3);
638
  }
 
 
 
 
 
 
639
  h2 {
640
  margin: 25px 0 15px 0;
641
  color: #e0e7ff;
642
  font-size: 1.5em;
643
  }
 
644
  .endpoint-grid {
645
  display: grid;
646
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
647
  gap: 15px;
648
  margin: 20px 0;
649
  }
 
650
  .endpoint {
651
  background: rgba(255, 255, 255, 0.05);
652
  padding: 20px;
 
654
  border-left: 4px solid #667eea;
655
  transition: transform 0.2s, background 0.2s;
656
  }
 
657
  .endpoint:hover {
658
  background: rgba(255, 255, 255, 0.1);
659
  transform: translateY(-2px);
660
  }
 
661
  .endpoint strong {
662
  display: block;
663
  font-size: 1.1em;
664
  margin-bottom: 8px;
665
  color: #a78bfa;
666
  }
 
667
  .endpoint code {
668
  background: rgba(0, 0, 0, 0.3);
669
  padding: 4px 8px;
 
672
  display: inline-block;
673
  margin: 5px 0;
674
  }
 
675
  .feature-list {
676
  display: grid;
677
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
678
  gap: 10px;
679
  margin: 20px 0;
680
  }
 
681
  .feature {
682
  background: rgba(255, 255, 255, 0.05);
683
  padding: 12px;
684
  border-radius: 8px;
685
  text-align: center;
686
  }
 
687
  .footer {
688
  text-align: center;
689
  margin-top: 30px;
 
691
  border-top: 1px solid rgba(255, 255, 255, 0.1);
692
  color: #a0aec0;
693
  }
 
694
  .badge {
695
  display: inline-block;
696
  padding: 4px 10px;
 
699
  font-size: 0.85em;
700
  margin: 5px 5px 0 0;
701
  }
 
702
  @media (max-width: 768px) {
703
+ .container { padding: 20px; }
704
+ h1 { font-size: 2em; }
 
 
 
 
 
705
  }
706
  </style>
707
  </head>
 
785
 
786
  @app.get("/api/health")
787
  async def health_check():
 
788
  return {
789
  "status": "healthy" if AI_AVAILABLE else "degraded",
790
  "ai_available": AI_AVAILABLE,
 
800
 
801
  @app.get("/api/models")
802
  async def list_models():
 
803
  model_info = {}
804
  for name, info in models.items():
805
  model_info[name] = {
 
823
  max_size: int = Query(1024, ge=256, le=2048, description="Maximum dimension"),
824
  quality: int = Query(95, ge=50, le=100, description="Output quality (JPEG only)")
825
  ):
 
826
  start_time = time.time()
827
 
828
  try:
 
829
  validate_image_file(file)
830
 
 
831
  contents = await file.read()
832
  if len(contents) == 0:
833
  raise HTTPException(400, "Empty file")
834
+ if len(contents) > 20 * 1024 * 1024:
835
  raise HTTPException(400, "File too large (max 20MB)")
836
 
837
  logger.info(f"Processing image: {file.filename} ({len(contents)/1024:.1f}KB)")
 
839
  if not AI_AVAILABLE:
840
  raise HTTPException(503, "AI service temporarily unavailable")
841
 
 
842
  loop = asyncio.get_event_loop()
843
  process_func = functools.partial(
844
  process_image_sync,
 
853
 
854
  processing_time = time.time() - start_time
855
 
 
856
  filename = Path(file.filename).stem
857
  headers = {
858
  "X-Processing-Time": f"{processing_time:.3f}",
 
888
  frame_skip: int = Query(1, ge=0, le=5, description="Skip frames (0=none)"),
889
  format: str = Query("mp4", description="Output format (mp4, avi, webm, mov)")
890
  ):
 
891
  start_time = time.time()
892
 
893
  try:
 
894
  validate_video_file(file)
895
 
 
896
  contents = await file.read()
897
  if len(contents) == 0:
898
  raise HTTPException(400, "Empty file")
899
+ if len(contents) > 100 * 1024 * 1024:
900
  raise HTTPException(400, "File too large (max 100MB)")
901
 
902
  logger.info(f"Processing video: {file.filename} ({len(contents)/1024/1024:.1f}MB)")
 
904
  if not AI_AVAILABLE:
905
  raise HTTPException(503, "AI service temporarily unavailable")
906
 
 
907
  input_suffix = Path(file.filename).suffix or '.mp4'
908
  input_path = tempfile.mktemp(suffix=input_suffix, dir="/tmp/uploads")
909
 
 
912
 
913
  output_path = None
914
  try:
 
915
  output_ext = format.lower()
916
  if transparent and output_ext in ['mp4', 'mov']:
917
+ output_ext = 'webm'
918
  format = 'webm'
919
  logger.info("Switched to WebM format for transparency support")
920
 
921
  output_path = tempfile.mktemp(suffix=f'.{output_ext}', dir="/tmp/outputs")
922
 
 
923
  loop = asyncio.get_event_loop()
924
  process_func = functools.partial(
925
  process_video_reliable,
 
937
 
938
  processing_time = time.time() - start_time
939
 
 
940
  background_tasks.add_task(cleanup_temp_files, input_path)
941
 
 
942
  media_types = {
943
  "mp4": "video/mp4",
944
  "avi": "video/x-msvideo",
 
947
  }
948
  media_type = media_types.get(result["format"].lower(), "video/mp4")
949
 
 
950
  filename = Path(file.filename).stem
951
  headers = {
952
  "X-Processing-Time": f"{processing_time:.3f}",
 
983
  logger.error(f"Video API error: {str(e)}")
984
  raise HTTPException(500, f"Video processing failed: {str(e)[:100]}")
985
 
 
 
986
  @app.exception_handler(HTTPException)
987
  async def http_exception_handler(request, exc):
 
988
  return JSONResponse(
989
  status_code=exc.status_code,
990
  content={
 
996
 
997
  @app.exception_handler(Exception)
998
  async def general_exception_handler(request, exc):
 
999
  logger.error(f"Unhandled exception: {str(exc)}")
1000
  return JSONResponse(
1001
  status_code=500,
 
1007
  }
1008
  )
1009
 
 
 
1010
  @app.on_event("startup")
1011
  async def startup_event():
 
1012
  logger.info("=" * 50)
1013
  logger.info("🚀 Background Remover Pro v8.2.0 Starting...")
1014
  logger.info(f"📊 AI Available: {AI_AVAILABLE}")
 
1018
 
1019
  @app.on_event("shutdown")
1020
  async def shutdown_event():
 
1021
  logger.info("Shutting down...")
1022
  cpu_executor.shutdown(wait=True)
1023
 
 
1024
  for dir_path in ["/tmp/uploads", "/tmp/outputs"]:
1025
  try:
1026
  if os.path.exists(dir_path):
 
1028
  except:
1029
  pass
1030
 
 
 
1031
  if __name__ == "__main__":
1032
  import uvicorn
1033