댓글 카드를 컷 구간 안에 배치하는 경로 추가

_load_comment_cards를 _card_paths/_load_comment_cards/_cards_by_cut로 분리했다.
자동 탭은 카드가 어느 컷 소속인지(card_cuts)만 넘기면 그 컷 구간 안에서 균등
배치되어, 한 컷의 카드가 모자라도 다음 컷 카드가 앞으로 밀리지 않는다(기존
전체 균등 배치의 약점). 카드 시간은 무음 제거 전 placements 직후에 계산하고,
자막과 동일하게 _remap_caps()로 재매핑한다 — 카드 튜플(start,end,path)이
자막 튜플(start,end,text)과 모양이 같아 그대로 재사용 가능하다. card_cuts가
없으면 기존 _load_comment_cards 폴백으로 떨어져 파일/유튜브 구간 탭은 영향
없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 12:18:33 +09:00
parent 90ff2b174f
commit 0a0b43efc2

View File

@ -35,24 +35,17 @@ _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_DOWNLOADS = os.path.join(_ROOT, ".downloads")
def _load_comment_cards(folder: str, dur: float, min_sec: float = 3.0,
fixed: bool = False):
"""지정 폴더의 이미지를 영상 길이에 맞춰 하단에 균등 배치.
def _card_paths(folder: str):
"""댓글 카드 이미지 경로 목록. 배치 방식과 무관하게 '순서'만 정한다.
장수: n = min(카드 , max(1, floor(dur/min_sec))) 카드 하나가 min_sec 밑으로
내려가지 않는 상한. 배치 간격은 항상 dur/n 카드가 모자라도 끝까지 없이
채워지고(간격이 3 이상으로 늘어남), 넘치면 초과분은 버린다.
fixed=True 늘리지 않고 min_sec 고정 모자라면 뒤는 비운다
(편집하면서 부분삭제를 많이 하는 경우 카드가 늘어나 있으면 타이밍이 꼬여서).
정렬 규칙:
- 모든 파일명이 숫자로 시작하면 숫자순(1, 2, 10 )
- 아니면 저장(생성) 순서 = 다운로드한 순서
folder 비었거나 없으면 [] (댓글 카드 없음).
Returns: [(start, end, path)]
folder 비었거나 없으면 [].
"""
import re
folder = (folder or "").strip().strip('"')
if not folder or not os.path.isdir(folder) or dur <= 0:
if not folder or not os.path.isdir(folder):
return []
imgs = [os.path.join(folder, f) for f in os.listdir(folder)
if os.path.splitext(f)[1].lower() in (".png", ".jpg", ".jpeg", ".webp")]
@ -67,7 +60,26 @@ def _load_comment_cards(folder: str, dur: float, min_sec: float = 3.0,
imgs.sort(key=_leadnum) # 파일명 숫자순
else:
imgs.sort(key=lambda p: os.path.getctime(p)) # 저장(생성) 순서
return imgs
def _load_comment_cards(folder: str, dur: float, min_sec: float = 3.0,
fixed: bool = False):
"""지정 폴더의 이미지를 영상 길이에 맞춰 하단에 균등 배치.
장수: n = min(카드 , max(1, floor(dur/min_sec))) 카드 하나가 min_sec 밑으로
내려가지 않는 상한. 배치 간격은 항상 dur/n 카드가 모자라도 끝까지 없이
채워지고(간격이 3 이상으로 늘어남), 넘치면 초과분은 버린다.
fixed=True 늘리지 않고 min_sec 고정 모자라면 뒤는 비운다
(편집하면서 부분삭제를 많이 하는 경우 카드가 늘어나 있으면 타이밍이 꼬여서).
함수는 컷을 모른다. 자동 탭처럼 '어느 컷에 붙일지' 정해진 경우엔
`_cards_by_cut()` 쓴다. 폴더 지정 경로(파일/유튜브 구간 ) 계속 함수를 쓴다.
Returns: [(start, end, path)]
"""
imgs = _card_paths(folder)
if not imgs or dur <= 0:
return []
n = min(len(imgs), max(1, int(dur // min_sec)))
if fixed: # 3초 고정 — 뒤가 비어도 늘리지 않음
return [(i * min_sec, min((i + 1) * min_sec, dur), path)
@ -77,6 +89,40 @@ def _load_comment_cards(folder: str, dur: float, min_sec: float = 3.0,
for i, path in enumerate(imgs[:n])]
def _cards_by_cut(paths, card_cuts, placements, dur: float, *,
min_sec: float = 3.0, fixed: bool = False):
"""카드를 '소속 컷 구간 안'에 배치 — 자동 탭 전용.
card_cuts[i] = i번째 카드가 속한 인덱스. 안에서는 균등 분할
(fixed=True min_sec 고정, 뒷부분은 비움).
단위인가: 전체 균등 배치(`_load_comment_cards`) 경계를 몰라서
3 얘기하는 댓글이 7 위에 뜬다. 안에서 계산하면 컷이 차도
**다음 카드가 앞으로 밀리지 않는다.**
Returns: [(start, end, path)] 시간순
"""
if not paths or not card_cuts or not placements or dur <= 0:
return []
groups: dict = {}
for i, ci in enumerate(card_cuts[:len(paths)]):
if isinstance(ci, int) and 0 <= ci < len(placements):
groups.setdefault(ci, []).append(i)
out = []
for ci, idxs in groups.items():
p0, p1 = placements[ci]
p1 = min(p1, dur)
if p1 <= p0:
continue # 영상 실제 길이 밖 컷은 버린다
step = min_sec if fixed else (p1 - p0) / len(idxs)
for k, i in enumerate(idxs):
s = p0 + k * step
if s >= p1:
break # fixed 로 컷을 넘치면 뒤는 비운다
out.append((s, min(s + step, p1), paths[i]))
out.sort(key=lambda t: t[0])
return out
def _elapsed_kept(keep_sorted, t: float) -> float:
"""원본 시간 t 가 무음 제거 후(압축) 타임라인에서 놓이는 위치 = t 이전의 보존 길이 합."""
tot = 0.0
@ -311,6 +357,7 @@ async def process_paste(
scene_split: bool = False,
comments_dir: str = "",
cards_fixed: bool = False,
card_cuts: Optional[List[int]] = None,
bg_white: bool = False,
remove_silence: bool = False,
asr_bottom: bool = False,
@ -370,6 +417,11 @@ async def process_paste(
eff_caps = [(p0, min(p1, dur), ef) for (p0, p1), (_, _, _, ef) in zip(placements, cuts)
if ef and p0 < dur]
# 댓글 카드(자동 탭): '몇 번 컷 소속'만 받아 여기서 시간을 만든다.
# 서버가 시간을 확정하면 무음 제거 때 자막만 당겨지고 카드는 혼자 어긋난다.
cut_cards = _cards_by_cut(_card_paths(comments_dir), card_cuts or [],
placements, dur, fixed=cards_fixed)
video_clips = [(0.0, dur)] # 병합본 = 한 덩어리(재컷 없음)
timeline_dur = dur
@ -384,6 +436,7 @@ async def process_paste(
keep = sorted(keep)
bottom_caps = _remap_caps(bottom_caps, keep)
eff_caps = _remap_caps(eff_caps, keep)
cut_cards = _remap_caps(cut_cards, keep)
video_clips = keep
timeline_dur = sum(e - s for s, e in keep)
yield {"type": "log",
@ -437,10 +490,12 @@ async def process_paste(
video_clips = split_clips_at_scenes(video_clips, scenes)
yield {"type": "log", "msg": f"장면전환 {len(scenes)}곳 → 세그먼트 {len(video_clips)}"}
# 댓글 카드(선택): 지정 폴더의 1,2,3… 을 3초씩 하단에 순서대로
cards = _load_comment_cards(comments_dir, timeline_dur, fixed=cards_fixed)
# 댓글 카드: 컷 소속이 지정됐으면 그 컷 구간 안(위에서 계산), 아니면 폴더 균등 배치
cards = cut_cards or _load_comment_cards(comments_dir, timeline_dur, fixed=cards_fixed)
if cards:
yield {"type": "log", "msg": f"댓글 카드 {len(cards)}개 하단 삽입(3초 간격)"}
yield {"type": "log",
"msg": f"댓글 카드 {len(cards)}개 하단 삽입"
+ ("(컷별 배치)" if cut_cards else "(전체 균등)")}
path = await asyncio.to_thread(
lambda: build_bg_template_draft(