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,242 @@
|
||||
"""texts.md 기반 키워드 자동 추출
|
||||
|
||||
원칙:
|
||||
- 실제 Figma 에 있는 텍스트 노드만 사용 (texts.md)
|
||||
- analysis.md 의 수동 '후보 키워드' 는 사용하지 않음
|
||||
(구조어·레이아웃 서술어 등 Figma 텍스트에 없는 curator 추가분 포함되어 오염됨)
|
||||
|
||||
파이프라인:
|
||||
texts.md → list item 파싱 → 길이 1자 / 기호 / 숫자만 필터 →
|
||||
synonyms.yaml 치환 → Kiwi 명사·외국어·숫자 추출 (길이>=2) →
|
||||
frame 별 키워드 set
|
||||
|
||||
산출:
|
||||
- get_frame_keywords_from_texts(frame_id) → set
|
||||
- load_frames_from_texts() → {fid: {'keywords': set, 'content': title, ...}}
|
||||
- FIGMA_KEYWORDS_FROM_TEXTS.md : 생성 리포트 (검수용)
|
||||
|
||||
마스터 1171281171 은 전체 Figma 텍스트 통합 메타. 32 프레임(01~32)은
|
||||
1171281172~1171281213. 마스터는 참고용으로만 사용 (corpus-wide stats).
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from phase_common import load_synonyms, normalize_with_synonyms
|
||||
from methods import _get_kiwi
|
||||
|
||||
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
|
||||
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
# 32 프레임 (01~32)
|
||||
TARGET_FRAME_IDS = sorted([
|
||||
d.name for d in BLOCKS_DIR.iterdir()
|
||||
if d.is_dir() and d.name.startswith("1171") and d.name != "1171281171"
|
||||
])
|
||||
|
||||
NOUN_TAGS = {'NNG', 'NNP', 'NR', 'SL', 'SN', 'SH'}
|
||||
MIN_LEN = 2
|
||||
# 단일 1글자 숫자/기호 이미 MIN_LEN 으로 배제. 추가 blacklist 는 보수적으로.
|
||||
STOPWORDS = {
|
||||
# 범용 수식어 (사용자 명시)
|
||||
'정의', '특징', '개요', '구성', '결론', '장점', '단점', '고객',
|
||||
'적극', '구체', '소극',
|
||||
}
|
||||
|
||||
|
||||
def parse_texts_md(path):
|
||||
"""texts.md 의 list item 만 추출 (`- item` 라인)."""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
items = []
|
||||
for ln in text.split("\n"):
|
||||
m = re.match(r'^-\s+(.+)$', ln.strip())
|
||||
if not m:
|
||||
continue
|
||||
item = m.group(1).strip()
|
||||
# 기호 단독 제거
|
||||
if item in {"-", "–", "—", "(", ")", "/", "*", "**", "."}:
|
||||
continue
|
||||
# 단일 한자/문자 (技, 術 같은 장식)
|
||||
if len(item) <= 1:
|
||||
continue
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def extract_keywords_from_text_items(items, synonyms):
|
||||
"""list of text items → set of keywords (Kiwi nouns + synonym norm)."""
|
||||
joined = "\n".join(items)
|
||||
normalized = normalize_with_synonyms(joined, synonyms)
|
||||
|
||||
kiwi = _get_kiwi()
|
||||
tokens = kiwi.tokenize(normalized)
|
||||
|
||||
out = []
|
||||
seen = set()
|
||||
for tok in tokens:
|
||||
if tok.tag not in NOUN_TAGS:
|
||||
continue
|
||||
if len(tok.form) < MIN_LEN:
|
||||
continue
|
||||
if tok.form in STOPWORDS:
|
||||
continue
|
||||
if tok.form not in seen:
|
||||
seen.add(tok.form)
|
||||
out.append(tok.form)
|
||||
return out
|
||||
|
||||
|
||||
def get_frame_keywords_from_texts(frame_id, synonyms=None):
|
||||
"""단일 프레임의 texts.md → 키워드 리스트."""
|
||||
if synonyms is None:
|
||||
synonyms = load_synonyms()
|
||||
path = BLOCKS_DIR / frame_id / "texts.md"
|
||||
if not path.exists():
|
||||
return []
|
||||
items = parse_texts_md(path)
|
||||
return extract_keywords_from_text_items(items, synonyms)
|
||||
|
||||
|
||||
def load_frames_from_texts():
|
||||
"""32 프레임의 texts.md 기반 키워드 로드.
|
||||
content 는 analysis.md 에서 재사용 (요약 문장)."""
|
||||
from phase_common import parse_analysis_md
|
||||
synonyms = load_synonyms()
|
||||
frames = {}
|
||||
for fid in TARGET_FRAME_IDS:
|
||||
d = BLOCKS_DIR / fid
|
||||
kws = get_frame_keywords_from_texts(fid, synonyms)
|
||||
# content (Figma 주제 요약): analysis.md 의 '## 내용' 섹션 재사용
|
||||
analysis_path = d / "analysis.md"
|
||||
content = ""
|
||||
title_original = ""
|
||||
if analysis_path.exists():
|
||||
parsed = parse_analysis_md(analysis_path)
|
||||
content = parsed.get("content", "")
|
||||
frames[fid] = {"keywords": kws, "content": content}
|
||||
return frames
|
||||
|
||||
|
||||
def write_report():
|
||||
"""검수 리포트 생성."""
|
||||
from phase_common import load_32_frames, compute_df_idf_tier, load_frame_index
|
||||
synonyms = load_synonyms()
|
||||
idx_data, frame_to_short = load_frame_index()
|
||||
|
||||
# Legacy (analysis.md 후보 키워드)
|
||||
legacy_frames = load_32_frames()
|
||||
# New (texts.md 추출)
|
||||
new_frames = load_frames_from_texts()
|
||||
|
||||
lines = []
|
||||
lines.append("# Figma 키워드 — texts.md 자동 추출 (Phase 23 용)")
|
||||
lines.append("")
|
||||
lines.append("**배경**: 기존 analysis.md 의 `후보 키워드` 필드는 curator(Claude Opus 4.7)가 "
|
||||
"수동 태깅한 것으로, 구조어(3열비교, 표, 다이어그램)와 weak/row label(정의, 특징, 개요 등)이 "
|
||||
"혼재되어 Phase 23 keyword 축을 오염시킴.")
|
||||
lines.append("")
|
||||
lines.append("**해결**: 각 프레임의 `texts.md` (Figma MCP 로 추출된 실제 텍스트 노드) 를 기준으로 "
|
||||
"Kiwi 명사/외국어/숫자만 자동 추출. 구조어는 애초에 Figma 텍스트에 등장하지 않으므로 "
|
||||
"자연스럽게 배제됨.")
|
||||
lines.append("")
|
||||
lines.append("**파이프라인**:")
|
||||
lines.append("```")
|
||||
lines.append("texts.md의 '- item' list →")
|
||||
lines.append(" 길이 1자 / 단일 기호 필터 →")
|
||||
lines.append(" synonyms.yaml 정규화 (필수 요건→필수조건, 디지털 전환→DX 등) →")
|
||||
lines.append(" Kiwi 명사·외국어·숫자 추출 (길이 ≥ 2) →")
|
||||
lines.append(" STOPWORDS 제거 (정의, 특징, 개요, 결론, 장점, 단점, 고객, 적극, 구체, 소극) →")
|
||||
lines.append(" 프레임별 키워드 set")
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
|
||||
# 전체 통계
|
||||
all_legacy = set().union(*[set(v['keywords']) for v in legacy_frames.values()])
|
||||
all_new = set().union(*[set(v['keywords']) for v in new_frames.values()])
|
||||
|
||||
lines.append("## 1. 전체 비교")
|
||||
lines.append("")
|
||||
lines.append(f"| 구분 | 총 키워드 (중복 포함) | 고유 키워드 | 프레임당 평균 |")
|
||||
lines.append("|------|--------------------|------------|------------|")
|
||||
leg_total = sum(len(v['keywords']) for v in legacy_frames.values())
|
||||
new_total = sum(len(v['keywords']) for v in new_frames.values())
|
||||
lines.append(f"| Legacy (analysis.md 후보 키워드, 수동) | {leg_total} | {len(all_legacy)} | {leg_total/len(legacy_frames):.1f} |")
|
||||
lines.append(f"| **New (texts.md 자동 추출)** | **{new_total}** | **{len(all_new)}** | **{new_total/len(new_frames):.1f}** |")
|
||||
lines.append("")
|
||||
|
||||
# 구조어/weak_row가 New 에서 얼마나 제거되는지
|
||||
from keyword_refinement import classify_keyword
|
||||
new_structure_leakage = [k for k in all_new if classify_keyword(k) == 'structure']
|
||||
new_weak_leakage = [k for k in all_new if classify_keyword(k) == 'weak_row']
|
||||
|
||||
lines.append(f"**New 에 섞인 구조어 (leakage)**: {len(new_structure_leakage)}개")
|
||||
if new_structure_leakage:
|
||||
lines.append(f"- {', '.join(sorted(new_structure_leakage))}")
|
||||
lines.append("")
|
||||
lines.append(f"**New 에 섞인 weak_row**: {len(new_weak_leakage)}개")
|
||||
if new_weak_leakage:
|
||||
lines.append(f"- {', '.join(sorted(new_weak_leakage))}")
|
||||
lines.append("")
|
||||
|
||||
# 프레임별 before/after
|
||||
lines.append("## 2. 프레임별 Legacy vs New")
|
||||
lines.append("")
|
||||
lines.append("| # | Frame | Legacy 수 | New 수 | 차이 | New에 있지만 Legacy에 없음 (신규) | Legacy에 있지만 New에 없음 (제거) |")
|
||||
lines.append("|---|-------|----------|--------|------|--------------------------|--------------------------|")
|
||||
|
||||
ordered = sorted(new_frames.keys(), key=lambda f: frame_to_short.get(f, "99"))
|
||||
for fid in ordered:
|
||||
sid = frame_to_short.get(fid, "?")
|
||||
legacy_kws = set(legacy_frames.get(fid, {}).get('keywords', []))
|
||||
new_kws = set(new_frames[fid]['keywords'])
|
||||
added = new_kws - legacy_kws
|
||||
removed = legacy_kws - new_kws
|
||||
diff = len(new_kws) - len(legacy_kws)
|
||||
added_str = ", ".join(sorted(added)[:8])
|
||||
if len(added) > 8:
|
||||
added_str += f"… (+{len(added)-8})"
|
||||
removed_str = ", ".join(sorted(removed)[:8])
|
||||
if len(removed) > 8:
|
||||
removed_str += f"… (+{len(removed)-8})"
|
||||
lines.append(
|
||||
f"| {sid} | {fid} | {len(legacy_kws)} | {len(new_kws)} | {diff:+d} | "
|
||||
f"{added_str or '-'} | {removed_str or '-'} |"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# 프레임별 New 키워드 풀
|
||||
lines.append("## 3. 프레임별 New 키워드 (텍스트 기반)")
|
||||
lines.append("")
|
||||
for fid in ordered:
|
||||
sid = frame_to_short.get(fid, "?")
|
||||
kws = new_frames[fid]['keywords']
|
||||
lines.append(f"### {sid} ({fid}) — {len(kws)}개")
|
||||
lines.append("")
|
||||
lines.append(", ".join(f"`{k}`" for k in kws))
|
||||
lines.append("")
|
||||
|
||||
out = HERE / "FIGMA_KEYWORDS_FROM_TEXTS.md"
|
||||
out.write_text("\n".join(lines), encoding="utf-8")
|
||||
print(f"완료: {out}")
|
||||
|
||||
|
||||
def main():
|
||||
write_report()
|
||||
# 빠른 통계
|
||||
new_frames = load_frames_from_texts()
|
||||
from phase_common import load_32_frames
|
||||
legacy_frames = load_32_frames()
|
||||
leg_unique = len(set().union(*[set(v['keywords']) for v in legacy_frames.values()]))
|
||||
new_unique = len(set().union(*[set(v['keywords']) for v in new_frames.values()]))
|
||||
leg_avg = sum(len(v['keywords']) for v in legacy_frames.values()) / len(legacy_frames)
|
||||
new_avg = sum(len(v['keywords']) for v in new_frames.values()) / len(new_frames)
|
||||
print()
|
||||
print(f"Legacy (analysis.md): 고유 {leg_unique}, 프레임당 평균 {leg_avg:.1f}")
|
||||
print(f"New (texts.md): 고유 {new_unique}, 프레임당 평균 {new_avg:.1f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user