Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정
Phase W: - weight 비율 초기 배정 (space_allocator header 높이 반영) - block_assembler 공통 조립 함수 (filled/assembled 통합) - filled → Selenium 측정 → context 저장 - sidebar overflow 확장 + body 재배분 - sub_layouts 사전 계산 (이미지 누락 해결) Phase V': - 팝업 링크 우측상단 배치 (인라인 → position:absolute) - 표 내용 Kei 판단 (공란 크기 계산 → 행/열 산출 → Kei 요약) - 출처 라벨 삭제 + 이미지 아래 캡션 배치 - after 공란 제거 (결론 바로 위까지 body/sidebar 채움) 추가: - V-10 bold 키워드: 기계적 추출 → Kei 문맥 판단 - ** 마크다운 → <strong> 변환 - [이미지:] 마커 제거 (bold 변환 전 처리) - grid-template-rows AFTER 크기 반영 (Sonnet final) - assemble_stage2 CSS font-size override, white-space fix - 하드코딩 전수 검토 완료 - 본심 여러 topic 텍스트 합침 Phase X 계획 문서 작성 (동적 역할 구조) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
235
scripts/test_phase_t_full.py
Normal file
235
scripts/test_phase_t_full.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""Phase T 전체 파이프라인 시뮬레이션 (Stage 0 ~ Stage 5).
|
||||
|
||||
API 호출을 mock으로 대체하여 코드 경로 전체를 검증.
|
||||
실제 Kei 응답(기존 run) + mock Sonnet/Selenium으로 전 Stage 통과 여부 확인.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
# ── 실제 데이터 로드 ──
|
||||
RUN_DIR = Path("data/runs/1774922951020")
|
||||
STAGE_0_DIR = Path("data/runs/20260401_151426")
|
||||
|
||||
stage0_ctx = json.loads((STAGE_0_DIR / "stage_0_context.json").read_text(encoding="utf-8"))
|
||||
raw_content = stage0_ctx["raw_content"]
|
||||
analysis_1a = json.loads((RUN_DIR / "step1_analysis.json").read_text(encoding="utf-8"))
|
||||
concepts_1b = json.loads((RUN_DIR / "step1b_concepts.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
# ── Mock 응답 정의 ──
|
||||
|
||||
async def mock_classify_content(content):
|
||||
"""Stage 1A mock: 실제 Kei 응답 반환"""
|
||||
return analysis_1a
|
||||
|
||||
|
||||
async def mock_refine_concepts(content, analysis):
|
||||
"""Stage 1B mock: 실제 Kei 1B 응답을 analysis에 병합하여 반환"""
|
||||
result = dict(analysis)
|
||||
concepts = concepts_1b.get("concepts", [])
|
||||
for t in result.get("topics", []):
|
||||
match = next((c for c in concepts if c.get("id") == t.get("id")), None)
|
||||
if match:
|
||||
t["relation_type"] = match.get("relation_type", "")
|
||||
t["expression_hint"] = match.get("expression_hint", "")
|
||||
t["source_data"] = match.get("source_data", "")
|
||||
return result
|
||||
|
||||
|
||||
# Stage 2 mock: generate_with_retry → mock HTML 반환
|
||||
MOCK_BODY_HTML = """<div style="overflow:hidden; font-size:12px;">
|
||||
<div class="bg" style="padding:10px;">
|
||||
<h3 style="font-size:11px;">용어 혼용</h3>
|
||||
<p style="font-size:11px;">DX와 BIM이 혼용되어 사용되고 있다</p>
|
||||
</div>
|
||||
<div style="height:12px;"></div>
|
||||
<div class="core" style="padding:10px;">
|
||||
<h3 style="font-size:12px;">DX와 핵심기술의 올바른 관계</h3>
|
||||
<p style="font-size:12px;">DX는 BIM, GIS, 디지털트윈을 포함하는 상위개념이다</p>
|
||||
<div class="key-msg" style="font-size:14px; font-weight:bold;">BIM ≠ DX</div>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
MOCK_SIDEBAR_HTML = """<div style="overflow:hidden; font-size:10px; padding-left:14px; text-indent:-14px;">
|
||||
<h3 style="font-size:10px;">용어 정의</h3>
|
||||
<div style="padding-left:14px; text-indent:-14px;">
|
||||
<p>건설산업: 종합산업</p>
|
||||
<p>BIM: 정보관리도구</p>
|
||||
<p>DX: 디지털 전환</p>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
MOCK_FOOTER_HTML = """<div style="background:linear-gradient(135deg,#1e40af,#3b82f6); padding:14px 30px; text-align:center; border-radius:6px;">
|
||||
<span style="font-size:14px; font-weight:bold; color:white;">BIM은 DX의 기초가 되는 일부분이다</span>
|
||||
</div>"""
|
||||
|
||||
MOCK_GENERATED = {
|
||||
"body_html": MOCK_BODY_HTML,
|
||||
"sidebar_html": MOCK_SIDEBAR_HTML,
|
||||
"footer_html": MOCK_FOOTER_HTML,
|
||||
"reasoning": "mock",
|
||||
}
|
||||
|
||||
MOCK_VERIFICATION = {} # verify_all_areas 결과
|
||||
|
||||
|
||||
async def mock_generate_with_retry(content, analysis, container_specs, preset, images=None):
|
||||
"""Stage 2 mock"""
|
||||
from src.content_verifier import VerificationResult
|
||||
verification = {
|
||||
"body_bg": VerificationResult(passed=True, area_name="body_bg", score=1.0),
|
||||
"body_core": VerificationResult(passed=True, area_name="body_core", score=1.0),
|
||||
"sidebar": VerificationResult(passed=True, area_name="sidebar", score=1.0),
|
||||
"footer": VerificationResult(passed=True, area_name="footer", score=1.0),
|
||||
}
|
||||
return MOCK_GENERATED, verification
|
||||
|
||||
|
||||
def mock_render_slide_from_html(generated, analysis, preset):
|
||||
"""Stage 3 mock"""
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="UTF-8">
|
||||
<style>.slide {{width:1280px;height:720px;padding:40px;}}</style>
|
||||
</head><body><div class="slide">
|
||||
<div class="area-header"><h1>{analysis.get('title','')}</h1></div>
|
||||
<div class="area-body">{generated.get('body_html','')}</div>
|
||||
<div class="area-sidebar">{generated.get('sidebar_html','')}</div>
|
||||
<div class="area-footer">{generated.get('footer_html','')}</div>
|
||||
</div></body></html>"""
|
||||
|
||||
|
||||
def mock_measure_rendered_heights(html):
|
||||
"""Stage 4 L4 mock: overflow 없음"""
|
||||
return {
|
||||
"zones": {
|
||||
"body": {"scrollHeight": 400, "clientHeight": 490, "overflowed": False},
|
||||
"sidebar": {"scrollHeight": 300, "clientHeight": 490, "overflowed": False},
|
||||
"footer": {"scrollHeight": 55, "clientHeight": 60, "overflowed": False},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def mock_capture_slide_screenshot(html):
|
||||
"""Stage 4 L5 mock: 빈 스크린샷"""
|
||||
return "" # 빈 문자열 → 비전 품질 게이트 스킵
|
||||
|
||||
|
||||
async def run_full_simulation():
|
||||
"""전체 파이프라인 시뮬레이션"""
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
def check(name, condition, detail=""):
|
||||
nonlocal passed, failed
|
||||
if condition:
|
||||
print(f" ✅ {name}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" ❌ {name}")
|
||||
if detail:
|
||||
print(f" → {detail}")
|
||||
failed += 1
|
||||
|
||||
# Mock 패치 적용
|
||||
# _retry_kei는 async fn을 await하는 래퍼이므로, mock도 await 해야 함
|
||||
async def mock_retry_kei(fn, *a, **kw):
|
||||
return await fn(*a, **kw)
|
||||
|
||||
with patch("src.pipeline._retry_kei", side_effect=mock_retry_kei), \
|
||||
patch("src.kei_client.classify_content", side_effect=mock_classify_content), \
|
||||
patch("src.kei_client.refine_concepts", side_effect=mock_refine_concepts), \
|
||||
patch("src.content_verifier.generate_with_retry", side_effect=mock_generate_with_retry), \
|
||||
patch("src.renderer.render_slide_from_html", side_effect=mock_render_slide_from_html), \
|
||||
patch("src.slide_measurer.measure_rendered_heights", side_effect=mock_measure_rendered_heights), \
|
||||
patch("src.slide_measurer.capture_slide_screenshot", side_effect=mock_capture_slide_screenshot), \
|
||||
patch("src.image_utils.get_image_sizes", return_value={}), \
|
||||
patch("src.image_utils.embed_images", side_effect=lambda html, bp: html):
|
||||
|
||||
from src.pipeline import generate_slide
|
||||
|
||||
events = []
|
||||
print("── 전체 파이프라인 실행 ──")
|
||||
try:
|
||||
async for event in generate_slide(raw_content):
|
||||
events.append(event)
|
||||
evt_type = event.get("event", "")
|
||||
evt_data = event.get("data", "")
|
||||
if evt_type == "progress":
|
||||
print(f" 📌 {evt_data}")
|
||||
elif evt_type == "error":
|
||||
print(f" ❌ ERROR: {evt_data}")
|
||||
elif evt_type == "result":
|
||||
print(f" 📄 result: {len(evt_data)}자 HTML")
|
||||
except Exception as e:
|
||||
print(f" 💥 EXCEPTION: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
check("파이프라인 예외 없음", False, str(e))
|
||||
print(f"\n{'═' * 55}")
|
||||
print(f" 전체 시뮬레이션: {passed} passed, {failed} failed")
|
||||
print(f"{'═' * 55}")
|
||||
return False
|
||||
|
||||
print()
|
||||
|
||||
# 이벤트 검증
|
||||
event_types = [e["event"] for e in events]
|
||||
check("progress 이벤트 존재", "progress" in event_types)
|
||||
check("error 이벤트 없음", "error" not in event_types,
|
||||
f"errors: {[e['data'] for e in events if e['event']=='error']}")
|
||||
check("result 이벤트 존재", "result" in event_types)
|
||||
|
||||
# result HTML 검증
|
||||
result_events = [e for e in events if e["event"] == "result"]
|
||||
if result_events:
|
||||
html = result_events[0]["data"]
|
||||
check("HTML 비어있지 않음", len(html) > 100, f"길이: {len(html)}")
|
||||
check("HTML에 slide 클래스", "slide" in html)
|
||||
check("HTML에 body 영역", "area-body" in html or "body_html" in html or "bg" in html)
|
||||
check("HTML에 sidebar 영역", "area-sidebar" in html or "sidebar" in html)
|
||||
check("HTML에 footer 영역", "area-footer" in html or "footer" in html)
|
||||
else:
|
||||
check("result HTML", False, "result 이벤트 없음")
|
||||
|
||||
# 스냅샷 파일 확인
|
||||
import glob
|
||||
latest_runs = sorted(glob.glob("data/runs/2026*"), reverse=True)
|
||||
if latest_runs:
|
||||
run_dir = latest_runs[0]
|
||||
files = [Path(f).name for f in glob.glob(f"{run_dir}/*.json")]
|
||||
print(f"\n 스냅샷 폴더: {Path(run_dir).name}")
|
||||
print(f" 저장된 파일: {files}")
|
||||
check("stage_0 스냅샷", "stage_0_context.json" in files)
|
||||
check("stage_1a 스냅샷", "stage_1a_context.json" in files)
|
||||
check("stage_1b 스냅샷", "stage_1b_context.json" in files)
|
||||
check("stage_1_5a 스냅샷", "stage_1_5a_context.json" in files)
|
||||
check("stage_1_7 스냅샷", "stage_1_7_context.json" in files)
|
||||
check("stage_1_5b 스냅샷", "stage_1_5b_context.json" in files)
|
||||
check("stage_2 스냅샷", "stage_2_context.json" in files)
|
||||
check("stage_3 스냅샷", "stage_3_context.json" in files)
|
||||
check("stage_4 스냅샷", "stage_4_context.json" in files)
|
||||
check("final 스냅샷", "final_context.json" in files)
|
||||
check("final.html 저장", "final.html" in [Path(f).name for f in glob.glob(f"{run_dir}/*")])
|
||||
else:
|
||||
check("스냅샷 폴더", False, "run 폴더 없음")
|
||||
|
||||
print(f"\n{'═' * 55}")
|
||||
print(f" 전체 파이프라인 시뮬레이션: {passed} passed, {failed} failed")
|
||||
if failed == 0:
|
||||
print(" 전체 통과 ✅")
|
||||
else:
|
||||
print(f" ❌ {failed}개 실패")
|
||||
print(f"{'═' * 55}")
|
||||
return failed == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(run_full_simulation())
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user