From 2525fb52315d4f5cc968a0dac70677c8ca4c67bb Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 16:59:33 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EA=B0=81=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=8C=90=EB=B3=84=EC=9D=84=20=EC=BB=B7=201=EA=B0=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AA=A8=EB=93=A0=20=EC=BB=B7=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - is_whole() → is_time_based(): 컷 개수와 무관하게 모든 자막이 비었으면 참 * 통짜 모드(1개 컷) + 유튜브 구간 탭(2개 이상 자막 없는 구간)을 둘 다 지원 * 자막 없으면 내용 추천 불가 → 시각 기반으로 배정 - _whole_picks() → _time_based_picks(): 여러 컷 반복 처리 * used 집합을 컷 사이에 공유해 댓글 중복 금지 (앞 컷 우선) * 각 컷의 quota에 맞춰 슬롯 배정, 빈 슬롯은 좋아요 상위로 채움 - build_highlight_cuts() 호출부 업데이트 * 기존 통짜 모드는 그대로 동작 (1개 컷 + 빈 자막 = 참) * 새 규칙이 유튜브 구간 탭으로 확장 검증: - 자막 판별 조건 (empty/whitespace 포함 처리) - 컷 간 중복 금지 확인 - 자동 탭 회귀 테스트 통과 Co-Authored-By: Claude Opus 5 (1M context) --- capcut_agent/recommend.py | 53 ++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/capcut_agent/recommend.py b/capcut_agent/recommend.py index ac48e9b..20fe441 100644 --- a/capcut_agent/recommend.py +++ b/capcut_agent/recommend.py @@ -184,29 +184,42 @@ def ai_pick_cuts(cuts, comments, quotas, *, model: str = AI_MODEL, return _rows_to_picks(rows) -def is_whole(cuts) -> bool: - """통짜 하이라이트인가 — 컷 1개 + 자막 없음(app._whole_hl 이 만드는 모양). +def is_time_based(cuts) -> bool: + """시각 기반으로 배정할 컷 묶음인가 — **모든 컷의 자막이 비었는가**. - 통짜는 자막이 없어 내용 추천의 근거가 없다. 대신 타임라인 시각 = 원본 시각이라 - 시각으로 정확히 맞출 수 있다(스펙 §4.1). + 자막이 없으면 내용 추천의 근거가 없다. 대신 이런 묶음(통짜 모드·유튜브 구간 탭)은 + 구간을 그대로 이어붙이므로 타임라인 시각 = 원본 시각이 성립해 시각으로 맞출 수 있다. + 컷 1개짜리 통짜는 이 규칙의 특수 케이스다(스펙 §1). """ - return len(cuts) == 1 and not (cuts[0].get("bottom") or "").strip() + if not cuts: + return False + return all(not (c.get("bottom") or "").strip() for c in cuts) -def _whole_picks(cut, comments, n: int) -> List[dict]: - """통짜 — 슬롯마다 그 시간대 언급 댓글, 빈 슬롯은 좋아요 상위로 메움.""" - slots = match_slots(comments, cut["start"], cut["end"] - cut["start"], n) - used = {i for i in slots if i is not None} +def _time_based_picks(cuts, comments, quotas) -> List[List[dict]]: + """컷마다 3초 슬롯 배정 — 슬롯 시간대 언급 댓글, 빈 슬롯은 좋아요 상위로 채움. + + `used` 를 컷 사이에 공유해 한 댓글이 두 컷에 들어가지 않게 한다(앞 컷 우선). + """ + used: set = set() no_ts = [c for c in comments if not c.get("times")] - fill = iter(top_liked(no_ts, used, n)) - out: List[dict] = [] - for s in slots: - if s is not None: - out.append({"idx": s, "why": "ts"}) - continue - nxt = next(fill, None) - if nxt is not None: - out.append({"idx": nxt, "why": "like"}) + out: List[List[dict]] = [] + for i, cut in enumerate(cuts): + n = quotas[i] if i < len(quotas) else 0 + slots = match_slots(comments, cut["start"], cut["end"] - cut["start"], n, + exclude=used) + used.update(s for s in slots if s is not None) + fill = iter(top_liked(no_ts, used, n)) + picks: List[dict] = [] + for s in slots: + if s is not None: + picks.append({"idx": s, "why": "ts"}) + continue + nxt = next(fill, None) + if nxt is not None: + picks.append({"idx": nxt, "why": "like"}) + used.add(nxt) + out.append(picks) return out @@ -225,8 +238,8 @@ def build_highlight_cuts(hl, comments, *, key=None): return [], 0, False quotas = quotas_for(cuts) ai_failed = False - if is_whole(cuts): - picks = [_whole_picks(cuts[0], comments, quotas[0])] + if is_time_based(cuts): + picks = _time_based_picks(cuts, comments, quotas) else: ai = ai_pick_cuts(cuts, comments, quotas, key=key) ai_failed = ai is None