From 005beba4c33e9983c8b9c47168f8d86f1be0e66c Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 12:07:48 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=98=EC=9D=B4=EB=9D=BC=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=20=EC=A1=B0=EB=A6=BD=20=E2=80=94=20build=5Fhighlight=5Fcuts()?= =?UTF-8?q?=20=EB=AA=A8=EB=93=9C=20=EB=B6=84=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 통짜 모드(컷 1개 + 자막 없음)는 시각 슬롯으로 배정, 컷 있는 모드는 Gemini 추천. is_whole() 판별, _whole_picks() 슬롯 채우기, build_highlight_cuts() 조립. import에 match_slots, top_liked 추가. Co-Authored-By: Claude Opus 5 (1M context) --- capcut_agent/recommend.py | 50 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/capcut_agent/recommend.py b/capcut_agent/recommend.py index 162155f..0d3865a 100644 --- a/capcut_agent/recommend.py +++ b/capcut_agent/recommend.py @@ -14,7 +14,7 @@ import urllib.request from typing import Dict, List from . import prompts -from .comments import MAX_TIMES, match_ranges +from .comments import MAX_TIMES, match_ranges, match_slots, top_liked from .correct import _gemini_key # 카드 1장이 차지하는 기준 시간(초). pipeline._load_comment_cards(min_sec) 과 같은 값. @@ -153,3 +153,51 @@ def ai_pick_cuts(cuts, comments, quotas, *, model: str = AI_MODEL, OSError): # URLError, TimeoutError are OSError subclasses; ValueError covers UnicodeDecodeError return {} # 429 포함 — 폴백은 호출부 몫 return _parse_ai(raw) + + +def is_whole(cuts) -> bool: + """통짜 하이라이트인가 — 컷 1개 + 자막 없음(app._whole_hl 이 만드는 모양). + + 통짜는 자막이 없어 내용 추천의 근거가 없다. 대신 타임라인 시각 = 원본 시각이라 + 시각으로 정확히 맞출 수 있다(스펙 §4.1). + """ + return len(cuts) == 1 and not (cuts[0].get("bottom") or "").strip() + + +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} + 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"}) + return out + + +def build_highlight_cuts(hl, comments, *, key=None): + """하이라이트 하나 → (cuts[], need). 모드는 컷 모양으로 판별한다. + + Returns: ([{"i","sec","bottom","quota","picks"}], need) + need = Σ quota — 기존 int(total//3) 을 대체한다(컷 경계에 맞추는 쪽이 맞다). + Gemini 실패는 여기서 흡수된다(ai_pick_cuts 가 {} 를 준다) — 예외를 올리지 않는다. + """ + cuts = (hl.get("paste") or {}).get("cuts") or [] + if not cuts: + return [], 0 + quotas = quotas_for(cuts) + if is_whole(cuts): + picks = [_whole_picks(cuts[0], comments, quotas[0])] + else: + ai = ai_pick_cuts(cuts, comments, quotas, key=key) + picks = build_cut_picks(cuts, comments, ai, quotas) + out = [{"i": i, "sec": round(c["end"] - c["start"], 1), + "bottom": c.get("bottom") or "", "quota": quotas[i], "picks": picks[i]} + for i, c in enumerate(cuts)] + return out, sum(quotas)