시각 기반 판별을 컷 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)
|
return _rows_to_picks(rows)
|
||||||
|
|
||||||
|
|
||||||
def is_whole(cuts) -> bool:
|
def is_time_based(cuts) -> bool:
|
||||||
"""통짜 하이라이트인가 — 컷 1개 + 자막 없음(app._whole_hl 이 만드는 모양).
|
"""시각 기반으로 배정할 컷 묶음인가 — **모든 컷의 자막이 비었는가**.
|
||||||
|
|
||||||
통짜는 자막이 없어 내용 추천의 근거가 없다. 대신 타임라인 시각 = 원본 시각이라
|
자막이 없으면 내용 추천의 근거가 없다. 대신 이런 묶음(통짜 모드·유튜브 구간 탭)은
|
||||||
시각으로 정확히 맞출 수 있다(스펙 §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]:
|
def _time_based_picks(cuts, comments, quotas) -> List[List[dict]]:
|
||||||
"""통짜 — 슬롯마다 그 시간대 언급 댓글, 빈 슬롯은 좋아요 상위로 메움."""
|
"""컷마다 3초 슬롯 배정 — 슬롯 시간대 언급 댓글, 빈 슬롯은 좋아요 상위로 채움.
|
||||||
slots = match_slots(comments, cut["start"], cut["end"] - cut["start"], n)
|
|
||||||
used = {i for i in slots if i is not None}
|
`used` 를 컷 사이에 공유해 한 댓글이 두 컷에 들어가지 않게 한다(앞 컷 우선).
|
||||||
|
"""
|
||||||
|
used: set = set()
|
||||||
no_ts = [c for c in comments if not c.get("times")]
|
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))
|
fill = iter(top_liked(no_ts, used, n))
|
||||||
out: List[dict] = []
|
picks: List[dict] = []
|
||||||
for s in slots:
|
for s in slots:
|
||||||
if s is not None:
|
if s is not None:
|
||||||
out.append({"idx": s, "why": "ts"})
|
picks.append({"idx": s, "why": "ts"})
|
||||||
continue
|
continue
|
||||||
nxt = next(fill, None)
|
nxt = next(fill, None)
|
||||||
if nxt is not 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
|
return out
|
||||||
|
|
||||||
|
|
||||||
@ -225,8 +238,8 @@ def build_highlight_cuts(hl, comments, *, key=None):
|
|||||||
return [], 0, False
|
return [], 0, False
|
||||||
quotas = quotas_for(cuts)
|
quotas = quotas_for(cuts)
|
||||||
ai_failed = False
|
ai_failed = False
|
||||||
if is_whole(cuts):
|
if is_time_based(cuts):
|
||||||
picks = [_whole_picks(cuts[0], comments, quotas[0])]
|
picks = _time_based_picks(cuts, comments, quotas)
|
||||||
else:
|
else:
|
||||||
ai = ai_pick_cuts(cuts, comments, quotas, key=key)
|
ai = ai_pick_cuts(cuts, comments, quotas, key=key)
|
||||||
ai_failed = ai is None
|
ai_failed = ai is None
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user