Files
C.E.L_Slide_test2/tests/matching/pipeline_10_holdout_labeling_prep.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

303 lines
13 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 10 — Holdout 평가용 합리성 라벨링 자료 준비.
목적:
Holdout 3 섹션 (01-1, 02-1, 02-2.1) 각각에 대해 V1/V2/V3/V4 의 1위 선택을
사용자가 ○/△/× 로 라벨링할 수 있도록 자료를 준비.
평가 방식 (사용자 지침):
"정답 프레임" 단일 지목이 아니라 **"이 프레임이 이 섹션에 합리적으로 적용 가능한가"** 를 평가.
- rational (○): 적용 가능
- ambiguous (△): 조건부/논란
- irrational (×): 적용 불가
출력:
- HOLDOUT_LABELING.html — 각 Holdout 섹션 × 4 버전 1위 프레임을 이미지 + MDX 원문과 함께 표시
- holdout_labeling_template.yaml — 사용자가 rating 필드를 채워 넣을 템플릿
"""
import base64
from pathlib import Path
import sys
import yaml
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
PNG_DIR = (HERE / '..' / '..' / 'data' / 'figma_previews').resolve()
HTML_PATH = HERE / 'HOLDOUT_LABELING.html'
TEMPLATE_PATH = HERE / 'holdout_labeling_template.yaml'
HOLDOUT_SIDS = ['01-1', '02-1', '02-2.1']
_IMG_CACHE = {}
def img_data_uri(frame_num):
if frame_num in _IMG_CACHE:
return _IMG_CACHE[frame_num]
p = PNG_DIR / f'{frame_num:02d}.png'
if not p.exists():
return ''
uri = 'data:image/png;base64,' + base64.b64encode(p.read_bytes()).decode('ascii')
_IMG_CACHE[frame_num] = uri
return uri
def extract_mdx_raw(sid):
from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR
cfg = MDX_SECTIONS[sid]
p = MDX_DIR / cfg['file']
lines = p.read_text(encoding='utf-8').split('\n')
start_idx = None
for i, ln in enumerate(lines):
if ln.strip() == cfg['start'].strip():
start_idx = i
break
end_idx = len(lines)
if cfg.get('end_prefix'):
for i in range(start_idx + 1, len(lines)):
if lines[i].strip().startswith(cfg['end_prefix']):
end_idx = i
break
section = lines[start_idx:end_idx]
return section[0].lstrip('#').strip(), '\n'.join(section)
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:
raw = sinfo['source_text_raw']
out[fnum] = raw
break
if fnum not in out:
out[fnum] = f'Frame {fnum}'
return out
def get_v_picks(sid, v1, v2, v3, v4):
"""각 V 의 1위 반환 — {v, frame_number, metric, extra}"""
picks = {}
# V1
top = v1['mdx_sections'][sid]['rank_by_matching_score'][0]
picks['V1'] = {
'frame_number': top['frame_number'],
'metric_label': 'matching_score',
'metric_value': round(top['matching_score'], 3),
'extra': None,
}
# V2
v2_top = sorted(v2['mdx_sections'][sid]['v2_rerank'], key=lambda x: x['v2_rank'])[0]
picks['V2'] = {
'frame_number': v2_top['frame_number'],
'metric_label': 'semantic_score',
'metric_value': round(v2_top['semantic_score'], 3),
'extra': None,
}
# V3
v3_top = sorted(v3['mdx_sections'][sid]['v3_rerank'], key=lambda x: x['v3_rank'])[0]
picks['V3'] = {
'frame_number': v3_top['frame_number'],
'metric_label': 'structure_compat',
'metric_value': round(v3_top['structure_compat'], 3),
'extra': v3_top.get('fig_layout'),
}
# V4
v4_top = sorted(v4['mdx_sections'][sid]['v4_judgments'], key=lambda x: x['v4_rank'])[0]
picks['V4'] = {
'frame_number': v4_top['frame_number'],
'metric_label': 'confidence',
'metric_value': round(v4_top['confidence'], 3),
'extra': v4_top['label'],
}
return picks
def render_html(sections_data):
style = """
body { font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1400px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; }
h1 { border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; }
h2 { margin-top: 2.5em; background: #e0e7ff; padding: 0.6em 0.9em; border-left: 4px solid #0a6; border-radius: 4px; }
h3 { margin-top: 1.5em; color: #1a365d; }
.section-block { background: #fff; border-radius: 8px; border: 1px solid #e2e8f0; padding: 1em 1.2em; margin: 1.5em 0; }
.mdx-excerpt { background: #fafafa; border-left: 3px solid #64748b; padding: 0.8em 1em; margin: 0.5em 0 1em; font-size: 0.9em; white-space: pre-wrap; font-family: ui-monospace, monospace; max-height: 300px; overflow: auto; }
table.v-grid { width: 100%; border-collapse: collapse; table-layout: fixed; }
table.v-grid th { background: #1e293b; color: #fff; padding: 10px; text-align: center; font-weight: 700; width: 25%; }
table.v-grid td { border: 1px solid #cbd5e1; padding: 12px; text-align: center; vertical-align: top; background: #fff; }
table.v-grid img { max-width: 240px; height: auto; border: 1px solid #cbd5e1; border-radius: 4px; display: block; margin: 0 auto 8px; }
.frame-label { display: block; font-weight: 600; color: #2563eb; font-size: 1.05em; margin-bottom: 2px; }
.frame-desc { display: block; color: #64748b; font-size: 0.85em; margin: 4px 0 8px; min-height: 2em; }
.metric { margin-top: 8px; padding-top: 8px; border-top: 1px dashed #e2e8f0; font-size: 0.9em; }
.metric .key { color: #64748b; }
.metric .value { color: #059669; font-weight: 600; font-family: ui-monospace, monospace; }
.rating-box { margin-top: 10px; padding: 8px; background: #fffbeb; border: 2px dashed #fbbf24; border-radius: 4px; font-size: 0.9em; color: #92400e; }
.rating-box b { color: #78350f; }
.label-use_as_is { background: #d1fae5; color: #065f46; padding: 2px 8px; border-radius: 3px; }
.label-light_edit { background: #dbeafe; color: #1e40af; padding: 2px 8px; border-radius: 3px; }
.label-restructure { background: #fef3c7; color: #92400e; padding: 2px 8px; border-radius: 3px; }
.label-reject { background: #fee2e2; color: #991b1b; padding: 2px 8px; border-radius: 3px; }
.guide { background: #eef2ff; border-left: 4px solid #4338ca; padding: 1em 1.2em; border-radius: 4px; margin: 1em 0 2em; }
.guide strong { color: #3730a3; }
"""
body = [
'<h1>Holdout 합리성 라벨링 자료</h1>',
'<div class="guide">',
'<p><strong>평가 기준</strong>: 각 버전 (V1/V2/V3/V4) 의 1위 선택 프레임이 해당 섹션에 <b>합리적으로 적용 가능한가</b>.</p>',
'<ul>',
'<li><b>합리적 (rational ○)</b>: 이 프레임이 그 섹션에 맞게 적용 가능</li>',
'<li><b>애매 (ambiguous △)</b>: 조건부 가능, 논란 있음</li>',
'<li><b>비합리적 (irrational ×)</b>: 섹션과 안 맞음</li>',
'</ul>',
'<p>라벨링은 <code>holdout_labeling_template.yaml</code> 의 <code>rating</code> 필드를 채워 주세요. (값: <code>rational</code> / <code>ambiguous</code> / <code>irrational</code>)</p>',
'</div>',
'<div class="guide" style="background:#fef2f2;border-left-color:#dc2626">',
'<p><strong>⚠ 평가 시 주의 (반드시 읽어 주세요)</strong></p>',
'<ol style="margin:0.5em 0 0 1.2em;padding:0">',
'<li><b>V4 라벨에 끌려가지 마세요.</b> <code>use_as_is</code>/<code>reject</code> 는 template-fit-v1 이 판정한 결과일 뿐. '
'평가는 "V4 라벨이 맞는가" 가 아니라 "<b>이 프레임이 이 섹션에 실제로 합리적인가</b>" 를 사용자 눈으로 판단. '
'V4 가 <code>reject</code> 라고 해도 사용자가 보기에 합리적이면 <code>rational</code> 로 표시.</li>',
'<li><b>현 단계는 1위만 평가.</b> "대안 탐색" 관점(2~3위가 더 합리적일 수 있음)은 나중 단계에서 확장. '
'지금은 V2 실사용 가치 검증 목적이므로 1위로 충분.</li>',
'</ol>',
'</div>',
]
for sid, data in sections_data.items():
body.append(f'<h2>{sid}{data["mdx_title"]}</h2>')
body.append('<div class="section-block">')
body.append('<h3>MDX 원문</h3>')
body.append(f'<div class="mdx-excerpt">{data["mdx_raw_html"]}</div>')
body.append('<h3>V1 ~ V4 각 1위 선택</h3>')
body.append('<table class="v-grid">')
body.append('<thead><tr>')
for v in ['V1', 'V2', 'V3', 'V4']:
subtitle = {
'V1': '키워드 baseline',
'V2': '+ 의미 (ko-sroberta)',
'V3': '+ 구조 (Figma×MDX)',
'V4': '+ 판정 (template-fit)',
}[v]
body.append(f'<th>{v}<br><small style="font-weight:normal;color:#cbd5e1">{subtitle}</small></th>')
body.append('</tr></thead><tbody><tr>')
for v in ['V1', 'V2', 'V3', 'V4']:
pick = data['picks'][v]
fn = pick['frame_number']
desc = data['descriptions'].get(fn, '')
metric_value_html = f'<span class="value">{pick["metric_value"]}</span>'
extra_html = ''
if pick['extra']:
if v == 'V4':
extra_html = f'<div style="margin-top:6px"><span class="label-{pick["extra"]}">{pick["extra"]}</span></div>'
else:
extra_html = f'<div style="margin-top:4px;font-size:0.8em;color:#64748b">layout: {pick["extra"]}</div>'
body.append('<td>')
body.append(f'<img src="{img_data_uri(fn)}" alt="Frame {fn}"/>')
body.append(f'<span class="frame-label">Frame {fn}</span>')
body.append(f'<span class="frame-desc">{desc}</span>')
body.append(f'<div class="metric"><span class="key">{pick["metric_label"]}</span> {metric_value_html}{extra_html}</div>')
body.append(f'<div class="rating-box"><b>라벨</b>: <code>sid={sid} · version={v} · rating=?</code></div>')
body.append('</td>')
body.append('</tr></tbody></table>')
body.append('</div>')
html = f"""<!DOCTYPE html>
<html lang="ko">
<head><meta charset="utf-8"><title>Holdout 라벨링</title><style>{style}</style></head>
<body>
{''.join(body)}
</body></html>"""
HTML_PATH.write_text(html, encoding='utf-8')
def build_template(sections_data):
template = {
'meta': {
'purpose': 'Holdout 3 섹션 × V1~V4 1위의 합리성 라벨링',
'scale': {
'rational': '○ 이 프레임이 섹션에 합리적으로 적용 가능',
'ambiguous': '△ 조건부 가능 / 논란 있음',
'irrational': '× 섹션과 안 맞음',
},
'instruction': (
'각 sections.<sid>.picks.<V>.rating 을 '
'rational / ambiguous / irrational 중 하나로 채우세요. '
'comment 는 선택 — 판단 근거 짧게.'
),
'caution': [
'V4 라벨(use_as_is/reject)에 끌려가지 말 것 — '
'평가는 "V4 라벨이 맞는가"가 아니라 "이 프레임이 이 섹션에 실제로 합리적인가"를 사용자 눈으로 판단',
'현 단계는 1위만 평가 — 대안 탐색(2~3위 합리성) 은 다음 단계로 분리',
],
},
'sections': {},
}
for sid, data in sections_data.items():
picks_out = {}
for v in ['V1', 'V2', 'V3', 'V4']:
p = data['picks'][v]
picks_out[v] = {
'frame_number': p['frame_number'],
'metric': {p['metric_label']: p['metric_value']},
**({'extra': p['extra']} if p['extra'] else {}),
'rating': None, # <-- 사용자가 채울 필드
'comment': None,
}
template['sections'][sid] = {
'mdx_title': data['mdx_title'],
'picks': picks_out,
}
TEMPLATE_PATH.write_text(
yaml.safe_dump(template, allow_unicode=True, sort_keys=False, width=1000),
encoding='utf-8',
)
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 = yaml.safe_load((HERE / 'v3_structure_rerank_result.yaml').read_text(encoding='utf-8'))
v4 = yaml.safe_load((HERE / 'v4_template_fit_result.yaml').read_text(encoding='utf-8'))
auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8'))
descriptions = build_frame_descriptions(auto)
sections_data = {}
for sid in HOLDOUT_SIDS:
mdx_title, mdx_raw = extract_mdx_raw(sid)
picks = get_v_picks(sid, v1, v2, v3, v4)
sections_data[sid] = {
'mdx_title': mdx_title,
'mdx_raw_html': mdx_raw.replace('<', '&lt;').replace('>', '&gt;'),
'picks': picks,
'descriptions': descriptions,
}
render_html(sections_data)
build_template(sections_data)
print("=" * 70)
print("Holdout 라벨링 자료 생성 완료")
print("=" * 70)
print(f" html: {HTML_PATH}")
print(f" template: {TEMPLATE_PATH}")
print()
print("사용자 작업:")
print(" 1. HOLDOUT_LABELING.html 을 열어 3 섹션 × 4 버전 1위 프레임 확인")
print(" 2. holdout_labeling_template.yaml 의 rating 필드 채우기")
print(" (rational / ambiguous / irrational)")
print(" 3. 완료 후 알려주시면 평가 리포트 생성")
if __name__ == '__main__':
main()