Files
C.E.L_Slide_test2/tests/matching/pipeline_14_single_sample.py
T
KyeongminandClaude Opus 4.8 b836e79ee1 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>
2026-07-02 17:03:42 +09:00

244 lines
11 KiB
Python
Raw 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.
"""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()