"""Phase 24 — 정제 anchor + content summary + legacy structure ontology 공식: 점수 = 0.5 × 키워드 + 0.3 × 내용 + 0.2 × 구조(legacy ontology) - 키워드 축: keyword_base 기반 (Phase 22 과 동일) - 내용 축: MDX summary ↔ frame.content (Phase 23 와 동일) - 구조 축: **legacy structure ontology** (structure_ontology.yaml 의 `frames:` 블록) - detect_mdx_structure_v3 로 MDX 구조 추출 - structural_match_v3 로 family/surface/semantic_role 속성 교집합 매칭 - Phase 21b 방식 유지 (tie-breaker 가중치) **주의 — Phase 25 (template-fit-v1) 과 차이**: - Phase 23 구조 = family/semantic_role (legacy frames: 블록) - Phase 25 구조 = visual_pattern (cardinality + relation_type + slot_coverage) + adaptation_cost + not_suits - 즉 Phase 23 는 "구조가 얼마나 도움 되는지" retrieval 확인, Phase 25 은 "이 디자인을 실제로 쓸 수 있는지" application 판단. """ import sys import pickle from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from phase_common import ( TARGET_UNITS, load_keyword_base, load_32_frames, compute_df_idf_tier, extract_mdx_keywords, keyword_score, load_target_units, load_frame_index, normalize_with_keyword_base, ) from structure_v3 import load_ontology, detect_mdx_structure_v3, structural_match_v3 from embeddings import embed_texts, cosine from detect_mdx import detect_mdx_analysis from template_fit import load_templates_v1, intent_compat_with_source HERE = Path(__file__).parent PNG_REL = "../../data/figma_previews/" W_KW, W_CONTENT, W_STRUCT = 0.5, 0.3, 0.2 def run(): kb = load_keyword_base() frames = load_32_frames() df, idf, tier, N = compute_df_idf_tier(frames) vocab = set() for v in frames.values(): vocab.update(v["keywords"]) units_full, units_title = load_target_units() idx_data, frame_to_short = load_frame_index() ontology = load_ontology() templates_v1 = load_templates_v1() # structure_intent 조회용 fids = list(frames.keys()) frame_texts = [frames[fid].get("content", "") for fid in fids] frame_vecs = embed_texts(frame_texts) reports = [] hits = 0 for uid, display, correct_sid, *_ in TARGET_UNITS: text_orig = units_full[uid] text = normalize_with_keyword_base(text_orig, kb) title = units_title[uid] mdx_kws = extract_mdx_keywords(text_orig, vocab, keyword_base=kb) mdx_schema = detect_mdx_structure_v3(text, title) # content 축: summary 기반 (Phase 23 와 동일) analysis = detect_mdx_analysis(text_orig, title, anchor_vocab=None) mdx_summary = analysis['summary'] mdx_vec = embed_texts([mdx_summary])[0] c_scores = {fids[i]: max(0.0, min(1.0, cosine(mdx_vec, frame_vecs[i]))) for i in range(len(fids))} mdx_intents = analysis.get('structure_intent', []) ranked = [] for fid, v in frames.items(): fig_kws = set(v["keywords"]) k_s, inter = keyword_score(mdx_kws, fig_kws, idf, tier) c_s = c_scores[fid] legacy_s, aligns = structural_match_v3(mdx_schema, ontology.get(fid, {})) # structure_intent 호환도 (templates_v1 에서 frame intent 조회) tpl_v1 = templates_v1.get(fid, {}) frame_intents = tpl_v1.get('visual_pattern', {}).get('structure_intent', []) ic, ic_source = intent_compat_with_source(mdx_intents, frame_intents) # 구조 점수 = legacy 50% + intent 50% struct_combined = 0.5 * legacy_s + 0.5 * ic final = W_KW * k_s + W_CONTENT * c_s + W_STRUCT * struct_combined ranked.append((fid, final, { "kw": k_s, "content": c_s, "struct": struct_combined, "legacy_struct": legacy_s, "intent_compat": ic, "intent_source": ic_source, "frame_intents": frame_intents, "aligns": aligns, "inter": sorted(inter), })) ranked.sort(key=lambda x: -x[1]) top_sid = frame_to_short.get(ranked[0][0], "?") if top_sid == correct_sid: hits += 1 margin = ranked[0][1] - ranked[1][1] reports.append({ "uid": uid, "display": display, "correct_sid": correct_sid, "mdx_kws": sorted(mdx_kws), "mdx_title": title, "mdx_summary": mdx_summary, "mdx_schema": mdx_schema, "mdx_intents": mdx_intents, "top3": ranked[:3], "margin": margin, }) return { "phase": 24, "desc": "정제 anchor + content summary + (legacy structure + structure_intent)", "formula": f"{W_KW} × 키워드 + {W_CONTENT} × 내용(summary) + {W_STRUCT} × (0.5 legacy + 0.5 intent)", "weights": (W_KW, W_CONTENT, W_STRUCT), "hits": hits, "reports": reports, "frame_to_short": frame_to_short, "idx_data": idx_data, "frames": frames, "ontology": ontology, } def write_md(r): lines = [] lines.append("# Phase 24 — 정제 anchor + content summary + legacy structure ontology") lines.append("") lines.append(f"**공식**: `{r['formula']}`") lines.append(f"**결과: {r['hits']}/4 정답**") lines.append("") lines.append("**Phase 23 대비**:") lines.append("- **legacy structure ontology** 축 추가 (family/semantic_role/columns 교집합)") lines.append("- MDX 측: `detect_mdx_structure_v3()` 로 `### 서브섹션`, 표 헤더, 볼드 블릿 기반 schema emit") lines.append("- Figma 측: `structure_ontology.yaml` 의 legacy `frames:` 블록 (32 프레임 family/semantic_role)") lines.append("- confidence low 인 MDX 구조는 점수 0") lines.append("") lines.append("**Phase 25 (template-fit-v1) 과 차이**:") lines.append("- Phase 23 구조 = family/semantic_role **legacy ontology** (retrieval 용)") lines.append("- Phase 25 구조 = **visual_pattern** (cardinality + relation + slot_coverage) + adaptation_cost + not_suits (application 용)") lines.append("- Phase 23 는 '구조가 매칭에 기여하는가' 확인,") lines.append(" Phase 25 은 '이 디자인을 실제로 쓸 수 있는가' 판단") lines.append("") # 요약 lines.append("## 1. TARGET별 결과") lines.append("") lines.append("| MDX | 정답 | 1위 (kw / content / struct / 최종) | 2위 | margin |") lines.append("|-----|------|-----------------------------------|-----|--------|") for rep in r["reports"]: top1 = r["frame_to_short"].get(rep["top3"][0][0], "?") top2 = r["frame_to_short"].get(rep["top3"][1][0], "?") mark = "✓" if top1 == rep["correct_sid"] else "✗" bd1 = rep["top3"][0][2] lines.append( f"| {rep['display']} | {rep['correct_sid']} | " f"{top1} ({bd1['kw']:.2f}/{bd1['content']:.2f}/{bd1['struct']:.2f}/{rep['top3'][0][1]:.3f}) {mark} | " f"{top2} ({rep['top3'][1][1]:.3f}) | {rep['margin']:.3f} |" ) lines.append("") # Top-3 lines.append("## 2. Top-3 매트릭스") lines.append("") lines.append("| 콘텐츠 | 1순위 | 2순위 | 3순위 |") lines.append("|--------|-------|-------|-------|") for rep in r["reports"]: row = [f"**{rep['display']}**
정답 Frame **{rep['correct_sid']}**"] for rank_idx in range(3): fid, score, _ = rep["top3"][rank_idx] sid = r["frame_to_short"].get(fid, "?") info = r["idx_data"].get(sid, {}) png = info.get("png", "") title = info.get("title_text", "").strip().replace("\n", " ")[:15] + "…" inner = f"![{sid}]({PNG_REL}{png})
**{sid}** ({score:.3f})
{title}" if sid == rep["correct_sid"]: cell = f"
🎯 정답
{inner}
" else: cell = inner row.append(cell) lines.append("| " + " | ".join(row) + " |") lines.append("") # 상세 lines.append("## 3. 유닛별 세부") lines.append("") for rep in r["reports"]: lines.append(f"### {rep['display']} — 정답 Frame **{rep['correct_sid']}**") lines.append("") lines.append(f"**MDX 구조 (v3 emit)**") lines.append("```yaml") for k, vv in rep["mdx_schema"].items(): lines.append(f"{k}: {vv}") lines.append("```") lines.append("") lines.append(f"**MDX 키워드 ({len(rep['mdx_kws'])}개)**: {', '.join(rep['mdx_kws'])}") lines.append("") for rank, (fid, score, bd) in enumerate(rep["top3"], 1): sid = r["frame_to_short"].get(fid, "?") info = r["idx_data"].get(sid, {}) png = info.get("png", "") fig_content = r["frames"][fid].get("content", "") fig_schema = r["ontology"].get(fid, {}) common = ", ".join(bd["inter"]) if bd["inter"] else "(없음)" align_str = ", ".join(bd["aligns"]) if bd["aligns"] else "-" is_correct = sid == rep["correct_sid"] prefix = "🎯 " if is_correct else "" lines.append(f"**{prefix}{rank}위 Frame {sid}** {'(정답)' if is_correct else ''}") lines.append("") lines.append(f"![{sid}]({PNG_REL}{png})") lines.append("") lines.append(f"- **매칭 내용**: {fig_content}") lines.append(f"- **매칭 키워드**: {common}") lines.append(f"- **Figma schema**: family=`{fig_schema.get('family')}`, " f"role=`{fig_schema.get('semantic_role')}`, " f"conf=`{fig_schema.get('confidence')}`") lines.append(f"- **구조 속성 정렬**: {align_str}") lines.append(f"- **축별 점수**: 키워드 {bd['kw']:.3f} / 내용 {bd['content']:.3f} / 구조 {bd['struct']:.2f}") lines.append(f"- **최종**: **{score:.3f}**") lines.append("") # 한계 lines.append("## 4. 한계") lines.append("") lines.append("- **구조 = 구조만 기록**: family/semantic_role 은 '디자인이 어떻게 생겼나'를 말할 뿐 '이 콘텐츠가 들어갈 수 있나'를 판정하지 못함.") lines.append("- **slot 개념 부재**: 4열 디자인에 3개 MDX를 넣을 수 있는지 여부가 드러나지 않음.") lines.append("- **adaptation 비용 없음**: 재구성이 필요한 경우와 원본 그대로 쓰는 경우의 구분 없음.") lines.append("- **라우팅 없음**: 점수만 제공, use_as_is / light_edit / restructure / reject 같은 운영 판단 불가.") lines.append("- **다음 단계**: Phase 25 template-fit-v1 — 매칭을 '얼마나 닮았나'가 아니라 '얼마나 끼워 넣을 수 있나'로 재정의.") lines.append("") out = HERE / "MATRIX_PHASE24.md" out.write_text("\n".join(lines), encoding="utf-8") print(f"완료: {out}") def main(): result = run() serializable = dict(result) serializable.pop("frames", None) serializable.pop("ontology", None) serializable.pop("idx_data", None) with open(HERE / "_phase24_results.pkl", "wb") as f: pickle.dump(serializable, f) write_md(result) print(f"Phase 24: {result['hits']}/4 정답") if __name__ == "__main__": main()