"""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})
**{sid}** ({score:.3f})
{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단계 가중치 부여
핵심(1~3개)×1.0, 중간(4~9개)×0.5, 일반(10개↑)×0.1" " | MDX 본문을 Kiwi 형태소 분석기로 추출 → Figma 키워드 DB에 있는 단어만 남김" " | 공식: `점수 = 0.5 × 키워드 겹침 + 0.3 × 내용 유사도`
① 키워드: IDF 가중 Jaccard
② 내용: ko-reranker(Cross-encoder) 로컬 모델" " | 32개 프레임 전부 점수 계산 → Top-1 선정" " |" ) # 예시 행 lines.append( "| Frame 13 (필수조건)
" f"![13]({png_rel}13.png)
" "내용: \"DX 시행 3대 필수조건 — 기술/사람/자연\"
키워드: 필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건, 기반, 창의성, 3요소…" " | '필수조건' 1/32 → 핵심 ×1.0
'여건' 1/32 → 핵심 ×1.0
'디지털' 5/32 → 중간 ×0.5" " | MDX03-1 원문: \"DX 시행을 위한 필수 요건 — 기술·사람·자연\"
Kiwi → 127개 토큰
Figma DB 교집합 → 22개: DX, 필수, 요건, 기술, 디지털, 사람, 역량, 자연, 여건…" " | MDX03-1 vs Frame 13
키워드 점수: 0.289
내용 점수: 0.731
최종: 0.5×0.289 + 0.3×0.731 = **0.364**" " | MDX03-1 결과
1위 Frame 13 → 0.364
2위 Frame 15 → 0.269
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}**
정답 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})
**{sid}** ({score:.3f})
{title}" if sid == correct_sid: cell = f"
🎯 정답
{inner}
" 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"**🎯 {rank}위 Frame {sid} (정답)**") 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()