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

227 lines
8.2 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.
"""Phase 16 — 3축 매칭 (구조 × 내용 × 핵심키워드) 테스트
5개 프레임 수동 태깅(v2 스키마) + 4개 MDX 유닛 매칭
매칭 공식:
score = w_kw × 핵심키워드_Jaccard + w_struct × 구조_일치 + w_content × 내용_Cross점수
MDX 측 (코드로 실시간 추출):
- 구조: _detect_mdx_layout (본문 블릿 수, 표 유무)
- 내용: MDX 제목 (대제목 / 중제목 / 소제목)
- 핵심키워드: Kiwi 형태소 중 corpus-rare만 (IDF 기반)
"""
import sys
import math
import collections
import json
import re
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).parent))
from common import load_figma_texts
from extract_units import extract_units
from methods import (
_get_kiwi, _extract_content_tokens,
_detect_mdx_layout, _get_cross_encoder,
)
from phase10 import extract_titles_only_mdx, TARGET_UNITS
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
PREVIEW_DIR = ROOT / "data" / "figma_previews"
def load_v2():
p = Path(__file__).parent / "metadata_db_v2_sample.yaml"
with open(p, encoding="utf-8") as f:
return yaml.safe_load(f)["frames"]
def extract_mdx_3axis(unit_id, units_dict, mdx_titles):
"""MDX에서 코드로 3축 추출"""
text = units_dict[unit_id]
# 구조
structure = _detect_mdx_layout(text)
# 내용 = 대/중/소 제목 합친 것
content = mdx_titles[unit_id]
# 핵심키워드: Kiwi 내용어 전체 (corpus-rare 필터는 후속에서 vocab 기반)
kiwi = _get_kiwi()
tokens = _extract_content_tokens(text, kiwi)
return structure, content, set(tokens)
def jaccard_weighted(set_a, set_b, vocab_idf):
"""IDF 가중 Jaccard (vocab_idf가 있으면 사용, 없으면 단순 Jaccard)"""
if not set_a or not set_b:
return 0.0
inter = set_a & set_b
union = set_a | set_b
if vocab_idf:
num = sum(vocab_idf.get(c, 0.5) for c in inter)
den = sum(vocab_idf.get(c, 0.5) for c in union)
return num / den if den > 0 else 0
return len(inter) / len(union)
def structural_match(mdx_structure, fig_structure):
"""구조 일치도: exact=1, 유사 layout=0.5, 불일치=0"""
if not mdx_structure or not fig_structure:
return 0.0
if mdx_structure == fig_structure:
return 1.0
# 유사 그룹 (예: 3col-parallel vs persona-3col는 둘 다 3열)
if "3col" in mdx_structure and "3col" in fig_structure:
return 0.5
if "2col" in mdx_structure and "2col" in fig_structure:
return 0.5
if "parallel" in mdx_structure and "parallel" in fig_structure:
return 0.3
return 0.0
def cross_content_score(mdx_content, fig_content):
"""내용 의미 유사도 — Cross-encoder"""
model = _get_cross_encoder()
raw = model.predict([[mdx_content, fig_content]], show_progress_bar=False)
# sigmoid
import math as _m
return 1 / (1 + _m.exp(-float(raw[0])))
def method_3axis(mdx_structure, mdx_content, mdx_keywords,
v2_frames, w_kw=0.5, w_struct=0.2, w_content=0.3, verbose=False):
"""v2 프레임 대상 3축 가중 매칭"""
# IDF: v2_frames 전체의 핵심키워드 corpus에서
df = collections.Counter()
N = len(v2_frames)
for fid, entry in v2_frames.items():
for c in entry.get("핵심키워드", []):
df[c] += 1
idf = {c: math.log(N / cnt) if cnt > 0 else 0 for c, cnt in df.items()}
# MDX 핵심키워드 = Kiwi 토큰 중 v2 vocab에 있는 것만 (canonicalization)
vocab = set()
for entry in v2_frames.values():
vocab.update(entry.get("핵심키워드", []))
vocab.update(entry.get("일반키워드", []))
mdx_kw_canonical = mdx_keywords & vocab
scores = []
breakdown = {}
for fid, entry in v2_frames.items():
fig_kw = set(entry.get("핵심키워드", []))
fig_struct = entry.get("구조", "")
fig_content = entry.get("내용", "")
kw_s = jaccard_weighted(mdx_kw_canonical, fig_kw, idf)
struct_s = structural_match(mdx_structure, fig_struct)
content_s = cross_content_score(mdx_content, fig_content) if fig_content else 0.0
final = w_kw * kw_s + w_struct * struct_s + w_content * content_s
scores.append((fid, final))
breakdown[fid] = {
"kw": kw_s,
"struct": struct_s,
"content": content_s,
"final": final,
"inter_kw": sorted(mdx_kw_canonical & fig_kw),
}
return sorted(scores, key=lambda x: -x[1]), breakdown
def main():
units = extract_units()
v2 = load_v2()
mdx_titles = {}
for uid, _, _, fname, mid, sub in TARGET_UNITS:
mdx_titles[uid] = extract_titles_only_mdx(fname, mid, sub)
with open(PREVIEW_DIR / "index.json", encoding="utf-8") as f:
idx_data = json.load(f)
frame_to_short = {info["frame_id"]: sid for sid, info in idx_data.items()}
# 5 프레임에 대한 short ID 미리 확인
for fid in v2:
sid = frame_to_short.get(fid, "?")
print(f" v2 프레임 {fid} = short {sid} ({v2[fid]['구조']})")
print()
png_rel = "../../data/figma_previews/"
lines = []
lines.append("# Phase 16 — 3축 매칭 (구조×내용×핵심키워드) 테스트")
lines.append("")
lines.append("**5개 프레임 수동 v2 태깅** (13, 14, 15, 18, 29)")
lines.append("**4개 MDX 유닛** 매칭 테스트")
lines.append("")
lines.append(f"**매칭 공식**: `score = 0.5 × 핵심키워드_Jaccard + 0.2 × 구조_일치 + 0.3 × 내용_Cross`")
lines.append("")
# 결과 요약 전체
lines.append("## 결과 요약")
lines.append("")
lines.append("| MDX 유닛 | 정답 | Top-1 | 결과 |")
lines.append("|---------|------|-------|------|")
detail_sections = []
for uid, display, correct_sid, *_ in TARGET_UNITS:
mdx_struct, mdx_content, mdx_kw = extract_mdx_3axis(uid, units, mdx_titles)
ranked, breakdown = method_3axis(mdx_struct, mdx_content, mdx_kw, v2)
top_fid, top_score = ranked[0]
top_sid = frame_to_short.get(top_fid, "?")
mark = "✓" if top_sid == correct_sid else "✗"
lines.append(f"| {display} | **{correct_sid}** | **{top_sid}** ({top_score:.3f}) | {mark} |")
# 상세 섹션
d = []
d.append(f"### {display} — 정답 Frame **{correct_sid}**")
d.append("")
d.append(f"**MDX 추출 (코드):**")
d.append(f"- 구조: `{mdx_struct}`")
d.append(f"- 내용: `{mdx_content}`")
d.append(f"- 핵심키워드 (Kiwi 추출, {len(mdx_kw)}개 중 v2 vocab 교집합): "
f"`{sorted(mdx_kw & set().union(*[set(e.get('핵심키워드', []) + e.get('일반키워드', [])) for e in v2.values()]))[:15]}`")
d.append("")
d.append("**5개 후보 점수 분해:**")
d.append("")
d.append("| Frame | 핵심키워드 | 구조 | 내용 | 최종 | 공통키워드 |")
d.append("|-------|----------|------|------|------|-----------|")
for fid, _ in ranked:
sid = frame_to_short.get(fid, "?")
b = breakdown[fid]
mark_row = " ⭐" if sid == correct_sid else ""
common_kw = ", ".join(b["inter_kw"][:8])
d.append(f"| **{sid}**{mark_row} | {b['kw']:.3f} | {b['struct']:.2f} | "
f"{b['content']:.3f} | **{b['final']:.3f}** | {common_kw} |")
d.append("")
detail_sections.append("\n".join(d))
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 유닛별 상세 (3축 점수 분해)")
lines.append("")
for s in detail_sections:
lines.append(s)
out_path = Path(__file__).parent / "MATRIX_PHASE16.md"
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"완료: {out_path}")
# 콘솔 요약
print("\n=== 3축 매칭 결과 ===")
for uid, display, correct_sid, *_ in TARGET_UNITS:
mdx_struct, mdx_content, mdx_kw = extract_mdx_3axis(uid, units, mdx_titles)
ranked, _ = method_3axis(mdx_struct, mdx_content, mdx_kw, v2)
top_sid = frame_to_short.get(ranked[0][0], "?")
mark = "✓" if top_sid == correct_sid else "✗"
print(f" {uid}(정답={correct_sid}): Top-1={top_sid} ({ranked[0][1]:.3f}) {mark}")
if __name__ == "__main__":
main()