diff --git a/server/app.py b/server/app.py index 4d48125..dfe2f3b 100644 --- a/server/app.py +++ b/server/app.py @@ -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})