Haiss123 commited on
Commit
fe17941
·
verified ·
1 Parent(s): 461cfb8

Update detection_api.py

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