포함 내용: - Phase P/Q/R/S 설계 문서 (IMPROVEMENT-PHASE-*.md) - 영역별 검증 스크립트 (scripts/verify_*.py, test_*.py) - 블록 템플릿 추가 (cards, emphasis 변형) - 코드 수정: block_search, content_editor, design_director, slide_measurer - catalog.yaml 블록 목록 업데이트 - CLAUDE.md, PROGRESS.md, README.md 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
7.8 KiB
Python
188 lines
7.8 KiB
Python
"""Phase R' 테스트: 접근 C — 블록 CSS 참고 + AI 구조 결정.
|
||
|
||
기존 step1 결과를 재사용하여 html_generator로 HTML 직접 생성.
|
||
블록 선택(block_selector) 없음. 슬롯 채우기(fill_candidates) 없음.
|
||
AI가 콘텐츠에 맞는 HTML 구조를 직접 만든다.
|
||
|
||
사용법:
|
||
python scripts/test_phase_r_prime.py [run_id]
|
||
python scripts/test_phase_r_prime.py 1774736083771
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import sys
|
||
import time
|
||
import datetime
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).parent.parent
|
||
sys.path.insert(0, str(ROOT))
|
||
|
||
|
||
async def main(run_id: str):
|
||
from src.html_generator import generate_slide_html
|
||
from src.html_validator import validate_and_clean_html
|
||
from src.renderer import render_slide_from_html
|
||
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
|
||
from src.design_director import select_preset, LAYOUT_PRESETS
|
||
from src.space_allocator import calculate_container_specs
|
||
import base64
|
||
|
||
run_dir = ROOT / "data" / "runs" / run_id
|
||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
out_dir = ROOT / "data" / "runs" / f"{run_id}_rprime_{timestamp}"
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
print(f"[Phase R' 테스트] run={run_id}")
|
||
print(f" 입력: {run_dir}")
|
||
print(f" 출력: {out_dir}")
|
||
print()
|
||
|
||
# ── Step 1 결과 로딩 (기존 것 재사용) ──
|
||
analysis = json.loads((run_dir / "step1_analysis.json").read_text(encoding="utf-8"))
|
||
concepts = json.loads((run_dir / "step1b_concepts.json").read_text(encoding="utf-8"))
|
||
|
||
concept_map = {c["id"]: c for c in concepts.get("concepts", [])}
|
||
for topic in analysis.get("topics", []):
|
||
tid = topic["id"]
|
||
if tid in concept_map:
|
||
topic["relation_type"] = concept_map[tid].get("relation_type", "none")
|
||
topic["expression_hint"] = concept_map[tid].get("expression_hint", "")
|
||
topic["source_data"] = concept_map[tid].get("source_data", "")
|
||
|
||
# 원본 콘텐츠
|
||
content = """# 건설산업 DX의 올바른 이해
|
||
|
||
## 용어의 혼용
|
||
건설산업에서 DX(Digital Transformation)와 BIM(Building Information Modeling)이 동일 개념으로 인식되고 있다.
|
||
실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다.
|
||
|
||
## 혼용 대표 사례
|
||
1. 스마트 건설 활성화 방안(2022.07): 추진과제를 건설산업 디지털화로 명시하면서 실행과제는 BIM 전면 도입에 국한
|
||
2. 제7차 건설기술진흥 기본계획(2023.12): 추진방향을 디지털 전환으로 제시하면서 추진과제는 BIM 도입으로 한정
|
||
|
||
## DX와 핵심기술의 올바른 관계
|
||
DX는 BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념이다.
|
||
- GIS: 지리적 데이터를 공간 분석하여 시각적으로 표현
|
||
- BIM: 시설물 생애주기 정보를 3차원 모델로 통합 관리
|
||
- 디지털 트윈: 현실 객체를 디지털로 동일하게 구현
|
||
|
||
## 용어별 정의
|
||
- 건설산업: 광범위한 기술을 통합 융합하여 만드는 종합산업
|
||
- BIM: 3차원 모델 기반으로 통합 관리하는 정보 관리 도구
|
||
- DX: 업무방식과 가치 창출 구조를 전환하는 과정 및 결과
|
||
|
||
## 핵심 요약
|
||
BIM은 DX의 기초가 되는 일부분이다. 각 용어의 정의와 상호관계에 대한 체계적 정립이 필요하다.
|
||
"""
|
||
|
||
topics = analysis["topics"]
|
||
t0 = time.time()
|
||
|
||
# ── 컨테이너 계산 (유지) ──
|
||
preset_name = select_preset(analysis)
|
||
preset = LAYOUT_PRESETS[preset_name]
|
||
container_specs = calculate_container_specs(
|
||
analysis.get("page_structure", {}), topics, preset
|
||
)
|
||
|
||
print(f"[{time.time()-t0:.1f}s] 컨테이너 계산:")
|
||
for role, spec in container_specs.items():
|
||
print(f" {role}: {spec.height_px}px × {spec.width_px}px, topics={spec.topic_ids}")
|
||
|
||
_save(out_dir, "step1c_containers.json", {
|
||
role: {"height_px": s.height_px, "width_px": s.width_px, "topic_ids": s.topic_ids}
|
||
for role, s in container_specs.items()
|
||
})
|
||
|
||
# ══════════════════════════════════════
|
||
# ★ Phase R' 핵심: AI HTML 직접 생성
|
||
# block_selector 없음. fill_candidates 없음.
|
||
# ══════════════════════════════════════
|
||
print(f"\n[{time.time()-t0:.1f}s] ★ AI HTML 생성 중... (블록 선택 없음, AI가 구조 결정)")
|
||
|
||
generated = await generate_slide_html(
|
||
content=content,
|
||
analysis=analysis,
|
||
container_specs=container_specs,
|
||
preset=preset,
|
||
)
|
||
|
||
# HTML 정화 + 검증
|
||
generated = validate_and_clean_html(generated)
|
||
|
||
_save(out_dir, "step2_generated.json", {
|
||
"body_html_length": len(generated.get("body_html", "")),
|
||
"sidebar_html_length": len(generated.get("sidebar_html", "")),
|
||
"footer_html_length": len(generated.get("footer_html", "")),
|
||
"reasoning": generated.get("reasoning", ""),
|
||
})
|
||
|
||
print(f"[{time.time()-t0:.1f}s] HTML 생성 완료:")
|
||
print(f" body: {len(generated.get('body_html', ''))}자")
|
||
print(f" sidebar: {len(generated.get('sidebar_html', ''))}자")
|
||
print(f" footer: {len(generated.get('footer_html', ''))}자")
|
||
print(f" 구조 결정 근거: {generated.get('reasoning', '')[:100]}")
|
||
|
||
# ── 렌더링 (AI HTML을 프레임에 삽입) ──
|
||
print(f"\n[{time.time()-t0:.1f}s] 렌더링...")
|
||
|
||
html = render_slide_from_html(generated, analysis, preset)
|
||
_save(out_dir, "step3_rendered.html", html)
|
||
_save(out_dir, "final.html", html)
|
||
|
||
# ── Selenium 측정 ──
|
||
print(f"[{time.time()-t0:.1f}s] Selenium 측정...")
|
||
|
||
measurement = await asyncio.to_thread(measure_rendered_heights, html)
|
||
_save(out_dir, "step4_measurement.json", measurement)
|
||
|
||
slide = measurement.get("slide", {})
|
||
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px "
|
||
f"{'✅' if not slide.get('overflowed') else '❌'}")
|
||
|
||
for name, data in measurement.get("containers", {}).items():
|
||
status = "✅" if not data.get("overflowed") else f"❌ +{data.get('excess_px', 0)}px"
|
||
print(f" {name}: {data.get('scrollHeight', 0)}px / {data.get('allocatedHeight', 0)}px {status}")
|
||
|
||
# ── 스크린샷 ──
|
||
screenshot_b64 = await asyncio.to_thread(capture_slide_screenshot, html)
|
||
if screenshot_b64:
|
||
import base64 as b64
|
||
(out_dir / "screenshot.png").write_bytes(b64.b64decode(screenshot_b64))
|
||
print(f"\n[{time.time()-t0:.1f}s] 스크린샷: {out_dir / 'screenshot.png'}")
|
||
|
||
total = time.time() - t0
|
||
print(f"\n{'='*50}")
|
||
print(f"Phase R' 테스트 완료: {total:.1f}초")
|
||
print(f" 블록 선택: 없음 (AI가 HTML 구조 직접 생성)")
|
||
print(f" 슬롯 채우기: 없음 (AI가 텍스트 직접 포함)")
|
||
print(f" 결과: {out_dir}")
|
||
print(f"{'='*50}")
|
||
|
||
|
||
def _save(out_dir, name, data):
|
||
path = out_dir / name
|
||
if isinstance(data, str):
|
||
path.write_text(data, encoding="utf-8")
|
||
else:
|
||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import logging
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
||
datefmt="%H:%M:%S",
|
||
)
|
||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||
logging.getLogger("selenium").setLevel(logging.WARNING)
|
||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||
|
||
run_id = sys.argv[1] if len(sys.argv) > 1 else "1774736083771"
|
||
asyncio.run(main(run_id))
|