"""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()