Files
C.E.L_Slide_test2/scripts/verify_3issues.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

249 lines
11 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' 검증: 3가지 문제를 각각 Kei API에 요청하여 가능 여부 확인.
검증 1: 배경 사례 2건이 박스 안에 온전히 들어가는 HTML
검증 2: DX/GIS/BIM/디지털트윈 상호 관계 시각화 HTML
검증 3: 용어 정의 풀 텍스트 + 출처 포함 HTML
각각 독립적으로 Kei API 호출 → 렌더링 → 스크린샷.
"""
from __future__ import annotations
import asyncio, json, sys, time, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.sse_utils import stream_sse_tokens
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.config import settings
import httpx
out_dir = ROOT / "data" / "runs" / f"verify_3issues_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"출력: {out_dir}\n")
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
t0 = time.time()
# ═══════════════════════════════════════
# 검증 1: 배경 — 문제 제기 + 사례 2건이 176px 안에 들어가는 HTML
# ═══════════════════════════════════════
print("=== 검증 1: 배경 사례 박스 ===")
prompt_1 = """다음 콘텐츠를 176px 높이 × 707px 너비의 다크 배경 박스 안에 HTML로 만들어라.
## 콘텐츠
- 제목: "현실 — 용어의 혼용"
- 본문: "건설산업에서 DX와 BIM이 동일 개념으로 인식되고 있다. 실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다."
- 사례 1: "스마트 건설 활성화 방안(2022.07) — 추진과제: 건설산업 디지털화, 실행과제: BIM 전면 도입, BIM 전문인력 양성"
- 사례 2: "제7차 건설기술진흥 기본계획(2023.12) — 추진방향: 디지털 전환을 통한 스마트 건설 확산, 추진과제: BIM 도입으로 건설산업 디지털화"
## 요구사항
1. 다크 배경(#1e293b → #0f172a 그라데이션), 흰 텍스트
2. 제목: #93c5fd 색상
3. 사례 2건을 가로 나란히 카드로 배치 (border-left: 3px solid #60a5fa)
4. 사례 제목: #fbbf24 (노란색)
5. **176px 높이 안에 모든 내용이 들어가야 한다. 넘치면 안 된다.**
6. 본문과 사례의 텍스트를 축약하지 마라. 위에 제공한 텍스트 그대로 사용.
7. 폰트 크기를 줄여서라도 176px 안에 맞춰라 (최소 10px까지 허용)
## 출력
HTML + inline <style>만 반환. 설명 없이 코드만.
```html
(여기에 HTML)
```"""
html_1 = await _call_kei(kei_url, prompt_1)
if html_1:
wrapped_1 = _wrap_in_slide(html_1, 707, 176)
m_1 = await asyncio.to_thread(measure_rendered_heights, wrapped_1)
s_1 = await asyncio.to_thread(capture_slide_screenshot, wrapped_1)
_save(out_dir, "verify1_background.html", wrapped_1)
if s_1:
(out_dir / "verify1_background.png").write_bytes(base64.b64decode(s_1))
slide = m_1.get("slide", {})
print(f" [{time.time()-t0:.0f}s] 결과: {slide.get('scrollHeight', 0)}px / 720px")
print(f" HTML: {len(html_1)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 생성 실패")
# ═══════════════════════════════════════
# 검증 2: DX/GIS/BIM/디지털트윈 상호 관계 시각화
# ═══════════════════════════════════════
print("\n=== 검증 2: DX 관계 시각화 ===")
prompt_2 = """다음 관계를 시각화하는 HTML을 만들어라. 크기: 707px 너비 × 293px 높이.
## 관계 구조
- DX(디지털 전환)는 상위개념이다.
- DX 안에 GIS, BIM, 디지털 트윈이 포함된다.
- GIS, BIM, 디지털 트윈은 서로 연결/융합되어 DX를 실현한다.
- "BIM ≠ DX" — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다.
## 각 기술 설명 (원본 그대로 사용)
- DX: "BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념"
- GIS: "지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공"
- BIM: "시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구"
- 디지털 트윈: "현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술"
## 시각화 요구사항
1. DX를 큰 원 또는 큰 박스로, 그 안에 GIS/BIM/디지털트윈을 포함
2. GIS, BIM, 디지털 트윈은 서로 겹치거나 연결되어 융합을 표현 (벤 다이어그램, 겹치는 원, 또는 연결선)
3. 하단에 "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다" 강조 박스
4. 색상: DX 파란(#2563eb), GIS/BIM/디지털트윈 각각 다른 색조의 파란
5. **293px 높이 안에 맞춰라**
6. SVG 또는 CSS로 시각화
## 출력
HTML + inline <style>만 반환. 설명 없이 코드만.
```html
(여기에 HTML)
```"""
html_2 = await _call_kei(kei_url, prompt_2)
if html_2:
wrapped_2 = _wrap_in_slide(html_2, 707, 293)
m_2 = await asyncio.to_thread(measure_rendered_heights, wrapped_2)
s_2 = await asyncio.to_thread(capture_slide_screenshot, wrapped_2)
_save(out_dir, "verify2_hierarchy.html", wrapped_2)
if s_2:
(out_dir / "verify2_hierarchy.png").write_bytes(base64.b64decode(s_2))
slide = m_2.get("slide", {})
print(f" [{time.time()-t0:.0f}s] 결과: {slide.get('scrollHeight', 0)}px / 720px")
print(f" HTML: {len(html_2)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 생성 실패")
# ═══════════════════════════════════════
# 검증 3: 용어 정의 풀 텍스트 + 출처
# ═══════════════════════════════════════
print("\n=== 검증 3: 용어 정의 (풀 텍스트 + 출처) ===")
prompt_3 = """다음 3개 용어의 정의를 sidebar 카드로 만들어라. 크기: 380px 너비 × 490px 높이.
## 용어 (원본 텍스트를 100% 그대로 사용. 한 글자도 바꾸지 마라.)
1. 건설산업
정의: "부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업"
2. BIM (Building Information Modeling)
정의: "형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구"
출처: "건설산업 BIM 기본지침, 국토교통부, 2020"
3. DX (Digital Transformation)
정의: "디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. 단순한 기술 도입이 아닌, 산업의 새로운 방향을 정립"
출처: "IBM Institute for Business Value, 2011"
## 요구사항
1. 카드 스타일: 배경 #f8fafc, 테두리 1px solid #e2e8f0, border-radius 8px
2. 번호: 원형 #2563eb 배경 + 흰 숫자
3. 정의 텍스트를 축약하지 마라. 위에 제공한 텍스트를 한 글자도 빠짐없이 그대로 넣어라.
4. 출처가 있으면 이탤릭 작은 글씨(10px, #94a3b8)로 표시
5. 490px 높이 안에 여유 있게 배치 (공간이 충분함)
6. 상단에 "용어 정의" 구분선 라벨 (좌우 선 + 중앙 텍스트)
## 출력
HTML + inline <style>만 반환. 설명 없이 코드만.
```html
(여기에 HTML)
```"""
html_3 = await _call_kei(kei_url, prompt_3)
if html_3:
wrapped_3 = _wrap_in_slide(html_3, 380, 490)
m_3 = await asyncio.to_thread(measure_rendered_heights, wrapped_3)
s_3 = await asyncio.to_thread(capture_slide_screenshot, wrapped_3)
_save(out_dir, "verify3_definitions.html", wrapped_3)
if s_3:
(out_dir / "verify3_definitions.png").write_bytes(base64.b64decode(s_3))
slide = m_3.get("slide", {})
print(f" [{time.time()-t0:.0f}s] 결과: {slide.get('scrollHeight', 0)}px / 720px")
print(f" HTML: {len(html_3)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 생성 실패")
print(f"\n총 소요: {time.time()-t0:.0f}")
print(f"결과: {out_dir}")
async def _call_kei(kei_url: str, prompt: str) -> str | None:
"""Kei API 호출하여 HTML 코드 추출."""
import re
import httpx
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST", f"{kei_url}/api/message",
json={"message": prompt, "session_id": "verify-3issues", "mode_hint": "chat"},
timeout=None,
) as response:
if response.status_code != 200:
return None
from src.sse_utils import stream_sse_tokens
full_text = await stream_sse_tokens(response)
if not full_text:
return None
# ```html ... ``` 블록 추출
match = re.search(r"```html\s*(.*?)```", full_text, re.DOTALL)
if match:
return match.group(1).strip()
# <div나 <style로 시작하는 HTML 직접 추출
match = re.search(r"(<(?:div|style|section)[^>]*>.*)", full_text, re.DOTALL)
if match:
return match.group(1).strip()
return full_text.strip()
except Exception as e:
print(f" Kei API 오류: {e}")
return None
def _wrap_in_slide(inner_html: str, width: int, height: int) -> str:
"""HTML 조각을 측정 가능한 슬라이드 프레임으로 감싼다."""
return f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden;
background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
padding: 40px;
}}
.test-container {{
width: {width}px;
max-height: {height}px;
overflow: visible;
}}
</style>
</head><body>
<div class="slide">
<div class="test-container">
{inner_html}
</div>
</div>
</body></html>"""
def _save(out_dir, name, data):
(out_dir / name).write_text(data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)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)
asyncio.run(main())