fix: 컷별 댓글 배정을 컷 우선이 아닌 순위(라운드) 우선으로 변경

컷 하나를 4순위까지 다 채운 뒤 다음 컷으로 넘어가면, 앞 컷의 약한 근거(4순위
좋아요)가 뒤 컷의 강한 근거(3순위 단어 겹침)보다 먼저 댓글을 가로챈다. 라운드를
바깥 루프로 두어(전 컷의 ts → 전 컷의 ai → 전 컷의 word → 전 컷의 like) 순위가
컷 순서를 이기게 했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 18:47:55 +09:00
parent 011c042778
commit 7c74fec079

View File

@ -97,7 +97,12 @@ def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]],
단어 겹침이 AI 다음인 이유: Gemini 는 문맥까지 보고 고르니 더 정확하지만, 503 등으로 단어 겹침이 AI 다음인 이유: Gemini 는 문맥까지 보고 고르니 더 정확하지만, 503 등으로
실패하거나(`ai_picks=None`) 문맥상 연결을 놓칠 때(예: "와인 뱉는 장면" ↔ "싱크대로 달려간 실패하거나(`ai_picks=None`) 문맥상 연결을 놓칠 때(예: "와인 뱉는 장면" ↔ "싱크대로 달려간
이유"는 겹치는 단어가 없다) 그물을 하나 더 치는 것 — 네트워크 없이도 항상 동작한다. 이유"는 겹치는 단어가 없다) 그물을 하나 더 치는 것 — 네트워크 없이도 항상 동작한다.
중복은 앞 컷이 가져간다(뒤 컷은 다음 후보로 밀린다).
⚠ 컷별로 4단계를 다 채우고 다음 컷으로 넘어가면 안 된다 — 앞 컷의 약한 근거(4순위
좋아요)가 뒤 컷의 강한 근거(3순위 단어 겹침)보다 먼저 댓글을 가져가 버린다. 그래서
**단계(라운드)를 바깥 루프, 컷을 안쪽 루프**로 둔다 — 전 컷의 ts를 다 채운 뒤에야
전 컷의 ai로, 그다음에야 word로 넘어간다. 순위가 컷 순서보다 우선한다.
중복은 앞 컷이 가져간다(뒤 컷은 다음 후보로 밀린다) — `used` 는 라운드·컷을 통틀어 공유.
ai_picks: {컷인덱스: [댓글idx …]} — Gemini 실패 시 None/{} 어느 쪽을 넘겨도 ai_picks: {컷인덱스: [댓글idx …]} — Gemini 실패 시 None/{} 어느 쪽을 넘겨도
타임스탬프만으로 채운다(`ai_pick_cuts` 는 실패를 None 으로 알린다). 타임스탬프만으로 채운다(`ai_pick_cuts` 는 실패를 None 으로 알린다).
@ -107,33 +112,43 @@ def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]],
valid = {c["idx"] for c in comments} valid = {c["idx"] for c in comments}
no_ts = [c for c in comments if not c.get("times")] no_ts = [c for c in comments if not c.get("times")]
used: set = set() used: set = set()
out: List[List[dict]] = [] quota = [quotas[i] if i < len(quotas) else 0 for i in range(len(cuts))]
out: List[List[dict]] = [[] for _ in cuts]
# 1라운드: ⭐시각
for i, cut in enumerate(cuts): for i, cut in enumerate(cuts):
quota = quotas[i] if i < len(quotas) else 0
picks: List[dict] = []
for idx in match_ranges(comments, [(cut["start"], cut["end"])]): for idx in match_ranges(comments, [(cut["start"], cut["end"])]):
if len(picks) >= quota: if len(out[i]) >= quota[i]:
break break
if idx not in used: if idx not in used:
picks.append({"idx": idx, "why": "ts"}) out[i].append({"idx": idx, "why": "ts"})
used.add(idx) used.add(idx)
# 2라운드: 🤖AI
for i in range(len(cuts)):
if len(out[i]) >= quota[i]:
continue
for idx in (ai_picks.get(i) or []): for idx in (ai_picks.get(i) or []):
if len(picks) >= quota: if len(out[i]) >= quota[i]:
break break
if idx in valid and idx not in used: if idx in valid and idx not in used:
picks.append({"idx": idx, "why": "ai"}) out[i].append({"idx": idx, "why": "ai"})
used.add(idx) used.add(idx)
if len(picks) < quota: # 3라운드: 🔤단어겹침
for idx in _word_match_ranked(cut, comments, used): for i, cut in enumerate(cuts):
if len(picks) >= quota: if len(out[i]) >= quota[i]:
break continue
picks.append({"idx": idx, "why": "word"}) for idx in _word_match_ranked(cut, comments, used):
used.add(idx) if len(out[i]) >= quota[i]:
if len(picks) < quota: break
for idx in top_liked(no_ts, used, quota - len(picks)): out[i].append({"idx": idx, "why": "word"})
picks.append({"idx": idx, "why": "like"}) used.add(idx)
used.add(idx) # 4라운드: ➕좋아요
out.append(picks) for i in range(len(cuts)):
if len(out[i]) >= quota[i]:
continue
for idx in top_liked(no_ts, used, quota[i] - len(out[i])):
out[i].append({"idx": idx, "why": "like"})
used.add(idx)
return out return out