"""ffmpeg scene 필터 기반 시각적 장면전환 감지. 화면이 확 바뀌는 지점(컷/앵글전환/B롤)을 찾아 그 timestamp(초, 소스 기준)를 반환. draft 의 비디오 세그먼트를 이 지점에서 추가로 쪼개면, 말이 이어져도(무음 없음) 장면이 바뀌는 곳에 캡컷 편집 컷이 생긴다. 시간은 그대로라 자막 싱크에 영향 없음. """ from __future__ import annotations import re import subprocess from typing import List, Tuple Segment = Tuple[float, float] _PTS = re.compile(r"pts_time:([\d.]+)") def detect_scene_changes(video_path: str, *, threshold: float = 0.4) -> List[float]: """장면전환 timestamp(초, 소스 기준) 리스트. threshold=scene score(0~1) 임계. 낮을수록 민감(과분할), 높을수록 큰 전환만. 0.4 = 실제 컷 위주. """ cmd = [ "ffmpeg", "-hide_banner", "-nostats", "-i", video_path, "-vf", f"select='gt(scene,{threshold})',showinfo", "-an", "-f", "null", "-", ] # showinfo 는 stderr 로 출력. silence.py 와 동일하게 stderr→stdout 병합. proc = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", ) times: List[float] = [] for line in (proc.stdout or "").splitlines(): if "showinfo" not in line: continue m = _PTS.search(line) if m: times.append(float(m.group(1))) return sorted(set(times)) def split_clips_at_scenes( video_clips: List[Segment], scene_times: List[float], *, min_len: float = 0.4, ) -> List[Segment]: """보존 구간(소스 시간)을 장면전환 지점에서 추가로 분할. 인접 분할이라 누적 길이·타임라인 매핑이 동일 → 자막 싱크 영향 없음. 분할 후 min_len 미만 조각은 앞 조각과 병합(너무 잘게 쪼개짐 방지). """ if not scene_times: return video_clips out: List[Segment] = [] for s, e in video_clips: # 이 구간 내부의 장면전환 지점만(양끝 여유 배제) cuts = [t for t in scene_times if s + min_len <= t <= e - min_len] if not cuts: out.append((s, e)) continue prev = s for t in cuts: if t - prev >= min_len: out.append((prev, t)) prev = t out.append((prev, e)) return out