컷별 댓글 추천 작업을 태스크 단위로 되돌릴 수 있게 버전관리를 시작한다. .gitignore 로 영상·캐시(.downloads 2.7G, .comments 72M, .media 28M)와 비밀키(.gemini_key)를 제외했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
70 lines
2.4 KiB
Python
70 lines
2.4 KiB
Python
"""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
|