YoungjaeDev Claude commited on
Commit
b95781b
·
1 Parent(s): cacaeb4

fix: CodeRabbit 리뷰 반영 - 보안 취약점 수정 및 파라미터 조정

Browse files

- tempfile.mktemp() -> NamedTemporaryFile(delete=False) (CWE-377 방지)
- os.system() -> subprocess.run() 리스트 인자 (shell injection 방지)
- 비디오 길이 검증 추가 (120s GPU 타임아웃 대비)
- fall_threshold: 0.7 -> 0.85 (가이드라인 0.8-0.9)
- post_fall_frames: 3 -> 15 (2.5초 검증)
- gradio 버전 핀 고정 (==4.44.0)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. README.md +1 -1
  2. app.py +37 -10
  3. requirements.txt +1 -1
README.md CHANGED
@@ -34,7 +34,7 @@ YOLOv11-Pose + ST-GCN 2-stage 파이프라인을 사용한 실시간 낙상 감
34
 
35
  ## 파라미터 설명
36
 
37
- - **낙상 판정 임계값 (Fall Threshold)**: 이 값 이상의 확률이면 낙상으로 판정 (기본값: 0.7)
38
  - **키포인트 표시 (Keypoint Mode)**:
39
  - `all`: 전체 17개 키포인트 표시
40
  - `major`: 주요 9개 키포인트만 표시 (코, 어깨, 엉덩이, 무릎, 발목)
 
34
 
35
  ## 파라미터 설명
36
 
37
+ - **낙상 판정 임계값 (Fall Threshold)**: 이 값 이상의 확률이면 낙상으로 판정 (기본값: 0.85, 권장: 0.8-0.9)
38
  - **키포인트 표시 (Keypoint Mode)**:
39
  - `all`: 전체 17개 키포인트 표시
40
  - `major`: 주요 9개 키포인트만 표시 (코, 어깨, 엉덩이, 무릎, 발목)
app.py CHANGED
@@ -16,6 +16,7 @@ HF Spaces Zero GPU 환경에서 실행됩니다.
16
  """
17
 
18
  import os
 
19
  import sys
20
  import tempfile
21
  import time
@@ -168,11 +169,11 @@ def get_pipeline():
168
  stgcn_checkpoint=stgcn_checkpoint,
169
  window_size=60,
170
  conf_threshold=0.5,
171
- fall_threshold=0.7,
172
  temporal_window=5,
173
  stgcn_stride=5,
174
  alert_duration=150,
175
- post_fall_frames=3,
176
  device=str(device),
177
  debug=False,
178
  headless=False,
@@ -294,8 +295,23 @@ def process_video(
294
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
295
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
296
 
297
- # 출력 비디오 설정
298
- output_path = tempfile.mktemp(suffix=".mp4")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
300
  # Info panel 추가로 높이 80px 증가
301
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height + 80))
@@ -341,8 +357,19 @@ def process_video(
341
 
342
  # H.264 코덱으로 재인코딩 (브라우저 호환)
343
  progress(0.9, desc="비디오 인코딩 중...")
344
- output_h264 = tempfile.mktemp(suffix=".mp4")
345
- os.system(f'ffmpeg -y -i "{output_path}" -c:v libx264 -preset fast -crf 23 "{output_h264}" -loglevel quiet')
 
 
 
 
 
 
 
 
 
 
 
346
 
347
  # mp4v 임시 파일 삭제
348
  if os.path.exists(output_path):
@@ -409,12 +436,12 @@ def create_demo() -> gr.Blocks:
409
 
410
  with gr.Accordion("고급 설정", open=False):
411
  fall_threshold = gr.Slider(
412
- minimum=0.5,
413
  maximum=0.95,
414
- value=0.7,
415
  step=0.05,
416
  label="낙상 판정 임계값",
417
- info=" 이상의 확률이면 낙상으로 판정합니다."
418
  )
419
  viz_keypoints = gr.Radio(
420
  choices=["all", "major"],
@@ -454,7 +481,7 @@ def create_demo() -> gr.Blocks:
454
 
455
  if examples:
456
  gr.Examples(
457
- examples=[[ex, 0.7, "all"] for ex in examples[:3]],
458
  inputs=[video_input, fall_threshold, viz_keypoints],
459
  outputs=[video_output, prob_graph, result_text],
460
  fn=process_video,
 
16
  """
17
 
18
  import os
19
+ import subprocess
20
  import sys
21
  import tempfile
22
  import time
 
169
  stgcn_checkpoint=stgcn_checkpoint,
170
  window_size=60,
171
  conf_threshold=0.5,
172
+ fall_threshold=0.85, # 가이드라인 권장: 0.8-0.9 (false positive <5%)
173
  temporal_window=5,
174
  stgcn_stride=5,
175
  alert_duration=150,
176
+ post_fall_frames=15, # 2.5초 @ 30fps with stride=5 (가이드라인: 2-3초)
177
  device=str(device),
178
  debug=False,
179
  headless=False,
 
295
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
296
  total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
297
 
298
+ # 비디오 길이 검증 (120s GPU 타임아웃 대비)
299
+ if fps > 0:
300
+ video_duration = total_frames / fps
301
+ # 처리 시간 추정: 대략 실시간의 1.5배 + 인코딩 10초
302
+ estimated_time = video_duration * 1.5 + 10
303
+ if estimated_time > 110: # 120s 타임아웃에 여유 두기
304
+ cap.release()
305
+ return None, None, (
306
+ f"비디오가 너무 깁니다. "
307
+ f"비디오 길이: {video_duration:.1f}초, "
308
+ f"예상 처리 시간: {estimated_time:.1f}초 (제한: 110초). "
309
+ f"60초 이내의 비디오를 업로드하세요."
310
+ )
311
+
312
+ # 출력 비디오 설정 (보안: NamedTemporaryFile 사용)
313
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
314
+ output_path = tmp.name
315
  fourcc = cv2.VideoWriter_fourcc(*'mp4v')
316
  # Info panel 추가로 높이 80px 증가
317
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height + 80))
 
357
 
358
  # H.264 코덱으로 재인코딩 (브라우저 호환)
359
  progress(0.9, desc="비디오 인코딩 중...")
360
+ # 보안: NamedTemporaryFile 사용 (CWE-377 방지)
361
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
362
+ output_h264 = tmp.name
363
+ # 보안: subprocess.run 사용 (shell injection 방지)
364
+ subprocess.run(
365
+ [
366
+ 'ffmpeg', '-y', '-i', output_path,
367
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
368
+ output_h264, '-loglevel', 'quiet'
369
+ ],
370
+ check=False,
371
+ capture_output=True
372
+ )
373
 
374
  # mp4v 임시 파일 삭제
375
  if os.path.exists(output_path):
 
436
 
437
  with gr.Accordion("고급 설정", open=False):
438
  fall_threshold = gr.Slider(
439
+ minimum=0.7,
440
  maximum=0.95,
441
+ value=0.85,
442
  step=0.05,
443
  label="낙상 판정 임계값",
444
+ info="권장: 0.8-0.9 (false positive <5% 목표)"
445
  )
446
  viz_keypoints = gr.Radio(
447
  choices=["all", "major"],
 
481
 
482
  if examples:
483
  gr.Examples(
484
+ examples=[[ex, 0.85, "all"] for ex in examples[:3]],
485
  inputs=[video_input, fall_threshold, viz_keypoints],
486
  outputs=[video_output, prob_graph, result_text],
487
  fn=process_video,
requirements.txt CHANGED
@@ -9,7 +9,7 @@ torchvision>=0.15.0
9
  ultralytics>=8.0.0
10
 
11
  # Web UI
12
- gradio>=4.0.0
13
  plotly>=5.0.0
14
 
15
  # Computer Vision
 
9
  ultralytics>=8.0.0
10
 
11
  # Web UI
12
+ gradio==4.44.0 # HF Spaces sdk_version과 일치
13
  plotly>=5.0.0
14
 
15
  # Computer Vision