wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규 - Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가 - templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신 - tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분) - tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가 - docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서 - scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸 - .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외 미완성 작업의 보존용 스냅샷 커밋 (2026-07-02) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"""Pipeline Step 14 — 단일 샘플 (MDX 01-2 × V1 Top-3 프레임) 제대로 정리.
|
||||
|
||||
사용자 지시:
|
||||
1. 1 MDX 샘플만 집중 (01-2)
|
||||
2. Top-3 프레임 모두 같은 포맷 (정답 포함)
|
||||
3. MDX 제목 "DX와 BIM의 구분" 팝업 내용 기준 (섹션 전체 아님)
|
||||
4. 이미지:텍스트 5:5 레이아웃
|
||||
5. 1-element 세트는 skip (의미 없는 "세트")
|
||||
6. 실제 데이터 정리 (내가 지어내지 않음)
|
||||
|
||||
출력: SAMPLE_01_2_V1_MATCHING.html (A4, Top-3 포함)
|
||||
"""
|
||||
import base64
|
||||
import re
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
PNG_DIR = (HERE / '..' / '..' / 'data' / 'figma_previews').resolve()
|
||||
OUT_PATH = HERE / 'SAMPLE_01_2_V1_MATCHING.html'
|
||||
|
||||
SID = '01-2'
|
||||
POPUP_TITLE = 'DX와 BIM의 구분'
|
||||
POPUP_DESC = (
|
||||
'01.mdx §2 "용어간 상호관계" 의 펼침 팝업. '
|
||||
'BIM 과 DX 를 범위·S/W·프로세스·성과품·활용·확장성·수행개념·수행주체·'
|
||||
'발주처·설계사·시공사 등 12 가지 관점에서 행별로 비교한 표.'
|
||||
)
|
||||
POPUP_EXCERPT_LINES = [
|
||||
'BIM ≪ DX (Engineering + Management 통합) | 범위 | Only 3D (형상 구현 중심)',
|
||||
'제작·운영 (상용+전용 40~80개, Rhino, Sketchup, Blender...) | S/W | 모델 제작용 상용 SW (Revit, Civil 3D, Navisworks, Autocad)',
|
||||
'근본적 문제의식을 통한 개선 | 프로세스 | 기존 2D 설계 방식 유지',
|
||||
'공학 정보 및 콘텐츠 연계 / 도면, 수량, 시공계획 등 일식 | 성과품 | 3D 모델 중심 / 기존 성과품 유지',
|
||||
]
|
||||
|
||||
|
||||
def img_uri(fn):
|
||||
p = PNG_DIR / f'{fn:02d}.png'
|
||||
if not p.exists():
|
||||
return ''
|
||||
return 'data:image/png;base64,' + base64.b64encode(p.read_bytes()).decode('ascii')
|
||||
|
||||
|
||||
def trim_list(items, max_n):
|
||||
n = len(items)
|
||||
if n == 0:
|
||||
return '—'
|
||||
shown = items[:max_n]
|
||||
if n > max_n:
|
||||
return ', '.join(shown) + f' 등 <b>{n}개</b>'
|
||||
return ', '.join(shown) + f' ({n}개)'
|
||||
|
||||
|
||||
def trim_sets(sets_list, max_n):
|
||||
n = len(sets_list)
|
||||
if n == 0:
|
||||
return '—'
|
||||
shown = sets_list[:max_n]
|
||||
formatted = ', '.join('[' + ', '.join(s) + ']' for s in shown)
|
||||
if n > max_n:
|
||||
return formatted + f' 등 <b>{n}개</b>'
|
||||
return formatted + f' ({n}개)'
|
||||
|
||||
|
||||
A4_CSS = """
|
||||
@page { size: A4 portrait; margin: 12mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
|
||||
background: white; color: #1e293b; margin: 0; font-size: 9pt; line-height: 1.4;
|
||||
}
|
||||
@media screen {
|
||||
body { max-width: 210mm; margin: 10mm auto; box-shadow: 0 4px 24px rgba(0,0,0,0.12); padding: 12mm; background: #fff; }
|
||||
}
|
||||
@media print { .match-block { page-break-inside: avoid; } }
|
||||
h1 { font-size: 13pt; border-bottom: 2pt solid #2563eb; padding-bottom: 3pt; margin: 0 0 4pt 0; color: #0f172a; }
|
||||
h2 { font-size: 10pt; color: #1e293b; margin: 6pt 0 3pt; padding: 2pt 5pt; background: #e0e7ff; border-left: 3pt solid #0a6; border-radius: 2pt; }
|
||||
.meta-bar { color: #64748b; font-size: 8pt; font-style: italic; margin-bottom: 4pt; }
|
||||
.mdx-card { background: #f8fafc; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0 6pt; }
|
||||
.mdx-card .title { font-weight: 700; color: #0f172a; font-size: 9.5pt; margin-bottom: 2pt; }
|
||||
.mdx-card .popup-label { display: inline-block; padding: 1pt 5pt; background: #eef2ff; color: #3730a3; border-radius: 8pt; font-size: 7.5pt; font-weight: 700; margin-left: 4pt; vertical-align: middle; }
|
||||
.mdx-card .desc { color: #475569; font-size: 8pt; margin: 2pt 0; }
|
||||
.mdx-card ul { margin: 2pt 0 0 0; padding-left: 12pt; font-size: 8pt; color: #475569; line-height: 1.45; }
|
||||
.mdx-card li { margin: 1pt 0; }
|
||||
|
||||
/* 5:5 레이아웃 — 이미지 | 텍스트 */
|
||||
.match-block { display: table; width: 100%; border-radius: 3pt; padding: 5pt; margin: 4pt 0; border: 0.7pt solid #cbd5e1; background: #fafafa; }
|
||||
.match-block.answer { border: 1.5pt solid #dc2626; background: #fffbeb; }
|
||||
.match-row { display: table-row; }
|
||||
.match-image { display: table-cell; width: 50%; vertical-align: top; padding-right: 6pt; }
|
||||
.match-image img { width: 100%; max-width: 84mm; border: 0.5pt solid #94a3b8; border-radius: 3pt; display: block; }
|
||||
.match-image .frame-label { margin-top: 3pt; font-weight: 700; color: #2563eb; font-size: 9.5pt; }
|
||||
.match-image .frame-label.answer { color: #dc2626; }
|
||||
.match-image .frame-desc { font-size: 7.5pt; color: #64748b; margin-top: 1pt; line-height: 1.3; }
|
||||
.match-detail { display: table-cell; width: 50%; vertical-align: top; padding-left: 6pt; border-left: 0.5pt dashed #cbd5e1; }
|
||||
.score-row { font-size: 10pt; font-weight: 700; color: #0f172a; margin-bottom: 3pt; }
|
||||
.score-row code { background: #fff3cd; color: #78350f; padding: 1pt 5pt; border-radius: 2pt; font-size: 9.5pt; }
|
||||
|
||||
.kw-line { margin: 3pt 0; font-size: 8pt; line-height: 1.4; }
|
||||
.kw-line strong { color: #0f172a; font-size: 8.5pt; }
|
||||
.hit-chip { color: #065f46; background: #d1fae5; padding: 1pt 4pt; border-radius: 2pt; font-size: 7.5pt; display: inline-block; margin: 0.5pt 0; }
|
||||
.miss-chip { color: #991b1b; background: #fee2e2; padding: 1pt 4pt; border-radius: 2pt; font-size: 7.5pt; display: inline-block; margin: 0.5pt 0; }
|
||||
|
||||
.note { background: #fef3c7; border-left: 3pt solid #f59e0b; padding: 4pt 7pt; margin: 5pt 0; font-size: 8pt; color: #78350f; border-radius: 2pt; }
|
||||
.formula-box { background: #eff6ff; border: 0.7pt solid #93c5fd; border-radius: 3pt; padding: 6pt 10pt; margin: 0 0 6pt 0; font-size: 9pt; color: #1e3a8a; line-height: 1.5; }
|
||||
.formula-box code { background: #dbeafe; color: #1e40af; padding: 1pt 4pt; border-radius: 2pt; font-size: 8.5pt; }
|
||||
.calc-line { font-size: 8pt; color: #475569; margin-top: 3pt; font-family: ui-monospace, monospace; }
|
||||
.calc-line code { background: #fff3cd; color: #78350f; padding: 0 3pt; border-radius: 2pt; font-size: 8pt; }
|
||||
"""
|
||||
|
||||
|
||||
FRAME_TITLES = {
|
||||
# 01-2 Top-3 프레임의 설명 (descriptions 기반 수동 캐시)
|
||||
18: 'BIM과 DX의 이해 — 다면 비교 표 (compare-rows)',
|
||||
27: '건설산업의 고부가가치화 — central-split-synthesis',
|
||||
11: '시공단계 BIM 모델·정보 활용 구분 — cards-3-category',
|
||||
}
|
||||
|
||||
|
||||
def render_frame_block(rank, rank_row, detail, answer_num):
|
||||
"""단일 프레임 매칭 블록 (5:5 이미지/텍스트)."""
|
||||
fn = rank_row['frame_number']
|
||||
score = rank_row['matching_score']
|
||||
is_answer = (fn == answer_num)
|
||||
|
||||
# 핵심
|
||||
standalone = detail['standalone']
|
||||
s_hit = sorted(standalone['hit'])
|
||||
s_miss = sorted([t for t in standalone['total'] if t not in standalone['hit']])
|
||||
|
||||
# 세트 (1-element 제외)
|
||||
groups = detail['keyword_group']['groups']
|
||||
hit_sets = []
|
||||
miss_sets = []
|
||||
for g in sorted(groups, key=lambda x: -x.get('coverage', 0)):
|
||||
kws = list(g.get('keywords', []))
|
||||
if len(kws) < 2:
|
||||
continue
|
||||
if g.get('coverage', 0) >= 0.5:
|
||||
hit_sets.append(kws)
|
||||
else:
|
||||
miss_sets.append(kws)
|
||||
|
||||
# 연관
|
||||
related = detail['related']
|
||||
r_hit_list = sorted(related['hit'])
|
||||
r_miss_ct = related['total_count'] - related['hit_count']
|
||||
|
||||
frame_desc = FRAME_TITLES.get(fn, f'Frame {fn}')
|
||||
|
||||
rank_label = {1: '1순위', 2: '2순위', 3: '3순위'}.get(rank, f'{rank}순위')
|
||||
answer_badge = ' 🎯 정답' if is_answer else ''
|
||||
box_class = 'match-block answer' if is_answer else 'match-block'
|
||||
label_class = 'frame-label answer' if is_answer else 'frame-label'
|
||||
|
||||
html = []
|
||||
html.append(f'<div class="{box_class}"><div class="match-row">')
|
||||
|
||||
# Left: image 50%
|
||||
html.append('<div class="match-image">')
|
||||
html.append(f'<img src="{img_uri(fn)}" alt="Frame {fn}"/>')
|
||||
html.append(f'<div class="{label_class}">Frame {fn}{answer_badge} <span style="color:#64748b;font-weight:400;font-size:8.5pt">({rank_label})</span></div>')
|
||||
html.append(f'<div class="frame-desc">{frame_desc}</div>')
|
||||
html.append('</div>')
|
||||
|
||||
# Right: detail 50%
|
||||
html.append('<div class="match-detail">')
|
||||
# 점수 + 계산식
|
||||
s_score_val = standalone['score']
|
||||
g_score_val = detail['keyword_group'].get('score_avg', 0)
|
||||
r_score_val = related['score'] if 'score' in related else (related['hit_count'] / related['total_count'] if related['total_count'] else 0)
|
||||
ceiling = (0.30 if standalone['total_count'] > 0 else 0) + 0.50 + 0.20
|
||||
html.append(f'<div class="score-row">매칭점수 <code>{score:.3f}</code>'
|
||||
+ (f' <span style="color:#64748b;font-weight:400;font-size:8pt">(최대 {ceiling:.2f})</span>' if ceiling < 1.0 else '')
|
||||
+ '</div>')
|
||||
html.append(f'<div class="calc-line">'
|
||||
f'= 0.30 × <code>{s_score_val:.3f}</code> + '
|
||||
f'0.50 × <code>{g_score_val:.3f}</code> + '
|
||||
f'0.20 × <code>{r_score_val:.3f}</code></div>')
|
||||
|
||||
# 핵심
|
||||
html.append('<div class="kw-line"><strong>핵심 키워드</strong><br/>')
|
||||
html.append(f' <span class="hit-chip">(o) {trim_list(s_hit, 5)}</span><br/>')
|
||||
html.append(f' <span class="miss-chip">(x) {trim_list(s_miss, 5)}</span>')
|
||||
html.append('</div>')
|
||||
|
||||
# 세트
|
||||
html.append('<div class="kw-line"><strong>키워드 세트</strong><br/>')
|
||||
html.append(f' <span class="hit-chip">(o) {trim_sets(hit_sets, 3)}</span><br/>')
|
||||
html.append(f' <span class="miss-chip">(x) {trim_sets(miss_sets, 2)}</span>')
|
||||
html.append('</div>')
|
||||
|
||||
# 연관
|
||||
html.append('<div class="kw-line"><strong>연관 키워드</strong><br/>')
|
||||
html.append(f' <span class="hit-chip">(o) {trim_list(r_hit_list, 5)}</span><br/>')
|
||||
html.append(f' <span class="miss-chip">(x) 미매칭 <b>{r_miss_ct}개</b></span>')
|
||||
html.append('</div>')
|
||||
|
||||
html.append('</div></div></div>')
|
||||
return ''.join(html)
|
||||
|
||||
|
||||
def main():
|
||||
v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8'))
|
||||
v1_sec = v1['mdx_sections'][SID]
|
||||
answer = v1_sec['answer_frame_number']
|
||||
top3 = v1_sec['rank_by_matching_score'][:3]
|
||||
|
||||
html_parts = []
|
||||
html_parts.append(f'<!DOCTYPE html><html lang="ko"><head><meta charset="utf-8">'
|
||||
f'<title>MDX 01-2 매칭 결과</title><style>{A4_CSS}</style></head><body>')
|
||||
|
||||
# 공식
|
||||
html_parts.append('<div class="formula-box">')
|
||||
html_parts.append('매칭점수 = '
|
||||
'<code>0.30 × 핵심 키워드 hit 비율</code> + '
|
||||
'<code>0.50 × 키워드 세트 평균 coverage</code> + '
|
||||
'<code>0.20 × 연관 키워드 hit 비율</code>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# MDX
|
||||
html_parts.append('<div class="mdx-card">')
|
||||
html_parts.append(f'<div class="title">MDX 01-2 · {POPUP_TITLE}</div>')
|
||||
html_parts.append('<ul>')
|
||||
for ln in POPUP_EXCERPT_LINES:
|
||||
html_parts.append(f'<li>{ln}</li>')
|
||||
html_parts.append('</ul>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Top-3 프레임
|
||||
for idx, r in enumerate(top3, start=1):
|
||||
fid = r['frame_id']
|
||||
detail = v1_sec['per_frame_detail'][fid]
|
||||
html_parts.append(render_frame_block(idx, r, detail, answer))
|
||||
|
||||
html_parts.append('</body></html>')
|
||||
OUT_PATH.write_text('\n'.join(html_parts), encoding='utf-8')
|
||||
print(f'생성: {OUT_PATH}')
|
||||
print(f' 절대경로: D:\\ad-hoc\\kei\\design_agent\\tests\\matching\\SAMPLE_01_2_V1_MATCHING.html')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user