webolavo commited on
Commit
799f675
·
verified ·
1 Parent(s): 242e181

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -269
app.py CHANGED
@@ -1,40 +1,39 @@
 
1
  import sys
2
  import types
3
  import importlib.util
 
 
 
 
 
 
 
 
 
 
4
  import os
5
  import time
6
  import uuid
7
  import threading
8
  import subprocess
9
- from io import BytesIO
10
-
11
- import cv2
12
  import torch
 
13
  from PIL import Image
14
- from contextlib import asynccontextmanager
 
 
 
15
  from fastapi import FastAPI, HTTPException, UploadFile, File
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from fastapi.responses import FileResponse, HTMLResponse
18
- from transformers import (
19
- BlipProcessor,
20
- BlipForQuestionAnswering,
21
- AutoProcessor,
22
- AutoModelForCausalLM,
23
- )
24
-
25
- # --- flash_attn mock for CPU environments that don't provide it ---
26
- flash_mock = types.ModuleType("flash_attn")
27
- flash_mock.__version__ = "2.0.0"
28
- flash_mock.__spec__ = importlib.util.spec_from_loader("flash_attn", loader=None)
29
- sys.modules["flash_attn"] = flash_mock
30
- sys.modules["flash_attn.flash_attn_interface"] = types.ModuleType("flash_attn.flash_attn_interface")
31
- sys.modules["flash_attn.bert_padding"] = types.ModuleType("flash_attn.bert_padding")
32
 
33
- # --- Configuration ---
34
- BLIP_MODEL_ID = os.getenv("BLIP_MODEL_ID", "Salesforce/blip-vqa-base")
35
- FLORENCE_MODEL_ID = os.getenv("FLORENCE_MODEL_ID", "microsoft/Florence-2-base-ft")
36
  FRAMES_PER_SECOND = 1
37
- TEMP_DIR = os.getenv("TEMP_DIR", "/tmp/video_filter")
38
  os.makedirs(TEMP_DIR, exist_ok=True)
39
 
40
  BLIP_QUESTIONS = [
@@ -52,83 +51,70 @@ FLORENCE_QUESTION = (
52
  "Answer yes or no only."
53
  )
54
 
55
- MODEL_DATA = {}
56
  MODEL_STATUS = {"status": "loading", "message": "جاري تحميل النماذج..."}
57
 
58
-
59
  def load_models():
60
  try:
61
- print("Loading BLIP...", flush=True)
62
  MODEL_STATUS.update({"status": "loading", "message": "جاري تحميل BLIP..."})
63
  start = time.time()
64
  MODEL_DATA["blip_processor"] = BlipProcessor.from_pretrained(BLIP_MODEL_ID)
65
- MODEL_DATA["blip_model"] = BlipForQuestionAnswering.from_pretrained(
66
- BLIP_MODEL_ID,
67
- torch_dtype=torch.float32,
68
  ).eval()
69
- print(f"BLIP ready in {time.time() - start:.1f}s", flush=True)
70
 
71
- print("Loading Florence-2...", flush=True)
72
  MODEL_STATUS.update({"status": "loading", "message": "جاري تحميل Florence-2..."})
73
  start = time.time()
74
  MODEL_DATA["florence_processor"] = AutoProcessor.from_pretrained(
75
- FLORENCE_MODEL_ID,
76
- trust_remote_code=True,
77
  )
78
  MODEL_DATA["florence_model"] = AutoModelForCausalLM.from_pretrained(
79
  FLORENCE_MODEL_ID,
80
  torch_dtype=torch.float32,
81
  trust_remote_code=True,
82
- attn_implementation="eager",
83
  ).eval()
84
- print(f"Florence-2 ready in {time.time() - start:.1f}s", flush=True)
 
 
85
 
86
- MODEL_STATUS.update({"status": "ready", "message": "النماذج جاهزة"})
87
- print("All models loaded", flush=True)
88
  except Exception as e:
89
  MODEL_STATUS.update({"status": "error", "message": str(e)})
90
- print(f"Error loading models: {e}", flush=True)
91
-
92
 
93
  @asynccontextmanager
94
  async def lifespan(app: FastAPI):
95
  thread = threading.Thread(target=load_models, daemon=True)
96
  thread.start()
97
- print("Server started, models are loading in background", flush=True)
98
  yield
99
  MODEL_DATA.clear()
100
 
101
-
102
  app = FastAPI(
103
  title="Video Female Filter",
104
- description="تحليل الفيديو وإزالة المقاطع غير المرغوبة",
105
  version="1.1.0",
106
- lifespan=lifespan,
107
  )
108
 
109
  app.add_middleware(
110
  CORSMiddleware,
111
  allow_origins=["*"],
112
- allow_credentials=False,
113
  allow_methods=["*"],
114
  allow_headers=["*"],
115
  )
116
 
117
-
118
- def ensure_models_ready():
119
- if MODEL_STATUS["status"] != "ready":
120
- raise HTTPException(
121
- status_code=503,
122
- detail=f"النماذج لم تكتمل بعد: {MODEL_STATUS['message']}",
123
- )
124
-
125
-
126
  def run_blip(image: Image.Image) -> dict:
127
- processor = MODEL_DATA["blip_processor"]
128
- model = MODEL_DATA["blip_model"]
129
  yes_answers = {}
130
- no_answers = {}
131
-
132
  for question in BLIP_QUESTIONS:
133
  inputs = processor(image, question, return_tensors="pt")
134
  with torch.no_grad():
@@ -138,232 +124,140 @@ def run_blip(image: Image.Image) -> dict:
138
  yes_answers[question] = answer
139
  else:
140
  no_answers[question] = answer
141
-
142
  return {"yes": yes_answers, "no": no_answers}
143
 
144
-
145
  def run_florence(image: Image.Image) -> str:
146
  processor = MODEL_DATA["florence_processor"]
147
- model = MODEL_DATA["florence_model"]
148
- task = "<VQA>"
149
- prompt = f"{task}{FLORENCE_QUESTION}"
150
- inputs = processor(text=prompt, images=image, return_tensors="pt")
151
-
152
  with torch.no_grad():
153
  generated_ids = model.generate(
154
  input_ids=inputs["input_ids"],
155
  pixel_values=inputs["pixel_values"],
156
  max_new_tokens=10,
157
- do_sample=False,
158
  )
159
-
160
  generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
161
  parsed = processor.post_process_generation(
162
- generated_text,
163
- task=task,
164
- image_size=(image.width, image.height),
165
  )
166
  return parsed.get(task, "").strip().lower()
167
 
168
-
169
  def is_female_in_frame(image: Image.Image) -> tuple[bool, str]:
170
  blip_result = run_blip(image)
171
- yes_q = blip_result["yes"]
172
-
173
  if "is there a woman in this image?" in yes_q:
174
  return True, "blip_woman"
175
-
176
  if not yes_q:
177
  return False, "blip_clean"
178
-
179
  florence_answer = run_florence(image)
180
  if "yes" in florence_answer:
181
  return True, "florence_confirmed"
182
-
183
  return False, "florence_clean"
184
 
185
-
186
- def run_cmd(command: list[str], fail_message: str):
187
- result = subprocess.run(command, capture_output=True, text=True)
188
- if result.returncode != 0:
189
- stderr = (result.stderr or "").strip()
190
- print(f"{fail_message}: {stderr}", flush=True)
191
- raise RuntimeError(f"{fail_message}: {stderr or 'unknown ffmpeg error'}")
192
-
193
-
194
- def normalize_segments(segments: list[list[float]], duration_sec: float) -> list[list[float]]:
195
- clipped = []
196
- for start, end in segments:
197
- s = max(0.0, min(start, duration_sec))
198
- e = max(0.0, min(end, duration_sec))
199
- if e - s >= 0.05:
200
- clipped.append([s, e])
201
-
202
- if not clipped:
203
- return []
204
-
205
- clipped.sort(key=lambda x: x[0])
206
- merged = [clipped[0]]
207
-
208
- for s, e in clipped[1:]:
209
- last = merged[-1]
210
- if s <= last[1]:
211
- last[1] = max(last[1], e)
212
- else:
213
- merged.append([s, e])
214
-
215
- return merged
216
-
217
-
218
- def build_keep_segments(female_segments: list[list[float]], duration_sec: float) -> list[list[float]]:
219
- keep_segments = []
220
- prev_end = 0.0
221
-
222
- for s, e in female_segments:
223
- if prev_end < s:
224
- keep_segments.append([prev_end, s])
225
- prev_end = e
226
-
227
- if prev_end < duration_sec:
228
- keep_segments.append([prev_end, duration_sec])
229
-
230
- return [seg for seg in keep_segments if seg[1] - seg[0] >= 0.05]
231
-
232
-
233
- def render_clean_video(input_path: str, output_path: str, keep_segments: list[list[float]]):
234
- if not keep_segments:
235
- raise RuntimeError("No clean segments to keep")
236
-
237
- video_parts = []
238
- audio_parts = []
239
-
240
- for i, (start, end) in enumerate(keep_segments):
241
- video_parts.append(
242
- f"[0:v]trim=start={start:.3f}:end={end:.3f},setpts=PTS-STARTPTS[v{i}]"
243
- )
244
- audio_parts.append(
245
- f"[0:a]atrim=start={start:.3f}:end={end:.3f},asetpts=PTS-STARTPTS[a{i}]"
246
- )
247
-
248
- video_concat_inputs = "".join(f"[v{i}]" for i in range(len(keep_segments)))
249
- audio_concat_inputs = "".join(f"[a{i}]" for i in range(len(keep_segments)))
250
-
251
- filter_with_audio = (
252
- ";".join(video_parts + audio_parts)
253
- + f";{video_concat_inputs}concat=n={len(keep_segments)}:v=1:a=0[vout]"
254
- + f";{audio_concat_inputs}concat=n={len(keep_segments)}:v=0:a=1[aout]"
255
- )
256
-
257
- cmd_with_audio = [
258
- "ffmpeg", "-y", "-i", input_path,
259
- "-filter_complex", filter_with_audio,
260
- "-map", "[vout]",
261
- "-map", "[aout]",
262
- "-c:v", "mpeg4", "-q:v", "4",
263
- "-c:a", "aac", "-b:a", "128k",
264
- "-movflags", "+faststart",
265
- output_path,
266
- ]
267
-
268
  try:
269
- run_cmd(cmd_with_audio, "ffmpeg failed while building clean video with audio")
270
- return
271
- except RuntimeError:
272
- pass
273
-
274
- filter_video_only = (
275
- ";".join(video_parts)
276
- + f";{video_concat_inputs}concat=n={len(keep_segments)}:v=1:a=0[vout]"
277
- )
278
-
279
- cmd_video_only = [
280
- "ffmpeg", "-y", "-i", input_path,
281
- "-filter_complex", filter_video_only,
282
- "-map", "[vout]",
283
- "-c:v", "mpeg4", "-q:v", "4",
284
- "-an",
285
- "-movflags", "+faststart",
286
- output_path,
287
- ]
288
-
289
- run_cmd(cmd_video_only, "ffmpeg failed while building clean video without audio")
290
-
291
-
 
 
 
 
292
  @app.get("/", response_class=HTMLResponse)
293
  def root():
294
- index_path = os.path.join(os.path.dirname(__file__), "index.html")
295
- if not os.path.exists(index_path):
296
- return HTMLResponse("<h1>API is running</h1>", status_code=200)
297
- return FileResponse(index_path, media_type="text/html; charset=utf-8")
298
-
299
 
300
  @app.get("/health")
301
  def health():
302
  return {
303
- "status": MODEL_STATUS["status"],
304
- "message": MODEL_STATUS["message"],
305
- "blip_loaded": "blip_model" in MODEL_DATA,
306
- "florence_loaded": "florence_model" in MODEL_DATA,
307
  }
308
 
309
-
310
- @app.post("/analyze-file")
311
- async def analyze_file(file: UploadFile = File(...)):
312
- ensure_models_ready()
 
 
313
 
314
  if not file.content_type or not file.content_type.startswith("image/"):
315
  raise HTTPException(status_code=400, detail="الملف ليس صورة")
316
 
317
  try:
318
- image_bytes = await file.read()
319
- image = Image.open(BytesIO(image_bytes)).convert("RGB")
320
- has_female, reason = is_female_in_frame(image)
321
-
322
- return {
323
- "has_female": has_female,
324
- "decision": "BLOCK" if has_female else "ALLOW",
325
- "reason": reason,
326
- "status": "success",
327
- }
328
  except Exception as e:
329
- raise HTTPException(status_code=500, detail=str(e))
330
 
 
 
 
 
 
 
 
331
 
 
332
  @app.post("/analyze-video")
333
  async def analyze_video(file: UploadFile = File(...)):
334
- ensure_models_ready()
 
 
335
 
336
  if not file.content_type or not file.content_type.startswith("video/"):
337
  raise HTTPException(status_code=400, detail="الملف ليس فيديو")
338
 
339
- job_id = str(uuid.uuid4())[:8]
340
- input_path = f"{TEMP_DIR}/{job_id}_input.mp4"
341
  output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
342
 
 
343
  with open(input_path, "wb") as f:
344
  f.write(await file.read())
345
 
346
- cap = None
347
  try:
348
- cap = cv2.VideoCapture(input_path)
349
- if not cap.isOpened():
350
- raise HTTPException(status_code=400, detail="تعذر قراءة ملف الفيديو")
351
-
352
- fps = cap.get(cv2.CAP_PROP_FPS) or 25
353
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
354
- if total_frames <= 0:
355
- raise HTTPException(status_code=400, detail="الفيديو فارغ أو غير مدعوم")
356
 
357
- duration_sec = total_frames / fps
358
- print(f"Video info: frames={total_frames}, fps={fps:.2f}, duration={duration_sec:.2f}s", flush=True)
359
 
360
- frame_interval = max(1, int(fps / FRAMES_PER_SECOND))
361
  female_segments = []
362
- analysis_log = []
363
- in_female_seg = False
364
- seg_start = 0.0
365
- frame_idx = 0
366
- start_time = time.time()
367
 
368
  while True:
369
  ret, frame = cap.read()
@@ -372,100 +266,148 @@ async def analyze_video(file: UploadFile = File(...)):
372
 
373
  if frame_idx % frame_interval == 0:
374
  current_sec = frame_idx / fps
375
- pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
376
  has_female, reason = is_female_in_frame(pil_image)
377
 
378
  analysis_log.append({
379
- "second": round(current_sec, 2),
380
  "has_female": has_female,
381
- "reason": reason,
382
  })
 
383
 
384
  if has_female and not in_female_seg:
385
  in_female_seg = True
386
- seg_start = max(0.0, current_sec - 0.5)
387
  elif not has_female and in_female_seg:
388
  in_female_seg = False
389
- female_segments.append([seg_start, min(duration_sec, current_sec + 0.5)])
390
 
391
  frame_idx += 1
392
 
393
  if in_female_seg:
394
  female_segments.append([seg_start, duration_sec])
395
 
 
396
  elapsed_analysis = round(time.time() - start_time, 2)
397
- female_segments = normalize_segments(female_segments, duration_sec)
398
 
 
399
  if not female_segments:
 
400
  return {
401
- "has_female": False,
402
- "female_segments": [],
403
- "analysis_log": analysis_log,
404
- "message": "الفيديو نظيف ولا يحتوي على ظهور نساء",
405
- "analysis_time": elapsed_analysis,
 
 
 
406
  "output_available": False,
407
- "status": "success",
408
  }
409
 
410
- keep_segments = build_keep_segments(female_segments, duration_sec)
 
 
 
 
 
 
 
 
411
 
412
  if not keep_segments:
 
413
  return {
414
- "has_female": True,
415
- "female_segments": female_segments,
416
- "analysis_log": analysis_log,
417
- "message": "الفيديو بالكامل يحتوي على ظهور نساء",
418
- "analysis_time": elapsed_analysis,
419
- "output_available": False,
420
- "status": "success",
 
 
 
421
  }
422
 
423
- render_clean_video(input_path, output_path, keep_segments)
424
-
425
- if not os.path.exists(output_path):
426
- raise RuntimeError("تعذر إنشاء ملف الفيديو النهائي")
427
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  total_removed = sum(e - s for s, e in female_segments)
429
 
 
 
 
430
  return {
431
- "has_female": True,
432
- "female_segments": female_segments,
433
- "kept_segments": keep_segments,
434
  "total_removed_sec": round(total_removed, 2),
435
- "analysis_log": analysis_log,
436
- "analysis_time": elapsed_analysis,
437
- "output_available": True,
438
- "output_job_id": job_id,
439
- "download_url": f"/download/{job_id}",
440
- "message": f"تم حذف {round(total_removed, 1)} ثانية من الفيديو",
441
- "status": "success",
 
442
  }
443
 
444
  except HTTPException:
445
  raise
446
  except Exception as e:
 
447
  raise HTTPException(status_code=500, detail=str(e))
448
- finally:
449
- if cap is not None:
450
- cap.release()
451
- if os.path.exists(input_path):
452
- os.remove(input_path)
453
-
454
 
 
455
  @app.get("/download/{job_id}")
456
  def download_video(job_id: str):
 
 
 
457
  output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
458
  if not os.path.exists(output_path):
459
- raise HTTPException(status_code=404, detail="الفيديو غير موجود")
460
-
461
  return FileResponse(
462
  output_path,
463
  media_type="video/mp4",
464
- filename="clean_video.mp4",
465
  )
466
 
467
 
468
  if __name__ == "__main__":
469
  import uvicorn
470
-
471
- uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT", "7860")))
 
1
+ # ─── flash_attn Mock ─────────────────────────────────────────────
2
  import sys
3
  import types
4
  import importlib.util
5
+
6
+ flash_mock = types.ModuleType("flash_attn")
7
+ flash_mock.__version__ = "2.0.0"
8
+ flash_mock.__spec__ = importlib.util.spec_from_loader("flash_attn", loader=None)
9
+ sys.modules["flash_attn"] = flash_mock
10
+ sys.modules["flash_attn.flash_attn_interface"] = types.ModuleType("flash_attn.flash_attn_interface")
11
+ sys.modules["flash_attn.bert_padding"] = types.ModuleType("flash_attn.bert_padding")
12
+ # ─────────────────────────────────────────────────────────────────
13
+
14
+ import io
15
  import os
16
  import time
17
  import uuid
18
  import threading
19
  import subprocess
 
 
 
20
  import torch
21
+ import cv2
22
  from PIL import Image
23
+ from transformers import (
24
+ BlipProcessor, BlipForQuestionAnswering,
25
+ AutoProcessor, AutoModelForCausalLM
26
+ )
27
  from fastapi import FastAPI, HTTPException, UploadFile, File
28
  from fastapi.middleware.cors import CORSMiddleware
29
  from fastapi.responses import FileResponse, HTMLResponse
30
+ from contextlib import asynccontextmanager
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ # ─── إعدادات ─────────────────────────────────────────────────────
33
+ BLIP_MODEL_ID = "Salesforce/blip-vqa-base"
34
+ FLORENCE_MODEL_ID = "microsoft/Florence-2-large-ft"
35
  FRAMES_PER_SECOND = 1
36
+ TEMP_DIR = "/tmp/video_filter"
37
  os.makedirs(TEMP_DIR, exist_ok=True)
38
 
39
  BLIP_QUESTIONS = [
 
51
  "Answer yes or no only."
52
  )
53
 
54
+ MODEL_DATA = {}
55
  MODEL_STATUS = {"status": "loading", "message": "جاري تحميل النماذج..."}
56
 
57
+ # ─── تحميل النماذج في background ─────────────────────────────────
58
  def load_models():
59
  try:
60
+ print("📥 Loading BLIP...", flush=True)
61
  MODEL_STATUS.update({"status": "loading", "message": "جاري تحميل BLIP..."})
62
  start = time.time()
63
  MODEL_DATA["blip_processor"] = BlipProcessor.from_pretrained(BLIP_MODEL_ID)
64
+ MODEL_DATA["blip_model"] = BlipForQuestionAnswering.from_pretrained(
65
+ BLIP_MODEL_ID, torch_dtype=torch.float32
 
66
  ).eval()
67
+ print(f"BLIP ready in {time.time()-start:.1f}s", flush=True)
68
 
69
+ print("📥 Loading Florence-2...", flush=True)
70
  MODEL_STATUS.update({"status": "loading", "message": "جاري تحميل Florence-2..."})
71
  start = time.time()
72
  MODEL_DATA["florence_processor"] = AutoProcessor.from_pretrained(
73
+ FLORENCE_MODEL_ID, trust_remote_code=True
 
74
  )
75
  MODEL_DATA["florence_model"] = AutoModelForCausalLM.from_pretrained(
76
  FLORENCE_MODEL_ID,
77
  torch_dtype=torch.float32,
78
  trust_remote_code=True,
79
+ attn_implementation="eager"
80
  ).eval()
81
+ print(f"Florence-2 ready in {time.time()-start:.1f}s", flush=True)
82
+ MODEL_STATUS.update({"status": "ready", "message": "النماذج جاهزة ✅"})
83
+ print("🎉 All models loaded!", flush=True)
84
 
 
 
85
  except Exception as e:
86
  MODEL_STATUS.update({"status": "error", "message": str(e)})
87
+ print(f" Error: {e}", flush=True)
 
88
 
89
  @asynccontextmanager
90
  async def lifespan(app: FastAPI):
91
  thread = threading.Thread(target=load_models, daemon=True)
92
  thread.start()
93
+ print("🚀 Server started! Models loading in background...", flush=True)
94
  yield
95
  MODEL_DATA.clear()
96
 
 
97
  app = FastAPI(
98
  title="Video Female Filter",
99
+ description="BLIP + Florence-2 | Video Analysis",
100
  version="1.1.0",
101
+ lifespan=lifespan
102
  )
103
 
104
  app.add_middleware(
105
  CORSMiddleware,
106
  allow_origins=["*"],
107
+ allow_credentials=True,
108
  allow_methods=["*"],
109
  allow_headers=["*"],
110
  )
111
 
112
+ # ─── دوال النماذج ─────────────────────────────────────────────────
 
 
 
 
 
 
 
 
113
  def run_blip(image: Image.Image) -> dict:
114
+ processor = MODEL_DATA["blip_processor"]
115
+ model = MODEL_DATA["blip_model"]
116
  yes_answers = {}
117
+ no_answers = {}
 
118
  for question in BLIP_QUESTIONS:
119
  inputs = processor(image, question, return_tensors="pt")
120
  with torch.no_grad():
 
124
  yes_answers[question] = answer
125
  else:
126
  no_answers[question] = answer
 
127
  return {"yes": yes_answers, "no": no_answers}
128
 
 
129
  def run_florence(image: Image.Image) -> str:
130
  processor = MODEL_DATA["florence_processor"]
131
+ model = MODEL_DATA["florence_model"]
132
+ task = "<VQA>"
133
+ prompt = f"{task}{FLORENCE_QUESTION}"
134
+ inputs = processor(text=prompt, images=image, return_tensors="pt")
 
135
  with torch.no_grad():
136
  generated_ids = model.generate(
137
  input_ids=inputs["input_ids"],
138
  pixel_values=inputs["pixel_values"],
139
  max_new_tokens=10,
140
+ do_sample=False
141
  )
 
142
  generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
143
  parsed = processor.post_process_generation(
144
+ generated_text, task=task,
145
+ image_size=(image.width, image.height)
 
146
  )
147
  return parsed.get(task, "").strip().lower()
148
 
 
149
  def is_female_in_frame(image: Image.Image) -> tuple[bool, str]:
150
  blip_result = run_blip(image)
151
+ yes_q = blip_result["yes"]
 
152
  if "is there a woman in this image?" in yes_q:
153
  return True, "blip_woman"
 
154
  if not yes_q:
155
  return False, "blip_clean"
 
156
  florence_answer = run_florence(image)
157
  if "yes" in florence_answer:
158
  return True, "florence_confirmed"
 
159
  return False, "florence_clean"
160
 
161
+ def run_ffmpeg(cmd: list) -> bool:
162
+ """تشغيل ffmpeg مع التحقق من النجاح"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  try:
164
+ result = subprocess.run(cmd, capture_output=True, text=True)
165
+ if result.returncode != 0:
166
+ print(f"⚠️ ffmpeg error: {result.stderr[:200]}", flush=True)
167
+ return False
168
+ return True
169
+ except Exception as e:
170
+ print(f"❌ ffmpeg exception: {e}", flush=True)
171
+ return False
172
+
173
+ def cleanup_temp_files(job_id: str):
174
+ """حذف الملفات المؤقتة بعد الانتهاء"""
175
+ patterns = ["_input.mp4", "_list.txt"]
176
+ for p in patterns:
177
+ f = f"{TEMP_DIR}/{job_id}{p}"
178
+ if os.path.exists(f):
179
+ try: os.remove(f)
180
+ except: pass
181
+ # حذف ملفات الـ segments
182
+ i = 0
183
+ while True:
184
+ seg = f"{TEMP_DIR}/{job_id}_seg_{i}.mp4"
185
+ if not os.path.exists(seg): break
186
+ try: os.remove(seg)
187
+ except: pass
188
+ i += 1
189
+
190
+ # ─── Endpoints ────────────────────────────────────────────────────
191
  @app.get("/", response_class=HTMLResponse)
192
  def root():
193
+ with open("index.html", "r", encoding="utf-8") as f:
194
+ return f.read()
 
 
 
195
 
196
  @app.get("/health")
197
  def health():
198
  return {
199
+ "status": MODEL_STATUS["status"],
200
+ "message": MODEL_STATUS["message"],
201
+ "blip_loaded": "blip_model" in MODEL_DATA,
202
+ "florence_loaded": "florence_model" in MODEL_DATA
203
  }
204
 
205
+ # ─── فحص سريع لصورة واحدة (frame) ───────────────────────────────
206
+ @app.post("/analyze-frame")
207
+ async def analyze_frame(file: UploadFile = File(...)):
208
+ """يستخدمه الفحص السريع في الـ frontend"""
209
+ if MODEL_STATUS["status"] != "ready":
210
+ raise HTTPException(status_code=503, detail=MODEL_STATUS["message"])
211
 
212
  if not file.content_type or not file.content_type.startswith("image/"):
213
  raise HTTPException(status_code=400, detail="الملف ليس صورة")
214
 
215
  try:
216
+ image = Image.open(io.BytesIO(await file.read())).convert("RGB")
 
 
 
 
 
 
 
 
 
217
  except Exception as e:
218
+ raise HTTPException(status_code=400, detail=str(e))
219
 
220
+ has_female, reason = is_female_in_frame(image)
221
+ return {
222
+ "decision": "BLOCK" if has_female else "ALLOW",
223
+ "has_female": has_female,
224
+ "reason": reason,
225
+ "status": "success"
226
+ }
227
 
228
+ # ─── تحليل الفيديو الكامل ─────────────────────────────────────────
229
  @app.post("/analyze-video")
230
  async def analyze_video(file: UploadFile = File(...)):
231
+
232
+ if MODEL_STATUS["status"] != "ready":
233
+ raise HTTPException(status_code=503, detail=MODEL_STATUS["message"])
234
 
235
  if not file.content_type or not file.content_type.startswith("video/"):
236
  raise HTTPException(status_code=400, detail="الملف ليس فيديو")
237
 
238
+ job_id = str(uuid.uuid4())[:8]
239
+ input_path = f"{TEMP_DIR}/{job_id}_input.mp4"
240
  output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
241
 
242
+ # حفظ الفيديو
243
  with open(input_path, "wb") as f:
244
  f.write(await file.read())
245
 
 
246
  try:
247
+ cap = cv2.VideoCapture(input_path)
248
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
 
 
 
249
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
250
+ duration_sec = total_frames / fps if fps > 0 else 0
 
251
 
252
+ print(f"📹 {total_frames} frames, {fps:.1f}fps, {duration_sec:.1f}s", flush=True)
 
253
 
254
+ frame_interval = max(1, int(fps / FRAMES_PER_SECOND))
255
  female_segments = []
256
+ analysis_log = []
257
+ in_female_seg = False
258
+ seg_start = 0.0
259
+ frame_idx = 0
260
+ start_time = time.time()
261
 
262
  while True:
263
  ret, frame = cap.read()
 
266
 
267
  if frame_idx % frame_interval == 0:
268
  current_sec = frame_idx / fps
269
+ pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
270
  has_female, reason = is_female_in_frame(pil_image)
271
 
272
  analysis_log.append({
273
+ "second": round(current_sec, 2),
274
  "has_female": has_female,
275
+ "reason": reason
276
  })
277
+ print(f" ⏱ {current_sec:.1f}s → {'🔴' if has_female else '🟢'} ({reason})", flush=True)
278
 
279
  if has_female and not in_female_seg:
280
  in_female_seg = True
281
+ seg_start = max(0.0, current_sec - 0.5)
282
  elif not has_female and in_female_seg:
283
  in_female_seg = False
284
+ female_segments.append([seg_start, min(current_sec + 0.5, duration_sec)])
285
 
286
  frame_idx += 1
287
 
288
  if in_female_seg:
289
  female_segments.append([seg_start, duration_sec])
290
 
291
+ cap.release()
292
  elapsed_analysis = round(time.time() - start_time, 2)
 
293
 
294
+ # ─── لا يوجد نساء ─────────────────────────────────────────
295
  if not female_segments:
296
+ cleanup_temp_files(job_id)
297
  return {
298
+ "has_female": False,
299
+ "female_segments": [],
300
+ "kept_segments": [[0.0, duration_sec]],
301
+ "total_removed_sec": 0,
302
+ "duration_sec": round(duration_sec, 2),
303
+ "analysis_log": analysis_log,
304
+ "message": "✅ الفيديو نظيف لا يحتوي على نساء",
305
+ "analysis_time": elapsed_analysis,
306
  "output_available": False,
307
+ "status": "success"
308
  }
309
 
310
+ # ─── بناء المقاطع النظيفة ─────────────────────────────────
311
+ keep_segments = []
312
+ prev_end = 0.0
313
+ for s, e in female_segments:
314
+ if prev_end < s - 0.1: # تجاهل فجوات أقل من 0.1s
315
+ keep_segments.append([round(prev_end, 3), round(s, 3)])
316
+ prev_end = e
317
+ if prev_end < duration_sec - 0.1:
318
+ keep_segments.append([round(prev_end, 3), round(duration_sec, 3)])
319
 
320
  if not keep_segments:
321
+ cleanup_temp_files(job_id)
322
  return {
323
+ "has_female": True,
324
+ "female_segments": female_segments,
325
+ "kept_segments": [],
326
+ "total_removed_sec": round(duration_sec, 2),
327
+ "duration_sec": round(duration_sec, 2),
328
+ "analysis_log": analysis_log,
329
+ "message": "⚠️ الفيديو كله يحتوي على نساء",
330
+ "analysis_time": elapsed_analysis,
331
+ "output_available": False,
332
+ "status": "success"
333
  }
334
 
335
+ # ─── قطع بـ ffmpeg ────────────────────────────────────────
336
+ segment_files = []
337
+ for i, (s, e) in enumerate(keep_segments):
338
+ seg_file = f"{TEMP_DIR}/{job_id}_seg_{i}.mp4"
339
+ ok = run_ffmpeg([
340
+ "ffmpeg", "-y",
341
+ "-i", input_path,
342
+ "-ss", str(s),
343
+ "-to", str(e),
344
+ "-c", "copy",
345
+ seg_file
346
+ ])
347
+ if ok and os.path.exists(seg_file) and os.path.getsize(seg_file) > 0:
348
+ segment_files.append(seg_file)
349
+
350
+ if not segment_files:
351
+ raise HTTPException(status_code=500, detail="فشل في إنشاء مقاطع الفيديو النظيفة")
352
+
353
+ # ─── دمج الـ segments ─────────────────────────────────────
354
+ list_file = f"{TEMP_DIR}/{job_id}_list.txt"
355
+ with open(list_file, "w") as f:
356
+ for seg in segment_files:
357
+ f.write(f"file '{seg}'\n")
358
+
359
+ ok = run_ffmpeg([
360
+ "ffmpeg", "-y",
361
+ "-f", "concat",
362
+ "-safe", "0",
363
+ "-i", list_file,
364
+ "-c", "copy",
365
+ output_path
366
+ ])
367
+
368
+ output_exists = ok and os.path.exists(output_path) and os.path.getsize(output_path) > 0
369
  total_removed = sum(e - s for s, e in female_segments)
370
 
371
+ # تنظيف الملفات المؤقتة (نبقي الـ output فقط)
372
+ cleanup_temp_files(job_id)
373
+
374
  return {
375
+ "has_female": True,
376
+ "female_segments": female_segments,
377
+ "kept_segments": keep_segments,
378
  "total_removed_sec": round(total_removed, 2),
379
+ "duration_sec": round(duration_sec, 2),
380
+ "analysis_log": analysis_log,
381
+ "analysis_time": elapsed_analysis,
382
+ "output_available": output_exists,
383
+ "output_job_id": job_id if output_exists else None,
384
+ "download_url": f"/download/{job_id}" if output_exists else None,
385
+ "message": f"✅ تم حذف {round(total_removed, 1)} ثانية من الفيديو",
386
+ "status": "success"
387
  }
388
 
389
  except HTTPException:
390
  raise
391
  except Exception as e:
392
+ print(f"❌ Error: {e}", flush=True)
393
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
394
 
395
+ # ─── تحميل الفيديو النظيف ─────────────────────────────────────────
396
  @app.get("/download/{job_id}")
397
  def download_video(job_id: str):
398
+ # تحقق من صحة الـ job_id
399
+ if not job_id.replace("-", "").isalnum():
400
+ raise HTTPException(status_code=400, detail="job_id غير صالح")
401
  output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
402
  if not os.path.exists(output_path):
403
+ raise HTTPException(status_code=404, detail="الفيديو غير موجود أو انتهت صلاحيته")
 
404
  return FileResponse(
405
  output_path,
406
  media_type="video/mp4",
407
+ filename="clean_video.mp4"
408
  )
409
 
410
 
411
  if __name__ == "__main__":
412
  import uvicorn
413
+ uvicorn.run(app, host="0.0.0.0", port=7860)