- 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>
1324 lines
76 KiB
Python
1324 lines
76 KiB
Python
"""Pipeline Step 16 — 4 페이지 deck 생성 (A4 1 페이지씩).
|
||
|
||
Page 1: TARGET (01-2) × V1~V4 Top-3 이미지 + 점수
|
||
Page 2: Holdout (02-2.1) × V1~V4 Top-3 이미지 + 점수
|
||
Page 3: V1~V4 각각이 무엇인가 + 어떻게 매칭되는가 (방법 설명)
|
||
Page 4: V4 가 Figma DB 를 어떻게 정리하는가 (Frame 12 예시)
|
||
"""
|
||
import base64
|
||
import sys
|
||
from collections import Counter
|
||
from pathlib import Path
|
||
import yaml
|
||
|
||
HERE = Path(__file__).parent
|
||
PNG_DIR = (HERE / '..' / '..' / 'data' / 'figma_previews').resolve()
|
||
|
||
DECK_01 = HERE / 'DECK_01_TARGET_01_2.html'
|
||
DECK_02 = HERE / 'DECK_02_HOLDOUT_02_2_1.html'
|
||
DECK_03 = HERE / 'DECK_03_METHODS.html'
|
||
DECK_04 = HERE / 'DECK_04_DB_STRUCTURE.html'
|
||
DECK_05 = HERE / 'DECK_05_STRUCTURE_KEYWORD.html'
|
||
DECK_06 = HERE / 'DECK_06_FIGMA_TO_DB.html'
|
||
DECK_07 = HERE / 'DECK_07_KEYWORD_PIPELINE.html'
|
||
|
||
_IMG_CACHE = {}
|
||
|
||
|
||
def img_uri(fn):
|
||
if fn in _IMG_CACHE:
|
||
return _IMG_CACHE[fn]
|
||
p = PNG_DIR / f'{fn:02d}.png'
|
||
if not p.exists():
|
||
return ''
|
||
uri = 'data:image/png;base64,' + base64.b64encode(p.read_bytes()).decode('ascii')
|
||
_IMG_CACHE[fn] = uri
|
||
return uri
|
||
|
||
|
||
def build_frame_descriptions(auto):
|
||
out = {}
|
||
for fid, info in auto['frame_stats'].items():
|
||
fnum = info['frame_number']
|
||
for set_id, sinfo in auto['source_text_sets'].items():
|
||
if sinfo['frame_id'] == fid and sinfo['source_text_index'] == 1:
|
||
out[fnum] = sinfo['source_text_raw']
|
||
break
|
||
if fnum not in out:
|
||
out[fnum] = f'Frame {fnum}'
|
||
return out
|
||
|
||
|
||
CSS_BASE = """
|
||
@page { size: A4 portrait; margin: 12mm; }
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", sans-serif;
|
||
background: white; color: #1e293b; margin: 0; font-size: 9.5pt; 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; }
|
||
}
|
||
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 { color: #64748b; font-size: 8pt; font-style: italic; margin-bottom: 5pt; }
|
||
.mdx-card { background: #f8fafc; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0 5pt; }
|
||
.mdx-card .title { font-weight: 700; color: #0f172a; font-size: 9.5pt; margin-bottom: 2pt; }
|
||
.popup-tag { 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 ul { margin: 2pt 0 0 0; padding-left: 12pt; font-size: 8pt; color: #475569; }
|
||
table { border-collapse: collapse; width: 100%; font-size: 9pt; }
|
||
th, td { border: 0.5pt solid #cbd5e1; padding: 3pt 5pt; text-align: left; vertical-align: top; }
|
||
th { background: #1e293b; color: white; font-weight: 700; font-size: 8.5pt; text-align: center; }
|
||
code { background: #f1f5f9; padding: 0 3pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; }
|
||
.formula-box { background: #eff6ff; border: 0.7pt solid #93c5fd; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0; font-size: 9pt; color: #1e3a8a; }
|
||
.label-use_as_is { background: #d1fae5; color: #065f46; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||
.label-light_edit { background: #dbeafe; color: #1e40af; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||
.label-restructure { background: #fef3c7; color: #92400e; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||
.label-reject { background: #fee2e2; color: #991b1b; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||
"""
|
||
|
||
CSS_DECK_12 = CSS_BASE + """
|
||
/* DECK 1, 2: V1~V4 × Top-3 매트릭스 */
|
||
table.cascade { table-layout: fixed; margin-top: 4pt; }
|
||
table.cascade th.method-col { width: 13%; background: #1e293b; color: white; }
|
||
table.cascade td.method-cell { background: #f1f5f9; text-align: center; vertical-align: middle; font-weight: 700; color: #1e293b; font-size: 9pt; padding: 3pt; }
|
||
table.cascade td.method-cell strong { color: #2563eb; font-size: 10pt; display: block; }
|
||
table.cascade td.method-cell small { font-size: 7pt; color: #64748b; display: block; margin-top: 2pt; line-height: 1.2; }
|
||
table.cascade td.frame-cell { width: 29%; text-align: center; vertical-align: top; padding: 3pt; }
|
||
table.cascade td.frame-cell img { max-width: 100%; max-height: 28mm; border: 0.5pt solid #94a3b8; border-radius: 2pt; display: block; margin: 0 auto; }
|
||
table.cascade td.frame-cell .frame-label { font-size: 8pt; font-weight: 600; color: #2563eb; margin-top: 2pt; }
|
||
table.cascade td.frame-cell .frame-desc { font-size: 7pt; color: #64748b; margin-top: 1pt; line-height: 1.2; min-height: 2em; }
|
||
table.cascade td.frame-cell .frame-score { font-size: 8pt; color: #059669; font-family: ui-monospace, monospace; margin-top: 2pt; font-weight: 600; }
|
||
table.cascade td.frame-cell.answer { background: #fffbeb; }
|
||
table.cascade td.frame-cell.answer .frame-label { color: #dc2626; }
|
||
table.cascade td.empty-cell { background: #f8fafc; text-align: center; color: #94a3b8; font-size: 11pt; vertical-align: middle; }
|
||
"""
|
||
|
||
CSS_DECK_3 = CSS_BASE + """
|
||
/* DECK 3: 방법 설명 */
|
||
.method-block { border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 8pt 10pt; margin: 5pt 0; background: #fafafa; }
|
||
.method-block .method-name { font-size: 12pt; font-weight: 700; color: #2563eb; margin-bottom: 2pt; }
|
||
.method-block .method-name .badge { display: inline-block; padding: 2pt 7pt; background: #2563eb; color: white; border-radius: 3pt; font-size: 9pt; margin-right: 6pt; }
|
||
.method-block .method-tagline { font-size: 9pt; color: #475569; margin-bottom: 5pt; font-style: italic; }
|
||
.method-block .method-formula { background: white; border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; margin: 3pt 0; }
|
||
.method-block .method-detail { font-size: 9pt; line-height: 1.5; color: #1e293b; }
|
||
.method-block .method-detail b { color: #0f172a; }
|
||
.cascade-arrow { text-align: center; font-size: 12pt; color: #64748b; margin: 2pt 0; }
|
||
"""
|
||
|
||
CSS_DECK_4 = CSS_BASE + """
|
||
/* DECK 4: MDX 1 vs Top-3 프레임 비교 표 */
|
||
@page { size: A4 portrait; margin: 10mm; }
|
||
table.cmp { width: 100%; border-collapse: collapse; font-size: 7.5pt; line-height: 1.32; table-layout: fixed; margin-top: 4pt; }
|
||
table.cmp col.lbl { width: 17%; }
|
||
table.cmp col.mdx { width: 19%; }
|
||
table.cmp col.frm { width: 21.33%; }
|
||
table.cmp th, table.cmp td { border: 0.4pt solid #cbd5e1; padding: 3pt 4pt; vertical-align: top; word-break: keep-all; overflow-wrap: anywhere; }
|
||
table.cmp td.lbl-col { background: #f1f5f9; font-weight: 700; color: #1e293b; }
|
||
table.cmp td.lbl-col .def { display: block; color: #64748b; font-size: 7pt; font-weight: 400; margin-top: 1pt; line-height: 1.3; }
|
||
table.cmp td.mdx-col { background: #fefce8; }
|
||
table.cmp td.frm-col { background: #f0f9ff; }
|
||
table.cmp tr.answer td.frm-col.is-ans { background: #fffbeb; box-shadow: inset 0 0 0 0.7pt #f59e0b; }
|
||
table.cmp tr.section-head td { background: #1e293b; color: white; font-weight: 700; padding: 4pt 6pt; font-size: 8.5pt; letter-spacing: 0.5pt; }
|
||
table.cmp tr.formula-row td { background: #eff6ff; font-family: ui-monospace, Menlo, monospace; font-size: 7.5pt; color: #1e3a8a; padding: 4pt 6pt; }
|
||
table.cmp tr.score-row td { background: #ecfdf5; font-family: ui-monospace, monospace; font-weight: 700; color: #059669; text-align: center; font-size: 8.5pt; }
|
||
table.cmp tr.score-row td.lbl-col, table.cmp tr.score-row td.mdx-col { background: #f1f5f9; color: #475569; text-align: left; font-weight: 600; font-family: -apple-system, sans-serif; }
|
||
table.cmp tr.img-row td { text-align: center; padding: 5pt 3pt; vertical-align: middle; }
|
||
table.cmp tr.img-row img { max-width: 100%; max-height: 30mm; border: 0.5pt solid #94a3b8; border-radius: 2pt; display: block; margin: 0 auto; }
|
||
table.cmp tr.img-row .name { font-size: 7.5pt; font-weight: 700; color: #2563eb; margin-top: 2pt; line-height: 1.2; }
|
||
table.cmp tr.img-row .name.ans { color: #dc2626; }
|
||
table.cmp .mdx-head { padding: 5pt 6pt; background: #fefce8; }
|
||
table.cmp .mdx-head .title { font-weight: 700; font-size: 8.5pt; color: #ca8a04; margin-bottom: 2pt; }
|
||
table.cmp .mdx-head .meta { font-size: 7pt; color: #64748b; margin-bottom: 2pt; line-height: 1.3; }
|
||
table.cmp .mdx-head ul { margin: 1pt 0; padding-left: 10pt; font-size: 7pt; line-height: 1.3; color: #1e293b; }
|
||
table.cmp tr.label-row td { text-align: center; font-size: 8pt; padding: 4pt; }
|
||
table.cmp tr.label-row td.lbl-col, table.cmp tr.label-row td.mdx-col { background: #f1f5f9; text-align: left; font-weight: 600; color: #475569; }
|
||
.routing-table { width: 100%; font-size: 7.5pt; border-collapse: collapse; margin-top: 5pt; }
|
||
.routing-table th { background: #1e293b; color: white; padding: 3pt 5pt; font-size: 8pt; }
|
||
.routing-table td { border: 0.4pt solid #cbd5e1; padding: 3pt 6pt; vertical-align: top; }
|
||
.routing-table td.center { text-align: center; }
|
||
.routing-table td.num { text-align: center; font-family: ui-monospace, monospace; font-weight: 700; }
|
||
table.cmp tr.method-row td { background: #fffbeb; padding: 5pt 8pt; font-size: 7.5pt; line-height: 1.45; color: #78350f; border-top: 0.7pt solid #f59e0b; }
|
||
table.cmp tr.method-row .head { font-weight: 700; color: #92400e; margin-bottom: 3pt; font-size: 8pt; display: block; }
|
||
table.cmp tr.method-row .item { margin: 2pt 0; }
|
||
table.cmp tr.method-row .item b { color: #78350f; }
|
||
table.cmp tr.method-row code { background: #fef3c7; padding: 0 3pt; border-radius: 2pt; font-size: 7pt; }
|
||
table.cmp tr.method-row .limit { color: #991b1b; font-weight: 600; }
|
||
"""
|
||
|
||
|
||
def html_wrap(title, css, body):
|
||
return f"""<!DOCTYPE html>
|
||
<html lang="ko"><head><meta charset="utf-8"><title>{title}</title>
|
||
<style>{css}</style></head><body>{body}</body></html>"""
|
||
|
||
|
||
# ============================================================
|
||
# DECK 1, 2 공통 — V1~V4 Top-3 매트릭스
|
||
# ============================================================
|
||
|
||
def render_cascade_deck(sid, mdx_title, popup_label, mdx_lines, v1, v2, v3_r5, v4_r2,
|
||
descriptions, is_target, page_subtitle):
|
||
v1_sec = v1['mdx_sections'][sid]
|
||
answer = v1_sec.get('answer_frame_number') if is_target else None
|
||
|
||
body = []
|
||
body.append(f'<h1>{page_subtitle}</h1>')
|
||
body.append('<div class="meta">매칭점수 = '
|
||
'<code>0.414×핵심 + 0.320×세트 + 0.265×연관</code> · '
|
||
'V2 ko-sroberta cosine · V3 v2 schema rerank · V4 template-fit-v1 판정')
|
||
body.append('</div>')
|
||
|
||
# MDX
|
||
body.append('<div class="mdx-card">')
|
||
body.append(f'<div class="title">MDX {sid} · {mdx_title}'
|
||
+ (f'<span class="popup-tag">{popup_label}</span>' if popup_label else '')
|
||
+ '</div>')
|
||
if mdx_lines:
|
||
body.append('<ul>')
|
||
for ln in mdx_lines:
|
||
body.append(f'<li>{ln}</li>')
|
||
body.append('</ul>')
|
||
body.append('</div>')
|
||
|
||
# V1~V4 cascade table — V4 는 32 전체 평가 결과 사용 (v4_full32)
|
||
v1_top3 = v1_sec['rank_by_matching_score'][:3]
|
||
v2_top3 = sorted(v2['mdx_sections'][sid]['v2_rerank'], key=lambda x: x['v2_rank'])[:3]
|
||
v3_top3 = sorted(v3_r5['mdx_sections'][sid]['v3_r5_rerank'], key=lambda x: x['v3_r5_rank'])[:3]
|
||
# V4: 32 전체 평가에서 reject 제외, confidence 내림차순 Top-3
|
||
v4_full_sec = v4_r2['mdx_sections'].get(sid, {}) # 여기서 v4_r2 는 v4_full32 로 교체됨
|
||
v4_all = v4_full_sec.get('judgments_full32', [])
|
||
v4_usable = [j for j in v4_all if j['label'] != 'reject'][:3]
|
||
v4_top3 = v4_usable
|
||
|
||
body.append('<table class="cascade">')
|
||
body.append('<thead><tr><th class="method-col">방식</th>'
|
||
'<th>1순위</th><th>2순위</th><th>3순위</th></tr></thead><tbody>')
|
||
|
||
def render_row(method_label, sub_label, top3, score_fmt, extra_fmt=None,
|
||
empty_message=None):
|
||
cells = [f'<td class="method-cell"><strong>{method_label}</strong><small>{sub_label}</small></td>']
|
||
# 후보 없는 경우 — 전체 colspan 으로 메시지
|
||
if not top3 and empty_message:
|
||
cells.append(f'<td class="frame-cell empty-cell" colspan="3">{empty_message}</td>')
|
||
return '<tr>' + ''.join(cells) + '</tr>'
|
||
# 후보 채우기 (3 미만이면 빈 셀로)
|
||
for i in range(3):
|
||
if i < len(top3):
|
||
r = top3[i]
|
||
fn = r['frame_number']
|
||
is_ans = answer is not None and fn == answer
|
||
cls = 'frame-cell answer' if is_ans else 'frame-cell'
|
||
score_html = score_fmt(r)
|
||
extra_html = extra_fmt(r) if extra_fmt else ''
|
||
badge = ' 🎯' if is_ans else ''
|
||
cells.append(
|
||
f'<td class="{cls}">'
|
||
f'<img src="{img_uri(fn)}" alt="{fn:02d}"/>'
|
||
f'<div class="frame-label">Frame {fn}{badge}</div>'
|
||
f'<div class="frame-desc">{descriptions.get(fn, "")[:30]}</div>'
|
||
f'<div class="frame-score">{score_html}</div>'
|
||
+ (f'<div style="margin-top:2pt">{extra_html}</div>' if extra_html else '')
|
||
+ '</td>'
|
||
)
|
||
else:
|
||
cells.append('<td class="frame-cell empty-cell">—</td>')
|
||
return '<tr>' + ''.join(cells) + '</tr>'
|
||
|
||
body.append(render_row('V1', '키워드', v1_top3,
|
||
lambda r: f"score {r['matching_score']:.3f}"))
|
||
body.append(render_row('V2', '+ 의미', v2_top3,
|
||
lambda r: f"sem {r['semantic_score']:.3f}"))
|
||
body.append(render_row('V3', '+ 구조', v3_top3,
|
||
lambda r: f"struct {r['v3_r5_total']:.3f}"))
|
||
# V4 — reject 제외, 사용 가능 후보만. 0 개면 "재구성 필요" 메시지.
|
||
rejected_count = sum(1 for j in v4_all if j['label'] == 'reject')
|
||
empty_msg = (
|
||
f'<div style="font-size:9pt;color:#991b1b;font-weight:700;padding:8pt">'
|
||
f'사용 가능한 프레임 없음<br/>'
|
||
f'<span style="font-size:8pt;color:#7f1d1d;font-weight:400">'
|
||
f'(32 프레임 전체 평가에서 모두 reject — 기존 프레임에 없는 신규 콘텐츠 → 재구성 필요)'
|
||
f'</span></div>'
|
||
)
|
||
body.append(render_row(
|
||
'V4', '+ 판정', v4_top3,
|
||
lambda r: f"conf {r['confidence']:.3f}",
|
||
lambda r: f'<span class="label-{r["label"]}">{r["label"]}</span>',
|
||
empty_message=empty_msg if not v4_top3 else None,
|
||
))
|
||
|
||
body.append('</tbody></table>')
|
||
|
||
# V4 후보 개수 안내 (32 전체 기준)
|
||
total_usable = v4_full_sec.get('usable_count', 0)
|
||
total_reject = v4_full_sec.get('reject_count', 0)
|
||
body.append(
|
||
f'<div style="margin-top:5pt;font-size:8pt;color:#475569">'
|
||
f'※ V4 는 32 프레임 전체 평가 결과에서 <code>reject</code> 제외 후 confidence 상위. '
|
||
f'<b>사용 가능 {total_usable}개 / reject {total_reject}개 (총 32)</b>'
|
||
f'</div>'
|
||
)
|
||
return ''.join(body)
|
||
|
||
|
||
# ============================================================
|
||
# DECK 1: TARGET 01-2
|
||
# ============================================================
|
||
|
||
def build_deck1(v1, v2, v3_r5, v4_r2, descriptions):
|
||
return html_wrap(
|
||
'01. TARGET 01-2 매칭',
|
||
CSS_DECK_12,
|
||
render_cascade_deck(
|
||
sid='01-2',
|
||
mdx_title='DX와 BIM의 구분',
|
||
popup_label='01.mdx §2 내부 <details> 팝업',
|
||
mdx_lines=[
|
||
'BIM ≪ DX (Engineering + Management 통합) | 범위 | Only 3D',
|
||
'제작·운영 (상용+전용 40~80개) | S/W | 모델 제작용 상용 SW (Revit, Civil 3D, Navisworks, Autocad)',
|
||
'근본적 문제의식을 통한 개선 | 프로세스 | 기존 2D 설계 방식 유지',
|
||
'공학 정보·콘텐츠 연계 | 성과품 | 3D 모델 중심',
|
||
],
|
||
v1=v1, v2=v2, v3_r5=v3_r5, v4_r2=v4_r2,
|
||
descriptions=descriptions,
|
||
is_target=True,
|
||
page_subtitle='01. TARGET (정답 있음) — MDX 01-2 × V1~V4 Top-3',
|
||
)
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# DECK 2: Holdout 02-2.1
|
||
# ============================================================
|
||
|
||
def build_deck2(v1, v2, v3_r5, v4_r2, descriptions):
|
||
return html_wrap(
|
||
'02. Holdout 02-2.1 매칭',
|
||
CSS_DECK_12,
|
||
render_cascade_deck(
|
||
sid='02-2.1',
|
||
mdx_title='업무 수행 과정(Process)의 변화',
|
||
popup_label='02.mdx §2.1 (정답 미지정 · 블라인드)',
|
||
mdx_lines=[
|
||
'생산 방식: 수작업 의존의 반복 업무 → SW 활용한 체계화된 방식으로 전환',
|
||
'인지·검토: 2D 도면 해석 → 3D 모델 기반의 직관적 인지·검토 체계로 전환',
|
||
'협업 구조: 개별 문서 중심 → 데이터 통합 기반의 정보 공유·관리 환경으로 전환',
|
||
'검증·대응: 사후 대응 중심 → 사전 검증 중심의 예방적 업무 방식으로 전환',
|
||
],
|
||
v1=v1, v2=v2, v3_r5=v3_r5, v4_r2=v4_r2,
|
||
descriptions=descriptions,
|
||
is_target=False,
|
||
page_subtitle='02. Holdout (정답 미지정) — MDX 02-2.1 × V1~V4 Top-3',
|
||
)
|
||
)
|
||
|
||
|
||
# ============================================================
|
||
# DECK 3: 방법 설명
|
||
# ============================================================
|
||
|
||
def build_deck3():
|
||
body = []
|
||
body.append('<h1>03. V1~V4 매칭 방법 — 무엇이고 어떻게 적용되는가</h1>')
|
||
body.append('<div class="meta">키워드 → 의미 → 구조 → 적용 가능성으로 신호를 누적해 캐스케이드 매칭.</div>')
|
||
|
||
# V1
|
||
body.append('<div class="method-block">')
|
||
body.append('<div class="method-name"><span class="badge">V1</span>키워드 매칭 (Baseline)</div>')
|
||
body.append('<div class="method-tagline">"이 단어들이 같이 나오나?" — 빠른 후보 필터</div>')
|
||
body.append('<div class="method-formula">'
|
||
'matching_score = 0.414 × 핵심(단독) + 0.320 × 세트(co-occurrence) + 0.265 × 연관'
|
||
'</div>')
|
||
body.append('<div class="method-detail">'
|
||
'· 프레임 텍스트에서 <b>핵심 / 세트 / 연관</b> 3계층 키워드 추출<br/>'
|
||
'· 가중치는 <b>Logistic Regression 학습</b> (TARGET 4 × 32, LOOCV 4/4)<br/>'
|
||
'· 32 프레임 중 1위 = 후보 프레임. <b>TARGET 4/4 정답</b>'
|
||
'</div>')
|
||
body.append('</div>')
|
||
|
||
body.append('<div class="cascade-arrow">↓ V1 Top-K 후보를 다음 축으로 재정렬</div>')
|
||
|
||
# V2
|
||
body.append('<div class="method-block">')
|
||
body.append('<div class="method-name"><span class="badge">V2</span>의미 매칭 (semantic rerank)</div>')
|
||
body.append('<div class="method-tagline">"단어가 달라도 의미가 같은가?"</div>')
|
||
body.append('<div class="method-formula">'
|
||
'similarity = cosine(MDX summary embedding, frame.content embedding) · '
|
||
'model = jhgan/ko-sroberta-multitask'
|
||
'</div>')
|
||
body.append('<div class="method-detail">'
|
||
'· V1 Top-K 후보 안에서 의미 유사도로 재정렬<br/>'
|
||
'· 키워드는 다르지만 같은 주제를 다룰 때 잡아냄'
|
||
'</div>')
|
||
body.append('</div>')
|
||
|
||
body.append('<div class="cascade-arrow">↓</div>')
|
||
|
||
# V3
|
||
body.append('<div class="method-block">')
|
||
body.append('<div class="method-name"><span class="badge">V3</span>구조 매칭 (structure rerank)</div>')
|
||
body.append('<div class="method-tagline">"이 콘텐츠를 담을 수 있는 레이아웃인가?"</div>')
|
||
body.append('<div class="method-formula">'
|
||
'score = 0.40 × layout_compat + 0.35 × content_affinity + 0.25 × structure_intent'
|
||
'</div>')
|
||
body.append('<div class="method-detail">'
|
||
'· content_affinity 12 enum (concept_definition / goal_axes / persona_benefit / ...)<br/>'
|
||
'· structure_intent 9 enum (binary_compare / multi_parallel / state_transition / ...)<br/>'
|
||
'· alternative_patterns — 대안 layout 관계 (cycle ↔ 3col-parallel 등)'
|
||
'</div>')
|
||
body.append('</div>')
|
||
|
||
body.append('<div class="cascade-arrow">↓</div>')
|
||
|
||
# V4
|
||
body.append('<div class="method-block">')
|
||
body.append('<div class="method-name"><span class="badge">V4</span>적용 가능성 판정 (template-fit-v1)</div>')
|
||
body.append('<div class="method-tagline">"실제로 쓸 수 있나? 어느 정도 수정해야 하나?"</div>')
|
||
body.append('<div class="method-formula">'
|
||
'multi-axis: anchor + cardinality + relation + slot + content - penalty<br/>'
|
||
'→ 라벨: '
|
||
'<span class="label-use_as_is">use_as_is</span> / '
|
||
'<span class="label-light_edit">light_edit</span> / '
|
||
'<span class="label-restructure">restructure</span> / '
|
||
'<span class="label-reject">reject</span>'
|
||
'</div>')
|
||
body.append('<div class="method-detail">'
|
||
'· 점수 1위 ≠ 실사용 가능. <b>판정 라벨</b>로 결정<br/>'
|
||
'· reject 면 기존 프레임에 없는 신규 콘텐츠 → 재구성 신호'
|
||
'</div>')
|
||
body.append('</div>')
|
||
|
||
return html_wrap('03. V1~V4 방법 설명', CSS_DECK_3, ''.join(body))
|
||
|
||
|
||
# ============================================================
|
||
# DECK 4: V4 → DB 구조 (Frame 12 예시)
|
||
# ============================================================
|
||
|
||
def build_deck4(ontology_r2, auto, v1, v3_r5, v4_full, descriptions):
|
||
"""DECK 4 — MDX → Figma 매칭 프로세스 예시 (MDX 01-2 ↔ Frame 18)
|
||
|
||
구성:
|
||
[상단] 좌:MDX 01-2 텍스트 | 우:Frame 18 이미지 (어떤 콘텐츠 ↔ 어떤 프레임)
|
||
[매칭] ① 키워드 매칭 (V1) — 매칭 결과 + 산식 + 점수
|
||
② 구조 매칭 (V3) — 4 항목 좌우 비교 + 산식 + 점수
|
||
③ 종합 판정 (V4) — confidence + 임계값 라우팅
|
||
"""
|
||
SHORT_ID = '18'
|
||
FRAME_NUM = 18
|
||
templates = ontology_r2['templates_v2']
|
||
fid = next(fid for fid, e in templates.items() if e.get('short_id') == SHORT_ID)
|
||
e = templates[fid]
|
||
vp = e['visual_pattern']
|
||
aff = e['content_affinity']
|
||
intent = e['structure_intent_v2']
|
||
alts = e.get('alternative_patterns', [])
|
||
slots = e.get('slots', [])
|
||
card = vp.get('cardinality', {})
|
||
|
||
# ─── 키워드 데이터 ───
|
||
detail = v1['mdx_sections']['01-2']['per_frame_detail'][fid]
|
||
standalone_kws = sorted(detail['standalone']['total'])
|
||
all_tokens = set()
|
||
for sid, s in auto['source_text_sets'].items():
|
||
if s['frame_id'] != fid:
|
||
continue
|
||
for t in s.get('terms', []):
|
||
all_tokens.add(t['token'])
|
||
related_kws = sorted(all_tokens - set(standalone_kws))
|
||
|
||
sets_for_frame = []
|
||
for sid, s in auto['source_text_sets'].items():
|
||
if s['frame_id'] != fid:
|
||
continue
|
||
sets_for_frame.append(s['source_text_raw'])
|
||
|
||
# ─── 한글 라벨 (영문 enum 은 보고용에서 숨김) ───
|
||
aff_ko = {
|
||
'comparative_matrix': '행렬형 비교 — 여러 항목을 행/열로 체계적 대조',
|
||
'capability_requirements': '역량 / 요건 명세',
|
||
'interrelation': '항목 간 상호관계',
|
||
'before_after_change': '변화 / 전환 (AS-IS → TO-BE)',
|
||
'concept_definition': '개념 정의',
|
||
'concept_comparison': '개념 비교',
|
||
'goal_axes': '목표 축',
|
||
'persona_benefit': '대상별 효익',
|
||
'process_steps': '단계 흐름',
|
||
'policy_requirements': '정책 / 요구사항',
|
||
'stakeholder_roles': '이해관계자 역할',
|
||
}
|
||
intent_ko = {
|
||
'matrix_coverage': '모든 비교축을 한눈에 보여주는 매트릭스',
|
||
'binary_compare': '2 항목 직접 비교',
|
||
'cycle_interrelation': '순환 / 상호 관계',
|
||
'state_transition': '상태 전환',
|
||
'sequence': '순차 단계',
|
||
'hierarchy': '위계',
|
||
'enumeration': '항목 나열',
|
||
'spotlight': '단일 강조',
|
||
}
|
||
slot_ko = {
|
||
'title': '제목', 'col_a_label': 'A 컬럼 라벨', 'col_b_label': 'B 컬럼 라벨',
|
||
'rows': '비교 행 데이터', 'banner_left': '좌측 배너', 'banner_right': '우측 배너',
|
||
'col_a_title': 'A 컬럼 제목', 'col_a_as_is': 'A AS-IS', 'col_a_to_be': 'A TO-BE',
|
||
'col_b_title': 'B 컬럼 제목', 'col_b_as_is': 'B AS-IS', 'col_b_to_be': 'B TO-BE',
|
||
}
|
||
layout_ko = {
|
||
'table-2col': '2단 표', 'table-3col': '3단 표', 'paired-rows': '쌍을 이룬 행',
|
||
'compare-2col': '2단 좌우 비교', 'compare-rows': '행 단위 비교 매트릭스',
|
||
'compare-2banner-top-2col-bottom': '상단 배너 + 하단 2단',
|
||
'2col-paired': '2단 짝맞춤', '2-boxes': '2개 박스 비교', 'central-split': '가운데 분할',
|
||
}
|
||
|
||
def ko_aff(k): return aff_ko.get(k, k)
|
||
def ko_intent(k): return intent_ko.get(k, k)
|
||
def ko_slot(k): return slot_ko.get(k, k)
|
||
def ko_layout(k): return layout_ko.get(k, k)
|
||
|
||
# ─── Top-3 프레임 (V4 reject 제외 confidence 순) ───
|
||
sys.path.insert(0, str(HERE))
|
||
from pipeline_08_v3_r4_structure_rerank import detect_mdx_v2_profile_r4, v3_r4_score
|
||
mdx_profile = detect_mdx_v2_profile_r4('01-2')
|
||
|
||
sec_v4 = v4_full['mdx_sections']['01-2']
|
||
usable = [j for j in sec_v4['judgments_full32'] if j['label'] != 'reject'][:3]
|
||
|
||
templates = ontology_r2['templates_v2']
|
||
answer_fn = v1['mdx_sections']['01-2'].get('answer_frame_number')
|
||
|
||
# 각 프레임 데이터 수집
|
||
cols = []
|
||
for j in usable:
|
||
fid = j['frame_id']
|
||
fn = j['frame_number']
|
||
tpl = templates[fid]
|
||
d = v1['mdx_sections']['01-2']['per_frame_detail'][fid]
|
||
v3s = v3_r4_score(mdx_profile, tpl)
|
||
|
||
# Frame 핵심 키워드 매칭
|
||
sk_total = int(d['standalone'].get('total_count', 0))
|
||
sk_hit = sorted(d['standalone'].get('hit', []))
|
||
sk_hit_count = int(d['standalone'].get('hit_count', 0))
|
||
|
||
# Frame 세트 매칭 — coverage > 0 인 세트들
|
||
groups = d['keyword_group'].get('groups', [])
|
||
g_total = int(d['keyword_group'].get('total_count', len(groups)))
|
||
hit_groups = [g for g in groups if g.get('coverage', 0) > 0]
|
||
# 가장 잘 매칭된 세트 (coverage 높은 순)
|
||
hit_groups.sort(key=lambda x: -x.get('coverage', 0))
|
||
|
||
# Frame 연관 키워드 매칭
|
||
rel_total = int(d['related'].get('total_count', 0))
|
||
rel_hit = sorted(d['related'].get('hit', []))
|
||
rel_hit_count = int(d['related'].get('hit_count', 0))
|
||
|
||
s_score = float(d['standalone']['score'])
|
||
g_score = float(d['keyword_group'].get('score_avg', 0))
|
||
r_score = float(d['related']['score'])
|
||
kw_score = 0.414 * s_score + 0.320 * g_score + 0.265 * r_score
|
||
|
||
cols.append({
|
||
'fid': fid, 'fn': fn, 'tpl': tpl,
|
||
'title': tpl['source']['title'],
|
||
'is_ans': fn == answer_fn,
|
||
# 키워드 매칭 정보 (MDX ↔ Frame)
|
||
'sk_total': sk_total, 'sk_hit': sk_hit, 'sk_hit_count': sk_hit_count,
|
||
'g_total': g_total, 'hit_groups': hit_groups,
|
||
'rel_total': rel_total, 'rel_hit': rel_hit, 'rel_hit_count': rel_hit_count,
|
||
's_score': s_score, 'g_score': g_score, 'r_score': r_score, 'kw_score': kw_score,
|
||
'v3_total': v3s['total'],
|
||
'v3_layout': v3s['layout_compat'],
|
||
'v3_aff': v3s['content_affinity'],
|
||
'v3_intent': v3s['structure_intent'],
|
||
'v4_conf': j['confidence'],
|
||
'v4_label': j['label'],
|
||
'v4_axes': j['axes'],
|
||
'aff': tpl['content_affinity']['primary'],
|
||
'intent': tpl['structure_intent_v2']['primary'],
|
||
'card': tpl['visual_pattern'].get('cardinality', {}),
|
||
'rel': tpl['visual_pattern'].get('relation_type'),
|
||
'slots': [s['id'] for s in tpl.get('slots', [])],
|
||
})
|
||
|
||
# ─── 한글 라벨 ───
|
||
aff_ko = {
|
||
'comparative_matrix': '행렬형 비교', 'concept_definition': '개념 정의',
|
||
'concept_comparison': '개념 비교', 'capability_requirements': '역량/요건 명세',
|
||
'interrelation': '상호관계', 'before_after_change': '변화/전환',
|
||
'goal_axes': '목표 축', 'persona_benefit': '대상별 효익',
|
||
'process_steps': '단계 흐름', 'policy_requirements': '정책/요구사항',
|
||
'stakeholder_roles': '이해관계자 역할',
|
||
}
|
||
intent_ko = {
|
||
'matrix_coverage': '매트릭스 (모든 축을 한눈에)',
|
||
'binary_compare': '2 항목 직접 비교',
|
||
'cycle_interrelation': '순환/상호 관계', 'state_transition': '상태 전환',
|
||
'sequence': '순차 단계', 'hierarchy': '위계',
|
||
'enumeration': '항목 나열', 'spotlight': '단일 강조',
|
||
}
|
||
label_ko = {
|
||
'use_as_is': ('그대로 사용', '#065f46', '#d1fae5'),
|
||
'light_edit': ('가벼운 편집', '#1e40af', '#dbeafe'),
|
||
'restructure': ('구조 재배치', '#92400e', '#fef3c7'),
|
||
'reject': ('사용 불가', '#991b1b', '#fee2e2'),
|
||
}
|
||
|
||
body = []
|
||
body.append('<h1>04. MDX 01-2 ↔ Top-3 프레임 매칭 비교 — DX 와 BIM 의 구분</h1>')
|
||
body.append('<div class="meta">DECK 01 의 정답 프레임 + 차순위 2개. '
|
||
'각 프레임이 어떤 키워드/구조로 MDX 와 매칭되었는지 항목별 비교.</div>')
|
||
|
||
body.append('<table class="cmp">')
|
||
body.append('<colgroup>'
|
||
'<col class="lbl"><col class="mdx">'
|
||
'<col class="frm"><col class="frm"><col class="frm">'
|
||
'</colgroup>')
|
||
|
||
# ─── 헤더 행: MDX 정보 + Frame 이미지들 ───
|
||
body.append('<tr class="img-row">')
|
||
body.append('<td class="lbl-col" style="background:#1e293b;color:white;text-align:center;'
|
||
'font-weight:700;font-size:8pt">항목</td>')
|
||
body.append('<td class="mdx-head">'
|
||
'<div class="title">MDX 01-2 · DX 와 BIM 의 구분</div>'
|
||
'<div class="meta">4개 비교축 × 2개 항목(BIM ↔ DX) 행렬 비교</div>'
|
||
'</td>')
|
||
for c in cols:
|
||
ans_cls = 'ans' if c['is_ans'] else ''
|
||
ans_mark = ' 🎯' if c['is_ans'] else ''
|
||
body.append(f'<td class="frm-col">'
|
||
f'<img src="{img_uri(c["fn"])}" alt="Frame {c["fn"]}"/>'
|
||
f'<div class="name {ans_cls}">Frame {c["fn"]}{ans_mark}<br/>'
|
||
f'<span style="font-size:7pt;color:#475569;font-weight:400">{c["title"]}</span>'
|
||
f'</div></td>')
|
||
body.append('</tr>')
|
||
|
||
# ─── < 키워드 매칭 > 섹션 ───
|
||
body.append('<tr class="section-head"><td colspan="5">< 키워드 매칭 > '
|
||
'— 프레임 키워드와 MDX 본문의 일치도</td></tr>')
|
||
|
||
# MDX 본문 단어 예시 (헤더에서 추출 — 본문 그대로의 단어들)
|
||
mdx_core_ex = 'BIM, DX, Autocad, Navisworks, Civil 3D 등'
|
||
mdx_set_ex = ('· Only 3D · 모델 제작용 상용 SW · '
|
||
'2D 설계방식 유지 등')
|
||
mdx_rel_ex = '2D, 3D, S/W, 모델, 제작, 상용, 운영, 설계, 방식 등'
|
||
|
||
# 핵심 키워드 — MDX 본문에서 매칭된 단어 vs 각 프레임의 매칭 결과
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">핵심 키워드'
|
||
'<span class="def">해당 프레임에만 등장하는 고유 키워드</span></td>')
|
||
body.append(f'<td class="mdx-col"><b>MDX 본문 핵심 단어</b><br/>{mdx_core_ex}</td>')
|
||
for c in cols:
|
||
if c['sk_hit_count'] == 0:
|
||
body.append(f'<td class="frm-col"><span style="color:#dc2626">일치 없음</span> '
|
||
f'<span style="color:#64748b">(0/{c["sk_total"]})</span></td>')
|
||
else:
|
||
kw_show = ', '.join(c['sk_hit'][:6])
|
||
rest = f', … <span style="color:#64748b">{c["sk_hit_count"]}/{c["sk_total"]} 일치</span>' \
|
||
if c['sk_hit_count'] > 6 \
|
||
else f' <span style="color:#64748b">({c["sk_hit_count"]}/{c["sk_total"]} 일치)</span>'
|
||
body.append(f'<td class="frm-col">{kw_show}{rest}</td>')
|
||
body.append('</tr>')
|
||
|
||
# 세트 키워드 — 매칭된 세트 raw text + coverage
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">세트 키워드'
|
||
'<span class="def">함께 등장하는 키워드 묶음</span></td>')
|
||
body.append(f'<td class="mdx-col"><b>MDX 본문 문장 단위</b><br/>{mdx_set_ex}</td>')
|
||
for c in cols:
|
||
hit_g = c['hit_groups']
|
||
if not hit_g:
|
||
body.append(f'<td class="frm-col"><span style="color:#dc2626">일치 없음</span> '
|
||
f'<span style="color:#64748b">(0/{c["g_total"]})</span></td>')
|
||
else:
|
||
shows = []
|
||
for g in hit_g[:2]:
|
||
raw = g.get('source_text_raw', '')
|
||
if len(raw) > 26:
|
||
raw = raw[:25] + '…'
|
||
cov = g.get('coverage', 0)
|
||
shows.append(f'· {raw} <span style="color:#059669">({cov:.0%})</span>')
|
||
hit_count = len(hit_g)
|
||
body.append(f'<td class="frm-col">{"<br/>".join(shows)}'
|
||
f'<br/><span style="color:#64748b">등 {hit_count}/{c["g_total"]} 세트 일치</span></td>')
|
||
body.append('</tr>')
|
||
|
||
# 연관 키워드
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">연관 키워드'
|
||
'<span class="def">여러 프레임에 공통 등장하는 보조 키워드</span></td>')
|
||
body.append(f'<td class="mdx-col"><b>MDX 본문 일반어</b><br/>{mdx_rel_ex}</td>')
|
||
for c in cols:
|
||
if c['rel_hit_count'] == 0:
|
||
body.append(f'<td class="frm-col"><span style="color:#dc2626">일치 없음</span></td>')
|
||
else:
|
||
kw_show = ', '.join(c['rel_hit'][:6])
|
||
body.append(f'<td class="frm-col">{kw_show}, … '
|
||
f'<span style="color:#64748b">{c["rel_hit_count"]}/{c["rel_total"]} 일치</span></td>')
|
||
body.append('</tr>')
|
||
|
||
# V1 점수 행
|
||
body.append('<tr class="score-row">')
|
||
body.append('<td class="lbl-col">키워드 점수<span class="def">V1 매칭 점수</span></td>')
|
||
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
|
||
for c in cols:
|
||
body.append(f'<td>{c["kw_score"]:.3f}</td>')
|
||
body.append('</tr>')
|
||
|
||
# 키워드 산식
|
||
body.append('<tr class="formula-row"><td colspan="5">'
|
||
'키워드 점수 = 0.414 × 핵심 + 0.320 × 세트 + 0.265 × 연관 '
|
||
'<span style="color:#64748b">(가중치 = Logistic Regression 학습, TARGET 4/4)</span>'
|
||
'</td></tr>')
|
||
|
||
# ─── < 구조 매칭 > 섹션 ───
|
||
body.append('<tr class="section-head"><td colspan="5">< 구조 매칭 > '
|
||
'— MDX 글 구조 분석 결과와 프레임 4 항목 대조</td></tr>')
|
||
|
||
# 콘텐츠 성격
|
||
mdx_aff = mdx_profile.get('content_affinity', {}).get('primary', '—')
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">콘텐츠 성격'
|
||
'<span class="def">콘텐츠 종류</span></td>')
|
||
body.append(f'<td class="mdx-col">{aff_ko.get(mdx_aff, mdx_aff)}</td>')
|
||
for c in cols:
|
||
body.append(f'<td class="frm-col">{aff_ko.get(c["aff"], c["aff"])}</td>')
|
||
body.append('</tr>')
|
||
|
||
# 시각 의도
|
||
mdx_intent = mdx_profile.get('structure_intent', {}).get('primary', '—')
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">시각 의도'
|
||
'<span class="def">레이아웃이 전달하는 시각적 표현 방식</span></td>')
|
||
body.append(f'<td class="mdx-col">{intent_ko.get(mdx_intent, mdx_intent)}</td>')
|
||
for c in cols:
|
||
body.append(f'<td class="frm-col">{intent_ko.get(c["intent"], c["intent"])}</td>')
|
||
body.append('</tr>')
|
||
|
||
# 편집 슬롯
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">편집 슬롯'
|
||
'<span class="def">텍스트 교체 가능한 영역</span></td>')
|
||
body.append('<td class="mdx-col">'
|
||
'<b>표에서 컬럼 2개 추출</b> — 각 컬럼의 헤더=라벨, 첫 행=본문<br/>'
|
||
'<span style="font-size:7pt;color:#1e293b">'
|
||
'[1] label=<b>"DX"</b>, body="BIM << DX (Engineering + Management 통합)"<br/>'
|
||
'[2] label=<b>"BIM"</b>, body="Only 3D (형상 구현 중심)"'
|
||
'</span><br/>'
|
||
'<span style="font-size:7pt;color:#dc2626">'
|
||
'※ 표면적 추출 — 첫 행만, 나머지 행 (S/W·프로세스·성과품) 무시됨'
|
||
'</span></td>')
|
||
for c in cols:
|
||
slot_s = float(c['v4_axes'].get('slot', 0))
|
||
# 분해: within(0.5) + labels(0.3) + bodies(0.2)
|
||
# MDX 01-2 의 detected item_count = 2
|
||
mdx_n = 2
|
||
within = c['card'].get('min', 0) <= mdx_n <= c['card'].get('max', 99)
|
||
within_part = '0.5 (항목수 일치)' if within else '0.0 <span style="color:#dc2626">(항목수 불일치)</span>'
|
||
body.append(f'<td class="frm-col">'
|
||
f'<b style="color:#059669">{slot_s:.2f}</b> = {within_part} + 0.3 (라벨) + 0.2 (본문)<br/>'
|
||
f'<span style="font-size:7pt;color:#64748b">'
|
||
f'프레임 슬롯 정의: ' + ', '.join(c["slots"][:4]) + '</span></td>')
|
||
body.append('</tr>')
|
||
|
||
# 항목수 / 관계
|
||
mdx_card = mdx_profile.get('cardinality', {}).get('ideal', '—')
|
||
mdx_rel = mdx_profile.get('relation_type', '—')
|
||
body.append('<tr>')
|
||
body.append('<td class="lbl-col">항목 수 / 관계'
|
||
'<span class="def">비교 단위 개수와 관계 유형(비교/병렬/순서)</span></td>')
|
||
body.append(f'<td class="mdx-col">{mdx_card}개 / {mdx_rel}</td>')
|
||
for c in cols:
|
||
cv = c['card'].get('ideal', '—')
|
||
body.append(f'<td class="frm-col">{cv}개 / {c["rel"]}</td>')
|
||
body.append('</tr>')
|
||
|
||
# V3 점수
|
||
body.append('<tr class="score-row">')
|
||
body.append('<td class="lbl-col">구조 점수<span class="def">V3 구조 매칭 점수</span></td>')
|
||
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
|
||
for c in cols:
|
||
body.append(f'<td>{c["v3_total"]:.3f}</td>')
|
||
body.append('</tr>')
|
||
|
||
# 구조 산식
|
||
body.append('<tr class="formula-row"><td colspan="5">'
|
||
'구조 점수 = 0.40 × 레이아웃 일치 + 0.35 × 콘텐츠 성격 일치 + 0.25 × 시각 의도 일치 '
|
||
'<span style="color:#64748b">(0~1)</span>'
|
||
'</td></tr>')
|
||
|
||
|
||
# ─── < 종합 판정 > 섹션 ───
|
||
body.append('<tr class="section-head"><td colspan="5">< 종합 판정 > '
|
||
'— 키워드 + 구조 + 의미 신호 가중 합산</td></tr>')
|
||
|
||
# confidence 행
|
||
body.append('<tr class="score-row">')
|
||
body.append('<td class="lbl-col">confidence<span class="def">V4 종합 점수 (0~1)</span></td>')
|
||
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
|
||
for c in cols:
|
||
body.append(f'<td style="font-size:10pt">{c["v4_conf"]:.3f}</td>')
|
||
body.append('</tr>')
|
||
|
||
# 라벨 행
|
||
body.append('<tr class="label-row">')
|
||
body.append('<td class="lbl-col">판정 라벨<span class="def">후속 작업 분기</span></td>')
|
||
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
|
||
for c in cols:
|
||
ko, fg, bg = label_ko.get(c['v4_label'], (c['v4_label'], '#000', '#eee'))
|
||
body.append(f'<td class="frm-col">'
|
||
f'<span style="background:{bg};color:{fg};padding:2pt 7pt;border-radius:3pt;'
|
||
f'font-weight:700;font-size:8pt">{c["v4_label"]}</span><br/>'
|
||
f'<span style="font-size:7pt;color:#475569">{ko}</span></td>')
|
||
body.append('</tr>')
|
||
|
||
# 종합 산식
|
||
body.append('<tr class="formula-row"><td colspan="5">'
|
||
'confidence = 0.25 × 핵심 키워드 + 0.20 × 항목 수 + 0.20 × 관계 유형 '
|
||
'+ 0.15 × 편집 슬롯 + 0.20 × 의미 임베딩 − 페널티'
|
||
'</td></tr>')
|
||
|
||
body.append('</table>')
|
||
|
||
# ─── 라벨 / 후속 작업 비용 표 ───
|
||
body.append('<table class="routing-table">'
|
||
'<thead><tr>'
|
||
'<th style="width:14%">판정 라벨</th>'
|
||
'<th style="width:12%">confidence</th>'
|
||
'<th style="width:42%">후속 작업</th>'
|
||
'<th style="width:32%">비용</th>'
|
||
'</tr></thead><tbody>')
|
||
body.append('<tr><td class="center"><span class="label-use_as_is">use_as_is</span></td>'
|
||
'<td class="num">≥ 0.90</td>'
|
||
'<td>슬롯에 텍스트만 매핑</td>'
|
||
'<td>자동, AI 호출 0회</td></tr>')
|
||
body.append('<tr><td class="center"><span class="label-light_edit">light_edit</span></td>'
|
||
'<td class="num">≥ 0.75</td>'
|
||
'<td>AI 가 슬롯 텍스트 다듬기</td>'
|
||
'<td>AI 1회 호출</td></tr>')
|
||
body.append('<tr><td class="center"><span class="label-restructure">restructure</span></td>'
|
||
'<td class="num">≥ 0.60</td>'
|
||
'<td>항목 수 조정 또는 대안 layout 으로 이동</td>'
|
||
'<td>사람 + AI 여러 호출</td></tr>')
|
||
body.append('<tr><td class="center"><span class="label-reject">reject</span></td>'
|
||
'<td class="num">< 0.60</td>'
|
||
'<td>새로 디자인</td>'
|
||
'<td>사람 디자인</td></tr>')
|
||
body.append('</tbody></table>')
|
||
|
||
return html_wrap('04. MDX ↔ Top-3 프레임 비교', CSS_DECK_4, ''.join(body))
|
||
|
||
|
||
# ============================================================
|
||
# DECK 5: 구조 매칭 4 항목 — MDX 분석 방법 상세
|
||
# ============================================================
|
||
|
||
CSS_DECK_5 = CSS_BASE + """
|
||
@page { size: A4 portrait; margin: 12mm; }
|
||
.sk-block { background: #fafafa; border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 6pt 9pt; margin: 5pt 0; }
|
||
.sk-block .sk-title { font-size: 11pt; font-weight: 700; color: #2563eb; margin-bottom: 3pt; }
|
||
.sk-block .sk-title .num { display: inline-block; background: #2563eb; color: white; width: 18pt; height: 18pt; line-height: 18pt; text-align: center; border-radius: 50%; font-size: 9pt; margin-right: 6pt; }
|
||
.sk-block .sk-title .accuracy { float: right; font-size: 8pt; padding: 2pt 7pt; border-radius: 8pt; font-weight: 700; }
|
||
.sk-block .accuracy.good { background: #d1fae5; color: #065f46; }
|
||
.sk-block .accuracy.weak { background: #fee2e2; color: #991b1b; }
|
||
.sk-block .what { font-size: 9pt; color: #475569; margin: 3pt 0; }
|
||
.sk-block .what b { color: #0f172a; }
|
||
.sk-block .how { background: white; border-left: 2.5pt solid #2563eb; padding: 4pt 8pt; margin: 4pt 0; font-size: 8.5pt; line-height: 1.5; }
|
||
.sk-block .how b { color: #1e3a8a; }
|
||
.sk-block table.dict { width: 100%; font-size: 8pt; border-collapse: collapse; margin-top: 3pt; }
|
||
.sk-block table.dict th { background: #1e293b; color: white; padding: 2pt 5pt; font-size: 7.5pt; text-align: left; }
|
||
.sk-block table.dict td { border: 0.4pt solid #cbd5e1; padding: 2pt 5pt; vertical-align: top; }
|
||
.sk-block table.dict td.cat { font-weight: 700; color: #1e3a8a; width: 30%; background: #eff6ff; font-family: ui-monospace, monospace; font-size: 7.5pt; }
|
||
.sk-block table.dict td.kws { color: #475569; font-size: 7.5pt; }
|
||
.sk-block .example { background: #fffbeb; border: 0.5pt solid #f59e0b; border-radius: 2pt; padding: 4pt 7pt; margin-top: 4pt; font-size: 8pt; color: #78350f; }
|
||
.sk-block .example b { color: #92400e; }
|
||
.sk-block .limit { background: #fee2e2; color: #991b1b; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 600; }
|
||
"""
|
||
|
||
|
||
def build_deck5_structure_keyword():
|
||
"""DECK 5 — 구조 매칭 4 항목 각각이 MDX 본문을 어떻게 분석하나"""
|
||
body = []
|
||
body.append('<h1>05. 구조 매칭 4 항목 — MDX 본문을 어떻게 분석하나</h1>')
|
||
body.append('<div class="meta">DECK 04 의 「구조 매칭」 4 항목이 코드에서 실제로 어떻게 동작하는지 상세 설명. '
|
||
'각 항목별 정의 + 매칭 방법 + 사용하는 키워드 사전.</div>')
|
||
|
||
# ─── 1) 콘텐츠 성격 ───
|
||
body.append('<div class="sk-block">')
|
||
body.append('<div class="sk-title"><span class="num">1</span>콘텐츠 성격 (content_affinity)'
|
||
'<span class="accuracy weak">정확도 약함</span></div>')
|
||
body.append('<div class="what"><b>무엇인가</b> : MDX 콘텐츠가 어떤 종류의 정보를 담는가 '
|
||
'(개념 정의 / 비교 / 정책·요구사항 / 단계 흐름 / 변화 / 역할 등 12 카테고리)</div>')
|
||
body.append('<div class="how"><b>매칭 방법</b> : 사전 정의 12 카테고리의 키워드 사전을 가지고 '
|
||
'MDX 본문에서 각 키워드 등장 횟수를 카운트 → <b>가장 많이 매칭된 카테고리 = primary</b>. '
|
||
'(2위·3위 = secondary)</div>')
|
||
|
||
body.append('<table class="dict"><thead><tr><th style="width:32%">카테고리 (영문 enum / 한글)</th><th>매칭 키워드</th></tr></thead><tbody>')
|
||
aff_dict = [
|
||
('concept_definition', '개념 정의', '정의, 이란, 분류, 구분'),
|
||
('concept_comparison', '개념 비교', '비교, 대조, 차이, vs, VS, 상호관계'),
|
||
('goal_axes', '목표 축', '목표, 궁극적, 비전, 목적, 지향'),
|
||
('persona_benefit', '대상별 효익', '발주자, 설계자, 시공자, 기대효과, 혜택, 이익'),
|
||
('process_steps', '단계 흐름', '단계, 순서, Step, 1단계, 2단계, 흐름'),
|
||
('before_after_change', '변화 / 전환', 'AS-IS, TO-BE, 전환, 혁신, 변화, 이중 변환, 과정 <span style="color:#dc2626">(STRONG 패턴 필수)</span>'),
|
||
('capability_requirements', '역량 / 요건', '필수, 요건, 필요, 역량, S/W, 도구'),
|
||
('comparative_matrix', '행렬형 비교', '다면, 다축, 관점별, 여러 관점, 축'),
|
||
('stakeholder_roles', '이해관계자 역할', '역할, 책임, 주도'),
|
||
('policy_requirements', '정책 / 요구사항', '정책, 제도, 도입, 거버넌스, 전면 도입, 국외, 국내, 선진, 현황, 사례'),
|
||
('tool_ecosystem', '도구 생태계', 'Revit, Navisworks, SketchUp, 소프트웨어, S/W 생태'),
|
||
('interrelation', '상호관계', '상호관계, 순환, 조화, 교차, 수렴, 3원, 순환도, 관계도'),
|
||
]
|
||
for cat, ko, kws in aff_dict:
|
||
body.append(f'<tr><td class="cat">{cat}<br/><span style="color:#475569;font-weight:400">{ko}</span></td><td class="kws">{kws}</td></tr>')
|
||
body.append('</tbody></table>')
|
||
|
||
body.append('<div class="example"><b>예시 — MDX 01-2 (DX 와 BIM 의 구분)</b><br/>'
|
||
'본문에 「국내, 도입, 사례, 현황」 다수 등장 → <code>policy_requirements</code> (정책/요구사항) 1위로 잘못 잡힘. '
|
||
'<span class="limit">한계 : 단순 카운트라 부정확</span> '
|
||
'(사람이 보면 「행렬형 비교」가 맞음)</div>')
|
||
body.append('</div>')
|
||
|
||
# ─── 2) 시각 의도 ───
|
||
body.append('<div class="sk-block">')
|
||
body.append('<div class="sk-title"><span class="num">2</span>시각 의도 (structure_intent)'
|
||
'<span class="accuracy weak">정확도 약함</span></div>')
|
||
body.append('<div class="what"><b>무엇인가</b> : 콘텐츠를 어떤 방식으로 시각적으로 표현하려는지 '
|
||
'(2 항목 비교 / 매트릭스 / 순환 / 단계 / 위계 / 강조 등 9 카테고리)</div>')
|
||
body.append('<div class="how"><b>매칭 방법</b> : 사전 9 카테고리 키워드 매칭 (콘텐츠 성격과 동일 방식) '
|
||
'+ 일부 카테고리는 <b>STRONG 구절 패턴 추가 검증</b> 필수 '
|
||
'(예: state_transition 은 「AS-IS / TO-BE」 같은 강한 구절 1개 이상)</div>')
|
||
|
||
body.append('<table class="dict"><thead><tr><th style="width:32%">카테고리 (영문 enum / 한글)</th><th>매칭 키워드</th></tr></thead><tbody>')
|
||
int_dict = [
|
||
('binary_compare', '2 항목 직접 비교', '2개 비교, 2개 개념, 대조, 양분'),
|
||
('state_transition', '상태 전환', 'AS-IS, TO-BE, 전환, 혁신, 이중 Transformation, 과정 <span style="color:#dc2626">(STRONG 필수)</span>'),
|
||
('cycle_interrelation', '순환 / 상호 관계', '상호관계, 순환, 조화, 교차, 3원'),
|
||
('multi_parallel', '다항목 병렬', '3개 병렬, 4개 병렬, 카드 3열, 3관점'),
|
||
('hierarchy', '위계', '<i style="color:#64748b">단일 키워드 매칭 금지 — STRONG 구절 패턴만 인정</i>'),
|
||
('sequence', '순차 단계', '단계, 순서, 흐름, step'),
|
||
('matrix_coverage', '매트릭스 (모든 축 한눈에)', '다면, 관점별, 여러 관점, 축별'),
|
||
('persona_mapping', '주체별 매핑', '주체별, 발주자/설계자/시공자, 역할별'),
|
||
('singleton_emphasis', '단일 강조', '강조, 문제, 진단, 약점'),
|
||
]
|
||
for cat, ko, kws in int_dict:
|
||
body.append(f'<tr><td class="cat">{cat}<br/><span style="color:#475569;font-weight:400">{ko}</span></td><td class="kws">{kws}</td></tr>')
|
||
body.append('</tbody></table>')
|
||
|
||
body.append('<div class="example"><b>예시 — MDX 01-2</b><br/>'
|
||
'제목·본문에 「상호관계」 단어가 있어 <code>cycle_interrelation</code> (순환/상호 관계) 1위로 잡힘. '
|
||
'<span class="limit">한계 : 도메인 키워드 사전이라 일반 표현은 못 잡음</span></div>')
|
||
body.append('</div>')
|
||
|
||
# ─── 3) 편집 슬롯 ───
|
||
body.append('<div class="sk-block">')
|
||
body.append('<div class="sk-title"><span class="num">3</span>편집 슬롯 (slot)'
|
||
'<span class="accuracy weak">표면적 검사만</span></div>')
|
||
body.append('<div class="what"><b>무엇인가</b> : 프레임에서 텍스트 교체 가능한 영역 '
|
||
'(예: 제목 / A 컬럼 라벨 / B 컬럼 라벨 / 행 데이터). '
|
||
'MDX 콘텐츠가 이 슬롯들을 채울 수 있는가가 핵심.</div>')
|
||
body.append('<div class="how"><b>매칭 방법</b> : MDX 본문에서 슬롯 후보를 자동 추출 → 후보의 라벨/본문 유무로 점수.<br/>'
|
||
' · <b>슬롯 후보 추출 우선순위</b> :<br/>'
|
||
' ① subsections (`##` 헤딩) 있으면 → 각 헤딩이 슬롯. label = 헤딩 텍스트, body = 헤딩 아래 본문<br/>'
|
||
' ② 표 있으면 → 각 컬럼이 슬롯. label = 컬럼 헤더, body = 첫 행 데이터<br/>'
|
||
' ③ 불릿 (`*`, `-`) 있으면 → 각 불릿이 슬롯. label = 불릿 첫 단어, body = 불릿 나머지<br/>'
|
||
' · <b>점수 계산</b> (휴리스틱) :<br/>'
|
||
' ① MDX 항목 수가 frame cardinality [min, max] 범위 안 → +0.5<br/>'
|
||
' ② 모든 후보에 라벨이 비어있지 않음 → +0.3<br/>'
|
||
' ③ 모든 후보에 본문이 비어있지 않음 → +0.2 / 최대 1.0</div>')
|
||
body.append('<div class="example"><b>예시 — MDX 01-2 ↔ Frame 18</b><br/>'
|
||
'MDX 본문에 표 발견 → 컬럼 2개 추출 :<br/>'
|
||
' [1] label=<b>"DX"</b>, body="BIM << DX (Engineering + Management 통합)"<br/>'
|
||
' [2] label=<b>"BIM"</b>, body="Only 3D (형상 구현 중심)"<br/>'
|
||
'→ Frame 18 cardinality 2/2/2 범위 내 (+0.5) + 라벨 둘 다 있음 (+0.3) + 본문 둘 다 있음 (+0.2) = <b>1.00</b><br/>'
|
||
'<span class="limit">한계 1 : 표 첫 행만 추출, 나머지 행 (S/W·프로세스·성과품) 무시</span><br/>'
|
||
'<span class="limit">한계 2 : "BIM → col_a_label, DX → col_b_label" 같은 frame 슬롯과의 의미 매핑 없음</span> '
|
||
'(후속 단계 텍스트 편집자 책임)</div>')
|
||
body.append('</div>')
|
||
|
||
# ─── 4) 항목 수 / 관계 ───
|
||
body.append('<div class="sk-block">')
|
||
body.append('<div class="sk-title"><span class="num">4</span>항목 수 / 관계 (cardinality + relation_type)'
|
||
'<span class="accuracy good">비교적 정확</span></div>')
|
||
body.append('<div class="what"><b>무엇인가</b> : MDX 콘텐츠의 비교 단위 개수와 관계 유형. '
|
||
'관계 유형 = <code>compare</code>(비교) / <code>parallel</code>(병렬) / <code>sequence</code>(순서) 중 하나.</div>')
|
||
body.append('<div class="how"><b>매칭 방법</b> : <code>detect_mdx_layout_v2</code> 함수가 MDX 본문에서 '
|
||
'구조 패턴을 자동 감지 → <code>MDX_LAYOUT_STRUCTURE</code> 사전에서 해당 layout 의 '
|
||
'family / relation_type / cardinality_ideal 가져옴.<br/>'
|
||
' · 표 형태 (`|`, `|---|`) 패턴 → table family<br/>'
|
||
' · `1.`, `2.`, `Step 1`, `→` 등 → sequence<br/>'
|
||
' · 카드 나열 / `*` 불릿 N개 → parallel + cardinality</div>')
|
||
body.append('<div class="example"><b>예시 — MDX 01-2</b><br/>'
|
||
'본문이 4 행 표 (범위/SW/프로세스/성과품 × BIM/DX) → <code>compare-rows</code> layout 감지 '
|
||
'→ family=table, relation_type=compare, cardinality=2. '
|
||
'<b style="color:#065f46">정확</b> (구조적 신호라 안정적)</div>')
|
||
body.append('</div>')
|
||
|
||
return html_wrap('05. 구조 매칭 4 항목 상세', CSS_DECK_5, ''.join(body))
|
||
|
||
|
||
# ============================================================
|
||
# DECK 6: Frame 18 → DB 저장 샘플
|
||
# ============================================================
|
||
|
||
CSS_DECK_6 = CSS_BASE + """
|
||
@page { size: A4 portrait; margin: 12mm; }
|
||
.db-header { display: table; width: 100%; margin-bottom: 6pt; }
|
||
.db-header > div { display: table-cell; vertical-align: middle; }
|
||
.db-header .img-col { width: 30%; text-align: center; padding-right: 10pt; }
|
||
.db-header .img-col img { max-width: 100%; max-height: 35mm; border: 0.5pt solid #94a3b8; border-radius: 3pt; }
|
||
.db-header .info-col .name { font-weight: 700; color: #2563eb; font-size: 11pt; }
|
||
.db-header .info-col .desc { font-size: 8.5pt; color: #475569; margin-top: 3pt; line-height: 1.5; }
|
||
.db-section { background: #f8fafc; border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 6pt 10pt; margin: 5pt 0; }
|
||
.db-section .sec-title { font-size: 10pt; font-weight: 700; color: #2563eb; margin-bottom: 4pt; padding-bottom: 2pt; border-bottom: 1pt solid #cbd5e1; }
|
||
.db-section .sec-desc { font-size: 8pt; color: #64748b; margin-bottom: 4pt; }
|
||
pre.yaml { background: #1e293b; color: #e2e8f0; border-radius: 3pt; padding: 6pt 10pt; font-family: ui-monospace, "Menlo", monospace; font-size: 7.5pt; line-height: 1.4; margin: 3pt 0; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||
pre.yaml .key { color: #7dd3fc; }
|
||
pre.yaml .str { color: #fde68a; }
|
||
pre.yaml .num { color: #86efac; }
|
||
pre.yaml .com { color: #94a3b8; font-style: italic; }
|
||
"""
|
||
|
||
|
||
def build_deck6_figma_to_db(ontology_r2, auto, v1):
|
||
"""DECK 6 — Frame 18 (BIM과 DX의 이해) 의 DB 저장 형태 샘플"""
|
||
SHORT_ID = '18'
|
||
FRAME_NUM = 18
|
||
templates = ontology_r2['templates_v2']
|
||
fid = next(fid for fid, e in templates.items() if e.get('short_id') == SHORT_ID)
|
||
tpl = templates[fid]
|
||
|
||
# 키워드 데이터
|
||
detail = v1['mdx_sections']['01-2']['per_frame_detail'][fid]
|
||
standalone_kws = sorted(detail['standalone'].get('total', []))
|
||
|
||
sets_for_frame = []
|
||
for sid_a, s in auto['source_text_sets'].items():
|
||
if s['frame_id'] != fid:
|
||
continue
|
||
sets_for_frame.append({
|
||
'raw': s['source_text_raw'],
|
||
'tokens': [t['token'] for t in s.get('terms', [])],
|
||
})
|
||
|
||
all_tokens = set()
|
||
for s in auto['source_text_sets'].values():
|
||
if s['frame_id'] != fid:
|
||
continue
|
||
for t in s.get('terms', []):
|
||
all_tokens.add(t['token'])
|
||
related_kws = sorted(all_tokens - set(standalone_kws))
|
||
|
||
# 구조 데이터 (templates_v2 에서)
|
||
vp = tpl['visual_pattern']
|
||
aff = tpl['content_affinity']
|
||
intent = tpl['structure_intent_v2']
|
||
alts = tpl.get('alternative_patterns', [])
|
||
slots = tpl.get('slots', [])
|
||
|
||
body = []
|
||
body.append('<h1>06. Figma 디자인이 들어오면 DB 에 어떻게 저장되나 — Frame 18 샘플</h1>')
|
||
body.append('<div class="meta">새 Figma 프레임이 입력되면 「키워드 추출」 + 「구조 분석」 두 갈래로 '
|
||
'DB 에 저장. 매칭 시 이 DB 가 후보 풀이 됨. Frame 18 (BIM과 DX의 이해) 실제 저장 형태.</div>')
|
||
|
||
# ─── 헤더 ───
|
||
body.append('<div class="db-header">')
|
||
body.append('<div class="img-col">')
|
||
body.append(f'<img src="{img_uri(FRAME_NUM)}" alt="Frame {FRAME_NUM}"/>')
|
||
body.append('</div>')
|
||
body.append('<div class="info-col">')
|
||
body.append(f'<div class="name">Frame {FRAME_NUM} · {tpl["source"]["title"]}</div>')
|
||
body.append(f'<div class="desc">레이아웃 : <code>{vp["layout"]}</code> '
|
||
f'(2 항목 × N 행 행렬형 비교)<br/>'
|
||
f'정답 매칭 사례 : MDX 01-2 (DX 와 BIM 의 구분) → use_as_is (confidence 0.946)</div>')
|
||
body.append('</div></div>')
|
||
|
||
# ─── 키워드 DB ───
|
||
body.append('<div class="db-section">')
|
||
body.append('<div class="sec-title">키워드 DB — 프레임 텍스트에서 추출</div>')
|
||
body.append('<div class="sec-desc">Figma 프레임의 모든 텍스트 노드를 토큰화 → '
|
||
'식별력(unique) / 구조성(co-occur) / 보조성(common) 3 계층으로 분류해 저장.</div>')
|
||
|
||
# YAML 형식으로 표시
|
||
yaml_kw = []
|
||
yaml_kw.append('<span class="com"># 핵심 키워드 (이 프레임에만 등장 — 식별력 강함)</span>')
|
||
yaml_kw.append(f'<span class="key">standalone</span>:')
|
||
yaml_kw.append(f' <span class="key">total_count</span>: <span class="num">{len(standalone_kws)}</span>')
|
||
yaml_kw.append(f' <span class="key">terms</span>:')
|
||
for k in standalone_kws[:8]:
|
||
yaml_kw.append(f' - <span class="str">{k}</span>')
|
||
if len(standalone_kws) > 8:
|
||
yaml_kw.append(f' <span class="com"># … 외 {len(standalone_kws)-8}개</span>')
|
||
yaml_kw.append('')
|
||
yaml_kw.append('<span class="com"># 키워드 세트 (같은 줄에 함께 등장하는 묶음 — 구조 단위)</span>')
|
||
yaml_kw.append(f'<span class="key">source_text_sets</span>:')
|
||
yaml_kw.append(f' <span class="key">total_count</span>: <span class="num">{len(sets_for_frame)}</span>')
|
||
yaml_kw.append(f' <span class="key">sets</span>:')
|
||
for s in sets_for_frame[:3]:
|
||
raw = s["raw"][:50] + ("…" if len(s["raw"]) > 50 else "")
|
||
toks = s["tokens"][:6]
|
||
toks_str = ', '.join(f'<span class="str">{t}</span>' for t in toks)
|
||
if len(s["tokens"]) > 6:
|
||
toks_str += f' <span class="com"># … 외 {len(s["tokens"])-6}개</span>'
|
||
yaml_kw.append(f' - <span class="key">raw</span>: <span class="str">"{raw}"</span>')
|
||
yaml_kw.append(f' <span class="key">tokens</span>: [{toks_str}]')
|
||
if len(sets_for_frame) > 3:
|
||
yaml_kw.append(f' <span class="com"># … 외 {len(sets_for_frame)-3}개 세트</span>')
|
||
yaml_kw.append('')
|
||
yaml_kw.append('<span class="com"># 연관 키워드 (여러 프레임 공통 — 보조 신호)</span>')
|
||
yaml_kw.append(f'<span class="key">related</span>:')
|
||
yaml_kw.append(f' <span class="key">total_count</span>: <span class="num">{len(related_kws)}</span>')
|
||
yaml_kw.append(f' <span class="key">terms</span>: ['
|
||
+ ', '.join(f'<span class="str">{k}</span>' for k in related_kws[:8])
|
||
+ f' <span class="com"># … 외 {len(related_kws)-8}개</span>]')
|
||
|
||
body.append('<pre class="yaml">' + '\n'.join(yaml_kw) + '</pre>')
|
||
body.append('</div>')
|
||
|
||
# ─── 구조 DB ───
|
||
body.append('<div class="db-section">')
|
||
body.append('<div class="sec-title">구조 DB — 프레임 분석 결과</div>')
|
||
body.append('<div class="sec-desc">Figma 프레임의 레이아웃·항목수·관계를 분석 + '
|
||
'AI / 사람이 콘텐츠 성격·시각 의도·슬롯·대안 레이아웃 라벨링 → 저장.</div>')
|
||
|
||
yaml_st = []
|
||
yaml_st.append('<span class="com"># 시각 패턴 (자동 감지)</span>')
|
||
yaml_st.append('<span class="key">visual_pattern</span>:')
|
||
yaml_st.append(f' <span class="key">layout</span>: <span class="str">{vp["layout"]}</span>'
|
||
f' <span class="com"># 표/카드 등 레이아웃 ID</span>')
|
||
yaml_st.append(f' <span class="key">family</span>: <span class="str">{vp.get("family")}</span>'
|
||
f' <span class="com"># table / cards / visual / emphasis</span>')
|
||
yaml_st.append(f' <span class="key">relation_type</span>: <span class="str">{vp.get("relation_type")}</span>'
|
||
f' <span class="com"># compare / parallel / sequence</span>')
|
||
card = vp.get('cardinality', {})
|
||
yaml_st.append(f' <span class="key">cardinality</span>:')
|
||
yaml_st.append(f' <span class="key">ideal</span>: <span class="num">{card.get("ideal")}</span>'
|
||
f' <span class="key">min</span>: <span class="num">{card.get("min")}</span>'
|
||
f' <span class="key">max</span>: <span class="num">{card.get("max")}</span>')
|
||
yaml_st.append('')
|
||
yaml_st.append('<span class="com"># 콘텐츠 성격 (12 카테고리)</span>')
|
||
yaml_st.append('<span class="key">content_affinity</span>:')
|
||
yaml_st.append(f' <span class="key">primary</span>: <span class="str">{aff["primary"]}</span>'
|
||
f' <span class="com"># 행렬형 비교</span>')
|
||
if aff.get('secondary'):
|
||
yaml_st.append(f' <span class="key">secondary</span>: ['
|
||
+ ', '.join(f'<span class="str">{s}</span>' for s in aff['secondary'])
|
||
+ ']')
|
||
yaml_st.append('')
|
||
yaml_st.append('<span class="com"># 시각 의도 (9 카테고리)</span>')
|
||
yaml_st.append('<span class="key">structure_intent</span>:')
|
||
yaml_st.append(f' <span class="key">primary</span>: <span class="str">{intent["primary"]}</span>'
|
||
f' <span class="com"># 매트릭스</span>')
|
||
if intent.get('secondary'):
|
||
yaml_st.append(f' <span class="key">secondary</span>: ['
|
||
+ ', '.join(f'<span class="str">{s}</span>' for s in intent['secondary'])
|
||
+ ']')
|
||
yaml_st.append('')
|
||
yaml_st.append('<span class="com"># 편집 슬롯 (텍스트 교체 가능 영역)</span>')
|
||
yaml_st.append('<span class="key">slots</span>:')
|
||
for s in slots:
|
||
yaml_st.append(f' - <span class="str">{s["id"]}</span>')
|
||
yaml_st.append('')
|
||
yaml_st.append('<span class="com"># 대안 레이아웃 (이 프레임으로 못 담을 때 대체 가능)</span>')
|
||
yaml_st.append('<span class="key">alternative_patterns</span>:')
|
||
for a in alts[:4]:
|
||
yaml_st.append(f' - <span class="key">pattern</span>: <span class="str">{a["pattern"]}</span>'
|
||
f' <span class="key">confidence</span>: <span class="num">{a["confidence"]:.2f}</span>')
|
||
if len(alts) > 4:
|
||
yaml_st.append(f' <span class="com"># … 외 {len(alts)-4}개</span>')
|
||
|
||
body.append('<pre class="yaml">' + '\n'.join(yaml_st) + '</pre>')
|
||
body.append('</div>')
|
||
|
||
# ─── 매칭 시 사용 ───
|
||
body.append('<div class="db-section" style="background:#f0fdf4;border-color:#86efac">')
|
||
body.append('<div class="sec-title" style="color:#065f46;border-color:#86efac">'
|
||
'저장된 DB 가 매칭 시 어떻게 쓰이나</div>')
|
||
body.append('<div style="font-size:8.5pt;color:#1e293b;line-height:1.6">')
|
||
body.append('· <b>키워드 매칭 (V1)</b> : <code>standalone</code> / <code>source_text_sets</code> / '
|
||
'<code>related</code> 가 MDX 본문 단어와 대조 → 키워드 점수<br/>')
|
||
body.append('· <b>구조 매칭 (V3)</b> : <code>visual_pattern.layout</code> / <code>content_affinity</code> / '
|
||
'<code>structure_intent</code> 가 MDX 의 자동 분석 결과와 대조 → 구조 점수<br/>')
|
||
body.append('· <b>종합 판정 (V4)</b> : <code>cardinality</code> / <code>relation_type</code> / '
|
||
'<code>slots</code> 가 V4 5축의 cardinality/relation/slot 점수에 사용 → confidence<br/>')
|
||
body.append('· <b>적용 불가 시</b> : <code>alternative_patterns</code> 의 후보로 fallback')
|
||
body.append('</div></div>')
|
||
|
||
return html_wrap('06. Frame → DB 저장 샘플', CSS_DECK_6, ''.join(body))
|
||
|
||
|
||
# ============================================================
|
||
# DECK 7: 키워드집 정리 과정 + Frame 18 샘플
|
||
# ============================================================
|
||
|
||
CSS_DECK_7 = CSS_BASE + """
|
||
@page { size: A4 portrait; margin: 12mm; }
|
||
.kp-section { margin: 5pt 0 8pt; }
|
||
.kp-section .kp-title { font-size: 11pt; font-weight: 700; color: #2563eb; padding: 3pt 7pt; background: #e0e7ff; border-left: 3pt solid #2563eb; border-radius: 2pt; margin-bottom: 4pt; }
|
||
.kp-section .kp-title .num { display: inline-block; background: #2563eb; color: white; width: 18pt; height: 18pt; line-height: 18pt; text-align: center; border-radius: 50%; font-size: 9pt; margin-right: 6pt; }
|
||
table.kp { width: 100%; font-size: 9pt; border-collapse: collapse; margin: 3pt 0; }
|
||
table.kp th { background: #1e293b; color: white; padding: 4pt 7pt; font-size: 8.5pt; text-align: left; }
|
||
table.kp td { border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; vertical-align: top; }
|
||
table.kp td.num { text-align: right; font-family: ui-monospace, monospace; font-weight: 700; color: #0f172a; }
|
||
table.kp tr.total td { background: #fef3c7; font-weight: 700; color: #78350f; }
|
||
.kp-rules { background: #fafafa; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 9pt; font-size: 8.5pt; line-height: 1.55; margin: 3pt 0; }
|
||
.kp-rules b { color: #0f172a; }
|
||
.kp-rules ul { margin: 2pt 0; padding-left: 14pt; }
|
||
.kp-rules code { background: #f1f5f9; padding: 0 4pt; border-radius: 2pt; font-family: ui-monospace, monospace; font-size: 8pt; }
|
||
.kp-result { background: #ecfdf5; border: 0.7pt solid #059669; border-radius: 3pt; padding: 5pt 9pt; font-size: 9pt; color: #065f46; margin: 4pt 0; }
|
||
.kp-result b { color: #064e3b; font-size: 11pt; }
|
||
.kp-sample { background: #fffbeb; border: 0.7pt solid #f59e0b; border-radius: 3pt; padding: 6pt 10pt; margin: 4pt 0; }
|
||
.kp-sample .sample-head { font-size: 9.5pt; font-weight: 700; color: #92400e; margin-bottom: 4pt; }
|
||
.kp-sample .row { font-size: 8.5pt; line-height: 1.6; margin: 3pt 0; }
|
||
.kp-sample .row .label { display: inline-block; min-width: 22mm; font-weight: 700; color: #78350f; }
|
||
.kp-sample .row .count { color: #b45309; font-weight: 700; }
|
||
.kp-sample .row .terms { color: #1e293b; }
|
||
"""
|
||
|
||
|
||
def build_deck7_keyword_pipeline(auto, v1):
|
||
"""DECK 7 — 키워드집 생성 과정 (수집 → 전처리 → 분류) + Frame 18 샘플"""
|
||
# Frame 18 샘플 데이터
|
||
fid_18 = next((fid for fid, info in auto['frame_stats'].items()
|
||
if info.get('frame_number') == 18), None)
|
||
detail_18 = v1['mdx_sections']['01-2']['per_frame_detail'][fid_18]
|
||
sk_18 = sorted(detail_18['standalone'].get('total', []))
|
||
sets_18 = [s['source_text_raw'] for s in auto['source_text_sets'].values()
|
||
if s['frame_id'] == fid_18]
|
||
all_tokens_18 = set()
|
||
for s in auto['source_text_sets'].values():
|
||
if s['frame_id'] != fid_18:
|
||
continue
|
||
for t in s.get('terms', []):
|
||
all_tokens_18.add(t['token'])
|
||
related_18 = sorted(all_tokens_18 - set(sk_18))
|
||
|
||
body = []
|
||
body.append('<h1>07. 키워드집 정리 과정 — 어떻게 1,713개로 정리되었나</h1>')
|
||
body.append('<div class="meta">Figma + BEPS + MDX 문장을 수집 → 형태소 분석 + 표기 통합 → '
|
||
'등장 범위별 분류 → 최종 1,713개 키워드 확정.</div>')
|
||
|
||
# ─── 1. 수집 ───
|
||
body.append('<div class="kp-section">')
|
||
body.append('<div class="kp-title"><span class="num">1</span>수집 — 3 출처에서 문장 수집</div>')
|
||
body.append('<table class="kp"><thead><tr>'
|
||
'<th>출처</th><th style="width:18%">입력 문서</th><th style="width:22%">수집된 문장</th>'
|
||
'</tr></thead><tbody>')
|
||
body.append('<tr><td>Figma 프레임 텍스트</td><td class="num">32 개</td><td class="num">586</td></tr>')
|
||
body.append('<tr><td>BEPS 마스터 텍스트</td><td class="num">1 개</td><td class="num">1,410</td></tr>')
|
||
body.append('<tr><td>MDX 검증 구간</td><td class="num">4 개</td><td class="num">129</td></tr>')
|
||
body.append('<tr class="total"><td>합계</td><td class="num">37 개</td><td class="num">2,125</td></tr>')
|
||
body.append('</tbody></table>')
|
||
body.append('</div>')
|
||
|
||
# ─── 2. 전처리 ───
|
||
body.append('<div class="kp-section">')
|
||
body.append('<div class="kp-title"><span class="num">2</span>전처리 — 형태소 분석 + 표기 통합</div>')
|
||
body.append('<table class="kp"><thead><tr>'
|
||
'<th>단계</th><th style="width:22%">전체 등장 수</th><th style="width:22%">고유 단어 수</th>'
|
||
'</tr></thead><tbody>')
|
||
body.append('<tr><td>형태소 분석 결과 (원시)</td><td class="num">13,913</td><td class="num">1,738</td></tr>')
|
||
body.append('<tr><td>전처리 후 (표기 통합 + 보존 규칙 적용)</td><td class="num">14,161</td><td class="num"><b>1,713</b></td></tr>')
|
||
body.append('</tbody></table>')
|
||
body.append('<div class="kp-rules">')
|
||
body.append('<b>적용 규칙</b>')
|
||
body.append('<ul>')
|
||
body.append('<li><b>중요 표기 보존</b> : <code>DX</code>, <code>BIM</code>, <code>S/W</code>, <code>H/W</code>, '
|
||
'<code>2D</code>, <code>3D</code>, <code>AS-IS</code>, <code>TO-BE</code> 등 13개 — 깨지면 안 되는 표기</li>')
|
||
body.append('<li><b>표기 통합</b> : "디지털 전환", "디지털전환" → 대표 표기 <code>DX</code> 로 합침</li>')
|
||
body.append('<li><b>제외</b> : 1글자 단어, 순수 숫자, 공백/기호만 있는 토큰</li>')
|
||
body.append('<li><b>품사 사용</b> : 일반명사 / 고유명사 / 외국어 표기 / 숫자 표기만 (조사·동사 등 제외)</li>')
|
||
body.append('</ul></div>')
|
||
body.append('</div>')
|
||
|
||
# ─── 3. 분류 ───
|
||
body.append('<div class="kp-section">')
|
||
body.append('<div class="kp-title"><span class="num">3</span>분류 — 등장 범위별 3 계층 (1,713 = 451 + 1,238 + 24)</div>')
|
||
body.append('<table class="kp"><thead><tr>'
|
||
'<th style="width:22%">계층</th><th style="width:14%">개수</th>'
|
||
'<th style="width:18%">등장 범위</th><th>의미 / 용도</th>'
|
||
'</tr></thead><tbody>')
|
||
body.append('<tr><td><b>핵심 키워드</b></td><td class="num">451</td>'
|
||
'<td>1 프레임에만</td>'
|
||
'<td>특정 프레임에만 등장 → <b>식별력 강함</b>. V1 매칭 가중치 0.414</td></tr>')
|
||
body.append('<tr><td><b>연관 키워드</b></td><td class="num">1,238</td>'
|
||
'<td>2~9 프레임</td>'
|
||
'<td>여러 프레임에 보조 등장 → <b>맥락 보강</b>. V1 매칭 가중치 0.265</td></tr>')
|
||
body.append('<tr><td><b>광범위 공통어</b></td><td class="num">24</td>'
|
||
'<td>10+ 프레임</td>'
|
||
'<td>너무 흔해 식별력 낮음 → 매칭에서 가중 작음</td></tr>')
|
||
body.append('<tr class="total"><td>합계</td><td class="num">1,713</td><td>—</td><td>전체 고유 키워드</td></tr>')
|
||
body.append('</tbody></table>')
|
||
body.append('<div style="font-size:8pt;color:#64748b;margin-top:2pt">'
|
||
'※ 위에 더해 <b>키워드 세트 524개</b> 별도 — 같은 프레임 내 함께 등장하는 키워드 묶음 '
|
||
'(frame-level 시그니처). V1 매칭 가중치 0.320'
|
||
'</div>')
|
||
body.append('</div>')
|
||
|
||
# ─── 결과 ───
|
||
body.append('<div class="kp-result">'
|
||
'<b>결과</b> 2,125 문장 → 형태소 분석 13,913 등장 (고유 1,738) → '
|
||
'전처리 후 <b>최종 1,713 고유 키워드</b> + <b>524 키워드 세트</b>.<br/>'
|
||
'키워드는 등장 범위로 3 계층 (451 / 1,238 / 24) — V1 매칭의 핵심·연관·광범위 신호로 사용.'
|
||
'</div>')
|
||
|
||
# ─── 4. 샘플 — Frame 18 ───
|
||
body.append('<div class="kp-sample">')
|
||
body.append('<div class="sample-head">샘플 — Frame 18 (BIM과 DX의 이해) 의 매칭 키워드</div>')
|
||
body.append(f'<div class="row"><span class="label">핵심 키워드</span>'
|
||
f'<span class="count">{len(sk_18)} 개</span> · '
|
||
f'<span class="terms">{", ".join(sk_18[:12])}'
|
||
f'{f", … 외 {len(sk_18)-12}개" if len(sk_18)>12 else ""}</span></div>')
|
||
sets_short = []
|
||
for s in sets_18[:4]:
|
||
sets_short.append(s[:30] + ('…' if len(s) > 30 else ''))
|
||
body.append(f'<div class="row"><span class="label">키워드 세트</span>'
|
||
f'<span class="count">{len(sets_18)} 개</span> · '
|
||
f'<span class="terms">{", ".join(sets_short)}'
|
||
f'{f", … 외 {len(sets_18)-4}개" if len(sets_18)>4 else ""}</span></div>')
|
||
body.append(f'<div class="row"><span class="label">연관 키워드</span>'
|
||
f'<span class="count">{len(related_18)} 개</span> · '
|
||
f'<span class="terms">{", ".join(related_18[:12])}'
|
||
f'{f", … 외 {len(related_18)-12}개" if len(related_18)>12 else ""}</span></div>')
|
||
body.append('<div style="font-size:8pt;color:#78350f;margin-top:5pt;border-top:0.5pt dashed #f59e0b;padding-top:3pt">'
|
||
f'→ Frame 18 은 핵심 {len(sk_18)} + 세트 {len(sets_18)} + 연관 {len(related_18)} 개 키워드를 가지고 '
|
||
'MDX 본문과 매칭. MDX 01-2 (DX와 BIM의 구분) 와는 핵심 25/{0} 일치, 세트 38/{1} 일치, 연관 75/{2} 일치 '
|
||
'→ 키워드 점수 0.930 → V4 confidence 0.946 → use_as_is (그대로 사용)'
|
||
.format(len(sk_18), len(sets_18), len(related_18))
|
||
+ '</div>')
|
||
body.append('</div>')
|
||
|
||
return html_wrap('07. 키워드집 정리 과정', CSS_DECK_7, ''.join(body))
|
||
|
||
|
||
# ============================================================
|
||
# 메인
|
||
# ============================================================
|
||
|
||
def main():
|
||
v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8'))
|
||
v2 = yaml.safe_load((HERE / 'v2_semantic_rerank_result.yaml').read_text(encoding='utf-8'))
|
||
v3_r5 = yaml.safe_load((HERE / 'v3_structure_rerank_r5_result.yaml').read_text(encoding='utf-8'))
|
||
# V4 = 32 전체 평가 (v4_full32_result.yaml) 사용
|
||
v4_r2 = yaml.safe_load((HERE / 'v4_full32_result.yaml').read_text(encoding='utf-8'))
|
||
auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8'))
|
||
ontology_r2 = yaml.safe_load((HERE / 'structure_ontology_v2_final_r2.yaml').read_text(encoding='utf-8'))
|
||
descriptions = build_frame_descriptions(auto)
|
||
|
||
DECK_01.write_text(build_deck1(v1, v2, v3_r5, v4_r2, descriptions), encoding='utf-8')
|
||
DECK_02.write_text(build_deck2(v1, v2, v3_r5, v4_r2, descriptions), encoding='utf-8')
|
||
DECK_03.write_text(build_deck3(), encoding='utf-8')
|
||
DECK_04.write_text(build_deck4(ontology_r2, auto, v1, v3_r5, v4_r2, descriptions), encoding='utf-8')
|
||
DECK_05.write_text(build_deck5_structure_keyword(), encoding='utf-8')
|
||
DECK_06.write_text(build_deck6_figma_to_db(ontology_r2, auto, v1), encoding='utf-8')
|
||
DECK_07.write_text(build_deck7_keyword_pipeline(auto, v1), encoding='utf-8')
|
||
|
||
print('=' * 70)
|
||
print('Deck 7 페이지 생성 완료 (A4 1 페이지씩)')
|
||
print('=' * 70)
|
||
for p in [DECK_01, DECK_02, DECK_03, DECK_04, DECK_05, DECK_06, DECK_07]:
|
||
size_kb = p.stat().st_size / 1024
|
||
print(f' {p.name:38} {size_kb:>8.0f} KB')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|