하이라이트 조립 — build_highlight_cuts() 모드 분기 구현

통짜 모드(컷 1개 + 자막 없음)는 시각 슬롯으로 배정, 컷 있는 모드는 Gemini 추천.
is_whole() 판별, _whole_picks() 슬롯 채우기, build_highlight_cuts() 조립.
import에 match_slots, top_liked 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 12:07:48 +09:00
parent 88768020bf
commit 005beba4c3

View File

@ -14,7 +14,7 @@ import urllib.request
from typing import Dict, List from typing import Dict, List
from . import prompts 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 from .correct import _gemini_key
# 카드 1장이 차지하는 기준 시간(초). pipeline._load_comment_cards(min_sec) 과 같은 값. # 카드 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 OSError): # URLError, TimeoutError are OSError subclasses; ValueError covers UnicodeDecodeError
return {} # 429 포함 — 폴백은 호출부 몫 return {} # 429 포함 — 폴백은 호출부 몫
return _parse_ai(raw) 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)