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