컷별 댓글 추천에 단어 겹침(3순위)·좋아요(4순위) 폴백 추가
Gemini 가 실패하거나(503 등) 문맥은 맞지만 겹치는 단어가 없어 못 잡는
경우("와인 뱉는 장면" ↔ "싱크대로 달려간 이유")가 각각 달라, 대체가 아니라
⭐시각 » 🤖AI 뒤에 한 단계 더 얹었다. 자막에서 조사를 뗀 어간까지 키워드로
뽑아 댓글 본문에 부분 문자열로 걸리면 매칭한다("넉살이"→"넉살"이 "넉살님"에
걸리는 게 핵심 케이스). 네트워크 없이 항상 동작해 Gemini 가 죽어도 0건은
안 나온다. 그래도 자리가 남으면 분:초 없는 댓글 중 좋아요 상위로 채운다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ea70d01a7
commit
b1c08908ae
@ -9,6 +9,7 @@ Gemini 없이 단위 검증이 된다.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@ -21,6 +22,17 @@ from .correct import _gemini_key
|
|||||||
# 카드 1장이 차지하는 기준 시간(초). pipeline._load_comment_cards(min_sec) 과 같은 값.
|
# 카드 1장이 차지하는 기준 시간(초). pipeline._load_comment_cards(min_sec) 과 같은 값.
|
||||||
CARD_SEC = 3.0
|
CARD_SEC = 3.0
|
||||||
|
|
||||||
|
# ── 단어 겹침 매칭(3순위) — Gemini 가 실패해도(503 등) 네트워크 없이 항상 채워지는 폴백 ──
|
||||||
|
# 흔해서 아무 컷에나 걸리는 일반 단어. 걸리면 오탐이 늘어날 뿐이라 미리 뺀다.
|
||||||
|
STOP = {"진짜", "너무", "정말", "그냥", "이거", "저거", "우리", "사람", "이번", "그거", "완전", "진심",
|
||||||
|
"이렇게", "그렇게", "하는", "했다", "있는", "없는", "보고", "보는", "같아", "같은", "이건", "저건",
|
||||||
|
"근데", "그리고", "하지만", "합니다", "입니다"}
|
||||||
|
# 한글 2글자 이상 / 알파벳 3글자 이상 / 숫자 2글자 이상만 키워드 후보로 본다(1글자는 아무 데나 걸린다).
|
||||||
|
TOK = re.compile(r"[가-힣]{2,}|[A-Za-z]{3,}|\d{2,}")
|
||||||
|
# 긴 조사부터 검사해야 짧은 조사가 먼저 걸려 어간이 덜 잘리는 일이 없다.
|
||||||
|
JOSA = ("이야", "에서", "으로", "까지", "부터", "라고", "이고", "은", "는", "이", "가", "을", "를",
|
||||||
|
"의", "에", "도", "만", "와", "과", "랑")
|
||||||
|
|
||||||
|
|
||||||
def quotas_for(cuts) -> List[int]:
|
def quotas_for(cuts) -> List[int]:
|
||||||
"""컷별 카드 장수 — max(1, floor(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장.
|
"""컷별 카드 장수 — max(1, floor(컷길이 / CARD_SEC)). 짧은 컷도 최소 1장.
|
||||||
@ -33,20 +45,67 @@ def quotas_for(cuts) -> List[int]:
|
|||||||
return [max(1, int((c["end"] - c["start"]) // CARD_SEC)) for c in cuts]
|
return [max(1, int((c["end"] - c["start"]) // CARD_SEC)) for c in cuts]
|
||||||
|
|
||||||
|
|
||||||
|
def _cut_keywords(text: str) -> List[str]:
|
||||||
|
"""자막 한 줄 → 댓글 매칭용 키워드(원본 토큰 + 조사 뗀 어간 후보).
|
||||||
|
|
||||||
|
"넉살이"(자막) 를 "넉살님"(댓글) 에 걸리게 하려면 조사를 떼야 한다. 뗀 결과가
|
||||||
|
2글자 미만이면 버린다(1글자 어간은 아무 댓글에나 걸려 오탐만 늘린다).
|
||||||
|
"""
|
||||||
|
out: List[str] = []
|
||||||
|
seen = set()
|
||||||
|
for tok in TOK.findall(text or ""):
|
||||||
|
if tok not in STOP and tok not in seen:
|
||||||
|
seen.add(tok)
|
||||||
|
out.append(tok)
|
||||||
|
for josa in JOSA:
|
||||||
|
if tok.endswith(josa) and len(tok) - len(josa) >= 2:
|
||||||
|
stem = tok[:-len(josa)]
|
||||||
|
if stem not in STOP and stem not in seen:
|
||||||
|
seen.add(stem)
|
||||||
|
out.append(stem)
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _word_match_ranked(cut, comments, used: set) -> List[int]:
|
||||||
|
"""단어 겹침 순위(3순위) — 자막 키워드가 댓글 본문에 부분 문자열로 들어 있는 개수.
|
||||||
|
|
||||||
|
부분 문자열로 보는 이유: 조사·어미가 댓글 쪽에 붙어 있어도(예: "넉살" ⊂ "넉살님")
|
||||||
|
흡수하려는 것. 점수 0은 제외, 점수 내림차순 → 좋아요 내림차순으로 정렬한다.
|
||||||
|
"""
|
||||||
|
keywords = _cut_keywords(cut.get("bottom") or "")
|
||||||
|
if not keywords:
|
||||||
|
return []
|
||||||
|
scored = []
|
||||||
|
for c in comments:
|
||||||
|
if c["idx"] in used:
|
||||||
|
continue
|
||||||
|
text = str(c.get("text") or "")
|
||||||
|
score = sum(1 for kw in keywords if kw in text)
|
||||||
|
if score > 0:
|
||||||
|
scored.append((score, c.get("likeCount", 0), c["idx"]))
|
||||||
|
scored.sort(key=lambda t: (-t[0], -t[1]))
|
||||||
|
return [idx for _, _, idx in scored]
|
||||||
|
|
||||||
|
|
||||||
def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]],
|
def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]],
|
||||||
quotas) -> List[List[dict]]:
|
quotas) -> List[List[dict]]:
|
||||||
"""컷별 추천 확정 — 타임스탬프 우선, 남는 자리만 AI, 전 컷 통틀어 중복 금지.
|
"""컷별 추천 확정 — ⭐시각 » 🤖AI » 🔤단어겹침 » ➕좋아요 순, 전 컷 통틀어 중복 금지.
|
||||||
|
|
||||||
타임스탬프가 AI보다 먼저인 이유: 그 컷의 **원본 구간**을 콕 집어 언급한 댓글은
|
타임스탬프가 AI보다 먼저인 이유: 그 컷의 **원본 구간**을 콕 집어 언급한 댓글은
|
||||||
근거가 확실하다. 추측(AI)을 이기게 둘 이유가 없다.
|
근거가 확실하다. 추측(AI·단어)을 이기게 둘 이유가 없다.
|
||||||
|
단어 겹침이 AI 다음인 이유: Gemini 는 문맥까지 보고 고르니 더 정확하지만, 503 등으로
|
||||||
|
실패하거나(`ai_picks=None`) 문맥상 연결을 놓칠 때(예: "와인 뱉는 장면" ↔ "싱크대로 달려간
|
||||||
|
이유"는 겹치는 단어가 없다) 그물을 하나 더 치는 것 — 네트워크 없이도 항상 동작한다.
|
||||||
중복은 앞 컷이 가져간다(뒤 컷은 다음 후보로 밀린다).
|
중복은 앞 컷이 가져간다(뒤 컷은 다음 후보로 밀린다).
|
||||||
|
|
||||||
ai_picks: {컷인덱스: [댓글idx …]} — Gemini 실패 시 None/{} 어느 쪽을 넘겨도
|
ai_picks: {컷인덱스: [댓글idx …]} — Gemini 실패 시 None/{} 어느 쪽을 넘겨도
|
||||||
타임스탬프만으로 채운다(`ai_pick_cuts` 는 실패를 None 으로 알린다).
|
타임스탬프만으로 채운다(`ai_pick_cuts` 는 실패를 None 으로 알린다).
|
||||||
Returns: 컷별 [{"idx": int, "why": "ts"|"ai"}]
|
Returns: 컷별 [{"idx": int, "why": "ts"|"ai"|"word"|"like"}]
|
||||||
"""
|
"""
|
||||||
ai_picks = ai_picks or {}
|
ai_picks = ai_picks or {}
|
||||||
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")]
|
||||||
used: set = set()
|
used: set = set()
|
||||||
out: List[List[dict]] = []
|
out: List[List[dict]] = []
|
||||||
for i, cut in enumerate(cuts):
|
for i, cut in enumerate(cuts):
|
||||||
@ -64,6 +123,16 @@ def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]],
|
|||||||
if idx in valid and idx not in used:
|
if idx in valid and idx not in used:
|
||||||
picks.append({"idx": idx, "why": "ai"})
|
picks.append({"idx": idx, "why": "ai"})
|
||||||
used.add(idx)
|
used.add(idx)
|
||||||
|
if len(picks) < quota:
|
||||||
|
for idx in _word_match_ranked(cut, comments, used):
|
||||||
|
if len(picks) >= quota:
|
||||||
|
break
|
||||||
|
picks.append({"idx": idx, "why": "word"})
|
||||||
|
used.add(idx)
|
||||||
|
if len(picks) < quota:
|
||||||
|
for idx in top_liked(no_ts, used, quota - len(picks)):
|
||||||
|
picks.append({"idx": idx, "why": "like"})
|
||||||
|
used.add(idx)
|
||||||
out.append(picks)
|
out.append(picks)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user