"""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()