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>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user