diff --git a/capcut_agent/pipeline.py b/capcut_agent/pipeline.py index 71a2e2c..e95164d 100644 --- a/capcut_agent/pipeline.py +++ b/capcut_agent/pipeline.py @@ -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",