추천 단계에 진행 표시 추가 (최대 450초 무반응 제거)

/auto/stream 은 하이라이트 5개를 순차로 돌며 각각 Gemini 를 부른다(timeout=90).
manifest 에 recommend 스텝이 없고 루프 안에 로그도 없어서, 마지막으로 보이는 게
"댓글 수집 done" 이고 그 뒤 최악 450초 동안 아무 이벤트도 안 나갔다.
사용자가 멈춘 줄 알고 새로고침하면 분석이 통째로 날아간다.

순차 호출 자체는 유지한다 — 동시에 5발을 쏘면 429 가 난다(Step 3 도 같은 이유로
시차 재시도를 쓴다). 대신 보이게만 만든다:
- 세 갈래 manifest(paste / wpaste / else) 전부에 recommend 스텝 추가
- 루프 앞뒤로 step start/done(detail = 처리한 하이라이트 수)
- 하이라이트마다 "ID n 컷별 댓글 추천 중… (i/N, 컷 M개)" 로그 한 줄

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 14:09:41 +09:00
parent 1e43530933
commit 4e479b50ce

View File

@ -12,6 +12,7 @@ import json
import os
import shutil
import subprocess
import time
import urllib.parse
import urllib.request
@ -354,6 +355,7 @@ async def auto_stream(aid: str) -> StreamingResponse:
yield _sse({"type": "manifest", "steps": [
{"id": "parse", "label": "오팔 JSON 파싱"},
{"id": "comments", "label": "댓글 수집 (h-lab)"},
{"id": "recommend", "label": "컷별 댓글 추천"},
]})
yield _sse({"type": "step", "id": "parse", "status": "start"})
import re as _re
@ -427,6 +429,7 @@ async def auto_stream(aid: str) -> StreamingResponse:
yield _sse({"type": "manifest", "steps": [
{"id": "parse", "label": "구간 JSON 파싱"},
{"id": "comments", "label": "댓글 수집 (h-lab)"},
{"id": "recommend", "label": "컷별 댓글 추천"},
]})
yield _sse({"type": "step", "id": "parse", "status": "start"})
try:
@ -444,6 +447,7 @@ async def auto_stream(aid: str) -> StreamingResponse:
if mode == "full":
steps.append({"id": "step3", "label": "편집안 생성 (Gemini, 구간별 동시)"})
steps.append({"id": "comments", "label": "댓글 수집 (h-lab)"})
steps.append({"id": "recommend", "label": "컷별 댓글 추천"})
yield _sse({"type": "manifest", "steps": steps})
# 댓글은 URL을 이미 아니까 Step1 과 동시에 수집
com_task = asyncio.create_task(asyncio.to_thread(hlab.fetch_comments, url))
@ -526,13 +530,21 @@ async def auto_stream(aid: str) -> StreamingResponse:
# 후보(candidates)는 분:초 언급이 아예 없는 댓글만 — 타임스탬프 댓글은
# 자기 구간의 ⭐에서 잡히므로, 다른 구간 얘기하는 댓글이 섞이지 않게.
#
# 하이라이트마다 Gemini 를 순차로 부른다(동시 호출은 429 를 부른다 — Step 3 도 같은 이유로
# 시차 재시도를 쓴다). 최악 5×90초라 진행 표시가 없으면 사용자가 멈춘 줄 알고 새로고침해
# 분석이 통째로 날아간다 → 스텝 + 하이라이트별 로그를 반드시 흘린다.
no_ts = [c for c in comments if not c["times"]]
for h in highlights:
if "paste" not in h:
continue
targets = [h for h in highlights if "paste" in h]
yield _sse({"type": "step", "id": "recommend", "status": "start"})
t_rec = time.perf_counter()
for n, h in enumerate(targets, 1):
matched = hlab.match_window(comments, h["start"], h["end"])
h["matched"] = matched
h["candidates"] = hlab.top_liked(no_ts, set(matched), len(no_ts))
yield _sse({"type": "log",
"msg": f"ID {h.get('id')} 컷별 댓글 추천 중… "
f"({n}/{len(targets)}, 컷 "
f"{len(h['paste'].get('cuts') or [])}개)"})
# 컷별 추천 — 실패해도 위의 matched/candidates 로 화면이 돌아간다.
# Gemini 호출이 섞여 있어 블로킹이므로 스레드로 뺀다.
try:
@ -549,6 +561,9 @@ async def auto_stream(aid: str) -> StreamingResponse:
warnings.append(
f"ID {h.get('id')} 컷별 추천 실패 — 기존 방식으로 표시 "
f"({type(exc).__name__}: {exc})")
yield _sse({"type": "step", "id": "recommend", "status": "done",
"elapsed": round(time.perf_counter() - t_rec, 1),
"detail": f"{len(targets)}개 하이라이트"})
yield _sse({"type": "result", "highlights": highlights,
"comments": comments, "warnings": warnings})