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:
2026-04-06 05:00:52 +09:00
parent 24eb1bc5ad
commit 1f7579cf64
64 changed files with 13955 additions and 696 deletions

167
scripts/gen_viz_layers.py Normal file
View File

@@ -0,0 +1,167 @@
"""Step 1~4를 같은 슬라이드 레이아웃 위에 레이어로 쌓아 PNG 생성."""
import json, urllib.parse, time, sys
from pathlib import Path
sys.path.insert(0, ".")
run_dir = Path("data/runs/20260402_091318")
ctx_1a = json.loads((run_dir / "stage_1a_context.json").read_text(encoding="utf-8"))
ctx_1b = json.loads((run_dir / "stage_1b_context.json").read_text(encoding="utf-8"))
ctx_15a = json.loads((run_dir / "stage_1_5a_context.json").read_text(encoding="utf-8"))
ctx_17 = json.loads((run_dir / "stage_1_7_context.json").read_text(encoding="utf-8"))
ctx_15b = json.loads((run_dir / "stage_1_5b_context.json").read_text(encoding="utf-8"))
topics = ctx_1b.get("topics", [])
containers = ctx_15a.get("containers", {})
fh = ctx_15a.get("font_hierarchy", {})
ratio = ctx_15a.get("container_ratio", [72, 28])
refs = ctx_17.get("references", {})
ps = ctx_1a.get("page_structure", {})
if "roles" in ps:
ps = ps["roles"]
containers_b = ctx_15b.get("containers", {})
topic_map = {t["id"]: t for t in topics}
slide_w, slide_h = 1280, 720
pad = 40
header_h = 66
gap = 20
footer_h = containers.get("결론", {}).get("height_px", 60)
inner_w = slide_w - pad * 2
body_pct = ratio[0] if ratio else 72
sidebar_pct = ratio[1] if len(ratio) > 1 else 28
body_w = int(inner_w * body_pct / 100)
sidebar_w = inner_w - body_w - gap
body_zone_h = slide_h - pad * 2 - header_h - footer_h - gap * 2
bg_h = containers.get("배경", {}).get("height_px", 117)
core_h = body_zone_h - bg_h - 12
L = {
"배경": {"x": pad, "y": pad+header_h+gap, "w": body_w, "h": bg_h},
"본심": {"x": pad, "y": pad+header_h+gap+bg_h+12, "w": body_w, "h": core_h},
"첨부": {"x": pad+body_w+gap, "y": pad+header_h+gap, "w": sidebar_w, "h": body_zone_h},
"결론": {"x": pad, "y": slide_h-pad-footer_h, "w": inner_w, "h": footer_h},
}
C = {"배경": "#dc2626", "본심": "#2563eb", "첨부": "#16a34a", "결론": "#7c3aed"}
def area(role, inner):
p = L[role]; c = C[role]
return (f'<div style="position:absolute;left:{p["x"]}px;top:{p["y"]}px;'
f'width:{p["w"]}px;height:{p["h"]}px;border:2px solid {c};'
f'border-radius:6px;overflow:hidden;background:{c}08;'
f'padding:6px;font-size:9px;line-height:1.4;">{inner}</div>')
def header(title):
return (f'<div style="position:absolute;left:{pad}px;top:{pad}px;width:{inner_w}px;'
f'height:{header_h}px;background:#f8fafc;border-bottom:3px solid #2563eb;'
f'display:flex;align-items:center;padding:0 20px;font-size:22px;'
f'font-weight:900;color:#1e293b;">건설산업 DX의 올바른 이해</div>')
def slide(step_title, areas_html):
return (f'<!DOCTYPE html><html><head><meta charset="UTF-8">'
f'<style>*{{margin:0;padding:0;box-sizing:border-box;}}'
f'body{{background:#e5e5e5;padding:10px;font-family:sans-serif;}}</style></head><body>'
f'<div style="font-size:14px;font-weight:bold;margin-bottom:6px;">{step_title}</div>'
f'<div style="width:{slide_w}px;height:{slide_h}px;background:white;'
f'position:relative;border:1px solid #ccc;">'
f'{header(step_title)}{areas_html}</div></body></html>')
# Step 1
a1 = ""
for role in L:
c = C[role]; p = L[role]
fk = {"배경":"bg","본심":"core","첨부":"sidebar","결론":"key_msg"}.get(role,"core")
fv = fh.get(fk, 12)
a1 += area(role,
f'<div style="text-align:center;margin-top:{p["h"]//2-15}px;">'
f'<b style="color:{c};font-size:13px;">{role}</b><br>'
f'<span style="color:#888;font-size:10px;">{p["w"]}x{p["h"]}px / font:{fv}px</span></div>')
# Step 2
a2 = ""
for role in L:
c = C[role]; info = ps.get(role, {}); tids = info.get("topic_ids", [])
w = info.get("weight", 0)
inner = f'<div style="font-size:8px;color:{c};font-weight:bold;">{role} (w:{w})</div>'
for tid in tids:
t = topic_map.get(tid, {})
inner += (f'<div style="background:white;border:1px solid #ddd;border-radius:3px;'
f'padding:3px;margin:2px 0;">'
f'<b style="font-size:8px;">T{tid}: {t.get("title","")[:25]}</b><br>'
f'<span style="font-size:7px;color:#888;">'
f'{t.get("purpose","")} / {t.get("relation_type","")}</span></div>')
a2 += area(role, inner)
# Step 3
a3 = ""
for role in L:
c = C[role]; ref = refs.get(role, {}); p = L[role]
bid = ref.get("block_id", "?"); vtype = ref.get("visual_type", "?")
info = ps.get(role, {}); tids = info.get("topic_ids", [])
tnames = ", ".join(f"T{tid}" for tid in tids)
mt = max(0, p["h"]//2-25)
a3 += area(role,
f'<div style="text-align:center;margin-top:{mt}px;">'
f'<div style="font-size:9px;color:{c};">{role} ({tnames})</div>'
f'<div style="font-size:14px;margin:2px 0;">📦</div>'
f'<div style="font-size:11px;font-weight:bold;">{bid}</div>'
f'<div style="font-size:8px;color:#888;">type: {vtype}</div></div>')
# Step 4
a4 = ""
for role in L:
c = C[role]; ref = refs.get(role, {}); p = L[role]
bid = ref.get("block_id", "?")
cb = containers_b.get(role, {}); db = cb.get("design_budget") or {}
text_h = db.get("text_height_px", 0)
avail_h = db.get("available_height_px", 0)
fits = db.get("fits", False)
total = max(text_h + avail_h, 1)
tp = int(text_h / total * 100)
bw = p["w"] - 20
fc = "green" if fits else "red"
a4 += area(role,
f'<div style="padding:2px;">'
f'<div style="font-size:9px;color:{c};font-weight:bold;">{role}: {bid}</div>'
f'<div style="display:flex;height:14px;border-radius:3px;overflow:hidden;margin:4px 0;width:{bw}px;">'
f'<div style="width:{tp}%;background:#ff6b6b;font-size:7px;color:white;text-align:center;line-height:14px;">텍스트{text_h}px</div>'
f'<div style="width:{100-tp}%;background:#51cf66;font-size:7px;color:white;text-align:center;line-height:14px;">여유{avail_h}px</div>'
f'</div>'
f'<div style="font-size:8px;color:{fc};font-weight:bold;">fits:{fits} / {p["w"]}x{p["h"]}px</div></div>')
htmls = {
"viz_1_containers": slide(f"Step 1: 컨테이너 포션과 위치 (비율 {body_pct}:{sidebar_pct})", a1),
"viz_2_content": slide("Step 2: 각 영역별 내용 배치", a2),
"viz_3_blocks": slide("Step 3: 블록 선택 결과", a3),
"viz_4_budget": slide("Step 4: 블록별 디자인 예산", a4),
}
for name, html in htmls.items():
(run_dir / f"{name}.html").write_text(html, encoding="utf-8")
# PNG
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
opts = Options()
opts.add_argument("--headless")
opts.add_argument("--no-sandbox")
opts.add_argument("--force-device-scale-factor=2")
driver = webdriver.Chrome(options=opts)
driver.set_window_size(1380, 820)
for name in htmls:
html = (run_dir / f"{name}.html").read_text(encoding="utf-8")
encoded = urllib.parse.quote(html, safe="")
driver.get(f"data:text/html;charset=utf-8,{encoded}")
time.sleep(2)
driver.save_screenshot(str(run_dir / f"{name}.png"))
print(f"{name}.png")
driver.quit()
print("완료")