webolavo commited on
Commit
0cfe6dd
ยท
verified ยท
1 Parent(s): 0d1652e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +317 -225
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # โ”€โ”€โ”€ flash_attn Mock โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
2
  import sys
3
  import types
4
  import importlib.util
@@ -9,7 +9,7 @@ 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
@@ -17,23 +17,27 @@ 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,79 +55,77 @@ FLORENCE_QUESTION = (
51
  "Answer yes or no only."
52
  )
53
 
54
- # โ”€โ”€โ”€ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ู„ู€ HuggingFace โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
55
- HF_TOKEN = os.environ.get("HF_TOKEN", None)
56
- if HF_TOKEN:
57
- from huggingface_hub import login
58
- login(token=HF_TOKEN)
59
- print("โœ… HuggingFace login successful", flush=True)
60
- else:
61
- print("โš ๏ธ HF_TOKEN not set - may hit rate limits", flush=True)
62
-
63
- MODEL_DATA = {}
64
  MODEL_STATUS = {"status": "loading", "message": "ุฌุงุฑูŠ ุชุญู…ูŠู„ ุงู„ู†ู…ุงุฐุฌ..."}
 
 
65
 
66
- # โ”€โ”€โ”€ ุชุญู…ูŠู„ ุงู„ู†ู…ุงุฐุฌ ููŠ background โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
67
- def load_models():
68
  try:
69
- print("๐Ÿ“ฅ Loading BLIP...", flush=True)
70
  MODEL_STATUS.update({"status": "loading", "message": "ุฌุงุฑูŠ ุชุญู…ูŠู„ BLIP..."})
71
  start = time.time()
72
  MODEL_DATA["blip_processor"] = BlipProcessor.from_pretrained(BLIP_MODEL_ID)
73
- MODEL_DATA["blip_model"] = BlipForQuestionAnswering.from_pretrained(
74
- BLIP_MODEL_ID, torch_dtype=torch.float32
 
75
  ).eval()
76
- print(f"โœ… BLIP ready in {time.time()-start:.1f}s", flush=True)
77
 
78
- print("๐Ÿ“ฅ Loading Florence-2...", flush=True)
79
  MODEL_STATUS.update({"status": "loading", "message": "ุฌุงุฑูŠ ุชุญู…ูŠู„ Florence-2..."})
80
  start = time.time()
81
  MODEL_DATA["florence_processor"] = AutoProcessor.from_pretrained(
82
- FLORENCE_MODEL_ID, trust_remote_code=True
 
83
  )
84
  MODEL_DATA["florence_model"] = AutoModelForCausalLM.from_pretrained(
85
  FLORENCE_MODEL_ID,
86
  torch_dtype=torch.float32,
87
  trust_remote_code=True,
88
- attn_implementation="eager"
89
  ).eval()
90
- print(f"โœ… Florence-2 ready in {time.time()-start:.1f}s", flush=True)
91
- MODEL_STATUS.update({"status": "ready", "message": "ุงู„ู†ู…ุงุฐุฌ ุฌุงู‡ุฒุฉ โœ…"})
92
- print("๐ŸŽ‰ All models loaded!", flush=True)
93
 
 
 
94
  except Exception as e:
95
  MODEL_STATUS.update({"status": "error", "message": str(e)})
96
- print(f"โŒ Error: {e}", flush=True)
 
97
 
98
  @asynccontextmanager
99
  async def lifespan(app: FastAPI):
100
  thread = threading.Thread(target=load_models, daemon=True)
101
  thread.start()
102
- print("๐Ÿš€ Server started! Models loading in background...", flush=True)
103
  yield
104
  MODEL_DATA.clear()
 
 
105
 
106
  app = FastAPI(
107
  title="Video Female Filter",
108
- description="BLIP + Florence-2 | Video Analysis",
109
- version="1.1.0",
110
- lifespan=lifespan
111
  )
112
 
113
  app.add_middleware(
114
  CORSMiddleware,
115
  allow_origins=["*"],
116
- allow_credentials=True,
117
  allow_methods=["*"],
118
  allow_headers=["*"],
119
  )
120
 
121
- # โ”€โ”€โ”€ ุฏูˆุงู„ ุงู„ู†ู…ุงุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
122
  def run_blip(image: Image.Image) -> dict:
123
- processor = MODEL_DATA["blip_processor"]
124
- model = MODEL_DATA["blip_model"]
125
  yes_answers = {}
126
- no_answers = {}
 
127
  for question in BLIP_QUESTIONS:
128
  inputs = processor(image, question, return_tensors="pt")
129
  with torch.no_grad():
@@ -133,290 +135,380 @@ def run_blip(image: Image.Image) -> dict:
133
  yes_answers[question] = answer
134
  else:
135
  no_answers[question] = answer
 
136
  return {"yes": yes_answers, "no": no_answers}
137
 
 
138
  def run_florence(image: Image.Image) -> str:
139
  processor = MODEL_DATA["florence_processor"]
140
- model = MODEL_DATA["florence_model"]
141
- task = "<VQA>"
142
- prompt = f"{task}{FLORENCE_QUESTION}"
143
- inputs = processor(text=prompt, images=image, return_tensors="pt")
144
  with torch.no_grad():
145
  generated_ids = model.generate(
146
  input_ids=inputs["input_ids"],
147
  pixel_values=inputs["pixel_values"],
148
  max_new_tokens=10,
149
- do_sample=False
150
  )
151
  generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
152
  parsed = processor.post_process_generation(
153
- generated_text, task=task,
154
- image_size=(image.width, image.height)
 
155
  )
156
  return parsed.get(task, "").strip().lower()
157
 
 
158
  def is_female_in_frame(image: Image.Image) -> tuple[bool, str]:
159
  blip_result = run_blip(image)
160
- yes_q = blip_result["yes"]
 
161
  if "is there a woman in this image?" in yes_q:
162
  return True, "blip_woman"
 
163
  if not yes_q:
164
  return False, "blip_clean"
 
165
  florence_answer = run_florence(image)
166
  if "yes" in florence_answer:
167
  return True, "florence_confirmed"
168
  return False, "florence_clean"
169
 
170
- def run_ffmpeg(cmd: list) -> bool:
171
- """ุชุดุบูŠู„ ffmpeg ู…ุน ุงู„ุชุญู‚ู‚ ู…ู† ุงู„ู†ุฌุงุญ"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  try:
173
- result = subprocess.run(cmd, capture_output=True, text=True)
174
- if result.returncode != 0:
175
- print(f"โš ๏ธ ffmpeg error: {result.stderr[:200]}", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  return False
177
- return True
178
- except Exception as e:
179
- print(f"โŒ ffmpeg exception: {e}", flush=True)
180
- return False
181
-
182
- def cleanup_temp_files(job_id: str):
183
- """ุญุฐู ุงู„ู…ู„ูุงุช ุงู„ู…ุคู‚ุชุฉ ุจุนุฏ ุงู„ุงู†ุชู‡ุงุก"""
184
- patterns = ["_input.mp4", "_list.txt"]
185
- for p in patterns:
186
- f = f"{TEMP_DIR}/{job_id}{p}"
187
- if os.path.exists(f):
188
- try: os.remove(f)
189
- except: pass
190
- # ุญุฐู ู…ู„ูุงุช ุงู„ู€ segments
191
- i = 0
192
- while True:
193
- seg = f"{TEMP_DIR}/{job_id}_seg_{i}.mp4"
194
- if not os.path.exists(seg): break
195
- try: os.remove(seg)
196
- except: pass
197
- i += 1
198
-
199
- # โ”€โ”€โ”€ Endpoints โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  @app.get("/", response_class=HTMLResponse)
201
  def root():
202
  with open("index.html", "r", encoding="utf-8") as f:
203
  return f.read()
204
 
 
205
  @app.get("/health")
206
  def health():
207
  return {
208
- "status": MODEL_STATUS["status"],
209
- "message": MODEL_STATUS["message"],
210
- "blip_loaded": "blip_model" in MODEL_DATA,
211
- "florence_loaded": "florence_model" in MODEL_DATA
212
  }
213
 
214
- # โ”€โ”€โ”€ ูุญุต ุณุฑูŠุน ู„ุตูˆุฑุฉ ูˆุงุญุฏุฉ (frame) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
215
- @app.post("/analyze-frame")
216
- async def analyze_frame(file: UploadFile = File(...)):
217
- """ูŠุณุชุฎุฏู…ู‡ ุงู„ูุญุต ุงู„ุณุฑูŠุน ููŠ ุงู„ู€ frontend"""
218
  if MODEL_STATUS["status"] != "ready":
219
- raise HTTPException(status_code=503, detail=MODEL_STATUS["message"])
 
 
 
220
 
221
  if not file.content_type or not file.content_type.startswith("image/"):
222
  raise HTTPException(status_code=400, detail="ุงู„ู…ู„ู ู„ูŠุณ ุตูˆุฑุฉ")
223
 
224
  try:
225
- image = Image.open(io.BytesIO(await file.read())).convert("RGB")
 
 
 
 
 
 
 
 
226
  except Exception as e:
227
- raise HTTPException(status_code=400, detail=str(e))
228
 
229
- has_female, reason = is_female_in_frame(image)
230
- return {
231
- "decision": "BLOCK" if has_female else "ALLOW",
232
- "has_female": has_female,
233
- "reason": reason,
234
- "status": "success"
235
- }
236
 
237
- # โ”€โ”€โ”€ ุชุญู„ูŠู„ ุงู„ููŠุฏูŠูˆ ุงู„ูƒุงู…ู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
238
  @app.post("/analyze-video")
239
  async def analyze_video(file: UploadFile = File(...)):
240
-
241
  if MODEL_STATUS["status"] != "ready":
242
- raise HTTPException(status_code=503, detail=MODEL_STATUS["message"])
 
 
 
243
 
244
  if not file.content_type or not file.content_type.startswith("video/"):
245
  raise HTTPException(status_code=400, detail="ุงู„ู…ู„ู ู„ูŠุณ ููŠุฏูŠูˆ")
246
 
247
- job_id = str(uuid.uuid4())[:8]
248
- input_path = f"{TEMP_DIR}/{job_id}_input.mp4"
249
  output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
250
 
251
- # ุญูุธ ุงู„ููŠุฏูŠูˆ
252
  with open(input_path, "wb") as f:
253
- f.write(await file.read())
254
-
255
- try:
256
- cap = cv2.VideoCapture(input_path)
257
- fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
258
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
259
- duration_sec = total_frames / fps if fps > 0 else 0
260
-
261
- print(f"๐Ÿ“น {total_frames} frames, {fps:.1f}fps, {duration_sec:.1f}s", flush=True)
262
-
263
- frame_interval = max(1, int(fps / FRAMES_PER_SECOND))
264
- female_segments = []
265
- analysis_log = []
266
- in_female_seg = False
267
- seg_start = 0.0
268
- frame_idx = 0
269
- start_time = time.time()
270
-
271
  while True:
272
- ret, frame = cap.read()
273
- if not ret:
274
  break
 
275
 
276
- if frame_idx % frame_interval == 0:
277
- current_sec = frame_idx / fps
278
- pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
279
- has_female, reason = is_female_in_frame(pil_image)
280
 
281
- analysis_log.append({
282
- "second": round(current_sec, 2),
283
- "has_female": has_female,
284
- "reason": reason
285
- })
286
- print(f" โฑ {current_sec:.1f}s โ†’ {'๐Ÿ”ด' if has_female else '๐ŸŸข'} ({reason})", flush=True)
287
 
288
- if has_female and not in_female_seg:
289
- in_female_seg = True
290
- seg_start = max(0.0, current_sec - 0.5)
291
- elif not has_female and in_female_seg:
292
- in_female_seg = False
293
- female_segments.append([seg_start, min(current_sec + 0.5, duration_sec)])
294
 
295
- frame_idx += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
  if in_female_seg:
298
  female_segments.append([seg_start, duration_sec])
 
299
 
300
- cap.release()
301
  elapsed_analysis = round(time.time() - start_time, 2)
302
 
303
- # โ”€โ”€โ”€ ู„ุง ูŠูˆุฌุฏ ู†ุณุงุก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
304
  if not female_segments:
305
- cleanup_temp_files(job_id)
306
  return {
307
- "has_female": False,
308
- "female_segments": [],
309
- "kept_segments": [[0.0, duration_sec]],
310
- "total_removed_sec": 0,
311
- "duration_sec": round(duration_sec, 2),
312
- "analysis_log": analysis_log,
313
- "message": "โœ… ุงู„ููŠุฏูŠูˆ ู†ุธูŠู ู„ุง ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก",
314
- "analysis_time": elapsed_analysis,
315
  "output_available": False,
316
- "status": "success"
317
  }
318
 
319
- # โ”€โ”€โ”€ ุจู†ุงุก ุงู„ู…ู‚ุงุทุน ุงู„ู†ุธูŠูุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
320
  keep_segments = []
321
- prev_end = 0.0
322
  for s, e in female_segments:
323
- if prev_end < s - 0.1: # ุชุฌุงู‡ู„ ูุฌูˆุงุช ุฃู‚ู„ ู…ู† 0.1s
324
- keep_segments.append([round(prev_end, 3), round(s, 3)])
325
  prev_end = e
326
- if prev_end < duration_sec - 0.1:
327
- keep_segments.append([round(prev_end, 3), round(duration_sec, 3)])
328
 
329
  if not keep_segments:
330
- cleanup_temp_files(job_id)
331
  return {
332
- "has_female": True,
333
- "female_segments": female_segments,
334
- "kept_segments": [],
335
- "total_removed_sec": round(duration_sec, 2),
336
- "duration_sec": round(duration_sec, 2),
337
- "analysis_log": analysis_log,
338
- "message": "โš ๏ธ ุงู„ููŠุฏูŠูˆ ูƒู„ู‡ ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก",
339
- "analysis_time": elapsed_analysis,
340
- "output_available": False,
341
- "status": "success"
342
  }
343
 
344
- # โ”€โ”€โ”€ ู‚ุทุน ุจู€ ffmpeg โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
345
- segment_files = []
346
- for i, (s, e) in enumerate(keep_segments):
347
- seg_file = f"{TEMP_DIR}/{job_id}_seg_{i}.mp4"
348
- ok = run_ffmpeg([
349
- "ffmpeg", "-y",
350
- "-i", input_path,
351
- "-ss", str(s),
352
- "-to", str(e),
353
- "-c", "copy",
354
- seg_file
355
- ])
356
- if ok and os.path.exists(seg_file) and os.path.getsize(seg_file) > 0:
357
- segment_files.append(seg_file)
358
-
359
- if not segment_files:
360
- raise HTTPException(status_code=500, detail="ูุดู„ ููŠ ุฅู†ุดุงุก ู…ู‚ุงุทุน ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠูุฉ")
361
-
362
- # โ”€โ”€โ”€ ุฏู…ุฌ ุงู„ู€ segments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
363
- list_file = f"{TEMP_DIR}/{job_id}_list.txt"
364
- with open(list_file, "w") as f:
365
- for seg in segment_files:
366
- f.write(f"file '{seg}'\n")
367
-
368
- ok = run_ffmpeg([
369
- "ffmpeg", "-y",
370
- "-f", "concat",
371
- "-safe", "0",
372
- "-i", list_file,
373
- "-c", "copy",
374
- output_path
375
- ])
376
-
377
- output_exists = ok and os.path.exists(output_path) and os.path.getsize(output_path) > 0
378
  total_removed = sum(e - s for s, e in female_segments)
379
 
380
- # ุชู†ุธูŠู ุงู„ู…ู„ูุงุช ุงู„ู…ุคู‚ุชุฉ (ู†ุจู‚ูŠ ุงู„ู€ output ูู‚ุท)
381
- cleanup_temp_files(job_id)
382
 
383
  return {
384
- "has_female": True,
385
- "female_segments": female_segments,
386
- "kept_segments": keep_segments,
387
  "total_removed_sec": round(total_removed, 2),
388
- "duration_sec": round(duration_sec, 2),
389
- "analysis_log": analysis_log,
390
- "analysis_time": elapsed_analysis,
391
- "output_available": output_exists,
392
- "output_job_id": job_id if output_exists else None,
393
- "download_url": f"/download/{job_id}" if output_exists else None,
394
- "message": f"โœ… ุชู… ุญุฐู {round(total_removed, 1)} ุซุงู†ูŠุฉ ู…ู† ุงู„ููŠุฏูŠูˆ",
395
- "status": "success"
396
  }
397
-
398
  except HTTPException:
 
399
  raise
400
  except Exception as e:
401
- print(f"โŒ Error: {e}", flush=True)
402
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
403
 
404
- # โ”€โ”€โ”€ ุชุญู…ูŠู„ ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
405
  @app.get("/download/{job_id}")
406
  def download_video(job_id: str):
407
- # ุชุญู‚ู‚ ู…ู† ุตุญุฉ ุงู„ู€ job_id
408
- if not job_id.replace("-", "").isalnum():
409
- raise HTTPException(status_code=400, detail="job_id ุบูŠุฑ ุตุงู„ุญ")
410
- output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
411
  if not os.path.exists(output_path):
412
- raise HTTPException(status_code=404, detail="ุงู„ููŠุฏูŠูˆ ุบูŠุฑ ู…ูˆุฌูˆุฏ ุฃูˆ ุงู†ุชู‡ุช ุตู„ุงุญูŠุชู‡")
413
  return FileResponse(
414
  output_path,
415
  media_type="video/mp4",
416
- filename="clean_video.mp4"
 
417
  )
418
 
419
 
420
  if __name__ == "__main__":
421
  import uvicorn
 
422
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ # --- flash_attn Mock ---------------------------------------------------------
2
  import sys
3
  import types
4
  import importlib.util
 
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
 
17
  import uuid
18
  import threading
19
  import subprocess
20
+
21
  import cv2
22
+ import torch
23
  from PIL import Image
24
+ from contextlib import asynccontextmanager
 
 
 
25
  from fastapi import FastAPI, HTTPException, UploadFile, File
26
  from fastapi.middleware.cors import CORSMiddleware
27
  from fastapi.responses import FileResponse, HTMLResponse
28
+ from starlette.background import BackgroundTask
29
+ from transformers import (
30
+ BlipProcessor,
31
+ BlipForQuestionAnswering,
32
+ AutoProcessor,
33
+ AutoModelForCausalLM,
34
+ )
35
 
36
+
37
+ BLIP_MODEL_ID = "Salesforce/blip-vqa-base"
38
  FLORENCE_MODEL_ID = "microsoft/Florence-2-large-ft"
39
  FRAMES_PER_SECOND = 1
40
+ TEMP_DIR = "/tmp/video_filter"
41
  os.makedirs(TEMP_DIR, exist_ok=True)
42
 
43
  BLIP_QUESTIONS = [
 
55
  "Answer yes or no only."
56
  )
57
 
58
+ MODEL_DATA = {}
 
 
 
 
 
 
 
 
 
59
  MODEL_STATUS = {"status": "loading", "message": "ุฌุงุฑูŠ ุชุญู…ูŠู„ ุงู„ู†ู…ุงุฐุฌ..."}
60
+ JOB_OUTPUTS = {}
61
+
62
 
63
+ def load_models() -> None:
 
64
  try:
65
+ print("Loading BLIP...", flush=True)
66
  MODEL_STATUS.update({"status": "loading", "message": "ุฌุงุฑูŠ ุชุญู…ูŠู„ BLIP..."})
67
  start = time.time()
68
  MODEL_DATA["blip_processor"] = BlipProcessor.from_pretrained(BLIP_MODEL_ID)
69
+ MODEL_DATA["blip_model"] = BlipForQuestionAnswering.from_pretrained(
70
+ BLIP_MODEL_ID,
71
+ torch_dtype=torch.float32,
72
  ).eval()
73
+ print(f"BLIP ready in {time.time() - start:.1f}s", flush=True)
74
 
75
+ print("Loading Florence-2...", flush=True)
76
  MODEL_STATUS.update({"status": "loading", "message": "ุฌุงุฑูŠ ุชุญู…ูŠู„ Florence-2..."})
77
  start = time.time()
78
  MODEL_DATA["florence_processor"] = AutoProcessor.from_pretrained(
79
+ FLORENCE_MODEL_ID,
80
+ trust_remote_code=True,
81
  )
82
  MODEL_DATA["florence_model"] = AutoModelForCausalLM.from_pretrained(
83
  FLORENCE_MODEL_ID,
84
  torch_dtype=torch.float32,
85
  trust_remote_code=True,
86
+ attn_implementation="eager",
87
  ).eval()
88
+ print(f"Florence-2 ready in {time.time() - start:.1f}s", flush=True)
 
 
89
 
90
+ MODEL_STATUS.update({"status": "ready", "message": "ุงู„ู†ู…ุงุฐุฌ ุฌุงู‡ุฒุฉ"})
91
+ print("All models loaded", flush=True)
92
  except Exception as e:
93
  MODEL_STATUS.update({"status": "error", "message": str(e)})
94
+ print(f"Error loading models: {e}", flush=True)
95
+
96
 
97
  @asynccontextmanager
98
  async def lifespan(app: FastAPI):
99
  thread = threading.Thread(target=load_models, daemon=True)
100
  thread.start()
101
+ print("Server started, models are loading in background", flush=True)
102
  yield
103
  MODEL_DATA.clear()
104
+ JOB_OUTPUTS.clear()
105
+
106
 
107
  app = FastAPI(
108
  title="Video Female Filter",
109
+ description="ุชุญู„ูŠู„ ุงู„ููŠุฏูŠูˆ ูˆุฅุฒุงู„ุฉ ู…ู‚ุงุทุน ุงู„ู†ุณุงุก | BLIP + Florence-2",
110
+ version="1.0.0",
111
+ lifespan=lifespan,
112
  )
113
 
114
  app.add_middleware(
115
  CORSMiddleware,
116
  allow_origins=["*"],
117
+ allow_credentials=False,
118
  allow_methods=["*"],
119
  allow_headers=["*"],
120
  )
121
 
122
+
123
  def run_blip(image: Image.Image) -> dict:
124
+ processor = MODEL_DATA["blip_processor"]
125
+ model = MODEL_DATA["blip_model"]
126
  yes_answers = {}
127
+ no_answers = {}
128
+
129
  for question in BLIP_QUESTIONS:
130
  inputs = processor(image, question, return_tensors="pt")
131
  with torch.no_grad():
 
135
  yes_answers[question] = answer
136
  else:
137
  no_answers[question] = answer
138
+
139
  return {"yes": yes_answers, "no": no_answers}
140
 
141
+
142
  def run_florence(image: Image.Image) -> str:
143
  processor = MODEL_DATA["florence_processor"]
144
+ model = MODEL_DATA["florence_model"]
145
+ task = "<VQA>"
146
+ prompt = f"{task}{FLORENCE_QUESTION}"
147
+ inputs = processor(text=prompt, images=image, return_tensors="pt")
148
  with torch.no_grad():
149
  generated_ids = model.generate(
150
  input_ids=inputs["input_ids"],
151
  pixel_values=inputs["pixel_values"],
152
  max_new_tokens=10,
153
+ do_sample=False,
154
  )
155
  generated_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
156
  parsed = processor.post_process_generation(
157
+ generated_text,
158
+ task=task,
159
+ image_size=(image.width, image.height),
160
  )
161
  return parsed.get(task, "").strip().lower()
162
 
163
+
164
  def is_female_in_frame(image: Image.Image) -> tuple[bool, str]:
165
  blip_result = run_blip(image)
166
+ yes_q = blip_result["yes"]
167
+
168
  if "is there a woman in this image?" in yes_q:
169
  return True, "blip_woman"
170
+
171
  if not yes_q:
172
  return False, "blip_clean"
173
+
174
  florence_answer = run_florence(image)
175
  if "yes" in florence_answer:
176
  return True, "florence_confirmed"
177
  return False, "florence_clean"
178
 
179
+
180
+ def run_ffmpeg_command(args: list[str]) -> None:
181
+ proc = subprocess.run(args, capture_output=True, text=True)
182
+ if proc.returncode != 0:
183
+ stderr_msg = (proc.stderr or "").strip()
184
+ if len(stderr_msg) > 600:
185
+ stderr_msg = stderr_msg[-600:]
186
+ raise RuntimeError(f"ffmpeg failed (exit={proc.returncode}): {stderr_msg}")
187
+
188
+
189
+ def merge_overlapping_segments(segments: list[list[float]], duration_sec: float) -> list[list[float]]:
190
+ if not segments:
191
+ return []
192
+
193
+ clipped = []
194
+ for s, e in segments:
195
+ s = max(0.0, min(s, duration_sec))
196
+ e = max(0.0, min(e, duration_sec))
197
+ if e > s:
198
+ clipped.append([s, e])
199
+
200
+ if not clipped:
201
+ return []
202
+
203
+ clipped.sort(key=lambda x: x[0])
204
+ merged = [clipped[0]]
205
+ for s, e in clipped[1:]:
206
+ last = merged[-1]
207
+ if s <= last[1]:
208
+ last[1] = max(last[1], e)
209
+ else:
210
+ merged.append([s, e])
211
+
212
+ return merged
213
+
214
+
215
+ def cleanup_files(paths: list[str]) -> None:
216
+ for p in paths:
217
+ try:
218
+ if p and os.path.exists(p):
219
+ os.remove(p)
220
+ except Exception:
221
+ pass
222
+
223
+
224
+ def cleanup_job_output(job_id: str) -> None:
225
+ output = JOB_OUTPUTS.pop(job_id, None)
226
+ if output:
227
+ cleanup_files([output])
228
+
229
+
230
+ def build_clean_video(
231
+ input_path: str,
232
+ output_path: str,
233
+ keep_segments: list[list[float]],
234
+ job_id: str,
235
+ ) -> bool:
236
+ segment_files = []
237
+ temp_files = []
238
+
239
  try:
240
+ for i, (start_sec, end_sec) in enumerate(keep_segments):
241
+ seg_file = f"{TEMP_DIR}/{job_id}_seg_{i}.mp4"
242
+ temp_files.append(seg_file)
243
+ run_ffmpeg_command(
244
+ [
245
+ "ffmpeg",
246
+ "-y",
247
+ "-ss",
248
+ f"{start_sec:.3f}",
249
+ "-to",
250
+ f"{end_sec:.3f}",
251
+ "-i",
252
+ input_path,
253
+ "-map",
254
+ "0:v:0?",
255
+ "-map",
256
+ "0:a:0?",
257
+ "-c:v",
258
+ "libx264",
259
+ "-preset",
260
+ "veryfast",
261
+ "-crf",
262
+ "23",
263
+ "-pix_fmt",
264
+ "yuv420p",
265
+ "-c:a",
266
+ "aac",
267
+ "-b:a",
268
+ "128k",
269
+ "-movflags",
270
+ "+faststart",
271
+ seg_file,
272
+ ]
273
+ )
274
+ if os.path.exists(seg_file) and os.path.getsize(seg_file) > 0:
275
+ segment_files.append(seg_file)
276
+
277
+ if not segment_files:
278
  return False
279
+
280
+ list_file = f"{TEMP_DIR}/{job_id}_list.txt"
281
+ temp_files.append(list_file)
282
+ with open(list_file, "w", encoding="utf-8") as f:
283
+ for seg in segment_files:
284
+ f.write(f"file '{seg}'\n")
285
+
286
+ run_ffmpeg_command(
287
+ [
288
+ "ffmpeg",
289
+ "-y",
290
+ "-f",
291
+ "concat",
292
+ "-safe",
293
+ "0",
294
+ "-i",
295
+ list_file,
296
+ "-c:v",
297
+ "libx264",
298
+ "-preset",
299
+ "veryfast",
300
+ "-crf",
301
+ "23",
302
+ "-pix_fmt",
303
+ "yuv420p",
304
+ "-c:a",
305
+ "aac",
306
+ "-b:a",
307
+ "128k",
308
+ "-movflags",
309
+ "+faststart",
310
+ output_path,
311
+ ]
312
+ )
313
+
314
+ return os.path.exists(output_path) and os.path.getsize(output_path) > 0
315
+ finally:
316
+ cleanup_files(temp_files)
317
+
318
+
319
  @app.get("/", response_class=HTMLResponse)
320
  def root():
321
  with open("index.html", "r", encoding="utf-8") as f:
322
  return f.read()
323
 
324
+
325
  @app.get("/health")
326
  def health():
327
  return {
328
+ "status": MODEL_STATUS["status"],
329
+ "message": MODEL_STATUS["message"],
330
+ "blip_loaded": "blip_model" in MODEL_DATA,
331
+ "florence_loaded": "florence_model" in MODEL_DATA,
332
  }
333
 
334
+
335
+ @app.post("/analyze-file")
336
+ async def analyze_file(file: UploadFile = File(...)):
 
337
  if MODEL_STATUS["status"] != "ready":
338
+ raise HTTPException(
339
+ status_code=503,
340
+ detail=f"ุงู„ู†ู…ุงุฐุฌ ู„ู… ุชูƒุชู…ู„ ุจุนุฏ: {MODEL_STATUS['message']}",
341
+ )
342
 
343
  if not file.content_type or not file.content_type.startswith("image/"):
344
  raise HTTPException(status_code=400, detail="ุงู„ู…ู„ู ู„ูŠุณ ุตูˆุฑุฉ")
345
 
346
  try:
347
+ image_bytes = await file.read()
348
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
349
+ has_female, reason = is_female_in_frame(image)
350
+ return {
351
+ "has_female": has_female,
352
+ "decision": "BLOCK" if has_female else "ALLOW",
353
+ "reason": reason,
354
+ "status": "success",
355
+ }
356
  except Exception as e:
357
+ raise HTTPException(status_code=500, detail=str(e))
358
 
 
 
 
 
 
 
 
359
 
 
360
  @app.post("/analyze-video")
361
  async def analyze_video(file: UploadFile = File(...)):
 
362
  if MODEL_STATUS["status"] != "ready":
363
+ raise HTTPException(
364
+ status_code=503,
365
+ detail=f"ุงู„ู†ู…ุงุฐุฌ ู„ู… ุชูƒุชู…ู„ ุจุนุฏ: {MODEL_STATUS['message']}",
366
+ )
367
 
368
  if not file.content_type or not file.content_type.startswith("video/"):
369
  raise HTTPException(status_code=400, detail="ุงู„ู…ู„ู ู„ูŠุณ ููŠุฏูŠูˆ")
370
 
371
+ job_id = str(uuid.uuid4())[:8]
372
+ input_path = f"{TEMP_DIR}/{job_id}_input.mp4"
373
  output_path = f"{TEMP_DIR}/{job_id}_output.mp4"
374
 
 
375
  with open(input_path, "wb") as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  while True:
377
+ chunk = await file.read(1024 * 1024)
378
+ if not chunk:
379
  break
380
+ f.write(chunk)
381
 
382
+ try:
383
+ cap = cv2.VideoCapture(input_path)
384
+ if not cap.isOpened():
385
+ raise HTTPException(status_code=400, detail="ุชุนุฐุฑ ูุชุญ ุงู„ููŠุฏูŠูˆ")
386
 
387
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25
388
+ if fps <= 0:
389
+ fps = 25
390
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
391
+ duration_sec = total_frames / fps if total_frames > 0 else 0.0
 
392
 
393
+ print(f"Video info: {total_frames} frames, {fps:.2f} fps", flush=True)
 
 
 
 
 
394
 
395
+ frame_interval = max(1, int(fps / FRAMES_PER_SECOND))
396
+ female_segments = []
397
+ analysis_log = []
398
+ in_female_seg = False
399
+ seg_start = 0.0
400
+ frame_idx = 0
401
+ start_time = time.time()
402
+
403
+ try:
404
+ while True:
405
+ ret, frame = cap.read()
406
+ if not ret:
407
+ break
408
+
409
+ if frame_idx % frame_interval == 0:
410
+ current_sec = frame_idx / fps
411
+ pil_image = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
412
+ has_female, reason = is_female_in_frame(pil_image)
413
+ analysis_log.append(
414
+ {
415
+ "second": round(current_sec, 2),
416
+ "has_female": has_female,
417
+ "reason": reason,
418
+ }
419
+ )
420
+
421
+ if has_female and not in_female_seg:
422
+ in_female_seg = True
423
+ seg_start = max(0.0, current_sec - 0.5)
424
+ elif not has_female and in_female_seg:
425
+ in_female_seg = False
426
+ female_segments.append([seg_start, current_sec + 0.5])
427
+
428
+ frame_idx += 1
429
+ finally:
430
+ cap.release()
431
 
432
  if in_female_seg:
433
  female_segments.append([seg_start, duration_sec])
434
+ female_segments = merge_overlapping_segments(female_segments, duration_sec)
435
 
 
436
  elapsed_analysis = round(time.time() - start_time, 2)
437
 
 
438
  if not female_segments:
 
439
  return {
440
+ "has_female": False,
441
+ "female_segments": [],
442
+ "analysis_log": analysis_log,
443
+ "message": "โœ… ุงู„ููŠุฏูŠูˆ ู†ุธูŠู ู„ุง ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก",
444
+ "analysis_time": elapsed_analysis,
 
 
 
445
  "output_available": False,
446
+ "status": "success",
447
  }
448
 
 
449
  keep_segments = []
450
+ prev_end = 0.0
451
  for s, e in female_segments:
452
+ if prev_end < s:
453
+ keep_segments.append([prev_end, s])
454
  prev_end = e
455
+ if prev_end < duration_sec:
456
+ keep_segments.append([prev_end, duration_sec])
457
 
458
  if not keep_segments:
 
459
  return {
460
+ "has_female": True,
461
+ "female_segments": female_segments,
462
+ "analysis_log": analysis_log,
463
+ "message": "โš ๏ธ ุงู„ููŠุฏูŠูˆ ูƒู„ู‡ ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก",
464
+ "analysis_time": elapsed_analysis,
465
+ "output_available": False,
466
+ "status": "success",
 
 
 
467
  }
468
 
469
+ output_ok = build_clean_video(input_path, output_path, keep_segments, job_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  total_removed = sum(e - s for s, e in female_segments)
471
 
472
+ if output_ok:
473
+ JOB_OUTPUTS[job_id] = output_path
474
 
475
  return {
476
+ "has_female": True,
477
+ "female_segments": female_segments,
478
+ "kept_segments": keep_segments,
479
  "total_removed_sec": round(total_removed, 2),
480
+ "analysis_log": analysis_log,
481
+ "analysis_time": elapsed_analysis,
482
+ "output_available": output_ok,
483
+ "output_job_id": job_id,
484
+ "download_url": f"/download/{job_id}",
485
+ "message": f"โœ… ุชู… ุญุฐู {round(total_removed, 1)} ุซุงู†ูŠุฉ ู…ู† ุงู„ููŠุฏูŠูˆ",
486
+ "status": "success",
 
487
  }
 
488
  except HTTPException:
489
+ cleanup_files([output_path])
490
  raise
491
  except Exception as e:
492
+ cleanup_files([output_path])
493
  raise HTTPException(status_code=500, detail=str(e))
494
+ finally:
495
+ cleanup_files([input_path])
496
+
497
 
 
498
  @app.get("/download/{job_id}")
499
  def download_video(job_id: str):
500
+ output_path = JOB_OUTPUTS.get(job_id, f"{TEMP_DIR}/{job_id}_output.mp4")
 
 
 
501
  if not os.path.exists(output_path):
502
+ raise HTTPException(status_code=404, detail="ุงู„ููŠุฏูŠูˆ ุบูŠุฑ ู…ูˆุฌูˆุฏ")
503
  return FileResponse(
504
  output_path,
505
  media_type="video/mp4",
506
+ filename="clean_video.mp4",
507
+ background=BackgroundTask(cleanup_job_output, job_id),
508
  )
509
 
510
 
511
  if __name__ == "__main__":
512
  import uvicorn
513
+
514
  uvicorn.run(app, host="0.0.0.0", port=7860)