Files
C.E.L_Slide_test2/scripts/test_phase_r_prime.py
kyeongmin 29f56187c0 Phase P~S 전체 작업물: 검증 스크립트, 블록 템플릿, 설계 문서, 코드 수정
포함 내용:
- 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>
2026-03-31 08:38:06 +09:00

188 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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))