Haiss123 commited on
Commit
36baa61
·
verified ·
1 Parent(s): 1fdaa97

Update detection_api.py

Browse files
Files changed (1) hide show
  1. detection_api.py +125 -743
detection_api.py CHANGED
@@ -1,41 +1,28 @@
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
10
- import json
11
  from pathlib import Path
12
  import uuid
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
- from gunicorn.app.base import BaseApplication
20
- from main import ContentModerator
21
 
22
  # Setup logging
23
- logging.basicConfig(
24
- level=logging.INFO,
25
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
26
- )
27
  logger = logging.getLogger(__name__)
28
 
29
- # Initialize FastAPI app
30
  app = FastAPI(
31
- title="Weapon & NSFW Detection API",
32
- description="API for detecting knives/dao, guns, fights and NSFW content in images and videos",
33
- version="2.0.0",
34
- docs_url="/docs",
35
- redoc_url="/redoc"
36
  )
37
 
38
- # Add CORS middleware
39
  app.add_middleware(
40
  CORSMiddleware,
41
  allow_origins=["*"],
@@ -44,775 +31,170 @@ app.add_middleware(
44
  allow_headers=["*"],
45
  )
46
 
 
 
 
 
 
47
 
48
- # Configuration optimized for CPU
49
- class Config:
50
- UPLOAD_DIR = Path("uploads")
51
- RESULTS_DIR = Path("results")
52
- PROCESSED_DIR = Path("processed")
53
- MAX_IMAGE_SIZE = 50 * 1024 * 1024 # 50MB for images
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()
70
-
71
- # Create necessary directories
72
- for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
73
- directory.mkdir(exist_ok=True)
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
- def StandaloneApplication(app, options=None):
84
- """Hàm bọc Gunicorn để chạy app"""
85
- from gunicorn.app.base import BaseApplication
86
-
87
- class _App(BaseApplication):
88
- def __init__(self, app, options=None):
89
- self.options = options or {}
90
- self.application = app
91
- super().__init__()
92
-
93
- def load_config(self):
94
- config = {
95
- key: value for key, value in self.options.items()
96
- if key in self.cfg.settings and value is not None
97
- }
98
- for key, value in config.items():
99
- self.cfg.set(key.lower(), value)
100
-
101
- def load(self):
102
- return self.application
103
-
104
- return _App(app, options)
105
-
106
-
107
- # Video Optimizer Class
108
- class VideoOptimizer:
109
-
110
- def __init__(self):
111
- self.frame_cache = {}
112
- self.cache_size = 20
113
-
114
- def get_optimal_settings(self, duration: float, total_frames: int) -> Dict:
115
- """Calculate optimal settings based on video duration"""
116
-
117
- if duration <= 5:
118
- return {
119
- 'frame_skip': 3,
120
- 'target_width': 416,
121
- 'max_frames': 50
122
- }
123
- elif duration <= 15:
124
- return {
125
- 'frame_skip': 8,
126
- 'target_width': 416,
127
- 'max_frames': 75
128
- }
129
- elif duration <= 30:
130
- return {
131
- 'frame_skip': 12,
132
- 'target_width': 320,
133
- 'max_frames': 100
134
- }
135
- else:
136
- return {
137
- 'frame_skip': 20,
138
- 'target_width': 320,
139
- 'max_frames': 150
140
- }
141
-
142
- def preprocess_frame(self, frame: np.ndarray, target_width: int = 416) -> np.ndarray:
143
- """Downscale frame for faster processing"""
144
- height, width = frame.shape[:2]
145
-
146
- if width > target_width:
147
- scale = target_width / width
148
- new_width = int(width * scale)
149
- new_height = int(height * scale)
150
- frame = cv2.resize(frame, (new_width, new_height),
151
- interpolation=cv2.INTER_LINEAR)
152
-
153
- return frame
154
-
155
- def get_frame_hash(self, frame: np.ndarray) -> str:
156
- """Generate hash for frame"""
157
- small = cv2.resize(frame, (8, 8))
158
- return hashlib.md5(small.tobytes()).hexdigest()
159
-
160
- def should_skip_frame(self, frame: np.ndarray) -> bool:
161
- """Check if frame is similar to cached frames"""
162
- frame_hash = self.get_frame_hash(frame)
163
-
164
- if frame_hash in self.frame_cache:
165
- return True
166
-
167
- # Maintain cache size
168
- if len(self.frame_cache) >= self.cache_size:
169
- # Remove oldest entry
170
- oldest = min(self.frame_cache, key=self.frame_cache.get)
171
- del self.frame_cache[oldest]
172
-
173
- self.frame_cache[frame_hash] = time.time()
174
- return False
175
-
176
- def clear_cache(self):
177
- """Clear frame cache"""
178
- self.frame_cache.clear()
179
-
180
-
181
- # Initialize video optimizer
182
- video_optimizer = VideoOptimizer()
183
-
184
-
185
- # ============== Response Models ==============
186
-
187
- class BoundingBox(BaseModel):
188
- x1: int
189
- y1: int
190
- x2: int
191
- y2: int
192
-
193
-
194
- class WeaponDetection(BaseModel):
195
- type: str
196
- class_name: str
197
- weapon_type: str
198
- confidence: float
199
- bbox: BoundingBox
200
- threat_level: str
201
- detection_method: str
202
-
203
-
204
- class NSFWDetection(BaseModel):
205
- type: str
206
- class_name: str
207
- confidence: float
208
- bbox: BoundingBox
209
- method: str
210
- skin_ratio: Optional[float] = None
211
-
212
-
213
- class FightDetection(BaseModel):
214
- type: str
215
- confidence: float
216
- bbox: BoundingBox
217
- persons_involved: int
218
- threat_level: str
219
-
220
-
221
- class ImageDetectionResponse(BaseModel):
222
- success: bool
223
- request_id: str
224
- timestamp: str
225
- image_info: Dict[str, Any]
226
- detections: Dict[str, List[Union[WeaponDetection, NSFWDetection, FightDetection]]]
227
- summary: Dict[str, Any]
228
- risk_level: str
229
- action_required: bool
230
- processing_time_ms: float
231
-
232
-
233
- class VideoDetectionResponse(BaseModel):
234
- success: bool
235
- request_id: str
236
- timestamp: str
237
- video_info: Dict[str, Any]
238
- total_frames_processed: int
239
- frame_detections: List[Dict[str, Any]]
240
- summary: Dict[str, Any]
241
- risk_level: str
242
- action_required: bool
243
- processing_time_ms: float
244
- optimization_used: Dict[str, Any]
245
 
246
 
247
- # ============== Startup/Shutdown Events ==============
 
 
 
 
 
 
 
248
 
 
 
249
  @app.on_event("startup")
250
  async def startup_event():
251
- """Initialize Content Moderator on startup"""
252
  global moderator
253
  try:
254
- logger.info("Initializing Content Moderator for CPU...")
255
-
256
- # Create CPU-optimized config
257
- cpu_config = {
258
- 'weapon_detection': {
259
- 'enabled': True,
260
- 'confidence_threshold': 0.5,
261
- 'knife_confidence': 0.5,
262
- 'fight_confidence': 0.45,
263
- 'model_size': 'yolo11n',
264
- 'use_enhancement': False, # Disable for CPU
265
- 'multi_pass': False, # Disable for CPU
266
- 'boost_knife_detection': True,
267
- 'fight_detection': True,
268
- 'fight_analysis': False # Disable complex analysis
269
- },
270
- 'nsfw_detection': {
271
- 'enabled': True,
272
- 'confidence_threshold': 0.7,
273
- 'skin_detection': False, # Disable for CPU
274
- 'pose_analysis': False,
275
- 'region_analysis': False
276
- },
277
- 'performance': {
278
- 'image_size': 320, # Small size for CPU
279
- 'batch_size': 1,
280
- 'half_precision': False,
281
- 'use_flash_attention': False,
282
- 'cpu_optimization': True
283
- },
284
- 'output': {
285
- 'save_detections': True,
286
- 'draw_boxes': False, # Disable to save CPU
287
- 'log_results': True
288
- }
289
- }
290
-
291
- moderator = ContentModerator(config=cpu_config)
292
-
293
- status = moderator.get_model_status()
294
- logger.info(f"Model Status: {status}")
295
- logger.info("✅ Content Moderator initialized successfully for CPU")
296
-
297
  except Exception as e:
298
- logger.error(f"Failed to initialize Content Moderator: {e}")
299
- moderator = None
300
-
301
-
302
- @app.on_event("shutdown")
303
- async def shutdown_event():
304
- """Cleanup on shutdown"""
305
- global moderator
306
- if moderator:
307
- logger.info("Shutting down Content Moderator...")
308
  moderator = None
309
- video_optimizer.clear_cache()
310
-
311
-
312
- # ============== Utility Functions ==============
313
-
314
- def generate_request_id() -> str:
315
- """Generate unique request ID"""
316
- return f"req_{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
317
-
318
-
319
- def validate_file_extension(filename: str, allowed_extensions: set) -> bool:
320
- """Validate file extension"""
321
- return Path(filename).suffix.lower() in allowed_extensions
322
-
323
-
324
- def validate_file_size(file_size: int, max_size: int) -> bool:
325
- """Validate file size"""
326
- return file_size <= max_size
327
-
328
-
329
- async def save_upload_file(upload_file: UploadFile, destination: Path) -> Path:
330
- """Save uploaded file to destination"""
331
- try:
332
- async with aiofiles.open(destination, 'wb') as f:
333
- content = await upload_file.read()
334
- await f.write(content)
335
- return destination
336
- except Exception as e:
337
- logger.error(f"Error saving file: {e}")
338
- raise
339
-
340
-
341
- def safe_dict(obj):
342
- """Convert object to dict safely"""
343
- if hasattr(obj, 'dict'):
344
- return obj.dict()
345
- elif isinstance(obj, dict):
346
- return obj
347
- else:
348
- return str(obj)
349
-
350
-
351
- def process_detections(raw_detections: List[Dict]) -> Dict[str, List]:
352
- """Process and categorize raw detections"""
353
- processed = {
354
- 'weapons': [],
355
- 'nsfw': [],
356
- 'fights': []
357
- }
358
-
359
- for det in raw_detections:
360
- if det['type'] == 'weapon':
361
- processed['weapons'].append(WeaponDetection(
362
- type=det['type'],
363
- class_name=det['class'],
364
- weapon_type=det.get('weapon_type', 'unknown'),
365
- confidence=det['confidence'],
366
- bbox=BoundingBox(
367
- x1=det['bbox'][0],
368
- y1=det['bbox'][1],
369
- x2=det['bbox'][2],
370
- y2=det['bbox'][3]
371
- ),
372
- threat_level=det.get('threat_level', 'medium'),
373
- detection_method=det.get('detection_method', 'yolo')
374
- ))
375
- elif det['type'] == 'nsfw':
376
- processed['nsfw'].append(NSFWDetection(
377
- type=det['type'],
378
- class_name=det['class'],
379
- confidence=det['confidence'],
380
- bbox=BoundingBox(
381
- x1=det['bbox'][0],
382
- y1=det['bbox'][1],
383
- x2=det['bbox'][2],
384
- y2=det['bbox'][3]
385
- ),
386
- method=det.get('method', 'classification'),
387
- skin_ratio=det.get('skin_ratio')
388
- ))
389
- elif det['type'] == 'fight':
390
- processed['fights'].append(FightDetection(
391
- type="fight",
392
- confidence=det['confidence'],
393
- bbox=BoundingBox(
394
- x1=det['bbox'][0],
395
- y1=det['bbox'][1],
396
- x2=det['bbox'][2],
397
- y2=det['bbox'][3]
398
- ),
399
- persons_involved=det.get('people_involved', 2),
400
- threat_level=det.get('threat_level', 'high')
401
- ))
402
-
403
- return processed
404
 
405
 
406
  # ============== API Endpoints ==============
407
 
408
- @app.get("/")
409
- async def root():
410
- """Root endpoint"""
411
- return {
412
- "message": "Weapon & NSFW Detection API",
413
- "version": "2.0.0",
414
- "status": "running" if moderator else "initializing",
415
- "cpu_optimized": True,
416
- "docs": "/docs"
417
- }
418
-
419
-
420
- @app.get("/status")
421
- async def get_status():
422
- """Check system status"""
423
- if moderator is None:
424
- return {
425
- "status": "error",
426
- "message": "Content Moderator not initialized"
427
- }
428
-
429
- return {
430
- "status": "ok",
431
- "model_status": moderator.get_model_status(),
432
- "memory_usage": moderator.get_memory_usage(),
433
- "cache_size": len(video_optimizer.frame_cache),
434
- "cpu_optimized": True
435
- }
436
 
 
 
 
 
437
 
438
- @app.post("/detect_n_k_f_g/images", response_model=ImageDetectionResponse)
439
- async def detect_image(
440
- file: UploadFile = File(...),
441
- return_annotated: bool = Form(False)
442
- ):
443
  """
444
- Detect weapons, fights, and NSFW content in images
445
- Optimized for CPU processing
446
- """
447
- if moderator is None:
448
- raise HTTPException(
449
- status_code=503,
450
- detail="Content Moderator not initialized"
451
- )
452
 
453
- request_id = generate_request_id()
454
- start_time = datetime.now()
455
 
456
  try:
457
- # Validate file
458
- if not validate_file_extension(file.filename, config.ALLOWED_IMAGE_EXTENSIONS):
459
- raise HTTPException(
460
- status_code=400,
461
- detail=f"Invalid file type"
462
- )
463
 
464
- # Read file
465
- file_content = await file.read()
466
- file_size = len(file_content)
467
 
468
- if not validate_file_size(file_size, config.MAX_IMAGE_SIZE):
469
- raise HTTPException(
470
- status_code=400,
471
- detail=f"File too large. Max: {config.MAX_IMAGE_SIZE / (1024 * 1024):.1f}MB"
472
- )
473
 
474
- # Save file
475
- upload_path = config.UPLOAD_DIR / "images" / f"{request_id}_{file.filename}"
476
- async with aiofiles.open(upload_path, 'wb') as f:
477
- await f.write(file_content)
478
 
479
  # Decode image
480
- nparr = np.frombuffer(file_content, np.uint8)
481
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
482
 
483
  if image is None:
484
- raise HTTPException(status_code=400, detail="Invalid image file")
485
-
486
- # Get image info
487
- height, width = image.shape[:2]
488
- image_info = {
489
- "filename": file.filename,
490
- "width": width,
491
- "height": height,
492
- "size_mb": round(file_size / (1024 * 1024), 2)
493
- }
494
-
495
- # Downscale for CPU if too large
496
- if width > 640:
497
- scale = 640 / width
498
- new_width = int(width * scale)
499
- new_height = int(height * scale)
500
- image = cv2.resize(image, (new_width, new_height))
501
- logger.info(f"Downscaled image from {width}x{height} to {new_width}x{new_height}")
502
-
503
- # Process image
504
  result = moderator.process_image(image)
505
 
506
- if not result:
507
- raise HTTPException(status_code=500, detail="Processing failed")
508
-
509
- # Process detections
510
- processed = process_detections(result['detections'])
511
-
512
- # Calculate summary
513
- summary = {
514
- "total_detections": len(result['detections']),
515
- "weapons": len(processed['weapons']),
516
- "nsfw": len(processed['nsfw']),
517
- "fights": len(processed['fights'])
518
- }
519
-
520
- # Determine risk level
521
- if len(processed['weapons']) > 0 or len(processed['fights']) > 0:
522
- risk_level = "high"
523
- elif len(processed['nsfw']) > 0:
524
- risk_level = "medium"
525
- else:
526
- risk_level = "safe"
527
-
528
- # Calculate processing time
529
- processing_time = (datetime.now() - start_time).total_seconds() * 1000
530
-
531
- return ImageDetectionResponse(
532
- success=True,
533
- request_id=request_id,
534
- timestamp=datetime.now().isoformat(),
535
- image_info=image_info,
536
- detections=processed,
537
- summary=summary,
538
- risk_level=risk_level,
539
- action_required=(summary["total_detections"] > 0),
540
- processing_time_ms=processing_time
541
  )
542
 
543
  except HTTPException:
544
  raise
545
  except Exception as e:
546
- logger.error(f"Error processing image: {e}")
547
- logger.error(traceback.format_exc())
548
- raise HTTPException(status_code=500, detail=str(e))
549
-
550
-
551
- @app.post("/detect_n_k_f_g/videos", response_model=VideoDetectionResponse)
552
- async def detect_video(
553
- file: UploadFile = File(...),
554
- quick_mode: bool = Form(True, description="Enable CPU optimizations"),
555
- adaptive_settings: bool = Form(True, description="Auto-adjust settings"),
556
- custom_frame_skip: Optional[int] = Form(None, ge=1, le=50)
557
- ):
558
  """
559
- Detect weapons, fights, and NSFW content in videos
560
- CPU-optimized with smart frame skipping
 
 
 
 
 
 
561
  """
 
562
  if moderator is None:
563
- raise HTTPException(
564
- status_code=503,
565
- detail="Content Moderator not initialized"
566
- )
567
 
568
- request_id = generate_request_id()
569
- start_time = datetime.now()
570
 
571
  try:
572
- # Validate file
573
- if not validate_file_extension(file.filename, config.ALLOWED_VIDEO_EXTENSIONS):
574
- raise HTTPException(
575
- status_code=400,
576
- detail="Invalid video format"
577
- )
578
-
579
- # Save video
580
- upload_path = config.UPLOAD_DIR / "videos" / f"{request_id}_{file.filename}"
581
- await save_upload_file(file, upload_path)
582
-
583
- # Check file size
584
- file_size = upload_path.stat().st_size
585
- if not validate_file_size(file_size, config.MAX_VIDEO_SIZE):
586
- upload_path.unlink()
587
- raise HTTPException(
588
- status_code=400,
589
- detail=f"File too large. Max: {config.MAX_VIDEO_SIZE / (1024 * 1024):.1f}MB"
590
- )
591
-
592
- # Open video
593
- cap = cv2.VideoCapture(str(upload_path))
594
- if not cap.isOpened():
595
- raise HTTPException(status_code=400, detail="Cannot open video file")
596
-
597
- # Get video info
598
- fps = cap.get(cv2.CAP_PROP_FPS)
599
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
600
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
601
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
602
- duration = total_frames / fps if fps > 0 else 0
603
-
604
- video_info = {
605
- "filename": file.filename,
606
- "width": width,
607
- "height": height,
608
- "fps": fps,
609
- "total_frames": total_frames,
610
- "duration_seconds": round(duration, 2),
611
- "size_mb": round(file_size / (1024 * 1024), 2)
612
- }
613
-
614
- # Get optimal settings
615
- if adaptive_settings:
616
- settings = video_optimizer.get_optimal_settings(duration, total_frames)
617
- frame_skip = custom_frame_skip or settings['frame_skip']
618
- target_width = settings['target_width']
619
- max_frames = settings['max_frames']
620
- else:
621
- frame_skip = custom_frame_skip or config.VIDEO_FRAME_SKIP
622
- target_width = config.VIDEO_TARGET_WIDTH
623
- max_frames = config.VIDEO_MAX_FRAMES
624
-
625
- logger.info(f"Video settings: skip={frame_skip}, width={target_width}, max={max_frames}")
626
-
627
- # Clear cache for new video
628
- video_optimizer.clear_cache()
629
-
630
- # Processing variables
631
- frame_detections = []
632
- frame_count = 0
633
- processed_count = 0
634
- threat_count = 0
635
- critical_threat = False
636
-
637
- # Aggregated statistics
638
- all_weapons = []
639
- all_nsfw = []
640
- all_fights = []
641
-
642
- # Temporary optimize settings for video processing
643
- if quick_mode:
644
- original_size = moderator.config['performance']['image_size']
645
- moderator.config['performance']['image_size'] = target_width
646
-
647
- # Process video
648
- while True:
649
- ret, frame = cap.read()
650
- if not ret:
651
- break
652
-
653
- frame_count += 1
654
-
655
- # Skip frames
656
- if frame_count % frame_skip != 0:
657
- continue
658
-
659
- # Check max frames limit
660
- if processed_count >= max_frames:
661
- logger.info(f"Reached max frames limit: {max_frames}")
662
- break
663
-
664
- # Preprocess frame
665
- frame = video_optimizer.preprocess_frame(frame, target_width)
666
-
667
- # Skip similar frames
668
- if video_optimizer.should_skip_frame(frame):
669
- continue
670
-
671
- processed_count += 1
672
-
673
- # Process frame
674
- result = moderator.process_image(frame)
675
-
676
- if result and result['detections']:
677
- # Process detections
678
- processed = process_detections(result['detections'])
679
-
680
- # Track threats
681
- current_threats = len(result['detections'])
682
- threat_count += current_threats
683
-
684
- # Check for critical threats
685
- for det in result['detections']:
686
- if det.get('threat_level') == 'critical':
687
- critical_threat = True
688
-
689
- # Store frame detection info (simplified)
690
- if current_threats > 0:
691
- frame_info = {
692
- "frame_number": frame_count,
693
- "timestamp_seconds": round(frame_count / fps, 2),
694
- "detections": {
695
- "weapons": len(processed['weapons']),
696
- "nsfw": len(processed['nsfw']),
697
- "fights": len(processed['fights'])
698
- },
699
- "threat_level": "critical" if critical_threat else "high"
700
- }
701
- frame_detections.append(frame_info)
702
-
703
- # Aggregate
704
- all_weapons.extend(processed['weapons'])
705
- all_nsfw.extend(processed['nsfw'])
706
- all_fights.extend(processed['fights'])
707
-
708
- # Early stopping
709
- if critical_threat and threat_count >= config.VIDEO_EARLY_STOP_THRESHOLD:
710
- logger.info(f"Critical threats detected ({threat_count}), early stopping")
711
- break
712
-
713
- # Progress log
714
- if processed_count % 20 == 0:
715
- elapsed = (datetime.now() - start_time).total_seconds()
716
- frames_per_sec = processed_count / elapsed if elapsed > 0 else 0
717
- logger.info(f"Processed {processed_count} frames in {elapsed:.1f}s ({frames_per_sec:.1f} fps)")
718
-
719
- # Restore original settings
720
- if quick_mode:
721
- moderator.config['performance']['image_size'] = original_size
722
-
723
- # Release video
724
- cap.release()
725
-
726
- # Clean up uploaded file
727
  try:
728
- upload_path.unlink()
729
  except:
730
  pass
731
 
732
- # Calculate summary
733
- summary = {
734
- "total_frames_analyzed": processed_count,
735
- "frames_with_detections": len(frame_detections),
736
- "total_detections": threat_count,
737
- "weapons": len(all_weapons),
738
- "nsfw": len(all_nsfw),
739
- "fights": len(all_fights)
740
- }
741
-
742
- # Determine risk level
743
- if critical_threat or len(all_weapons) > 5:
744
- risk_level = "critical"
745
- elif len(all_weapons) > 0 or len(all_fights) > 0:
746
- risk_level = "high"
747
- elif len(all_nsfw) > 0:
748
- risk_level = "medium"
749
- else:
750
- risk_level = "safe"
751
-
752
- # Calculate processing time
753
- processing_time = (datetime.now() - start_time).total_seconds() * 1000
754
-
755
- # Optimization info
756
- optimization_used = {
757
- "frame_skip": frame_skip,
758
- "resolution": target_width,
759
- "max_frames": max_frames,
760
- "frames_cached": len(video_optimizer.frame_cache),
761
- "early_stopped": critical_threat and threat_count >= config.VIDEO_EARLY_STOP_THRESHOLD
762
- }
763
-
764
- return VideoDetectionResponse(
765
- success=True,
766
- request_id=request_id,
767
- timestamp=datetime.now().isoformat(),
768
- video_info=video_info,
769
- total_frames_processed=processed_count,
770
- frame_detections=frame_detections[:50], # Limit to 50 detections
771
- summary=summary,
772
- risk_level=risk_level,
773
- action_required=(summary["total_detections"] > 0),
774
- processing_time_ms=processing_time,
775
- optimization_used=optimization_used
776
  )
777
 
778
  except HTTPException:
779
  raise
780
  except Exception as e:
781
- logger.error(f"Error processing video: {e}")
782
- logger.error(traceback.format_exc())
783
- raise HTTPException(status_code=500, detail=str(e))
784
- finally:
785
- # Clear cache after video processing
786
- video_optimizer.clear_cache()
787
-
788
-
789
- @app.delete("/cleanup")
790
- async def cleanup_old_files(hours: int = 24):
791
- """Clean up old files"""
792
- try:
793
- from datetime import timedelta
794
- cutoff_time = datetime.now() - timedelta(hours=hours)
795
-
796
- deleted_count = 0
797
- for directory in [config.UPLOAD_DIR, config.RESULTS_DIR, config.PROCESSED_DIR]:
798
- for subdir in ["images", "videos"]:
799
- path = directory / subdir
800
- if path.exists():
801
- for file in path.iterdir():
802
- if file.is_file():
803
- file_time = datetime.fromtimestamp(file.stat().st_mtime)
804
- if file_time < cutoff_time:
805
- file.unlink()
806
- deleted_count += 1
807
-
808
- return {
809
- "success": True,
810
- "deleted_files": deleted_count,
811
- "message": f"Deleted {deleted_count} files older than {hours} hours"
812
- }
813
- except Exception as e:
814
- logger.error(f"Cleanup error: {e}")
815
- return {"success": False, "error": str(e)}
816
 
817
 
818
  if __name__ == "__main__":
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
 
4
  import cv2
5
  import numpy as np
 
6
  import aiofiles
 
7
  from pathlib import Path
8
  import uuid
 
 
9
  import logging
10
+
11
+ # Import the smart sequential moderator
12
+ from sequential_moderation import SmartSequentialModerator
 
 
13
 
14
  # Setup logging
15
+ logging.basicConfig(level=logging.INFO)
 
 
 
16
  logger = logging.getLogger(__name__)
17
 
18
+ # Initialize FastAPI
19
  app = FastAPI(
20
+ title="Content Detection API",
21
+ description="Simple API for detecting inappropriate content",
22
+ version="2.0.0"
 
 
23
  )
24
 
25
+ # Add CORS
26
  app.add_middleware(
27
  CORSMiddleware,
28
  allow_origins=["*"],
 
31
  allow_headers=["*"],
32
  )
33
 
34
+ # Configuration
35
+ UPLOAD_DIR = Path("uploads")
36
+ UPLOAD_DIR.mkdir(exist_ok=True)
37
+ MAX_IMAGE_SIZE = 50 * 1024 * 1024 # 50MB
38
+ MAX_VIDEO_SIZE = 500 * 1024 * 1024 # 500MB
39
 
40
+ # Global moderator
41
+ moderator = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
+ # ============== Response Model ==============
45
+ class DetectionResponse(BaseModel):
46
+ """Simple response with counts and safety status"""
47
+ nude: int = 0
48
+ gun: int = 0
49
+ knife: int = 0
50
+ fight: int = 0
51
+ is_safe: bool = True
52
 
53
+
54
+ # ============== Startup ==============
55
  @app.on_event("startup")
56
  async def startup_event():
 
57
  global moderator
58
  try:
59
+ logger.info("🚀 Initializing Smart Sequential Moderator...")
60
+ moderator = SmartSequentialModerator()
61
+ logger.info("✅ Ready to process requests")
62
+ logger.info("📋 Pipeline: NSFW (0.75) → Weapons/Fights")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  except Exception as e:
64
+ logger.error(f"Failed to initialize: {e}")
 
 
 
 
 
 
 
 
 
65
  moderator = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
 
68
  # ============== API Endpoints ==============
69
 
70
+ @app.post("/detect/image", response_model=DetectionResponse)
71
+ async def detect_image(file: UploadFile = File(...)):
72
+ """
73
+ Detect inappropriate content in image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ Sequential processing:
76
+ 1. NSFW check (threshold: 0.75)
77
+ 2. If NSFW detected → stop and return
78
+ 3. If clean → check weapons & fights
79
 
80
+ Returns counts and safety status
 
 
 
 
81
  """
 
 
 
 
 
 
 
 
82
 
83
+ if moderator is None:
84
+ raise HTTPException(status_code=503, detail="Service not ready")
85
 
86
  try:
87
+ # Validate extension
88
+ allowed = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp'}
89
+ ext = Path(file.filename).suffix.lower()
 
 
 
90
 
91
+ if ext not in allowed:
92
+ raise HTTPException(400, f"Invalid type. Allowed: {allowed}")
 
93
 
94
+ # Read file
95
+ content = await file.read()
 
 
 
96
 
97
+ # Check size
98
+ if len(content) > MAX_IMAGE_SIZE:
99
+ raise HTTPException(400, f"File too large (max {MAX_IMAGE_SIZE // 1024 // 1024}MB)")
 
100
 
101
  # Decode image
102
+ nparr = np.frombuffer(content, np.uint8)
103
  image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
104
 
105
  if image is None:
106
+ raise HTTPException(400, "Cannot decode image")
107
+
108
+ # Process
109
+ logger.info(f"Processing image: {file.filename}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  result = moderator.process_image(image)
111
 
112
+ # Return
113
+ return DetectionResponse(
114
+ nude=result.nude_count,
115
+ gun=result.gun_count,
116
+ knife=result.knife_count,
117
+ fight=result.fight_count,
118
+ is_safe=result.is_safe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  )
120
 
121
  except HTTPException:
122
  raise
123
  except Exception as e:
124
+ logger.error(f"Error: {e}")
125
+ raise HTTPException(500, str(e))
126
+
127
+
128
+ @app.post("/detect/video", response_model=DetectionResponse)
129
+ async def detect_video(file: UploadFile = File(...)):
 
 
 
 
 
 
130
  """
131
+ Detect inappropriate content in video
132
+
133
+ Features:
134
+ - AUTO frame skipping based on duration
135
+ - Early stop after 3 NSFW detections
136
+ - Sequential processing per frame
137
+
138
+ Returns total counts and safety status
139
  """
140
+
141
  if moderator is None:
142
+ raise HTTPException(status_code=503, detail="Service not ready")
 
 
 
143
 
144
+ video_path = None
 
145
 
146
  try:
147
+ # Validate extension
148
+ allowed = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv'}
149
+ ext = Path(file.filename).suffix.lower()
150
+
151
+ if ext not in allowed:
152
+ raise HTTPException(400, f"Invalid type. Allowed: {allowed}")
153
+
154
+ # Save temporarily
155
+ video_id = f"vid_{uuid.uuid4().hex[:8]}"
156
+ video_path = UPLOAD_DIR / f"{video_id}{ext}"
157
+
158
+ async with aiofiles.open(video_path, 'wb') as f:
159
+ content = await file.read()
160
+ await f.write(content)
161
+
162
+ # Check size
163
+ size = video_path.stat().st_size
164
+ if size > MAX_VIDEO_SIZE:
165
+ video_path.unlink()
166
+ raise HTTPException(400, f"File too large (max {MAX_VIDEO_SIZE // 1024 // 1024}MB)")
167
+
168
+ # Process with auto settings
169
+ logger.info(f"Processing video: {file.filename} ({size // 1024 // 1024}MB)")
170
+ result = moderator.process_video(str(video_path))
171
+
172
+ # Clean up
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  try:
174
+ video_path.unlink()
175
  except:
176
  pass
177
 
178
+ # Return
179
+ return DetectionResponse(
180
+ nude=result['nude'],
181
+ gun=result['gun'],
182
+ knife=result['knife'],
183
+ fight=result['fight'],
184
+ is_safe=result['is_safe']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  )
186
 
187
  except HTTPException:
188
  raise
189
  except Exception as e:
190
+ logger.error(f"Error: {e}")
191
+ # Clean up on error
192
+ if video_path and video_path.exists():
193
+ try:
194
+ video_path.unlink()
195
+ except:
196
+ pass
197
+ raise HTTPException(500, str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
 
200
  if __name__ == "__main__":