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,328 @@
|
||||
"""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"<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"<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"<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"")
|
||||
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()
|
||||
Reference in New Issue
Block a user