컷별 카드 장수 공식을 floor로 통일 — round와 어긋나 카드가 조용히 버려지던 문제 수정
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) <noreply@anthropic.com>
This commit is contained in:
parent
754692a343
commit
c39b72518f
@ -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(
|
||||
|
||||
@ -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]]],
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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 배치 — 시간은 파이프라인이 계산한다
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user