# 파이프라인 프로세스 재검토 — 검증 시점 문제 진단 > Phase I 실행 완료 후 실제 구동 중 발견된 프로세스 구조 문제. > Phase I의 코드 변경(14개 항목)은 유효하나, **검증이 배치된 시점**이 부적절. --- ## 현재 프로세스 흐름 (as-is) ``` [1단계] Kei 실장 — 콘텐츠 분석 + 스토리라인 설계 ├ 1-A: 꼭지 추출 (Kei API) ├ 1-B: 컨셉 구체화 (Kei API) ├ 제목 중복 검증 (코드) └ 이미지 크기 측정 (Pillow) ↓ [2단계] 디자인 팀장 — 레이아웃 + 블록 매핑 ├ Step A: 프리셋 선택 (규칙 기반) ├ Step A-2: Opus 블록 추천 (Kei API) ├ Step B: Sonnet 블록 매핑 └ 블록 검증 (코드): 미등록 교체, zone 교정, pill-pair, 높이 예산 체크 ↓ [2.5단계] ⚠️ Kei 넘침 판단 — 예상 높이 기반 ↓ [3단계] Kei 편집자 — 텍스트 채움 (Kei API) ↓ [4단계] 디자인 실무자 — CSS 조정 + HTML 렌더링 (Sonnet + Jinja2) ↓ [5단계] 디자인 팀장 — 재검토 + 조정 루프 (Sonnet, 최대 2회) ↓ 미리보기 + HTML 다운로드 ``` --- ## 각 시점에서 알 수 있는 정보 | 시점 | 원본 텍스트 | 꼭지 분석 | 블록 배치 | 실제 텍스트 | 렌더링 HTML | 실제 높이 | |------|:---------:|:--------:|:--------:|:---------:|:----------:|:--------:| | 1단계 후 | O | O | - | - | - | - | | 2단계 후 | O | O | O | - | - | 예상만 | | 2.5단계 | O | O | O | **없음** | **없음** | 예상만 | | 3단계 후 | O | O | O | **O** | - | - | | 4단계 후 | O | O | O | O | **O** | 측정 가능 | | 5단계 | O | O | O | O | O | 측정 가능 | --- ## 문제 진단 (6건) ### 문제 1: 내용 없이 넘침 판단 **위치:** Stage 2.5 **현상:** Kei에게 "이 zone이 넘친다"고 전달하지만, 실제 텍스트가 없는 상태. 블록 타입의 예상 높이(medium=150px, large=250px)만으로 판단 요청. **문제:** Kei가 "trim할까 restructure할까"를 결정하려면 실제 콘텐츠를 봐야 하는데 볼 수 없음. 판단 근거가 부족한 상태에서 판단을 요청. --- ### 문제 2: 예상 높이 초과 → 판단 주체 잘못됨 **위치:** Stage 2.5 **현상:** Sonnet에게 이미 "zone 예산 490px, height_cost 확인해서 초과하지 마라"고 프롬프트로 지시함. 그런데도 예상 높이가 초과하면 그건 **Sonnet이 지시를 안 따른 것**. **문제:** Sonnet의 지시 불이행을 Kei에게 물어볼 문제가 아님. Sonnet을 다시 호출하거나 프롬프트를 개선할 문제. 판단 주체와 해결 주체가 불일치. --- ### 문제 3: 실제 HTML이 있는데 넘침을 안 봄 **위치:** Stage 5 **현상:** 렌더링된 HTML이 있고, 각 블록의 실제 텍스트 양도 알 수 있는 시점. 그러나 현재 Stage 5의 점검 항목은 "빈 블록, 채움 불균형, 정보량, HTML 구조"만. **문제:** 정작 "컨테이너에 실제로 넘치는가"는 점검 항목에 없음. 넘침을 확인할 수 있는 최적의 시점에서 확인하지 않음. --- ### 문제 4: 넘침 판단에 Kei가 없음 **위치:** Stage 5 **현상:** Stage 5 재검토는 Sonnet이 단독으로 수행. 조정도 expand/shrink/rewrite를 Sonnet이 결정. **문제:** 넘침 발생 시 "뭘 줄이고 뭘 팝업으로 분리할지"는 **콘텐츠 중요도 판단** — Kei가 해야 할 일. 현재 Stage 5에 Kei 참여 경로가 없음. --- ### 문제 5: 실제 렌더링 높이 측정 수단 없음 **위치:** 전체 파이프라인 **현상:** 파이프라인 어디에서도 렌더링된 HTML의 실제 px 높이를 측정하지 않음. - Stage 2: 블록 타입 기반 예상 높이 (HEIGHT_COST_PX: compact=70, medium=150, large=250, xlarge=400) - Stage 5: Sonnet이 HTML 코드를 읽고 눈대중으로 판단 **문제:** 예상 높이와 실제 높이는 다를 수 있음. 텍스트 양, CSS 조정, 폰트 크기에 따라 실제 높이가 달라지는데 이를 측정하는 코드가 없음. --- ### 문제 6: 넘침이 재검토 루프에 포함 안 됨 **위치:** Stage 5 루프 **현상:** Stage 5는 `재검토 → 조정 → fill_content(Stage 3) → render(Stage 4) → 재검토` 루프가 있음 (최대 2회). **문제:** 이 루프 안에 넘침 판단이 없음. 조정 후에도 여전히 넘칠 수 있는데, expand 조정으로 텍스트가 늘어나서 오히려 더 넘칠 수도 있음. 루프가 넘침을 감지하지 못함. --- ## 문제 요약 매트릭스 | # | 문제 | 위치 | 핵심 원인 | 영향 | |---|------|------|----------|------| | 1 | 내용 없이 넘침 판단 | 2.5 | 텍스트 채움 전에 판단 | Kei 판단 근거 부족 → 부정확한 결정 | | 2 | 예상 높이 초과 → Kei에게 물음 | 2.5 | 판단 주체 잘못됨 | Sonnet 지시 불이행을 Kei가 해결할 수 없음 | | 3 | HTML 있는데 넘침 안 봄 | 5 | 점검 항목 누락 | 실제 넘침 감지 못함 | | 4 | 넘침 판단에 Kei 없음 | 5 | Sonnet만 참여 | 콘텐츠 중요도 무시한 조정 | | 5 | 실제 높이 측정 없음 | 전체 | 측정 수단 부재 | 예상과 실제의 차이 감지 불가 | | 6 | 넘침이 루프에 없음 | 5 루프 | 넘침 체크 미포함 | 조정 후 넘침 악화 가능 | --- ## 원인 관계 ``` 근본 원인: Stage 2.5의 넘침 판단 위치가 기존 DOWNGRADE_MAP 위치를 그대로 따름 ↓ 메커니즘만 변경(DOWNGRADE → Kei), 시점은 재검토 안 함 ↓ 내용 없이 판단(문제 1) + 주체 잘못됨(문제 2) ↓ 실제 넘침이 감지되는 시점(Stage 4 이후)에는 검증 없음(문제 3, 4, 6) ↓ 애초에 실제 높이 측정 수단도 없음(문제 5) ``` --- ## 해결 방안 조사 결과 ### 방안 1: 실제 렌더링 높이 측정 (문제 5 해결) 현재 파이프라인에는 렌더링된 HTML의 실제 px 높이를 측정하는 수단이 없음. | 도구 | 정확도 | 속도 | CSS Grid | CSS 변수 | 커스텀 폰트 | 설치 상태 | |------|--------|------|----------|----------|------------|----------| | **Playwright** | 픽셀 정확 | 20~50ms/요소 | 완전 지원 | 완전 지원 | 완전 지원 | 미설치 | | Selenium | 픽셀 정확 | 50~150ms/요소 | 완전 지원 | 완전 지원 | 완전 지원 | **설치됨** (4.34.0) | | WeasyPrint | 제한적 | 200~500ms | 부분 지원 | 제한적 | 지원 | **설치됨** (65.1) | | 텍스트 추정 | ±15~30% 오차 | <1ms | 불가 | 불가 | 불가 | — | **권장: Playwright** — 가장 정확하고 빠름. 비동기 지원. headless Chromium 자동 설치. **차선: Selenium** — 이미 설치됨. 동기식이라 약간 느리지만 충분히 사용 가능. **측정 방식:** ```python # Playwright 예시 async with async_playwright() as p: browser = await p.chromium.launch() page = await browser.new_page(viewport={"width": 1280, "height": 720}) await page.set_content(html) # 각 zone의 실제 높이 측정 body_box = await page.locator("[data-zone='body']").bounding_box() actual_height = body_box["height"] # 실제 렌더링 px # overflow 감지: scrollHeight > clientHeight overflow = await page.evaluate(""" el => el.scrollHeight > el.clientHeight """, await page.query_selector("[data-zone='body']")) ``` --- ### 방안 2: Stage 2.5 → Stage 5로 이동 (문제 1, 2, 3, 4, 6 해결) **현재:** Stage 2.5에서 텍스트 없이 Kei 판단 → 근거 부족 **개선:** Stage 4(렌더링) 이후, Stage 5(재검토) 안에서 넘침 판단 ``` 현재: Stage 2 → [2.5 Kei 넘침 판단] → Stage 3 → Stage 4 → Stage 5(Sonnet만) 개선: Stage 2 → Stage 3 → Stage 4 → Stage 5(Sonnet 감지 + Kei 판단) ``` **Stage 5 역할 확장:** 1. **Sonnet이 감지**: 렌더링된 HTML + zone 예산 정보를 보고 넘침 여부 판단 2. **넘침이면 Kei에게 전달**: 실제 콘텐츠가 있는 상태에서 Kei가 판단 3. **Kei가 결정**: trim(텍스트 축약) 또는 restructure(팝업 분리) 4. **Sonnet이 실행**: CSS 조정 + 재렌더링 **Sonnet + Kei 협업 모델:** ``` Sonnet: "body zone이 520px인데 예산 490px. 30px 초과." ↓ Kei: "꼭지 3의 부연 설명을 축약하면 됨. 핵심은 유지." (trim) 또는 Kei: "12행 비교표는 팝업으로 분리. 슬라이드엔 요약만." (restructure) ↓ Sonnet: CSS 조정 + 재렌더링 ``` --- ### 방안 3: Stage 2 구조적 검증은 유지하되 역할 한정 (문제 2 해결) Stage 2의 `_validate_height_budget()`는 **구조적 검증만** 담당: - 금지 블록 교체 (BODY_FORBIDDEN_MAP) — 유지 - pill-pair 단독 금지 (I-7) — 유지 - 예상 높이 초과 — **경고만** (Kei 호출 안 함, Stage 5에서 처리) ```python # Stage 2: 경고만 출력, overflow 정보는 Stage 5에서 활용 if total > budget: logger.warning(f"[예상 높이 초과] {area}: {total}px > {budget}px (Stage 5에서 검증)") # Kei 호출 안 함. 실제 렌더링 후 Stage 5에서 정확히 감지. ``` **Sonnet 프롬프트(STEP_B_PROMPT) 개선:** - 현재: height_cost 매핑을 설명하지만 구체적 예시 없음 - 개선: 계산 예시 추가 + "초과 시 reason 필드에 설명" 명시 --- ### 방안 4: 넘침을 Stage 5 재검토 루프에 통합 (문제 6 해결) **현재 Stage 5 루프:** ``` 재검토(Sonnet) → 조정(expand/shrink/rewrite) → 재편집(Kei 편집자) → 재렌더링 → 재검토 ``` **개선 Stage 5 루프:** ``` 재검토(Sonnet, 넘침 포함) → 넘침 있으면: Kei 판단(trim/restructure) → 조정 적용(expand/shrink/rewrite/trim/restructure) → 재편집(Kei 편집자) → 재렌더링 → 재검토 ``` **Stage 5 프롬프트에 추가할 점검 항목:** ``` 6. 높이 제약: 각 zone이 예산을 초과하는가? - 자동 조정(shrink)으로 해결 가능 → shrink - 불가능 → overflow_detected (Kei 판단 필요) ``` **_apply_adjustments()에 추가할 action:** - `overflow_detected` → Kei API 호출 → trim/restructure 적용 --- ## 해결 방안 매트릭스 | 방안 | 해결하는 문제 | 필요 기술 | 구현 난이도 | |------|-------------|----------|------------| | 1. 실제 높이 측정 | 문제 5 | Playwright 또는 Selenium | 중 (의존성 추가) | | 2. 넘침 판단 Stage 5로 이동 | 문제 1, 2, 3, 4 | 코드 리팩토링 | 중 (Stage 2.5 제거, Stage 5 확장) | | 3. Stage 2 경고만 | 문제 2 | 코드 수정 | 소 (Kei 호출 제거, 경고만) | | 4. 넘침을 루프에 통합 | 문제 6 | Stage 5 프롬프트 + 코드 | 중 (새 action + Kei 연동) | **방안 1은 선택적** — Playwright/Selenium 없이도 Sonnet이 HTML을 읽고 넘침을 추정할 수 있음. 정확도는 떨어지지만 현실적. **방안 2+3+4는 필수** — 프로세스 구조 자체의 문제이므로 반드시 수정. --- ## 실행 계획: 프로세스 재설계 (방안 2+3+4) > 충돌/회귀/오류 검토 완료. Phase I 산출물 전부 재사용. 변경 파일 `pipeline.py`만. > Sonnet 신규 투입 0건. Kei API 호출 위치만 이동. 하드코딩/단발성 없음. ### 변경 전후 프로세스 비교 ``` [변경 전] Stage 1 → Stage 2 → [2.5 Kei 넘침 판단 ⚠️] → Stage 3 → Stage 4 → Stage 5(Sonnet만) [변경 후] Stage 1 → Stage 2(경고만) → Stage 3 → Stage 4 → Stage 5(Sonnet 감지 + Kei 판단) ``` ### 변경 상세 (5건, pipeline.py만) #### P-1: Stage 2.5 제거 **위치:** `pipeline.py` 91~136행 (46행) **작업:** 전체 삭제 **영향:** 없음. overflow 키는 layout_concept에 남아 Stage 5에서 참고. **Phase I 회귀 검토:** - `call_kei_overflow_judgment()` — 함수 삭제 안 함. 호출 위치만 Stage 5로 이동. - `_downgrade_fallback()` — 삭제 안 함. Stage 5에서 비상용. - `KEI_OVERFLOW_PROMPT` — 삭제 안 함. Stage 5에서 사용. --- #### P-2: `_review_balance()` 시그니처 + 프롬프트 확장 **위치:** `pipeline.py` 297~363행 **작업:** 1. 시그니처: `(html, layout_concept, content)` → `(html, layout_concept, content, analysis)` 추가 2. 프롬프트에 zone 예산 정보 + overflow 힌트 추가 3. 점검 항목 6번 추가: "높이 초과 — overflow_detected" 4. 출력 format에 `overflow_detected` action 추가 **변경 내용:** ```python # 시그니처 변경 async def _review_balance( html: str, layout_concept: dict[str, Any], content: str, analysis: dict[str, Any], # 추가 ) -> dict[str, Any] | None: # 프롬프트 추가 # 1. zone 예산 정보 (select_preset + LAYOUT_PRESETS에서) preset_name = select_preset(analysis) preset = LAYOUT_PRESETS.get(preset_name, {}) zone_budget_lines = [ f"- {name}: ~{z['budget_px']}px (너비 {z['width_pct']}%)" for name, z in preset.get("zones", {}).items() ] # 2. Stage 2 예상 overflow 힌트 (있으면) overflow_hint = layout_concept.get("overflow", []) # 3. 점검 항목 6번 "6. 높이 초과: 각 zone의 블록+텍스트가 예산을 초과하는가?\n" " - shrink로 해결 가능 → shrink\n" " - 불가능 (콘텐츠가 본질적으로 큼) → overflow_detected\n" # 4. action 추가 "- overflow_detected: 높이 초과로 Kei 판단 필요. 해당 zone과 초과 블록 명시.\n" ``` **충돌:** 기존 5개 점검 + 3개 action 변경 없음. 추가만. **Sonnet 역할:** 넘침 **감지만**. 판단은 Kei. --- #### P-3: Stage 5 루프에 Kei 넘침 판단 통합 **위치:** `pipeline.py` 155~180행 **작업:** 루프 내에서 overflow_detected 시 Kei 호출 추가 ```python for review_round in range(MAX_REVIEW_ROUNDS): review_result = await _review_balance(html, layout_concept, content, analysis) if not review_result or not review_result.get("needs_adjustment"): break # overflow_detected가 있으면 Kei에게 판단 요청 overflow_adjs = [ adj for adj in review_result.get("adjustments", []) if adj.get("action") == "overflow_detected" ] if overflow_adjs: # 실제 콘텐츠가 있는 상태에서 Kei 판단 overflow_context = _build_overflow_context(layout_concept, overflow_adjs) kei_judgment = await call_kei_overflow_judgment( overflow_context, content, analysis ) if kei_judgment is None: logger.warning("[DOWNGRADE 비상] Kei API 실패") for page in layout_concept.get("pages", []): _downgrade_fallback(page.get("blocks", []), overflow_context) else: # Kei 판단을 adjustments에 반영 (overflow_detected → kei_trim/restructure) _convert_kei_judgment(review_result, kei_judgment, analysis) # 모든 조정 적용 (기존 expand/shrink/rewrite + 신규 kei_trim) layout_concept = await _apply_adjustments(layout_concept, review_result, content) html = render_slide(layout_concept) ``` **호출되는 함수:** 모두 Phase I에서 만든 것 재사용 - `call_kei_overflow_judgment()` — kei_client.py (변경 없음, Kei API만 사용) - `_downgrade_fallback()` — design_director.py (변경 없음) **신규 헬퍼 함수 2개:** - `_build_overflow_context()` — overflow_adjs + layout_concept에서 실제 블록 데이터 추출 - `_convert_kei_judgment()` — Kei의 trim/restructure 결정을 review_result.adjustments에 반영 --- #### P-4: `_apply_adjustments()` — kei_trim action 추가 **위치:** `pipeline.py` 366~410행 **작업:** 기존 elif 체인에 kei_trim 분기 추가 ```python # 기존 expand/shrink/rewrite 로직 변경 없음 # 아래 elif만 추가: elif action == "kei_trim": max_chars = adj.get("max_chars", 200) if "char_guide" not in block: block["char_guide"] = {} for key in block.get("char_guide", {}): block["char_guide"][key] = min(block["char_guide"][key], max_chars) if not block["char_guide"]: block["char_guide"] = {"text": max_chars} logger.info(f"조정: {area} → kei_trim max_chars={max_chars}") elif action == "kei_restructure": block["detail_target"] = True if "data" in block: del block["data"] block["reason"] = f"재구성: {adj.get('detail', 'Kei 판단 팝업 분리')}" logger.info(f"조정: {area} → kei_restructure (detail_target)") ``` **충돌:** 없음. 기존 3개 action 변경 0행. 새 elif 추가만. --- #### P-5: 호출부 수정 **위치:** `pipeline.py` 156행 ```python # 현재: review_result = await _review_balance(html, layout_concept, content) # 변경: review_result = await _review_balance(html, layout_concept, content, analysis) ``` **영향:** 이 함수의 호출부는 pipeline.py 156행 1곳만. 다른 파일에서 호출하지 않음. --- ### 변경 파일 총괄 | 파일 | 변경 | Phase I 코드 영향 | |------|------|------------------| | `pipeline.py` | Stage 2.5 제거 + Stage 5 확장 + 헬퍼 2개 + action 2개 | Phase I 함수 재사용, 삭제 0건 | | `design_director.py` | **변경 없음** | — | | `kei_client.py` | **변경 없음** | — | | `content_editor.py` | **변경 없음** | — | | `sse_utils.py` | **변경 없음** | — | ### 검증 매트릭스 | 항목 | 결과 | |------|------| | Phase I 회귀 | **없음** — I-1~I-14 전부 유지, 함수/상수 삭제 0건 | | Kei API 사용 | **유지** — `call_kei_overflow_judgment()` 호출 위치만 Stage 5로 이동 | | Sonnet이 Kei 역할 대체 | **없음** — Sonnet은 감지만, 판단은 Kei만 | | 하드코딩 | **없음** — trim max_chars는 Kei가 결정 | | 단발성 수정 | **없음** — 범용 구조 (어떤 overflow에도 동작) | | 기존 코드 충돌 | **없음** — overflow 키가 중간 단계에서 무시되는 것 확인 | | DOWNGRADE 비상용 | **유지** — Stage 5에서 Kei 실패 시 동일하게 작동 | ### 실행 순서 1. P-1: Stage 2.5 제거 (pipeline.py 91~136행 삭제) 2. P-2: `_review_balance()` 시그니처 + 프롬프트 확장 3. P-3: Stage 5 루프에 Kei 연동 + 헬퍼 함수 2개 4. P-4: `_apply_adjustments()` kei_trim/kei_restructure action 추가 5. P-5: 호출부 `analysis` 파라미터 추가 --- ## 이력 | 날짜 | 내용 | |------|------| | 2026-03-26 | Phase I 실행 완료 후 프로세스 검증 중 발견. 6개 문제 진단. | | 2026-03-26 | 해결 방안 4개 조사. Playwright 높이 측정 + Stage 5 넘침 통합 방향 도출. | | 2026-03-26 | **실행 계획 확정.** 충돌/회귀/오류 검토 완료. P-1~P-5 5건, pipeline.py만 변경. Phase I 산출물 전부 재사용. |