From c39b72518ff18cba3d9522fcdcc99d1cae3317e4 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 15:05:14 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=B7=EB=B3=84=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=9E=A5=EC=88=98=20=EA=B3=B5=EC=8B=9D=EC=9D=84=20floor?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20=E2=80=94=20round=EC=99=80=20?= =?UTF-8?q?=EC=96=B4=EA=B8=8B=EB=82=98=20=EC=B9=B4=EB=93=9C=EA=B0=80=20?= =?UTF-8?q?=EC=A1=B0=EC=9A=A9=ED=9E=88=20=EB=B2=84=EB=A0=A4=EC=A7=80?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recommend.quotas_for()는 round, pipeline._cards_by_cut()의 컷당 장수 상한은 floor를 써서 둘이 어긋났다. 5초 컷처럼 round가 floor보다 큰 쪽으로 갈리는 컷은 UI가 카드 2장을 고르게 하고도 빌드 단계에서 상한(1장)에 걸려 뒤 1장이 로그 없이 버려졌다. 사용자 결정에 따라 floor로 통일(카드 한 장이 항상 3초 이상 — 기존 _load_comment_cards 규칙과 동일)하고, 무음 제거로 컷이 압축돼 여전히 카드가 버려지는 경우(이건 불가피)를 컷별 배치 로그에 표시해 더 이상 조용히 사라지지 않게 했다. 관련 설계/계획 문서의 공식·테스트 기대값도 floor 기준으로 맞췄다. Co-Authored-By: Claude Opus 5 (1M context) --- capcut_agent/pipeline.py | 12 +++++++++--- capcut_agent/recommend.py | 10 ++++++++-- .../plans/2026-08-04-컷별-댓글-추천.md | 18 +++++++++--------- .../specs/2026-08-04-컷별-댓글-추천-design.md | 5 ++++- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/capcut_agent/pipeline.py b/capcut_agent/pipeline.py index e95164d..3d87a9e 100644 --- a/capcut_agent/pipeline.py +++ b/capcut_agent/pipeline.py @@ -519,9 +519,15 @@ async def process_paste( # 컷 소속이 지정됐으면 그 컷 구간 안, 아니면 폴더 전체 균등 배치 cards = cut_cards or _load_comment_cards(comments_dir, timeline_dur, fixed=cards_fixed) if cards: - yield {"type": "log", - "msg": f"댓글 카드 {len(cards)}개 하단 삽입" - + ("(컷별 배치)" if cut_cards else "(전체 균등)")} + msg = f"댓글 카드 {len(cards)}개 하단 삽입" + if cut_cards: + # 무음 제거로 컷이 짧아지면 quotas_for(원본 길이)가 고른 장수보다 + # _cards_by_cut(압축 후 길이)의 상한이 작아질 수 있다 — 그 차이를 조용히 삼키지 않는다. + dropped = len(card_cuts or []) - len(cut_cards) + msg += "(컷별 배치" + (f", {dropped}장은 컷 길이가 짧아 제외)" if dropped > 0 else ")") + else: + msg += "(전체 균등)" + yield {"type": "log", "msg": msg} path = await asyncio.to_thread( lambda: build_bg_template_draft( diff --git a/capcut_agent/recommend.py b/capcut_agent/recommend.py index f7e4b60..ac48e9b 100644 --- a/capcut_agent/recommend.py +++ b/capcut_agent/recommend.py @@ -21,8 +21,14 @@ CARD_SEC = 3.0 def quotas_for(cuts) -> List[int]: - """컷별 카드 장수 — max(1, round(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장.""" - return [max(1, round((c["end"] - c["start"]) / CARD_SEC)) for c in cuts] + """컷별 카드 장수 — max(1, floor(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장. + + round 가 아니라 floor 인 이유: `pipeline._cards_by_cut` 의 컷당 장수 상한(무음 제거 후 + 길이 기준)·`pipeline._load_comment_cards` 의 규칙과 같은 공식이어야, 여기서 고른 카드가 + 빌드 단계에서 말없이 잘려나가지 않는다(round 를 쓰면 5초 컷처럼 상한보다 1장 더 골라 + 조용히 버려지는 경우가 생겼다). + """ + return [max(1, int((c["end"] - c["start"]) // CARD_SEC)) for c in cuts] def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]], diff --git a/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md b/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md index c1554b3..e5c6522 100644 --- a/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md +++ b/docs/superpowers/plans/2026-08-04-컷별-댓글-추천.md @@ -160,7 +160,7 @@ Gemini를 호출하지 않으므로 단독 검증이 된다. - Consumes: `comments.match_ranges` - Produces: - `CARD_SEC = 3.0` - - `quotas_for(cuts) -> List[int]` — 컷별 카드 장수 `max(1, round(len/CARD_SEC))` + - `quotas_for(cuts) -> List[int]` — 컷별 카드 장수 `max(1, floor(len/CARD_SEC))` - `build_cut_picks(cuts, comments, ai_picks, quotas) -> List[List[dict]]` — 컷별 `[{"idx": int, "why": "ts"|"ai"}]` @@ -172,9 +172,9 @@ from capcut_agent.recommend import quotas_for, build_cut_picks cuts = [{"start": 100.0, "end": 105.0, "bottom": "아까랑 완전 스타일이 달라"}, {"start": 200.0, "end": 206.0, "bottom": "우리에게 익숙한 평냥은"}] -assert quotas_for(cuts) == [2, 2], quotas_for(cuts) +assert quotas_for(cuts) == [1, 2], quotas_for(cuts) # floor(5/3)=1, floor(6/3)=2 assert quotas_for([{"start": 0.0, "end": 1.0, "bottom": ""}]) == [1] # 짧아도 최소 1 -assert quotas_for([{"start": 0.0, "end": 10.0, "bottom": ""}]) == [3] # round(10/3)=3 +assert quotas_for([{"start": 0.0, "end": 10.0, "bottom": ""}]) == [3] # floor(10/3)=3 cs = [ {"idx": 0, "likeCount": 50, "times": [101.0]}, # 컷0 언급 @@ -226,8 +226,8 @@ CARD_SEC = 3.0 def quotas_for(cuts) -> List[int]: - """컷별 카드 장수 — max(1, round(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장.""" - return [max(1, round((c["end"] - c["start"]) / CARD_SEC)) for c in cuts] + """컷별 카드 장수 — max(1, floor(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장.""" + return [max(1, int((c["end"] - c["start"]) // CARD_SEC)) for c in cuts] def build_cut_picks(cuts, comments, ai_picks: Dict[int, List[int]], @@ -584,14 +584,14 @@ multi = {"paste": {"cuts": [ {"start": 100.0, "end": 105.0, "bottom": "가", "effect": ""}, {"start": 200.0, "end": 206.0, "bottom": "나", "effect": ""}]}} cuts3, need3 = recommend.build_highlight_cuts(multi, cs, key="") -assert need3 == 4, need3 -assert cuts3[0]["picks"] == [{"idx": 1, "why": "ts"}, {"idx": 0, "why": "ts"}] +assert need3 == 3, need3 # floor(5/3)=1 + floor(6/3)=2 +assert cuts3[0]["picks"] == [{"idx": 1, "why": "ts"}] # quota 1 → 좋아요 높은 idx1 만 assert cuts3[1]["picks"] == [] assert cuts3[1]["bottom"] == "나" and cuts3[1]["sec"] == 6.0 # 댓글이 아예 없어도 터지지 않는다 c4, n4 = recommend.build_highlight_cuts(multi, [], key="") -assert n4 == 4 and all(x["picks"] == [] for x in c4) +assert n4 == 3 and all(x["picks"] == [] for x in c4) print("Task5 OK") ``` @@ -1293,7 +1293,7 @@ for tr in j["tracks"]: | §4.1 모드 분기 | Task 5 (`is_whole`) | | §4.2 컷 있는 모드 — Gemini + 타임스탬프 우선 + 중복 제거 | Task 2, 4, 5 | | §4.2 통짜 — 슬롯 배정 + 좋아요 메움 | Task 1, 5 | -| §4.3 `quota = max(1, round(len/3))`, `need = Σ quota` | Task 2, 5 | +| §4.3 `quota = max(1, floor(len/3))`, `need = Σ quota` | Task 2, 5 | | §4.4 `card_cuts` 전달 + `placements` 계산 + 무음 재매핑 | Task 7, 8, 9 | | §5 데이터 스키마 (`cuts[]`, `picks[].why`, `card_cuts`) | Task 5, 6, 8 | | §6 변경 파일 6개 | Task 1~9 | diff --git a/docs/superpowers/specs/2026-08-04-컷별-댓글-추천-design.md b/docs/superpowers/specs/2026-08-04-컷별-댓글-추천-design.md index fd319a5..75a4487 100644 --- a/docs/superpowers/specs/2026-08-04-컷별-댓글-추천-design.md +++ b/docs/superpowers/specs/2026-08-04-컷별-댓글-추천-design.md @@ -91,8 +91,11 @@ ### 4.3 카드 장수 -컷별 `quota_i = max(1, round(len_i / 3))`. 하이라이트의 `need = Σ quota_i` +컷별 `quota_i = max(1, floor(len_i / 3))`. 하이라이트의 `need = Σ quota_i` (기존 `int(total // 3)`을 대체 — 값이 ±1 다를 수 있으나 컷 경계에 맞추는 쪽이 맞다). +round가 아니라 floor인 이유: 빌드 단계(`_cards_by_cut`)의 컷당 장수 상한도 같은 +`max(1, floor(컷길이/3))`이라, round를 쓰면 5초 컷처럼 여기서 2장을 고르고도 +빌드에서 상한(1장)에 걸려 1장이 말없이 버려지는 불일치가 생긴다. ### 4.4 배치 — 시간은 파이프라인이 계산한다