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

329 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.
"""Phase 17 — 32개 프레임 analysis.md 전체 로드 + 3축 매칭 (runtime df 분류)
Phase 16은 5개 v2 샘플 yaml로 검증했는데, 이제 실전 32개 analysis.md로 재검증.
analysis.md = 정적 (구조/내용/후보키워드만)
runtime에 32개 로드 → df 계산 → 핵심/중간/일반 자동 분류 → 매칭
"""
import sys
import re
import math
import collections
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
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")
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
PREVIEW_DIR = ROOT / "data" / "figma_previews"
def parse_analysis_md(path):
"""analysis.md 파싱 → {layout, detail, content, keywords, ...}"""
text = path.read_text(encoding="utf-8")
result = {"keywords": [], "layout": "", "detail": "", "content": ""}
# Sections by ## headers
sections = re.split(r"\n## ", text)
for sec in sections[1:]:
lines = sec.split("\n", 1)
heading = lines[0].strip()
body = lines[1].strip() if len(lines) > 1 else ""
if heading == "구조":
for ln in body.split("\n"):
m = re.match(r"- \*\*layout\*\*:\s*(.+)", ln)
if m:
result["layout"] = m.group(1).strip()
m = re.match(r"- \*\*detail\*\*:\s*(.+)", ln)
if m:
result["detail"] = m.group(1).strip()
elif heading == "내용":
# 첫 문장 블록
result["content"] = body.split("\n\n")[0].strip()
elif heading == "후보 키워드":
# 쉼표로 분리
raw = body.split("\n\n")[0]
kws = [k.strip() for k in raw.split(",") if k.strip()]
result["keywords"] = kws
return result
def load_32_frames():
"""32개 프레임의 analysis.md를 로드"""
frames = {}
for d in sorted(BLOCKS_DIR.iterdir()):
if not d.is_dir():
continue
fid = d.name
if not fid.startswith("1171"):
continue
a = d / "analysis.md"
if a.exists():
frames[fid] = parse_analysis_md(a)
return frames
def compute_df_classes(frames):
"""전체 corpus에서 df 계산 → 후보 키워드별 핵심/중간/일반 분류
비율 기준: df/N ≤ 0.1 = 핵심, ≥ 0.3 = 일반, 그 사이 = 중간"""
df = collections.Counter()
N = len(frames)
for fid, v in frames.items():
for kw in set(v["keywords"]):
df[kw] += 1
tier = {}
for kw, cnt in df.items():
ratio = cnt / N
if ratio <= 0.10:
tier[kw] = "core"
elif ratio >= 0.30:
tier[kw] = "general"
else:
tier[kw] = "mid"
idf = {kw: math.log(N / cnt) if cnt > 0 else 0 for kw, cnt in df.items()}
return df, tier, idf, N
def extract_mdx_keywords(mdx_text, vocabulary):
"""MDX에서 Kiwi로 추출 후 vocabulary 교집합 (canonicalization)"""
kiwi = _get_kiwi()
tokens = _extract_content_tokens(mdx_text, kiwi)
return set(tokens) & vocabulary
def structural_match(mdx_layout, fig_layout):
if not mdx_layout or not fig_layout:
return 0.0
if mdx_layout == fig_layout:
return 1.0
if "3col" in mdx_layout and "3col" in fig_layout:
return 0.5
if "persona-3col" in fig_layout and "3col" in mdx_layout:
return 0.5
if "2col" in mdx_layout and "2col" in fig_layout:
return 0.5
if "table" in mdx_layout and "table" in fig_layout:
return 0.5
return 0.0
def match_3axis(mdx_layout, mdx_content, mdx_kws, frames, idf, tier,
w_kw=0.5, w_struct=0.2, w_content=0.3):
"""3축 매칭 — 핵심/중간/일반 가중 Jaccard + 구조 + 내용(Cross-encoder)"""
model = _get_cross_encoder()
# 내용 쌍 일괄 예측
pairs = [[mdx_content, frames[fid]["content"]] for fid in frames]
fids = list(frames.keys())
content_raw = model.predict(pairs, show_progress_bar=False)
# sigmoid로 [0,1] 정규화
content_scores = {fids[i]: 1 / (1 + math.exp(-float(content_raw[i])))
for i in range(len(fids))}
scores = []
for fid, v in frames.items():
fig_kws = set(v["keywords"])
fig_layout = v["layout"]
# 핵심키워드 Jaccard with tier weights
# core: idf full, mid: idf*0.5, general: idf*0.1
inter = mdx_kws & fig_kws
union = mdx_kws | fig_kws
tier_weight = {"core": 1.0, "mid": 0.5, "general": 0.1}
num = sum(idf.get(c, 0.5) * tier_weight.get(tier.get(c, "mid"), 0.5) for c in inter)
den = sum(idf.get(c, 0.5) * tier_weight.get(tier.get(c, "mid"), 0.5) for c in union)
kw_s = num / den if den > 0 else 0
struct_s = structural_match(mdx_layout, fig_layout)
content_s = content_scores[fid]
final = w_kw * kw_s + w_struct * struct_s + w_content * content_s
scores.append((fid, final, {
"kw": kw_s, "struct": struct_s, "content": content_s,
"final": final, "inter": sorted(inter),
}))
scores.sort(key=lambda x: -x[1])
return scores
def main():
units = extract_units()
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()}
# 32 frames 로드
frames = load_32_frames()
print(f"로드된 프레임: {len(frames)}개")
# df 분류
df, tier, idf, N = compute_df_classes(frames)
print(f"corpus: N={N}")
core_count = sum(1 for t in tier.values() if t == "core")
mid_count = sum(1 for t in tier.values() if t == "mid")
gen_count = sum(1 for t in tier.values() if t == "general")
print(f"키워드 분류: 핵심 {core_count}개, 중간 {mid_count}개, 일반 {gen_count}개 (총 {len(tier)}개)")
# vocabulary
vocab = set()
for v in frames.values():
vocab.update(v["keywords"])
print(f"vocabulary 크기: {len(vocab)}")
# 일반(general) 키워드 상위 10개 (어떤 게 일반어로 분류됐나)
general_kws = sorted([kw for kw, t in tier.items() if t == "general"],
key=lambda k: -df[k])
print(f"일반 키워드 (df 많음 상위): {general_kws[:10]}")
print()
# 4 MDX 유닛 매칭
print("=" * 70)
print("4개 MDX 유닛 × 32개 프레임 3축 매칭")
print("=" * 70)
hits = 0
reports = []
for uid, display, correct_sid, *_ in TARGET_UNITS:
text = units[uid]
mdx_layout = _detect_mdx_layout(text)
mdx_content = mdx_titles[uid]
mdx_kws = extract_mdx_keywords(text, vocab)
ranked = match_3axis(mdx_layout, mdx_content, mdx_kws, frames, idf, tier)
top3 = ranked[:3]
top_fid, top_score, top_bd = top3[0]
top_sid = frame_to_short.get(top_fid, "?")
mark = "✓" if top_sid == correct_sid else "✗"
if top_sid == correct_sid:
hits += 1
print(f"{uid} (정답={correct_sid})")
print(f" MDX 구조={mdx_layout}, 키워드 {len(mdx_kws)}개 ({sorted(mdx_kws)[:8]}...)")
print(f" Top1: Frame {top_sid} ({top_score:.3f}) {mark}")
print(f" kw={top_bd['kw']:.3f}, struct={top_bd['struct']:.2f}, content={top_bd['content']:.3f}")
reports.append((uid, display, correct_sid, top3, mdx_layout, mdx_content, mdx_kws))
print(f"\n=== 총 {hits}/4 ===")
# MD 리포트
png_rel = "../../data/figma_previews/"
def frame_cell(sid, score):
info = idx_data.get(sid, {})
png = info.get("png", "")
title = info.get("title_text", "").strip().replace("\n", " ") or ""
if len(title) > 15:
title = title[:15] + "…"
return f"![{sid}]({png_rel}{png})<br>**{sid}** ({score:.3f})<br>{title}"
lines = []
lines.append("# Phase 17 — MDX ↔ Figma 매칭 체계")
lines.append("")
# ═══ 1. 수행 프로세스 (요약) ═══
lines.append("## 1. 수행 프로세스 (요약)")
lines.append("")
# 헤더 한 줄: Step N + 제목 결합. 아래 행: 내용 / 예시
lines.append("| Step 1. Figma 정리 | Step 2. 단어 가중치 정리 | Step 3. MDX 키워드 추출 | Step 4. 유사도 계산 | Step 5. 최종 선정 |")
lines.append("|-------------------|------------------------|-----------------------|---------------------|-------------------|")
# 내용 행
lines.append(
"| 각 Figma 프레임마다 analysis.md 작성 (내용 요약 1문장 + 후보 키워드 15~25개)"
" | 32개 프레임에서 각 키워드 등장 프레임 수(df) 세어 3단계 가중치 부여<br>핵심(1~3개)×1.0, 중간(4~9개)×0.5, 일반(10개↑)×0.1"
" | MDX 본문을 Kiwi 형태소 분석기로 추출 → Figma 키워드 DB에 있는 단어만 남김"
" | 공식: `점수 = 0.5 × 키워드 겹침 + 0.3 × 내용 유사도`<br>① 키워드: IDF 가중 Jaccard<br>② 내용: ko-reranker(Cross-encoder) 로컬 모델"
" | 32개 프레임 전부 점수 계산 → Top-1 선정"
" |"
)
# 예시 행
lines.append(
"| Frame 13 (필수조건)<br>"
f"![13]({png_rel}13.png)<br>"
"내용: \"DX 시행 3대 필수조건 — 기술/사람/자연\"<br>키워드: 필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건, 기반, 창의성, 3요소…"
" | '필수조건' 1/32 → 핵심 ×1.0<br>'여건' 1/32 → 핵심 ×1.0<br>'디지털' 5/32 → 중간 ×0.5"
" | MDX03-1 원문: \"DX 시행을 위한 필수 요건 — 기술·사람·자연\"<br>Kiwi → 127개 토큰<br>Figma DB 교집합 → 22개: DX, 필수, 요건, 기술, 디지털, 사람, 역량, 자연, 여건…"
" | MDX03-1 vs Frame 13<br>키워드 점수: 0.289<br>내용 점수: 0.731<br>최종: 0.5×0.289 + 0.3×0.731 = **0.364**"
" | MDX03-1 결과<br>1위 Frame 13 → 0.364<br>2위 Frame 15 → 0.269<br>3위 Frame 18 → 0.193"
" |"
)
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 2. 테스트 결과")
lines.append("")
lines.append(f"**결과: {hits}/4 정답**")
lines.append("")
lines.append("| 콘텐츠 | 1순위 | 2순위 | 3순위 |")
lines.append("|--------|-------|-------|-------|")
for uid, display, correct_sid, top3_full, mdx_layout, mdx_content, mdx_kws in reports:
row = [f"**{display}**<br>정답 Frame **{correct_sid}**"]
for rank_idx in range(3):
fid, score, bd = top3_full[rank_idx]
sid = frame_to_short.get(fid, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
title = info.get("title_text", "").strip().replace("\n", " ") or ""
if len(title) > 15:
title = title[:15] + "…"
inner = f"![{sid}]({png_rel}{png})<br>**{sid}** ({score:.3f})<br>{title}"
if sid == correct_sid:
cell = f"<div style='background:#fff3cd;border:3px solid #dc2626;padding:8px;border-radius:6px'>🎯 <b>정답</b><br>{inner}</div>"
else:
cell = inner
row.append(cell)
lines.append("| " + " | ".join(row) + " |")
lines.append("")
# ═══ 3. 테스트 결과 세부 ═══
lines.append("---")
lines.append("")
lines.append("## 3. 테스트 결과 세부")
lines.append("")
for uid, display, correct_sid, top3_full, mdx_layout, mdx_content, mdx_kws in reports:
text = units[uid]
ranked = match_3axis(mdx_layout, mdx_content, mdx_kws, frames, idf, tier)
top3 = ranked[:3]
lines.append(f"### {display} — 정답 Frame **{correct_sid}**")
lines.append("")
lines.append("**MDX 추출 사항**")
lines.append("")
lines.append(f"- **내용**: {mdx_content}")
lines.append(f"- **키워드** ({len(mdx_kws)}개): {', '.join(sorted(mdx_kws))}")
lines.append("")
# 순위별 매칭 내용 / 키워드 / 점수
for rank, (fid, score, bd) in enumerate(top3, 1):
sid = frame_to_short.get(fid, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
fig_content = frames[fid].get("content", "")
common_kws = ", ".join(bd["inter"]) if bd["inter"] else "(없음)"
is_correct = sid == correct_sid
if is_correct:
lines.append(f"**<span style='background:#fff3cd;border:2px solid #dc2626;padding:2px 8px;border-radius:3px'>🎯 {rank}위 Frame {sid} (정답)</span>**")
else:
lines.append(f"**{rank}위 Frame {sid}**")
lines.append("")
lines.append(f"![{sid}]({png_rel}{png})")
lines.append("")
lines.append(f"- **매칭 내용**: {fig_content}")
lines.append(f"- **매칭 키워드**: {common_kws}")
lines.append(f"- **매칭 점수**: **{score:.3f}**")
lines.append("")
out_path = Path(__file__).parent / "MATRIX_PHASE17.md"
out_path.write_text("\n".join(lines), encoding="utf-8")
print(f"\n완료: {out_path}")
if __name__ == "__main__":
main()