자동 탭 컷별 추천 모델 교체(gemini-3.5-flash) + 429/503 재시도

gemini-2.5-flash 가 HTTP 503("high demand")을 자주 반환해(실측 3연속 503)
ai_pick_cuts 가 재시도 없이 즉시 포기, 하이라이트의 컷 전체가 🤖 0개가 되는
증상의 근본 원인이었다. plan.py 가 이미 쓰는 gemini-3.5-flash 로 모델을
통일하고, 429/503 한정으로 3초·8초 간격 재시도(최대 3회 시도)를 추가했다.
그 외 오류와 재시도 소진 시에는 기존과 동일하게 None 을 반환한다(예외 미노출).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 18:42:57 +09:00
parent 058f03265c
commit 9ea70d01a7

View File

@ -9,6 +9,8 @@ Gemini 없이 단위 검증이 된다.
from __future__ import annotations
import json
import time
import urllib.error
import urllib.request
from typing import Dict, List, Optional
@ -69,7 +71,10 @@ def build_cut_picks(cuts, comments, ai_picks: Optional[Dict[int, List[int]]],
# Gemini에 넘길 댓글 후보 수. 더 늘려도 채택률이 안 오르고 토큰만 먹는다.
AI_CANDIDATES = 150
TEXT_CAP = 200 # 댓글 본문 절단 길이
AI_MODEL = "gemini-2.5-flash" # correct.py 와 같은 무료 티어 모델
# gemini-2.5-flash 는 실측상 HTTP 503("high demand")이 잦아(3연속 503도 관측됨) 컷별
# 추천이 통째로 비는 원인이었다. plan.py 가 이미 쓰는 gemini-3.5-flash 로 통일 —
# 같은 프롬프트·같은 컷으로 실측해도 결과가 동등해 품질 손해는 없다.
AI_MODEL = "gemini-3.5-flash"
_ENDPOINT = ("https://generativelanguage.googleapis.com/v1beta/models/"
"{model}:generateContent?key={key}")
@ -168,13 +173,27 @@ def ai_pick_cuts(cuts, comments, quotas, *, model: str = AI_MODEL,
_ENDPOINT.format(model=model, key=key),
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"})
# 429("quota")·503("high demand")만 짧게 쉬고 재시도한다(최대 2회 더 = 총 3회 시도).
# 둘 다 "지금 당장은 안 됨"이지 "영영 안 됨"이 아니라서다. 그 외 오류는 재시도해 봐야
# 같은 결과라 즉시 포기한다.
RETRY_WAITS = (3.0, 8.0)
raw = None
for attempt in range(1 + len(RETRY_WAITS)):
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode("utf-8"))
raw = data["candidates"][0]["content"]["parts"][0]["text"]
break
except urllib.error.HTTPError as e:
if e.code in (429, 503) and attempt < len(RETRY_WAITS):
time.sleep(RETRY_WAITS[attempt])
continue
return None # 재시도 소진, 또는 재시도 대상이 아닌 HTTP 오류
except (KeyError, IndexError, json.JSONDecodeError, ValueError,
OSError): # URLError, TimeoutError are OSError subclasses; ValueError covers UnicodeDecodeError
return None # 429 포함 — 폴백은 호출부 몫
return None # 폴백은 호출부 몫
if raw is None: # 방어적 — 위 루프는 항상 break/return 으로 빠진다
return None
try:
rows = json.loads(raw)
except (json.JSONDecodeError, TypeError):