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:
269
scripts/test_phase_t_real.py
Normal file
269
scripts/test_phase_t_real.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""Phase T 실제 데이터 시뮬레이션.
|
||||
|
||||
기존 run(1774922951020)의 실제 Kei API 응답 + 실제 MDX로
|
||||
전 Stage를 시뮬레이션하여 설계 오류를 사전에 잡는다.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
# ── 실제 데이터 로드 ──
|
||||
RUN_DIR = Path("data/runs/1774922951020")
|
||||
STAGE_0_DIR = Path("data/runs/20260401_151426")
|
||||
|
||||
# Stage 0 결과 (실제 실행된 것)
|
||||
stage0_ctx = json.loads((STAGE_0_DIR / "stage_0_context.json").read_text(encoding="utf-8"))
|
||||
raw_content = stage0_ctx["raw_content"]
|
||||
normalized = stage0_ctx["normalized"]
|
||||
|
||||
# Kei 1A 실제 응답
|
||||
analysis_1a = json.loads((RUN_DIR / "step1_analysis.json").read_text(encoding="utf-8"))
|
||||
|
||||
# Kei 1B 실제 응답
|
||||
concepts_1b = json.loads((RUN_DIR / "step1b_concepts.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test():
|
||||
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
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 0: 이미 실행됨 — 결과 확인만
|
||||
# ══════════════════════════════════════
|
||||
print("── Stage 0: 실제 결과 확인 ──")
|
||||
check("clean_text", len(normalized["clean_text"]) > 200)
|
||||
check("title", normalized["title"] == "건설산업 DX의 올바른 이해")
|
||||
check("images", len(normalized["images"]) == 1)
|
||||
check("popups", len(normalized["popups"]) == 2, f"got {len(normalized['popups'])}")
|
||||
check("sections", len(normalized["sections"]) >= 3)
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 1A: 실제 Kei 응답 → Topic 모델 변환
|
||||
# ══════════════════════════════════════
|
||||
print("\n── Stage 1A: 실제 Kei 응답 → Topic 변환 ──")
|
||||
|
||||
from src.pipeline_context import Topic, FontHierarchy
|
||||
|
||||
# 실제 Kei 응답의 topic dict 구조 확인
|
||||
topics_raw = analysis_1a.get("topics", [])
|
||||
print(f" Kei 반환 topic 수: {len(topics_raw)}")
|
||||
print(f" Kei topic 키: {list(topics_raw[0].keys()) if topics_raw else '없음'}")
|
||||
|
||||
# 실제 변환 시도 (pipeline.py의 코드와 동일)
|
||||
try:
|
||||
topics = [Topic(**{k: v for k, v in t.items() if k in Topic.model_fields}) for t in topics_raw]
|
||||
check("Topic 변환 성공", True)
|
||||
for t in topics:
|
||||
print(f" topic {t.id}: {t.title} / {t.purpose} / role={t.role}")
|
||||
except Exception as e:
|
||||
check("Topic 변환", False, str(e))
|
||||
return False
|
||||
|
||||
# Kei가 안 주는 필드 확인
|
||||
kei_keys = set(topics_raw[0].keys()) if topics_raw else set()
|
||||
topic_keys = set(Topic.model_fields.keys())
|
||||
missing_from_kei = topic_keys - kei_keys
|
||||
extra_from_kei = kei_keys - topic_keys
|
||||
print(f" Topic 모델에 있고 Kei에 없는 필드: {missing_from_kei}")
|
||||
print(f" Kei에 있고 Topic 모델에 없는 필드: {extra_from_kei}")
|
||||
check("Kei 미제공 필드가 기본값으로 처리됨",
|
||||
all(hasattr(topics[0], f) for f in missing_from_kei))
|
||||
|
||||
# 1A 검증
|
||||
from src.validators import validate_stage_1a
|
||||
errors_1a = validate_stage_1a(analysis_1a, normalized["clean_text"])
|
||||
check(f"1A 검증 통과", not errors_1a)
|
||||
for e in errors_1a:
|
||||
print(f" {e['severity']}: {e.get('localization', '')}")
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 1B: 실제 Kei 1B 응답 병합
|
||||
# ══════════════════════════════════════
|
||||
print("\n── Stage 1B: 실제 Kei 1B 응답 병합 ──")
|
||||
|
||||
concepts = concepts_1b.get("concepts", [])
|
||||
print(f" Kei 1B 반환 수: {len(concepts)}")
|
||||
|
||||
# 병합 (pipeline.py의 코드와 동일)
|
||||
updated_topics = []
|
||||
for t in topics:
|
||||
match = next((c for c in concepts if c.get("id") == t.id), None)
|
||||
if match:
|
||||
updated = t.model_copy(update={
|
||||
"relation_type": match.get("relation_type", t.relation_type),
|
||||
"expression_hint": match.get("expression_hint", t.expression_hint),
|
||||
"source_data": match.get("source_data", t.source_data),
|
||||
})
|
||||
updated_topics.append(updated)
|
||||
else:
|
||||
updated_topics.append(t)
|
||||
|
||||
check("1B 병합 성공", len(updated_topics) == len(topics))
|
||||
for t in updated_topics:
|
||||
print(f" topic {t.id}: relation={t.relation_type}, hint={t.expression_hint[:30]}...")
|
||||
|
||||
# 1B 검증 (raw_content 포함 — popups 대조)
|
||||
from src.validators import validate_stage_1b
|
||||
errors_1b = validate_stage_1b(
|
||||
[t.model_dump() for t in updated_topics],
|
||||
normalized["clean_text"],
|
||||
raw_content=raw_content,
|
||||
)
|
||||
check(f"1B 검증 통과", not errors_1b)
|
||||
for e in errors_1b:
|
||||
print(f" {e['severity']}: {e.get('localization', '')}")
|
||||
if e.get("evidence"):
|
||||
print(f" 증거: {str(e['evidence'])[:100]}")
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 1.5a: 폰트 위계 + 동적 비율
|
||||
# ══════════════════════════════════════
|
||||
print("\n── Stage 1.5a: 폰트 위계 + 동적 비율 ──")
|
||||
|
||||
from src.pipeline_context import PipelineContext, create_context, NormalizedContent, Analysis, PageStructure
|
||||
from src.space_allocator import calculate_font_hierarchy, calculate_dynamic_ratio
|
||||
|
||||
# context 구성 (실제 데이터)
|
||||
ctx = create_context(raw_content)
|
||||
ctx = ctx.model_copy(update={
|
||||
"normalized": NormalizedContent(**normalized),
|
||||
"topics": updated_topics,
|
||||
"page_structure": PageStructure(roles=analysis_1a.get("page_structure", {})),
|
||||
"analysis": Analysis(
|
||||
core_message=analysis_1a.get("core_message", ""),
|
||||
title=analysis_1a.get("title", ""),
|
||||
),
|
||||
})
|
||||
|
||||
# 역할별 텍스트 양
|
||||
role_text_lengths = {}
|
||||
for role in ["배경", "본심", "첨부", "결론"]:
|
||||
role_text = ctx.get_role_content(role)
|
||||
role_text_lengths[role] = len(role_text)
|
||||
print(f" {role}: {len(role_text)}자")
|
||||
|
||||
fh_dict = calculate_font_hierarchy(role_text_lengths)
|
||||
try:
|
||||
fh = FontHierarchy(
|
||||
key_msg=fh_dict.get("핵심", 14), core=fh_dict.get("본심", 12),
|
||||
bg=fh_dict.get("배경", 11), sidebar=fh_dict.get("첨부", 10),
|
||||
)
|
||||
check("폰트 위계 생성", True)
|
||||
print(f" 위계: 핵심={fh.key_msg} > 본심={fh.core} >= 배경={fh.bg} > 첨부={fh.sidebar}")
|
||||
except Exception as e:
|
||||
check("폰트 위계", False, str(e))
|
||||
return False
|
||||
|
||||
ratio = calculate_dynamic_ratio(role_text_lengths, fh_dict)
|
||||
check("동적 비율 생성", ratio[0] + ratio[1] == 100)
|
||||
print(f" 비율: body:sidebar = {ratio[0]}:{ratio[1]}")
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 1.7: 참고 블록 선택
|
||||
# ══════════════════════════════════════
|
||||
print("\n── Stage 1.7: 참고 블록 선택 ──")
|
||||
|
||||
from src.block_reference import select_and_generate_references
|
||||
from src.space_allocator import calculate_container_specs
|
||||
from src.design_director import LAYOUT_PRESETS, select_preset
|
||||
|
||||
preset_name = select_preset(analysis_1a)
|
||||
preset = LAYOUT_PRESETS.get(preset_name, {})
|
||||
print(f" 프리셋: {preset_name}")
|
||||
|
||||
container_specs = calculate_container_specs(
|
||||
page_structure=analysis_1a.get("page_structure", {}),
|
||||
topics=[t.model_dump() for t in updated_topics],
|
||||
preset=preset,
|
||||
)
|
||||
print(f" 컨테이너: {', '.join(f'{r}={s.height_px}px' for r, s in container_specs.items())}")
|
||||
|
||||
refs = select_and_generate_references(
|
||||
[t.model_dump() for t in updated_topics],
|
||||
container_specs,
|
||||
analysis_1a.get("page_structure", {}),
|
||||
)
|
||||
check("참고 블록 선택", len(refs) >= 3)
|
||||
for role, ref in refs.items():
|
||||
html_len = len(ref.get("design_reference_html", ""))
|
||||
has_diff = "차별점" in ref.get("design_reference_html", "")
|
||||
print(f" {role}: {ref['block_id']} ({ref['visual_type']}, html={html_len}자, diff={'✅' if has_diff else '—'})")
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 1.5b: 디자인 예산
|
||||
# ══════════════════════════════════════
|
||||
print("\n── Stage 1.5b: 디자인 예산 ──")
|
||||
|
||||
from src.space_allocator import calculate_design_budget
|
||||
|
||||
for role, ref in refs.items():
|
||||
schema = ref.get("schema_info", {})
|
||||
spec = container_specs.get(role)
|
||||
if not spec:
|
||||
continue
|
||||
font_map = {"본심": fh.core, "배경": fh.bg, "첨부": fh.sidebar, "결론": fh.core}
|
||||
budget = calculate_design_budget(spec.height_px, spec.width_px, schema, font_map.get(role, 12))
|
||||
check(f"{role} 예산 (fits={budget['fits']})", True)
|
||||
print(f" {role}: container={spec.height_px}px, text={budget['text_height_px']}px, avail={budget['available_height_px']}px")
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# Stage 2: 프롬프트 supplement 생성
|
||||
# ══════════════════════════════════════
|
||||
print("\n── Stage 2: 프롬프트 supplement ──")
|
||||
|
||||
from src.html_generator import _build_phase_t_supplement
|
||||
|
||||
phase_t_ctx = {
|
||||
"font_hierarchy": fh.model_dump(),
|
||||
"container_ratio": ratio,
|
||||
"references": refs,
|
||||
"design_budgets": {
|
||||
role: calculate_design_budget(
|
||||
container_specs[role].height_px, container_specs[role].width_px,
|
||||
refs.get(role, {}).get("schema_info", {}),
|
||||
{"본심": fh.core, "배경": fh.bg, "첨부": fh.sidebar, "결론": fh.core}.get(role, 12)
|
||||
)
|
||||
for role in container_specs
|
||||
},
|
||||
}
|
||||
analysis_with_t = {**analysis_1a, "phase_t": phase_t_ctx}
|
||||
|
||||
for role in ["배경", "본심", "첨부", "결론"]:
|
||||
supp = _build_phase_t_supplement(role, analysis_with_t)
|
||||
has_font = "폰트 위계" in supp
|
||||
has_budget = "디자인 예산" in supp
|
||||
has_ref = "디자인 레퍼런스" in supp
|
||||
check(f"{role} supplement ({len(supp)}자)", len(supp) > 50)
|
||||
if not has_font:
|
||||
print(f" ⚠️ 폰트 위계 누락")
|
||||
if not has_budget:
|
||||
print(f" ⚠️ 디자인 예산 누락")
|
||||
|
||||
# ══════════════════════════════════════
|
||||
# 결과
|
||||
# ══════════════════════════════════════
|
||||
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 = test()
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user