From 4e479b50ce906f6f166d73530974c1a330d1459b Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 14:09:41 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EC=97=90=20=EC=A7=84=ED=96=89=20=ED=91=9C=EC=8B=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EC=B5=9C=EB=8C=80=20450=EC=B4=88=20=EB=AC=B4?= =?UTF-8?q?=EB=B0=98=EC=9D=91=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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) --- server/app.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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})