카드 배치를 무음 제거 뒤로 옮겨 3초 약속을 지킨다

카드 시간을 무음 제거 **전** 타임라인에서 만들고 _remap_caps 로 압축하고 있었다.
자막은 그게 맞다(발화 시각을 따라가야 하니까). 하지만 카드는 다르다:

- cards_fixed=True 는 "카드는 정확히 3초"라는 절대 길이 약속이라 압축하면 깨진다.
  컷 한가운데가 무음이면 3.0초짜리가 2.0초로 눌린다.
- fixed=False 에서도 3초 하한이 사라져 있었다. 기존 _load_comment_cards 는
  n = max(1, int(dur // 3)) 으로 카드당 3초를 보장하는데 _cards_by_cut 엔 그게 없었고,
  quota 는 **원본** 컷 길이로 계산되므로 무음이 절반인 24초 컷은 8장 → 압축 12초 →
  장당 1.5초가 됐다.

고친 방식:
- _remap_placements() 추가 — 카드는 '시간'이 아니라 '컷 구간 자체'를 옮긴다.
- _cards_by_cut 호출을 무음 제거 블록 뒤로 옮기고 압축된 구간·timeline_dur 로 계산.
  무음 제거가 꺼져 있으면 card_places = placements 그대로다.
- _cards_by_cut 에 컷당 장수 상한 min(len, max(1, floor(컷길이/min_sec))) 추가.
  초과분은 버린다 — _load_comment_cards 와 같은 규칙.
- 이제 필요 없어진 cut_cards 의 _remap_caps 호출 제거.

기존 탭(process_bg_template)과 _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 14:12:31 +09:00
parent 4e479b50ce
commit 499223a125

View File

@ -96,6 +96,13 @@ def _cards_by_cut(paths, card_cuts, placements, dur: float, *,
card_cuts[i] = i번째 카드가 속한 인덱스. 안에서는 균등 분할
(fixed=True min_sec 고정, 뒷부분은 비움).
placements/dur **최종(무음 제거 반영) 타임라인** 기준이어야 한다.
압축 시간으로 계산해 놓고 나중에 재매핑하면 fixed=True "정확히 3초" 약속이
깨지고(3초짜리가 2초로 눌린다) 3 하한도 사라진다.
컷당 장수 상한 = max(1, floor(컷길이/min_sec)) `_load_comment_cards` 같은 규칙.
초과분은 버린다(카드가 1초씩 번쩍이느니 빼는 낫다).
단위인가: 전체 균등 배치(`_load_comment_cards`) 경계를 몰라서
3 얘기하는 댓글이 7 위에 뜬다. 안에서 계산하면 컷이 차도
**다음 카드가 앞으로 밀리지 않는다.**
@ -113,7 +120,9 @@ def _cards_by_cut(paths, card_cuts, placements, dur: float, *,
p1 = min(p1, dur)
if p1 <= p0:
continue # 영상 실제 길이 밖 컷은 버린다
step = min_sec if fixed else (p1 - p0) / len(idxs)
n = min(len(idxs), max(1, int((p1 - p0) // min_sec))) # 카드당 min_sec 하한
idxs = idxs[:n]
step = min_sec if fixed else (p1 - p0) / n
for k, i in enumerate(idxs):
s = p0 + k * step
if s >= p1:
@ -144,6 +153,20 @@ def _remap_caps(caps, keep_sorted):
return out
def _remap_placements(placements, keep_sorted):
"""컷 구간 [(p0,p1)] 을 무음 제거 타임라인으로 재매핑.
자막(`_remap_caps`) 목적이 다르다. 자막은 '그 말이 나오는 시각' 따라가면 되지만
카드는 ** 구간 자체** 옮겨야 한다. 카드 시간을 압축 전에 만들어 두고 나중에
자막처럼 재매핑하면 `cards_fixed`(정확히 3) 2초로 눌리고, 3 하한도 사라진다.
구간을 먼저 옮기고 안에서 나누면 지켜진다.
통째로 무음이라 사라진 컷은 (x, x) 구간이 되고 `_cards_by_cut` 건너뛴다.
"""
return [(_elapsed_kept(keep_sorted, p0), _elapsed_kept(keep_sorted, p1))
for p0, p1 in placements]
def _safe_name(name: str) -> str:
s = "".join(c for c in name if c.isalnum() or c in (" ", "_", "-", ".")).strip()
return s[:60] or "video"
@ -417,11 +440,7 @@ 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)
card_places = placements # 카드 배치 기준 구간(무음 제거 시 압축본으로 교체)
video_clips = [(0.0, dur)] # 병합본 = 한 덩어리(재컷 없음)
timeline_dur = dur
@ -436,7 +455,8 @@ 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)
# 카드는 '시간'이 아니라 '컷 구간'을 옮긴다 — 아래에서 이 구간 안에 나눠 넣는다.
card_places = _remap_placements(placements, keep)
video_clips = keep
timeline_dur = sum(e - s for s, e in keep)
yield {"type": "log",
@ -490,7 +510,13 @@ async def process_paste(
video_clips = split_clips_at_scenes(video_clips, scenes)
yield {"type": "log", "msg": f"장면전환 {len(scenes)}곳 → 세그먼트 {len(video_clips)}"}
# 댓글 카드: 컷 소속이 지정됐으면 그 컷 구간 안(위에서 계산), 아니면 폴더 균등 배치
# 댓글 카드(자동 탭): '몇 번 컷 소속'만 받아 여기서 시간을 만든다.
# 서버가 시간을 확정하면 무음 제거 때 자막만 당겨지고 카드는 혼자 어긋난다.
# ⚠ 무음 제거 **뒤**에 계산한다 — 압축 전 시간으로 만들어 재매핑하면
# cards_fixed(정확히 3초)가 눌리고 카드당 3초 하한이 사라진다.
cut_cards = _cards_by_cut(_card_paths(comments_dir), card_cuts or [],
card_places, timeline_dur, fixed=cards_fixed)
# 컷 소속이 지정됐으면 그 컷 구간 안, 아니면 폴더 전체 균등 배치
cards = cut_cards or _load_comment_cards(comments_dir, timeline_dur, fixed=cards_fixed)
if cards:
yield {"type": "log",