process_bg_template을 bg_analyze/bg_draft로 분할

댓글 매칭을 받아쓰기 다음 단계에 끼워 넣으려면 파이프라인이 "받아쓰기까지"에서
한 번 멈출 수 있어야 한다. 다운로드~asr(bg_analyze)과 장면분할~draft(bg_draft)
두 조각으로 나누고, 기존 동작을 완전히 보존하는 얇은 래퍼(process_bg_template)로
다시 감쌌다. 이벤트 스트림을 분할 전/후로 떠서 문자 단위로 비교해 11개 이벤트가
완전히 동일함을 확인했다(state 이벤트는 밖으로 새지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-08-04 17:28:54 +09:00
parent 22a9758e74
commit 475b7b0f71

View File

@ -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], video_path: Optional[str],
draft_name: str, draft_name: str,
*, *,
title_top: str = "", title_top: str = "",
title_main: str = "", title_main: str = "",
channel: 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, youtube: Optional[dict] = None,
) -> AsyncIterator[dict]: ) -> AsyncIterator[dict]:
"""배경템플릿 파이프라인: [유튜브 구간 다운로드] → 무음컷 → 자막 → 드래프트. SSE 이벤트.""" """배경템플릿 파이프라인 앞부분: [유튜브 구간 다운로드] → probe → 무음컷 → 받아쓰기.
마지막에 `bg_draft` 이어줄 `{"type": "state", ...}` 낸다.
(오디오가 전부 무음이면 `error` 내고 조용히 끝난다 이때는 `state` 나온다.)
"""
t_all = time.perf_counter() t_all = time.perf_counter()
use_gemini = has_gemini_key() 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 입력 시) ── # ── 유튜브 여러 구간 다운로드 + 병합 (URL 입력 시) ──
if youtube: if youtube:
@ -301,6 +302,41 @@ async def process_bg_template(
"elapsed": round(time.perf_counter() - t, 1), "elapsed": round(time.perf_counter() - t, 1),
"detail": f"{method} · 자막 {len(captions)}"} "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: if scene_split:
yield {"type": "log", "msg": "장면전환 감지 중… (화면 바뀌는 컷 찾기)"} yield {"type": "log", "msg": "장면전환 감지 중… (화면 바뀌는 컷 찾기)"}
@ -314,7 +350,8 @@ async def process_bg_template(
yield {"type": "step", "id": "draft", "status": "start"} yield {"type": "step", "id": "draft", "status": "start"}
t = time.perf_counter() t = time.perf_counter()
frame, bg, pos = await asyncio.to_thread(_template_pos, bg_white) 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: if cards:
yield {"type": "log", "msg": f"댓글 카드 {len(cards)}개 하단 삽입(3초 간격)"} yield {"type": "log", "msg": f"댓글 카드 {len(cards)}개 하단 삽입(3초 간격)"}
path = await asyncio.to_thread( 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) ─────────────────── # ── 템플릿 레이아웃 (캔버스 1080×1920, 단위 = 픽셀, 위가 0) ───────────────────
# 레퍼런스 템플릿을 실측해 잡은 값. **여기만 고치면 전체 배치가 같이 움직인다.** # 레퍼런스 템플릿을 실측해 잡은 값. **여기만 고치면 전체 배치가 같이 움직인다.**
# (예전엔 배경.png 흰밴드 자동감지였는데, 좌표를 정확히 통제하려고 상수로 바꿨다) # (예전엔 배경.png 흰밴드 자동감지였는데, 좌표를 정확히 통제하려고 상수로 바꿨다)