From 475b7b0f710ffa091073792f24c62f214da23471 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Tue, 4 Aug 2026 17:28:54 +0900 Subject: [PATCH] =?UTF-8?q?process=5Fbg=5Ftemplate=EC=9D=84=20bg=5Fanalyze?= =?UTF-8?q?/bg=5Fdraft=EB=A1=9C=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 댓글 매칭을 받아쓰기 다음 단계에 끼워 넣으려면 파이프라인이 "받아쓰기까지"에서 한 번 멈출 수 있어야 한다. 다운로드~asr(bg_analyze)과 장면분할~draft(bg_draft) 두 조각으로 나누고, 기존 동작을 완전히 보존하는 얇은 래퍼(process_bg_template)로 다시 감쌌다. 이벤트 스트림을 분할 전/후로 떠서 문자 단위로 비교해 11개 이벤트가 완전히 동일함을 확인했다(state 이벤트는 밖으로 새지 않음). Co-Authored-By: Claude Opus 5 (1M context) --- capcut_agent/pipeline.py | 102 +++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/capcut_agent/pipeline.py b/capcut_agent/pipeline.py index 79936e2..b36d78a 100644 --- a/capcut_agent/pipeline.py +++ b/capcut_agent/pipeline.py @@ -202,29 +202,30 @@ BG_STEPS: List[Dict[str, str]] = [ ] -async def process_bg_template( +def bg_steps(youtube: Optional[dict]) -> List[Dict[str, str]]: + """배경템플릿 파이프라인의 manifest 스텝. 쪼갠 두 조각(bg_analyze/bg_draft)이 함께 내는 전체 목록.""" + steps = ([{"id": "download", "label": "유튜브 여러 구간 다운로드·병합"}] if youtube else []) + return steps + [{"id": "silence", "label": "무음·발화 분석"}, + {"id": "asr", "label": "받아쓰기 (Gemini/Whisper)"}, + {"id": "draft", "label": "템플릿 드래프트 생성"}] + + +async def bg_analyze( video_path: Optional[str], draft_name: str, *, title_top: str = "", title_main: str = "", channel: str = "", - video_scale: float = 1.0, - flip_horizontal: bool = False, - scene_split: bool = False, - comments_dir: str = "", - cards_fixed: bool = False, - bg_white: bool = False, youtube: Optional[dict] = None, ) -> AsyncIterator[dict]: - """배경템플릿 파이프라인: [유튜브 구간 다운로드] → 무음컷 → 자막 → 드래프트. SSE 이벤트.""" + """배경템플릿 파이프라인 앞부분: [유튜브 구간 다운로드] → probe → 무음컷 → 받아쓰기. + + 마지막에 `bg_draft` 로 이어줄 `{"type": "state", ...}` 를 낸다. + (오디오가 전부 무음이면 `error` 를 내고 조용히 끝난다 — 이때는 `state` 가 안 나온다.) + """ t_all = time.perf_counter() use_gemini = has_gemini_key() - steps = ([{"id": "download", "label": "유튜브 여러 구간 다운로드·병합"}] if youtube else []) - steps += [{"id": "silence", "label": "무음·발화 분석"}, - {"id": "asr", "label": "받아쓰기 (Gemini/Whisper)"}, - {"id": "draft", "label": "템플릿 드래프트 생성"}] - yield {"type": "manifest", "steps": steps} # ── 유튜브 여러 구간 다운로드 + 병합 (URL 입력 시) ── if youtube: @@ -301,6 +302,41 @@ async def process_bg_template( "elapsed": round(time.perf_counter() - t, 1), "detail": f"{method} · 자막 {len(captions)}개"} + yield {"type": "state", "state": { + "video_path": video_path, "meta": meta, "keep": keep, + "video_clips": video_clips, "captions": captions, "total": total, + "draft_name": draft_name, "title_top": title_top, "title_main": title_main, + "channel": channel, "t_all": t_all, + }} + + +async def bg_draft( + state: dict, + *, + video_scale: float = 1.0, + flip_horizontal: bool = False, + scene_split: bool = False, + comments_dir: str = "", + cards_fixed: bool = False, + bg_white: bool = False, + comment_cards: Optional[list] = None, +) -> AsyncIterator[dict]: + """배경템플릿 파이프라인 뒷부분: 장면전환 분할 → 댓글 카드 → 드래프트 생성 → result. + + `bg_analyze` 가 낸 `state` 를 이어받는다. `comment_cards` 가 주어지면 + `_load_comment_cards` 대신 그걸 쓴다(댓글 매칭 단계 준비). + """ + video_path = state["video_path"] + meta = state["meta"] + video_clips = state["video_clips"] + captions = state["captions"] + total = state["total"] + draft_name = state["draft_name"] + title_top = state["title_top"] + title_main = state["title_main"] + channel = state["channel"] + t_all = state["t_all"] + # ── 장면전환 분할 (선택): 컷이 바뀌는 지점에서 세그먼트 추가 분할 ── if scene_split: yield {"type": "log", "msg": "장면전환 감지 중… (화면 바뀌는 컷 찾기)"} @@ -314,7 +350,8 @@ async def process_bg_template( yield {"type": "step", "id": "draft", "status": "start"} t = time.perf_counter() frame, bg, pos = await asyncio.to_thread(_template_pos, bg_white) - cards = _load_comment_cards(comments_dir, total, fixed=cards_fixed) + cards = comment_cards if comment_cards is not None else _load_comment_cards( + comments_dir, total, fixed=cards_fixed) if cards: yield {"type": "log", "msg": f"댓글 카드 {len(cards)}개 하단 삽입(3초 간격)"} path = await asyncio.to_thread( @@ -344,6 +381,43 @@ async def process_bg_template( } +async def process_bg_template( + video_path: Optional[str], + draft_name: str, + *, + title_top: str = "", + title_main: str = "", + channel: str = "", + video_scale: float = 1.0, + flip_horizontal: bool = False, + scene_split: bool = False, + comments_dir: str = "", + cards_fixed: bool = False, + bg_white: bool = False, + youtube: Optional[dict] = None, +) -> AsyncIterator[dict]: + """배경템플릿 파이프라인 — 📁 파일 탭 / ▶ 유튜브 구간 탭용 얇은 래퍼. + + analyze/draft 두 조각을 연달아 부른다. `state` 이벤트는 밖으로 안 흘린다 + (기존 UI가 모르는 타입이라 흘리면 로그에 정체불명 이벤트가 찍힌다). + """ + yield {"type": "manifest", "steps": bg_steps(youtube)} + state = None + async for ev in bg_analyze(video_path, draft_name, title_top=title_top, + title_main=title_main, channel=channel, youtube=youtube): + if ev.get("type") == "state": + state = ev["state"] + continue + yield ev + if state is None: + return # analyze 가 error 로 끝난 경우 + async for ev in bg_draft(state, video_scale=video_scale, + flip_horizontal=flip_horizontal, scene_split=scene_split, + comments_dir=comments_dir, cards_fixed=cards_fixed, + bg_white=bg_white): + yield ev + + # ── 템플릿 레이아웃 (캔버스 1080×1920, 단위 = 픽셀, 위가 0) ─────────────────── # 레퍼런스 템플릿을 실측해 잡은 값. **여기만 고치면 전체 배치가 같이 움직인다.** # (예전엔 배경.png 흰밴드 자동감지였는데, 좌표를 정확히 통제하려고 상수로 바꿨다)