capcut-agent/capcut_agent/silence.py
hehihoho3@gmail.com bf1b387d6d chore: git 저장소 초기화 (기존 코드 스냅샷)
컷별 댓글 추천 작업을 태스크 단위로 되돌릴 수 있게 버전관리를 시작한다.
.gitignore 로 영상·캐시(.downloads 2.7G, .comments 72M, .media 28M)와
비밀키(.gemini_key)를 제외했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:36:05 +09:00

91 lines
3.2 KiB
Python

"""ffmpeg silencedetect 기반 무음 감지 → 발화(speech) 구간 추출.
핵심: 무음을 직접 찾고, 그 여집합 = 발화 구간. 발화 구간 양 끝에 padding 을
주어 단어 시작/끝이 잘리지 않게 한다(점프컷 특유의 '말 잘림' 방지).
"""
from __future__ import annotations
import re
import subprocess
from typing import List, Tuple
Segment = Tuple[float, float] # (start_sec, end_sec)
_SILENCE_START = re.compile(r"silence_start:\s*(-?[\d.]+)")
_SILENCE_END = re.compile(r"silence_end:\s*(-?[\d.]+)")
def _detect_silences(video_path: str, noise_db: float, min_silence: float) -> List[Segment]:
"""무음 구간 [(start, end), ...] (초) 반환."""
cmd = [
"ffmpeg", "-hide_banner", "-nostats", "-i", video_path,
"-af", f"silencedetect=noise={noise_db}dB:d={min_silence}",
"-f", "null", "-",
]
# silencedetect 로그는 stderr 로 나온다. 단, Windows에서 부모 프로세스(uvicorn)의
# stderr 핸들이 리다이렉트된 경우 capture_output 의 stderr PIPE 가 None 으로 잡히는
# 케이스가 있어, stderr→stdout 병합 후 stdout 을 읽는다(검증된 경로).
proc = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, encoding="utf-8", errors="replace",
)
log = proc.stdout or ""
silences: List[Segment] = []
cur_start = None
for line in log.splitlines():
m = _SILENCE_START.search(line)
if m:
cur_start = float(m.group(1))
continue
m = _SILENCE_END.search(line)
if m and cur_start is not None:
silences.append((cur_start, float(m.group(1))))
cur_start = None
return silences
def detect_speech_segments(
video_path: str,
duration: float,
*,
noise_db: float = -30.0,
min_silence: float = 0.4,
pad: float = 0.08,
min_speech: float = 0.2,
) -> List[Segment]:
"""무음의 여집합 = 발화 구간. padding/merge/최소길이 필터 적용.
Args:
duration: 전체 영상 길이(초). probe 로 미리 구한 값.
noise_db: 이 dB 이하를 무음으로 간주.
min_silence: 이 길이(초) 이상 지속돼야 무음으로 컷.
pad: 발화 구간 양 끝 여유(초). 말 잘림 방지.
min_speech: 이보다 짧은 발화 조각은 버림(노이즈성 컷 방지).
"""
silences = _detect_silences(video_path, noise_db, min_silence)
# 무음의 여집합 = 발화
speech: List[Segment] = []
cursor = 0.0
for s_start, s_end in silences:
if s_start > cursor:
speech.append((cursor, s_start))
cursor = max(cursor, s_end)
if cursor < duration:
speech.append((cursor, duration))
# padding (양 끝 확장) + 클램프
padded = [(max(0.0, s - pad), min(duration, e + pad)) for s, e in speech]
# padding 으로 겹친 구간 병합
merged: List[Segment] = []
for s, e in padded:
if merged and s <= merged[-1][1]:
merged[-1] = (merged[-1][0], max(merged[-1][1], e))
else:
merged.append((s, e))
# 최소 발화 길이 필터
return [(s, e) for s, e in merged if (e - s) >= min_speech]