시각 기반 판별을 컷 1개에서 모든 컷으로 일반화
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
6df1930d06
commit
2525fb5231
@ -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")]
|
||||
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))
|
||||
out: List[dict] = []
|
||||
picks: List[dict] = []
|
||||
for s in slots:
|
||||
if s is not None:
|
||||
out.append({"idx": s, "why": "ts"})
|
||||
picks.append({"idx": s, "why": "ts"})
|
||||
continue
|
||||
nxt = next(fill, None)
|
||||
if nxt is not None:
|
||||
out.append({"idx": nxt, "why": "like"})
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user