Haiss123 commited on
Commit
2a6e433
·
verified ·
1 Parent(s): ee3f39a

Update detection_api.py

Browse files
Files changed (1) hide show
  1. detection_api.py +385 -348
detection_api.py CHANGED
@@ -1,10 +1,9 @@
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Form
2
- from fastapi.responses import FileResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pydantic import BaseModel, Field
5
  from typing import List, Optional, Dict, Any, Union
6
  import cv2
7
- from gunicorn.app.base import BaseApplication
8
  import numpy as np
9
  from datetime import datetime
10
  import aiofiles
@@ -14,7 +13,10 @@ import uuid
14
  import traceback
15
  from concurrent.futures import ThreadPoolExecutor
16
  import logging
17
- import uvicorn
 
 
 
18
  from main import ContentModerator
19
 
20
  # Setup logging
@@ -42,19 +44,8 @@ app.add_middleware(
42
  allow_headers=["*"],
43
  )
44
 
45
- class StandaloneApplication(BaseApplication):
46
- def __init__(self, app, options=None):
47
- self.application = app
48
- self.options = options or {}
49
- super().__init__()
50
-
51
- def load_config(self):
52
- for key, value in self.options.items():
53
- self.cfg.set(key, value)
54
 
55
- def load(self):
56
- return self.application
57
- # Configuration
58
  class Config:
59
  UPLOAD_DIR = Path("uploads")
60
  RESULTS_DIR = Path("results")
@@ -63,10 +54,16 @@ class Config:
63
  MAX_VIDEO_SIZE = 500 * 1024 * 1024 # 500MB for videos
64
  ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp'}
65
  ALLOWED_VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv'}
66
- VIDEO_FRAME_SKIP = 5 # Process every 5th frame for performance
 
 
 
 
 
 
67
  CLEANUP_AFTER_HOURS = 24
68
- ENABLE_ANNOTATED_OUTPUT = True
69
- MAX_WORKERS = 4
70
 
71
 
72
  config = Config()
@@ -77,47 +74,126 @@ for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
77
  (directory / "images").mkdir(exist_ok=True)
78
  (directory / "videos").mkdir(exist_ok=True)
79
 
80
- # Global moderator instance (initialized on startup)
81
  moderator: Optional[ContentModerator] = None
82
 
83
  # Thread pool for background processing
84
  executor = ThreadPoolExecutor(max_workers=config.MAX_WORKERS)
85
 
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # ============== Response Models ==============
88
 
89
  class BoundingBox(BaseModel):
90
- x1: int = Field(..., description="Top-left x coordinate")
91
- y1: int = Field(..., description="Top-left y coordinate")
92
- x2: int = Field(..., description="Bottom-right x coordinate")
93
- y2: int = Field(..., description="Bottom-right y coordinate")
94
 
95
 
96
  class WeaponDetection(BaseModel):
97
- type: str = Field(..., description="Detection type (weapon)")
98
- class_name: str = Field(..., description="Weapon class (knife/dao/gun)")
99
- weapon_type: str = Field(..., description="Weapon category (blade/firearm)")
100
- confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
101
  bbox: BoundingBox
102
- threat_level: str = Field(..., description="Threat level (low/medium/high/critical)")
103
- detection_method: str = Field(..., description="Detection method used")
104
 
105
 
106
  class NSFWDetection(BaseModel):
107
- type: str = Field(..., description="Detection type (nsfw)")
108
- class_name: str = Field(..., description="NSFW class")
109
- confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
110
  bbox: BoundingBox
111
- method: str = Field(..., description="Detection method (classification/skin_detection/pose_analysis)")
112
- skin_ratio: Optional[float] = Field(None, description="Skin exposure ratio if applicable")
113
 
114
 
115
  class FightDetection(BaseModel):
116
- type: str = Field(default="fight", description="Detection type")
117
- confidence: float = Field(..., ge=0, le=1, description="Detection confidence")
118
  bbox: BoundingBox
119
- persons_involved: int = Field(..., description="Number of persons detected in fight")
120
- threat_level: str = Field(..., description="Threat level")
121
 
122
 
123
  class ImageDetectionResponse(BaseModel):
@@ -129,7 +205,6 @@ class ImageDetectionResponse(BaseModel):
129
  summary: Dict[str, Any]
130
  risk_level: str
131
  action_required: bool
132
- annotated_image_url: Optional[str] = None
133
  processing_time_ms: float
134
 
135
 
@@ -143,16 +218,73 @@ class VideoDetectionResponse(BaseModel):
143
  summary: Dict[str, Any]
144
  risk_level: str
145
  action_required: bool
146
- processed_video_url: Optional[str] = None
147
  processing_time_ms: float
 
148
 
149
 
150
- class ErrorResponse(BaseModel):
151
- success: bool = False
152
- error: str
153
- error_code: str
154
- timestamp: str
155
- request_id: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
 
158
  # ============== Utility Functions ==============
@@ -184,49 +316,14 @@ async def save_upload_file(upload_file: UploadFile, destination: Path) -> Path:
184
  raise
185
 
186
 
187
- def detect_fight_in_frame(image: np.ndarray, persons: List[Dict]) -> Optional[FightDetection]:
188
- """
189
- Detect potential fight based on person proximity and poses
190
- This is a simplified implementation - you may want to enhance this
191
- """
192
- if len(persons) < 2:
193
- return None
194
-
195
- # Check for overlapping or very close person bounding boxes
196
- for i in range(len(persons)):
197
- for j in range(i + 1, len(persons)):
198
- bbox1 = persons[i]['bbox']
199
- bbox2 = persons[j]['bbox']
200
-
201
- # Calculate center points
202
- center1_x = (bbox1[0] + bbox1[2]) / 2
203
- center1_y = (bbox1[1] + bbox1[3]) / 2
204
- center2_x = (bbox2[0] + bbox2[2]) / 2
205
- center2_y = (bbox2[1] + bbox2[3]) / 2
206
-
207
- # Calculate distance between centers
208
- distance = np.sqrt((center1_x - center2_x) ** 2 + (center1_y - center2_y) ** 2)
209
-
210
- # Calculate average person width
211
- avg_width = ((bbox1[2] - bbox1[0]) + (bbox2[2] - bbox2[0])) / 2
212
-
213
- # If persons are very close (distance less than average width)
214
- if distance < avg_width * 1.5:
215
- # Create combined bounding box
216
- min_x = min(bbox1[0], bbox2[0])
217
- min_y = min(bbox1[1], bbox2[1])
218
- max_x = max(bbox1[2], bbox2[2])
219
- max_y = max(bbox1[3], bbox2[3])
220
-
221
- return FightDetection(
222
- type="fight",
223
- confidence=0.7, # Simplified confidence
224
- bbox=BoundingBox(x1=min_x, y1=min_y, x2=max_x, y2=max_y),
225
- persons_involved=2,
226
- threat_level="high"
227
- )
228
-
229
- return None
230
 
231
 
232
  def process_detections(raw_detections: List[Dict]) -> Dict[str, List]:
@@ -268,215 +365,140 @@ def process_detections(raw_detections: List[Dict]) -> Dict[str, List]:
268
  skin_ratio=det.get('skin_ratio')
269
  ))
270
  elif det['type'] == 'fight':
271
- processed['fights'].append(det)
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  return processed
274
 
275
 
276
  # ============== API Endpoints ==============
277
 
278
- @app.on_event("startup")
279
- async def startup_event():
280
- """Initialize moderator on startup"""
281
- global moderator
282
- try:
283
- logger.info("Initializing Content Moderator...")
284
-
285
- # Custom configuration for API
286
- custom_config = {
287
- 'weapon_detection': {
288
- 'enabled': True,
289
- 'confidence_threshold': 0.5,
290
- 'knife_confidence': 0.25,
291
- 'model_size': 'yolo11n',
292
- 'classes': ['knife', 'dao', 'gun', 'rifle', 'pistol', 'weapon', 'fight'],
293
- 'use_enhancement': True,
294
- 'multi_pass': True,
295
- 'boost_knife_detection': True
296
- },
297
- 'nsfw_detection': {
298
- 'enabled': True,
299
- 'confidence_threshold': 0.7,
300
- 'skin_detection': True,
301
- 'pose_analysis': False, # Disabled for performance
302
- 'region_analysis': True
303
- },
304
- 'performance': {
305
- 'image_size': 640,
306
- 'batch_size': 1,
307
- 'half_precision': True,
308
- 'use_flash_attention': False,
309
- 'cpu_optimization': False
310
- },
311
- 'output': {
312
- 'save_detections': True,
313
- 'draw_boxes': True,
314
- 'log_results': True
315
- }
316
- }
317
-
318
- moderator = ContentModerator(config=custom_config)
319
- logger.info("✅ Content Moderator initialized successfully")
320
-
321
- # Log model status
322
- status = moderator.get_model_status()
323
- logger.info(f"Model Status: {json.dumps(status, indent=2)}")
324
-
325
- except Exception as e:
326
- logger.error(f"Failed to initialize Content Moderator: {e}")
327
- logger.error(traceback.format_exc())
328
-
329
-
330
- @app.on_event("shutdown")
331
- async def shutdown_event():
332
- """Cleanup on shutdown"""
333
- executor.shutdown(wait=True)
334
- logger.info("API shutdown complete")
335
 
336
 
337
- @app.get("/", response_model=Dict[str, Any])
338
- async def root():
339
- """API root endpoint with status information"""
340
- if moderator:
341
- status = moderator.get_model_status()
342
- return {
343
- "service": "Weapon & NSFW Detection API",
344
- "version": "2.0.0",
345
- "status": "operational",
346
- "models": status,
347
- "endpoints": {
348
- "image_detection": "/detect_n_k_f_g/images",
349
- "video_detection": "/detect_n_k_f_g/videos",
350
- "documentation": "/docs"
351
- }
352
- }
353
- else:
354
  return {
355
- "service": "Weapon & NSFW Detection API",
356
- "version": "2.0.0",
357
- "status": "initializing",
358
- "message": "Models are being loaded..."
359
  }
360
 
 
 
 
 
 
 
 
 
361
 
362
  @app.post("/detect_n_k_f_g/images", response_model=ImageDetectionResponse)
363
  async def detect_image(
364
- file: UploadFile = File(..., description="Image file to analyze"),
365
- enable_fight_detection: bool = Form(True, description="Enable fight detection"),
366
- return_annotated: bool = Form(True, description="Return annotated image")
367
  ):
368
  """
369
- Detect weapons (knife/dao/gun), fights, and NSFW content in images
370
-
371
- Supports: JPG, JPEG, PNG, BMP, GIF, WEBP
372
- Max size: 50MB
373
  """
 
 
 
 
 
 
374
  request_id = generate_request_id()
375
  start_time = datetime.now()
376
 
377
  try:
378
- # Validate file extension
379
  if not validate_file_extension(file.filename, config.ALLOWED_IMAGE_EXTENSIONS):
380
  raise HTTPException(
381
  status_code=400,
382
- detail=f"Invalid file type. Allowed: {', '.join(config.ALLOWED_IMAGE_EXTENSIONS)}"
383
  )
384
 
385
- # Check file size
386
  file_content = await file.read()
387
  file_size = len(file_content)
388
 
389
  if not validate_file_size(file_size, config.MAX_IMAGE_SIZE):
390
  raise HTTPException(
391
  status_code=400,
392
- detail=f"File too large. Maximum size: {config.MAX_IMAGE_SIZE / (1024 * 1024):.1f}MB"
393
  )
394
 
395
- # Save uploaded file
396
  upload_path = config.UPLOAD_DIR / "images" / f"{request_id}_{file.filename}"
397
  async with aiofiles.open(upload_path, 'wb') as f:
398
  await f.write(file_content)
399
 
400
- # Read image with OpenCV
401
  nparr = np.frombuffer(file_content, np.uint8)
402
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
403
 
404
  if image is None:
405
- raise HTTPException(status_code=400, detail="Invalid or corrupted image file")
406
 
407
  # Get image info
408
- height, width, channels = image.shape
409
  image_info = {
410
  "filename": file.filename,
411
  "width": width,
412
  "height": height,
413
- "channels": channels,
414
- "size_bytes": file_size,
415
  "size_mb": round(file_size / (1024 * 1024), 2)
416
  }
417
 
418
- # Process image with ContentModerator
419
- logger.info(f"Processing image {request_id}")
 
 
 
 
 
 
 
420
  result = moderator.process_image(image)
421
 
422
  if not result:
423
- raise HTTPException(status_code=500, detail="Detection processing failed")
424
-
425
- # Detect persons for potential fight detection
426
- persons = moderator.detect_persons(image)
427
-
428
- # Check for fights if enabled
429
- fight_detection = None
430
- if enable_fight_detection and len(persons) >= 2:
431
- fight_detection = detect_fight_in_frame(image, persons)
432
 
433
  # Process detections
434
  processed = process_detections(result['detections'])
435
 
436
- # Add fight detection if found
437
- if fight_detection:
438
- processed['fights'].append(fight_detection)
439
-
440
- # Save annotated image if requested
441
- annotated_url = None
442
- if return_annotated and config.ENABLE_ANNOTATED_OUTPUT:
443
- if 'annotated_image' in result:
444
- annotated_path = config.PROCESSED_DIR / "images" / f"{request_id}_annotated.jpg"
445
- cv2.imwrite(str(annotated_path), result['annotated_image'])
446
- annotated_url = f"/results/images/{request_id}_annotated.jpg"
447
- else:
448
- # Draw annotations manually if not provided
449
- annotated_image = moderator.draw_detections(image.copy(), result['detections'])
450
- annotated_path = config.PROCESSED_DIR / "images" / f"{request_id}_annotated.jpg"
451
- cv2.imwrite(str(annotated_path), annotated_image)
452
- annotated_url = f"/results/images/{request_id}_annotated.jpg"
453
-
454
  # Calculate summary
455
- total_weapons = len(processed['weapons'])
456
- total_nsfw = len(processed['nsfw'])
457
- total_fights = len(processed['fights'])
458
-
459
- knife_count = sum(
460
- 1 for w in processed['weapons'] if 'knife' in w.class_name.lower() or 'dao' in w.class_name.lower())
461
- gun_count = sum(1 for w in processed['weapons'] if
462
- 'gun' in w.class_name.lower() or 'pistol' in w.class_name.lower() or 'rifle' in w.class_name.lower())
463
-
464
  summary = {
465
- "total_detections": total_weapons + total_nsfw + total_fights,
466
- "weapons": {
467
- "total": total_weapons,
468
- "knives": knife_count,
469
- "guns": gun_count
470
- },
471
- "nsfw": total_nsfw,
472
- "fights": total_fights,
473
- "persons_detected": len(persons)
474
  }
475
 
476
- # Determine overall risk level
477
- if total_weapons > 0 or total_fights > 0:
478
- risk_level = "critical" if gun_count > 0 else "high"
479
- elif total_nsfw > 0:
480
  risk_level = "medium"
481
  else:
482
  risk_level = "safe"
@@ -493,62 +515,62 @@ async def detect_image(
493
  summary=summary,
494
  risk_level=risk_level,
495
  action_required=(summary["total_detections"] > 0),
496
- annotated_image_url=annotated_url,
497
  processing_time_ms=processing_time
498
  )
499
 
500
  except HTTPException:
501
  raise
502
  except Exception as e:
503
- logger.error(f"Error processing image {request_id}: {e}")
504
  logger.error(traceback.format_exc())
505
- raise HTTPException(
506
- status_code=500,
507
- detail=f"Internal server error: {str(e)}"
508
- )
509
 
510
 
511
  @app.post("/detect_n_k_f_g/videos", response_model=VideoDetectionResponse)
512
  async def detect_video(
513
- file: UploadFile = File(..., description="Video file to analyze"),
514
- frame_skip: int = Form(5, ge=1, le=30, description="Process every Nth frame"),
515
- max_frames: int = Form(1000, ge=10, le=5000, description="Maximum frames to process"),
516
- enable_fight_detection: bool = Form(True, description="Enable fight detection")
517
  ):
518
  """
519
- Detect weapons (knife/dao/gun), fights, and NSFW content in videos
520
- Supports: MP4, AVI, MOV, MKV, WEBM, FLV, WMV
521
- Max size: 500MB
522
- Note: Videos are automatically deleted after processing to save disk space
523
  """
 
 
 
 
 
 
524
  request_id = generate_request_id()
525
  start_time = datetime.now()
526
- upload_path = None
527
 
528
  try:
529
- # Validate file extension
530
  if not validate_file_extension(file.filename, config.ALLOWED_VIDEO_EXTENSIONS):
531
  raise HTTPException(
532
  status_code=400,
533
- detail=f"Invalid file type. Allowed: {', '.join(config.ALLOWED_VIDEO_EXTENSIONS)}"
534
  )
535
 
536
- # Save uploaded video
537
  upload_path = config.UPLOAD_DIR / "videos" / f"{request_id}_{file.filename}"
538
  await save_upload_file(file, upload_path)
539
 
540
- # Get file size
541
  file_size = upload_path.stat().st_size
542
  if not validate_file_size(file_size, config.MAX_VIDEO_SIZE):
 
543
  raise HTTPException(
544
  status_code=400,
545
- detail=f"File too large. Maximum size: {config.MAX_VIDEO_SIZE / (1024 * 1024):.1f}MB"
546
  )
547
 
548
  # Open video
549
  cap = cv2.VideoCapture(str(upload_path))
550
  if not cap.isOpened():
551
- raise HTTPException(status_code=400, detail="Invalid or corrupted video file")
552
 
553
  # Get video info
554
  fps = cap.get(cv2.CAP_PROP_FPS)
@@ -564,22 +586,43 @@ async def detect_video(
564
  "fps": fps,
565
  "total_frames": total_frames,
566
  "duration_seconds": round(duration, 2),
567
- "size_bytes": file_size,
568
  "size_mb": round(file_size / (1024 * 1024), 2)
569
  }
570
 
571
- # Process video frames
572
- logger.info(f"Processing video {request_id}: {total_frames} frames, skip={frame_skip}")
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
 
574
  frame_detections = []
575
  frame_count = 0
576
  processed_count = 0
 
 
577
 
578
  # Aggregated statistics
579
  all_weapons = []
580
  all_nsfw = []
581
  all_fights = []
582
 
 
 
 
 
 
 
583
  while True:
584
  ret, frame = cap.read()
585
  if not ret:
@@ -587,88 +630,97 @@ async def detect_video(
587
 
588
  frame_count += 1
589
 
590
- # Skip frames according to frame_skip parameter
591
  if frame_count % frame_skip != 0:
592
  continue
593
 
594
- # Limit maximum frames processed
595
  if processed_count >= max_frames:
596
  logger.info(f"Reached max frames limit: {max_frames}")
597
  break
598
 
 
 
 
 
 
 
 
599
  processed_count += 1
600
 
601
  # Process frame
602
  result = moderator.process_image(frame)
603
 
604
  if result and result['detections']:
605
- # Get persons for fight detection
606
- persons = moderator.detect_persons(frame)
607
-
608
- # Check for fights
609
- fight_detection = None
610
- if enable_fight_detection and len(persons) >= 2:
611
- fight_detection = detect_fight_in_frame(frame, persons)
612
-
613
  # Process detections
614
  processed = process_detections(result['detections'])
615
 
616
- if fight_detection:
617
- processed['fights'].append(fight_detection)
 
 
 
 
 
 
618
 
619
- # Store frame detection info
620
- if len(processed['weapons']) > 0 or len(processed['nsfw']) > 0 or len(processed['fights']) > 0:
621
  frame_info = {
622
  "frame_number": frame_count,
623
- "timestamp_seconds": frame_count / fps if fps > 0 else 0,
624
  "detections": {
625
- "weapons": [w.dict() for w in processed['weapons']],
626
- "nsfw": [n.dict() for n in processed['nsfw']],
627
- "fights": [f.dict() for f in processed['fights']]
628
- }
 
629
  }
630
  frame_detections.append(frame_info)
631
 
632
- # Aggregate statistics
633
  all_weapons.extend(processed['weapons'])
634
  all_nsfw.extend(processed['nsfw'])
635
  all_fights.extend(processed['fights'])
636
 
637
- # Log progress every 100 frames
638
- if processed_count % 100 == 0:
639
- logger.info(f"Processed {processed_count} frames...")
 
640
 
641
- # Release resources
 
 
 
 
 
 
 
 
 
 
642
  cap.release()
643
 
644
- # Calculate summary
645
- knife_count = sum(1 for w in all_weapons if 'knife' in w.class_name.lower() or 'dao' in w.class_name.lower())
646
- gun_count = sum(1 for w in all_weapons if 'gun' in w.class_name.lower() or 'pistol' in w.class_name.lower())
 
 
647
 
 
648
  summary = {
649
  "total_frames_analyzed": processed_count,
650
  "frames_with_detections": len(frame_detections),
651
- "total_detections": len(all_weapons) + len(all_nsfw) + len(all_fights),
652
- "weapons": {
653
- "total": len(all_weapons),
654
- "knives": knife_count,
655
- "guns": gun_count,
656
- "unique_frames": len(set(f["frame_number"] for f in frame_detections if f["detections"]["weapons"]))
657
- },
658
- "nsfw": {
659
- "total": len(all_nsfw),
660
- "unique_frames": len(set(f["frame_number"] for f in frame_detections if f["detections"]["nsfw"]))
661
- },
662
- "fights": {
663
- "total": len(all_fights),
664
- "unique_frames": len(set(f["frame_number"] for f in frame_detections if f["detections"]["fights"]))
665
- }
666
  }
667
 
668
- # Determine overall risk level
669
- if gun_count > 0 or len(all_fights) > 5:
670
  risk_level = "critical"
671
- elif knife_count > 0 or len(all_fights) > 0:
672
  risk_level = "high"
673
  elif len(all_nsfw) > 0:
674
  risk_level = "medium"
@@ -678,71 +730,58 @@ async def detect_video(
678
  # Calculate processing time
679
  processing_time = (datetime.now() - start_time).total_seconds() * 1000
680
 
 
 
 
 
 
 
 
 
 
681
  return VideoDetectionResponse(
682
  success=True,
683
  request_id=request_id,
684
  timestamp=datetime.now().isoformat(),
685
  video_info=video_info,
686
  total_frames_processed=processed_count,
687
- frame_detections=frame_detections,
688
  summary=summary,
689
  risk_level=risk_level,
690
  action_required=(summary["total_detections"] > 0),
691
- processed_video_url=None, # Always None since we don't save processed videos
692
- processing_time_ms=processing_time
693
  )
694
 
695
  except HTTPException:
696
  raise
697
  except Exception as e:
698
- logger.error(f"Error processing video {request_id}: {e}")
699
  logger.error(traceback.format_exc())
700
- raise HTTPException(
701
- status_code=500,
702
- detail=f"Internal server error: {str(e)}"
703
- )
704
  finally:
705
- # Always cleanup uploaded video file after processing
706
- if upload_path and upload_path.exists():
707
- try:
708
- upload_path.unlink()
709
- logger.info(f"Cleaned up uploaded video: {upload_path}")
710
- except Exception as cleanup_error:
711
- logger.warning(f"Failed to cleanup uploaded video {upload_path}: {cleanup_error}")
712
 
713
 
714
  @app.delete("/cleanup")
715
  async def cleanup_old_files(hours: int = 24):
716
- """Clean up old files from upload and results directories (excluding videos from uploads as they are auto-deleted)"""
717
  try:
718
  from datetime import timedelta
719
  cutoff_time = datetime.now() - timedelta(hours=hours)
720
 
721
  deleted_count = 0
722
-
723
- # Clean up images from all directories
724
  for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
725
- images_path = directory / "images"
726
- if images_path.exists():
727
- for file in images_path.iterdir():
728
- if file.is_file():
729
- file_time = datetime.fromtimestamp(file.stat().st_mtime)
730
- if file_time < cutoff_time:
731
- file.unlink()
732
- deleted_count += 1
733
-
734
- # Clean up any remaining uploaded videos (should be rare since they're auto-deleted)
735
- upload_videos_path = config.UPLOAD_DIR / "videos"
736
- if upload_videos_path.exists():
737
- for file in upload_videos_path.iterdir():
738
- if file.is_file():
739
- file_time = datetime.fromtimestamp(file.stat().st_mtime)
740
- if file_time < cutoff_time:
741
- file.unlink()
742
- deleted_count += 1
743
- logger.info(f"Cleaned up old uploaded video: {file}")
744
-
745
- # Note: No need to clean processed videos since we don't save them anymore
746
 
747
  return {
748
  "success": True,
@@ -751,10 +790,8 @@ async def cleanup_old_files(hours: int = 24):
751
  }
752
  except Exception as e:
753
  logger.error(f"Cleanup error: {e}")
754
- return {
755
- "success": False,
756
- "error": str(e)
757
- }
758
 
759
  if __name__ == "__main__":
760
  import os
 
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Form
2
+ from fastapi.responses import FileResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pydantic import BaseModel, Field
5
  from typing import List, Optional, Dict, Any, Union
6
  import cv2
 
7
  import numpy as np
8
  from datetime import datetime
9
  import aiofiles
 
13
  import traceback
14
  from concurrent.futures import ThreadPoolExecutor
15
  import logging
16
+ import hashlib
17
+ import time
18
+ from functools import lru_cache
19
+
20
  from main import ContentModerator
21
 
22
  # Setup logging
 
44
  allow_headers=["*"],
45
  )
46
 
 
 
 
 
 
 
 
 
 
47
 
48
+ # Configuration optimized for CPU
 
 
49
  class Config:
50
  UPLOAD_DIR = Path("uploads")
51
  RESULTS_DIR = Path("results")
 
54
  MAX_VIDEO_SIZE = 500 * 1024 * 1024 # 500MB for videos
55
  ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp'}
56
  ALLOWED_VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv'}
57
+
58
+ # CPU-optimized settings
59
+ VIDEO_FRAME_SKIP = 10 # Process every 10th frame by default
60
+ VIDEO_MAX_FRAMES = 100 # Maximum frames to process
61
+ VIDEO_TARGET_WIDTH = 416 # Downscale to this width
62
+ VIDEO_EARLY_STOP_THRESHOLD = 10 # Stop after N threats
63
+
64
  CLEANUP_AFTER_HOURS = 24
65
+ ENABLE_ANNOTATED_OUTPUT = False # Disable to save CPU
66
+ MAX_WORKERS = 2 # Reduced for CPU
67
 
68
 
69
  config = Config()
 
74
  (directory / "images").mkdir(exist_ok=True)
75
  (directory / "videos").mkdir(exist_ok=True)
76
 
77
+ # Global moderator instance
78
  moderator: Optional[ContentModerator] = None
79
 
80
  # Thread pool for background processing
81
  executor = ThreadPoolExecutor(max_workers=config.MAX_WORKERS)
82
 
83
 
84
+ # Video Optimizer Class
85
+ class VideoOptimizer:
86
+ """Optimized video processing for CPU environments"""
87
+
88
+ def __init__(self):
89
+ self.frame_cache = {}
90
+ self.cache_size = 20
91
+
92
+ def get_optimal_settings(self, duration: float, total_frames: int) -> Dict:
93
+ """Calculate optimal settings based on video duration"""
94
+
95
+ if duration <= 5:
96
+ return {
97
+ 'frame_skip': 3,
98
+ 'target_width': 416,
99
+ 'max_frames': 50
100
+ }
101
+ elif duration <= 15:
102
+ return {
103
+ 'frame_skip': 8,
104
+ 'target_width': 416,
105
+ 'max_frames': 75
106
+ }
107
+ elif duration <= 30:
108
+ return {
109
+ 'frame_skip': 12,
110
+ 'target_width': 320,
111
+ 'max_frames': 100
112
+ }
113
+ else:
114
+ return {
115
+ 'frame_skip': 20,
116
+ 'target_width': 320,
117
+ 'max_frames': 150
118
+ }
119
+
120
+ def preprocess_frame(self, frame: np.ndarray, target_width: int = 416) -> np.ndarray:
121
+ """Downscale frame for faster processing"""
122
+ height, width = frame.shape[:2]
123
+
124
+ if width > target_width:
125
+ scale = target_width / width
126
+ new_width = int(width * scale)
127
+ new_height = int(height * scale)
128
+ frame = cv2.resize(frame, (new_width, new_height),
129
+ interpolation=cv2.INTER_LINEAR)
130
+
131
+ return frame
132
+
133
+ def get_frame_hash(self, frame: np.ndarray) -> str:
134
+ """Generate hash for frame"""
135
+ small = cv2.resize(frame, (8, 8))
136
+ return hashlib.md5(small.tobytes()).hexdigest()
137
+
138
+ def should_skip_frame(self, frame: np.ndarray) -> bool:
139
+ """Check if frame is similar to cached frames"""
140
+ frame_hash = self.get_frame_hash(frame)
141
+
142
+ if frame_hash in self.frame_cache:
143
+ return True
144
+
145
+ # Maintain cache size
146
+ if len(self.frame_cache) >= self.cache_size:
147
+ # Remove oldest entry
148
+ oldest = min(self.frame_cache, key=self.frame_cache.get)
149
+ del self.frame_cache[oldest]
150
+
151
+ self.frame_cache[frame_hash] = time.time()
152
+ return False
153
+
154
+ def clear_cache(self):
155
+ """Clear frame cache"""
156
+ self.frame_cache.clear()
157
+
158
+
159
+ # Initialize video optimizer
160
+ video_optimizer = VideoOptimizer()
161
+
162
+
163
  # ============== Response Models ==============
164
 
165
  class BoundingBox(BaseModel):
166
+ x1: int
167
+ y1: int
168
+ x2: int
169
+ y2: int
170
 
171
 
172
  class WeaponDetection(BaseModel):
173
+ type: str
174
+ class_name: str
175
+ weapon_type: str
176
+ confidence: float
177
  bbox: BoundingBox
178
+ threat_level: str
179
+ detection_method: str
180
 
181
 
182
  class NSFWDetection(BaseModel):
183
+ type: str
184
+ class_name: str
185
+ confidence: float
186
  bbox: BoundingBox
187
+ method: str
188
+ skin_ratio: Optional[float] = None
189
 
190
 
191
  class FightDetection(BaseModel):
192
+ type: str
193
+ confidence: float
194
  bbox: BoundingBox
195
+ persons_involved: int
196
+ threat_level: str
197
 
198
 
199
  class ImageDetectionResponse(BaseModel):
 
205
  summary: Dict[str, Any]
206
  risk_level: str
207
  action_required: bool
 
208
  processing_time_ms: float
209
 
210
 
 
218
  summary: Dict[str, Any]
219
  risk_level: str
220
  action_required: bool
 
221
  processing_time_ms: float
222
+ optimization_used: Dict[str, Any]
223
 
224
 
225
+ # ============== Startup/Shutdown Events ==============
226
+
227
+ @app.on_event("startup")
228
+ async def startup_event():
229
+ """Initialize Content Moderator on startup"""
230
+ global moderator
231
+ try:
232
+ logger.info("Initializing Content Moderator for CPU...")
233
+
234
+ # Create CPU-optimized config
235
+ cpu_config = {
236
+ 'weapon_detection': {
237
+ 'enabled': True,
238
+ 'confidence_threshold': 0.5,
239
+ 'knife_confidence': 0.5,
240
+ 'fight_confidence': 0.45,
241
+ 'model_size': 'yolo11n',
242
+ 'use_enhancement': False, # Disable for CPU
243
+ 'multi_pass': False, # Disable for CPU
244
+ 'boost_knife_detection': True,
245
+ 'fight_detection': True,
246
+ 'fight_analysis': False # Disable complex analysis
247
+ },
248
+ 'nsfw_detection': {
249
+ 'enabled': True,
250
+ 'confidence_threshold': 0.7,
251
+ 'skin_detection': False, # Disable for CPU
252
+ 'pose_analysis': False,
253
+ 'region_analysis': False
254
+ },
255
+ 'performance': {
256
+ 'image_size': 320, # Small size for CPU
257
+ 'batch_size': 1,
258
+ 'half_precision': False,
259
+ 'use_flash_attention': False,
260
+ 'cpu_optimization': True
261
+ },
262
+ 'output': {
263
+ 'save_detections': True,
264
+ 'draw_boxes': False, # Disable to save CPU
265
+ 'log_results': True
266
+ }
267
+ }
268
+
269
+ moderator = ContentModerator(config=cpu_config)
270
+
271
+ status = moderator.get_model_status()
272
+ logger.info(f"Model Status: {status}")
273
+ logger.info("✅ Content Moderator initialized successfully for CPU")
274
+
275
+ except Exception as e:
276
+ logger.error(f"Failed to initialize Content Moderator: {e}")
277
+ moderator = None
278
+
279
+
280
+ @app.on_event("shutdown")
281
+ async def shutdown_event():
282
+ """Cleanup on shutdown"""
283
+ global moderator
284
+ if moderator:
285
+ logger.info("Shutting down Content Moderator...")
286
+ moderator = None
287
+ video_optimizer.clear_cache()
288
 
289
 
290
  # ============== Utility Functions ==============
 
316
  raise
317
 
318
 
319
+ def safe_dict(obj):
320
+ """Convert object to dict safely"""
321
+ if hasattr(obj, 'dict'):
322
+ return obj.dict()
323
+ elif isinstance(obj, dict):
324
+ return obj
325
+ else:
326
+ return str(obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
 
329
  def process_detections(raw_detections: List[Dict]) -> Dict[str, List]:
 
365
  skin_ratio=det.get('skin_ratio')
366
  ))
367
  elif det['type'] == 'fight':
368
+ processed['fights'].append(FightDetection(
369
+ type="fight",
370
+ confidence=det['confidence'],
371
+ bbox=BoundingBox(
372
+ x1=det['bbox'][0],
373
+ y1=det['bbox'][1],
374
+ x2=det['bbox'][2],
375
+ y2=det['bbox'][3]
376
+ ),
377
+ persons_involved=det.get('people_involved', 2),
378
+ threat_level=det.get('threat_level', 'high')
379
+ ))
380
 
381
  return processed
382
 
383
 
384
  # ============== API Endpoints ==============
385
 
386
+ @app.get("/")
387
+ async def root():
388
+ """Root endpoint"""
389
+ return {
390
+ "message": "Weapon & NSFW Detection API",
391
+ "version": "2.0.0",
392
+ "status": "running" if moderator else "initializing",
393
+ "cpu_optimized": True,
394
+ "docs": "/docs"
395
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
 
398
+ @app.get("/status")
399
+ async def get_status():
400
+ """Check system status"""
401
+ if moderator is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  return {
403
+ "status": "error",
404
+ "message": "Content Moderator not initialized"
 
 
405
  }
406
 
407
+ return {
408
+ "status": "ok",
409
+ "model_status": moderator.get_model_status(),
410
+ "memory_usage": moderator.get_memory_usage(),
411
+ "cache_size": len(video_optimizer.frame_cache),
412
+ "cpu_optimized": True
413
+ }
414
+
415
 
416
  @app.post("/detect_n_k_f_g/images", response_model=ImageDetectionResponse)
417
  async def detect_image(
418
+ file: UploadFile = File(...),
419
+ return_annotated: bool = Form(False)
 
420
  ):
421
  """
422
+ Detect weapons, fights, and NSFW content in images
423
+ Optimized for CPU processing
 
 
424
  """
425
+ if moderator is None:
426
+ raise HTTPException(
427
+ status_code=503,
428
+ detail="Content Moderator not initialized"
429
+ )
430
+
431
  request_id = generate_request_id()
432
  start_time = datetime.now()
433
 
434
  try:
435
+ # Validate file
436
  if not validate_file_extension(file.filename, config.ALLOWED_IMAGE_EXTENSIONS):
437
  raise HTTPException(
438
  status_code=400,
439
+ detail=f"Invalid file type"
440
  )
441
 
442
+ # Read file
443
  file_content = await file.read()
444
  file_size = len(file_content)
445
 
446
  if not validate_file_size(file_size, config.MAX_IMAGE_SIZE):
447
  raise HTTPException(
448
  status_code=400,
449
+ detail=f"File too large. Max: {config.MAX_IMAGE_SIZE / (1024 * 1024):.1f}MB"
450
  )
451
 
452
+ # Save file
453
  upload_path = config.UPLOAD_DIR / "images" / f"{request_id}_{file.filename}"
454
  async with aiofiles.open(upload_path, 'wb') as f:
455
  await f.write(file_content)
456
 
457
+ # Decode image
458
  nparr = np.frombuffer(file_content, np.uint8)
459
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
460
 
461
  if image is None:
462
+ raise HTTPException(status_code=400, detail="Invalid image file")
463
 
464
  # Get image info
465
+ height, width = image.shape[:2]
466
  image_info = {
467
  "filename": file.filename,
468
  "width": width,
469
  "height": height,
 
 
470
  "size_mb": round(file_size / (1024 * 1024), 2)
471
  }
472
 
473
+ # Downscale for CPU if too large
474
+ if width > 640:
475
+ scale = 640 / width
476
+ new_width = int(width * scale)
477
+ new_height = int(height * scale)
478
+ image = cv2.resize(image, (new_width, new_height))
479
+ logger.info(f"Downscaled image from {width}x{height} to {new_width}x{new_height}")
480
+
481
+ # Process image
482
  result = moderator.process_image(image)
483
 
484
  if not result:
485
+ raise HTTPException(status_code=500, detail="Processing failed")
 
 
 
 
 
 
 
 
486
 
487
  # Process detections
488
  processed = process_detections(result['detections'])
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  # Calculate summary
 
 
 
 
 
 
 
 
 
491
  summary = {
492
+ "total_detections": len(result['detections']),
493
+ "weapons": len(processed['weapons']),
494
+ "nsfw": len(processed['nsfw']),
495
+ "fights": len(processed['fights'])
 
 
 
 
 
496
  }
497
 
498
+ # Determine risk level
499
+ if len(processed['weapons']) > 0 or len(processed['fights']) > 0:
500
+ risk_level = "high"
501
+ elif len(processed['nsfw']) > 0:
502
  risk_level = "medium"
503
  else:
504
  risk_level = "safe"
 
515
  summary=summary,
516
  risk_level=risk_level,
517
  action_required=(summary["total_detections"] > 0),
 
518
  processing_time_ms=processing_time
519
  )
520
 
521
  except HTTPException:
522
  raise
523
  except Exception as e:
524
+ logger.error(f"Error processing image: {e}")
525
  logger.error(traceback.format_exc())
526
+ raise HTTPException(status_code=500, detail=str(e))
 
 
 
527
 
528
 
529
  @app.post("/detect_n_k_f_g/videos", response_model=VideoDetectionResponse)
530
  async def detect_video(
531
+ file: UploadFile = File(...),
532
+ quick_mode: bool = Form(True, description="Enable CPU optimizations"),
533
+ adaptive_settings: bool = Form(True, description="Auto-adjust settings"),
534
+ custom_frame_skip: Optional[int] = Form(None, ge=1, le=50)
535
  ):
536
  """
537
+ Detect weapons, fights, and NSFW content in videos
538
+ CPU-optimized with smart frame skipping
 
 
539
  """
540
+ if moderator is None:
541
+ raise HTTPException(
542
+ status_code=503,
543
+ detail="Content Moderator not initialized"
544
+ )
545
+
546
  request_id = generate_request_id()
547
  start_time = datetime.now()
 
548
 
549
  try:
550
+ # Validate file
551
  if not validate_file_extension(file.filename, config.ALLOWED_VIDEO_EXTENSIONS):
552
  raise HTTPException(
553
  status_code=400,
554
+ detail="Invalid video format"
555
  )
556
 
557
+ # Save video
558
  upload_path = config.UPLOAD_DIR / "videos" / f"{request_id}_{file.filename}"
559
  await save_upload_file(file, upload_path)
560
 
561
+ # Check file size
562
  file_size = upload_path.stat().st_size
563
  if not validate_file_size(file_size, config.MAX_VIDEO_SIZE):
564
+ upload_path.unlink()
565
  raise HTTPException(
566
  status_code=400,
567
+ detail=f"File too large. Max: {config.MAX_VIDEO_SIZE / (1024 * 1024):.1f}MB"
568
  )
569
 
570
  # Open video
571
  cap = cv2.VideoCapture(str(upload_path))
572
  if not cap.isOpened():
573
+ raise HTTPException(status_code=400, detail="Cannot open video file")
574
 
575
  # Get video info
576
  fps = cap.get(cv2.CAP_PROP_FPS)
 
586
  "fps": fps,
587
  "total_frames": total_frames,
588
  "duration_seconds": round(duration, 2),
 
589
  "size_mb": round(file_size / (1024 * 1024), 2)
590
  }
591
 
592
+ # Get optimal settings
593
+ if adaptive_settings:
594
+ settings = video_optimizer.get_optimal_settings(duration, total_frames)
595
+ frame_skip = custom_frame_skip or settings['frame_skip']
596
+ target_width = settings['target_width']
597
+ max_frames = settings['max_frames']
598
+ else:
599
+ frame_skip = custom_frame_skip or config.VIDEO_FRAME_SKIP
600
+ target_width = config.VIDEO_TARGET_WIDTH
601
+ max_frames = config.VIDEO_MAX_FRAMES
602
+
603
+ logger.info(f"Video settings: skip={frame_skip}, width={target_width}, max={max_frames}")
604
+
605
+ # Clear cache for new video
606
+ video_optimizer.clear_cache()
607
 
608
+ # Processing variables
609
  frame_detections = []
610
  frame_count = 0
611
  processed_count = 0
612
+ threat_count = 0
613
+ critical_threat = False
614
 
615
  # Aggregated statistics
616
  all_weapons = []
617
  all_nsfw = []
618
  all_fights = []
619
 
620
+ # Temporary optimize settings for video processing
621
+ if quick_mode:
622
+ original_size = moderator.config['performance']['image_size']
623
+ moderator.config['performance']['image_size'] = target_width
624
+
625
+ # Process video
626
  while True:
627
  ret, frame = cap.read()
628
  if not ret:
 
630
 
631
  frame_count += 1
632
 
633
+ # Skip frames
634
  if frame_count % frame_skip != 0:
635
  continue
636
 
637
+ # Check max frames limit
638
  if processed_count >= max_frames:
639
  logger.info(f"Reached max frames limit: {max_frames}")
640
  break
641
 
642
+ # Preprocess frame
643
+ frame = video_optimizer.preprocess_frame(frame, target_width)
644
+
645
+ # Skip similar frames
646
+ if video_optimizer.should_skip_frame(frame):
647
+ continue
648
+
649
  processed_count += 1
650
 
651
  # Process frame
652
  result = moderator.process_image(frame)
653
 
654
  if result and result['detections']:
 
 
 
 
 
 
 
 
655
  # Process detections
656
  processed = process_detections(result['detections'])
657
 
658
+ # Track threats
659
+ current_threats = len(result['detections'])
660
+ threat_count += current_threats
661
+
662
+ # Check for critical threats
663
+ for det in result['detections']:
664
+ if det.get('threat_level') == 'critical':
665
+ critical_threat = True
666
 
667
+ # Store frame detection info (simplified)
668
+ if current_threats > 0:
669
  frame_info = {
670
  "frame_number": frame_count,
671
+ "timestamp_seconds": round(frame_count / fps, 2),
672
  "detections": {
673
+ "weapons": len(processed['weapons']),
674
+ "nsfw": len(processed['nsfw']),
675
+ "fights": len(processed['fights'])
676
+ },
677
+ "threat_level": "critical" if critical_threat else "high"
678
  }
679
  frame_detections.append(frame_info)
680
 
681
+ # Aggregate
682
  all_weapons.extend(processed['weapons'])
683
  all_nsfw.extend(processed['nsfw'])
684
  all_fights.extend(processed['fights'])
685
 
686
+ # Early stopping
687
+ if critical_threat and threat_count >= config.VIDEO_EARLY_STOP_THRESHOLD:
688
+ logger.info(f"Critical threats detected ({threat_count}), early stopping")
689
+ break
690
 
691
+ # Progress log
692
+ if processed_count % 20 == 0:
693
+ elapsed = (datetime.now() - start_time).total_seconds()
694
+ frames_per_sec = processed_count / elapsed if elapsed > 0 else 0
695
+ logger.info(f"Processed {processed_count} frames in {elapsed:.1f}s ({frames_per_sec:.1f} fps)")
696
+
697
+ # Restore original settings
698
+ if quick_mode:
699
+ moderator.config['performance']['image_size'] = original_size
700
+
701
+ # Release video
702
  cap.release()
703
 
704
+ # Clean up uploaded file
705
+ try:
706
+ upload_path.unlink()
707
+ except:
708
+ pass
709
 
710
+ # Calculate summary
711
  summary = {
712
  "total_frames_analyzed": processed_count,
713
  "frames_with_detections": len(frame_detections),
714
+ "total_detections": threat_count,
715
+ "weapons": len(all_weapons),
716
+ "nsfw": len(all_nsfw),
717
+ "fights": len(all_fights)
 
 
 
 
 
 
 
 
 
 
 
718
  }
719
 
720
+ # Determine risk level
721
+ if critical_threat or len(all_weapons) > 5:
722
  risk_level = "critical"
723
+ elif len(all_weapons) > 0 or len(all_fights) > 0:
724
  risk_level = "high"
725
  elif len(all_nsfw) > 0:
726
  risk_level = "medium"
 
730
  # Calculate processing time
731
  processing_time = (datetime.now() - start_time).total_seconds() * 1000
732
 
733
+ # Optimization info
734
+ optimization_used = {
735
+ "frame_skip": frame_skip,
736
+ "resolution": target_width,
737
+ "max_frames": max_frames,
738
+ "frames_cached": len(video_optimizer.frame_cache),
739
+ "early_stopped": critical_threat and threat_count >= config.VIDEO_EARLY_STOP_THRESHOLD
740
+ }
741
+
742
  return VideoDetectionResponse(
743
  success=True,
744
  request_id=request_id,
745
  timestamp=datetime.now().isoformat(),
746
  video_info=video_info,
747
  total_frames_processed=processed_count,
748
+ frame_detections=frame_detections[:50], # Limit to 50 detections
749
  summary=summary,
750
  risk_level=risk_level,
751
  action_required=(summary["total_detections"] > 0),
752
+ processing_time_ms=processing_time,
753
+ optimization_used=optimization_used
754
  )
755
 
756
  except HTTPException:
757
  raise
758
  except Exception as e:
759
+ logger.error(f"Error processing video: {e}")
760
  logger.error(traceback.format_exc())
761
+ raise HTTPException(status_code=500, detail=str(e))
 
 
 
762
  finally:
763
+ # Clear cache after video processing
764
+ video_optimizer.clear_cache()
 
 
 
 
 
765
 
766
 
767
  @app.delete("/cleanup")
768
  async def cleanup_old_files(hours: int = 24):
769
+ """Clean up old files"""
770
  try:
771
  from datetime import timedelta
772
  cutoff_time = datetime.now() - timedelta(hours=hours)
773
 
774
  deleted_count = 0
 
 
775
  for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
776
+ for subdir in ["images", "videos"]:
777
+ path = directory / subdir
778
+ if path.exists():
779
+ for file in path.iterdir():
780
+ if file.is_file():
781
+ file_time = datetime.fromtimestamp(file.stat().st_mtime)
782
+ if file_time < cutoff_time:
783
+ file.unlink()
784
+ deleted_count += 1
 
 
 
 
 
 
 
 
 
 
 
 
785
 
786
  return {
787
  "success": True,
 
790
  }
791
  except Exception as e:
792
  logger.error(f"Cleanup error: {e}")
793
+ return {"success": False, "error": str(e)}
794
+
 
 
795
 
796
  if __name__ == "__main__":
797
  import os