"""Generate KEYWORD_INVENTORY report. This report explains the keyword dictionary pipeline in plain Korean: 1. which documents were collected, 2. how many text lines and tokens were extracted, 3. what preprocessing rules were applied, 4. how the final keywords are organized. """ from __future__ import annotations import sys from collections import Counter from pathlib import Path import markdown import yaml HERE = Path(__file__).parent sys.path.insert(0, str(HERE)) from keyword_normalizer import USER_DICT_NNG, USER_DICT_SL IMPORTANT_KEYWORDS = set(USER_DICT_SL) | set(USER_DICT_NNG) WIDE_FRAME_THRESHOLD = 10 def load_yaml(name: str) -> dict: return yaml.safe_load((HERE / name).read_text(encoding="utf-8")) def write_html(md_text: str) -> None: html_body = markdown.markdown(md_text, extensions=["tables"]) html = f""" 키워드집 생성 보고서 {html_body} """ (HERE / "KEYWORD_INVENTORY.html").write_text(html, encoding="utf-8") def main() -> None: nodes = load_yaml("actual_text_nodes.yaml") tokens_raw = load_yaml("actual_text_tokens.yaml") normalized = load_yaml("normalized_text_tokens.yaml") auto = load_yaml("auto_anchor_candidates.yaml") nodes_totals = nodes["meta"]["totals"] tokens_totals = tokens_raw["meta"]["totals"] corpus = normalized["corpus"] n_frames = len(normalized["frames"]) n_mdx = len(normalized["mdx"]) freq = corpus["token_frequency"] frame_count = corpus["token_frame_df"] mdx_count = corpus["token_mdx_df"] all_keywords = sorted( freq.keys(), key=lambda t: (-freq[t], -frame_count.get(t, 0), t), ) frame_numbers: dict[str, int] = {} frame_unique_map: dict[str, str] = {} for frame_id, stat in auto["frame_stats"].items(): frame_numbers[frame_id] = stat["frame_number"] for keyword in stat["unique_to_frame_tokens"]: frame_unique_map[keyword] = frame_id unique_keywords = sorted( [(kw, frame_unique_map.get(kw, "?")) for kw in all_keywords if frame_count.get(kw, 0) == 1], key=lambda x: (frame_numbers.get(x[1], 0), x[0]), ) wide_keywords = sorted( [kw for kw in all_keywords if frame_count.get(kw, 0) >= WIDE_FRAME_THRESHOLD], key=lambda kw: (-frame_count[kw], -freq[kw], kw), ) count_important = len(IMPORTANT_KEYWORDS & set(all_keywords)) count_unique = sum(1 for kw in all_keywords if frame_count.get(kw, 0) == 1) count_wide = len(wide_keywords) count_partial = len(all_keywords) - count_unique - count_wide df_hist = Counter() for kw in all_keywords: df = frame_count.get(kw, 0) if df == 1: df_hist["특정 프레임에만 등장"] += 1 elif df <= 3: df_hist["2~3개 프레임에 등장"] += 1 elif df <= 5: df_hist["4~5개 프레임에 등장"] += 1 elif df <= 9: df_hist["6~9개 프레임에 등장"] += 1 else: df_hist["10개 이상 프레임에 등장"] += 1 md: list[str] = [ "# 키워드집 생성 보고서", "", "이 문서는 Figma 프레임, BEPS 마스터, MDX 구간에서 문장을 수집하고, 전처리와 형태소 분석을 거쳐 최종 키워드집을 만든 과정을 정리합니다.", "", "보고서의 목적은 **어떤 문서에서 몇 개의 문장과 단어를 뽑았고, 어떤 규칙으로 정리했는지**를 확인하는 것입니다.", "", "---", "", "## 1. 입력 문서와 수집량", "", "### 수집 대상", "", "| 입력 문서 | 개수 | 설명 |", "|---|---:|---|", f"| Figma 프레임 텍스트 | **{n_frames}개** | `figma_to_html_agent/blocks/*/texts.md` |", "| BEPS 마스터 텍스트 | **1개** | 기준/마스터 텍스트 |", f"| MDX 구간 텍스트 | **{n_mdx}개** | 현재 검증 대상 MDX 구간 |", "", "### 수집된 원문 문장", "", "| 출처 | 고유 문장 수 |", "|---|---:|", f"| Figma 프레임 | {nodes_totals['frames_unique']:,} |", f"| BEPS | {nodes_totals['beps_unique']:,} |", f"| MDX | {nodes_totals['mdx_unique']:,} |", f"| **전체** | **{nodes_totals['total_unique']:,}** |", "", "---", "", "## 2. 전처리와 단어 추출 과정", "", "### 형태소 분석으로 뽑은 원 단어", "", "일반명사, 고유명사, 외국어 표기, 숫자 표기를 사용했습니다. 1글자 단어와 순수 숫자는 제외했습니다.", "", "| 단계 | 수치 |", "|---|---:|", f"| 형태소 분석 결과 전체 등장 수 | {tokens_totals['corpus_total_occurrences']:,} |", f"| 형태소 분석 결과 고유 단어 수 | {tokens_totals['corpus_unique']:,} |", "", "### 적용한 정리 규칙", "", "- `DX`, `BIM`, `S/W`, `H/W`, `2D`, `3D`처럼 깨지면 안 되는 표기는 중요 표기 키워드로 보존했습니다.", "- 괄호 안 설명은 필요한 경우 함께 펼쳐서 검색 가능한 단어로 반영했습니다.", "- 같은 뜻의 표기 차이는 대표 표기로 합쳤습니다. 예: `디지털 전환` 계열은 `DX`로 통합", "- 공백 차이, 슬래시 차이, 영문/국문 혼용 표기는 가능한 범위에서 하나로 정리했습니다.", "", "### 전처리 후 최종 키워드", "", "| 단계 | 수치 |", "|---|---:|", f"| 전처리 후 전체 등장 수 | {corpus['total_occurrences']:,} |", f"| **최종 고유 키워드 수** | **{corpus['unique_token_count']:,}** |", "", "---", "", "## 3. 최종 키워드집 정리 방식", "", "최종 키워드는 먼저 **등장 범위**로 나누었습니다. 아래 3개 구분은 서로 겹치지 않으며, 합계가 전체 키워드 수와 같습니다.", "", "### 등장 범위별 구분", "", "| 등장 범위 | 개수 | 의미 |", "|---|---:|---|", f"| 특정 프레임에만 등장 | {count_unique} | 32개 프레임 중 1개 프레임에만 등장하는 키워드 |", f"| 일부 프레임에 등장 | {count_partial} | 2~9개 프레임에 등장하는 키워드 |", f"| 여러 프레임에 넓게 등장 | {count_wide} | 10개 이상 프레임에 등장하는 넓은 키워드 |", f"| **합계** | **{count_unique + count_partial + count_wide}** | **전체 고유 키워드 수** |", "", "### 중요 표기 키워드", "", f"중요 표기 키워드는 **{count_important}개**입니다. `DX`, `BIM`, `S/W`처럼 원문 표기를 보존해야 하는 키워드입니다.", "이 표시는 등장 범위와 별개의 보조 표시이므로, 위 등장 범위별 개수에 따로 더하지 않습니다.", "", "---", "", "## 4. 등장 범위별 상세", "", f"### 중요 표기 키워드 ({count_important}개)", "", "| 키워드 | 종류 | 전체 등장 수 | 등장 프레임 수 | 등장 MDX 구간 수 |", "|---|---|---:|---:|---:|", ] for kw in sorted(IMPORTANT_KEYWORDS): if kw not in freq: continue kind = "영문/기호 보존" if kw in USER_DICT_SL else "구문 보존" md.append( f"| `{kw}` | {kind} | {freq[kw]} | {frame_count.get(kw, 0)} / {n_frames} | {mdx_count.get(kw, 0)} / {n_mdx} |" ) md += [ "", f"### 특정 프레임에만 등장하는 키워드 ({count_unique}개)", "", "특정 프레임에만 등장하므로, 단독으로도 강한 매칭 단서가 될 수 있습니다.", "", "| 키워드 | 소속 프레임 | 전체 등장 수 | 등장 MDX 구간 수 |", "|---|---|---:|---:|", ] for kw, frame_id in unique_keywords: fnum = frame_numbers.get(frame_id, "?") md.append(f"| `{kw}` | Frame {fnum} / {frame_id} | {freq[kw]} | {mdx_count.get(kw, 0)} / {n_mdx} |") md += [ "", f"### 여러 프레임에 넓게 등장하는 키워드 ({count_wide}개)", "", "여러 프레임에 넓게 등장하므로, 혼자서는 약한 단서입니다. 다른 키워드와 함께 묶음으로 볼 때 의미가 커집니다.", "", "| 키워드 | 등장 프레임 수 | 등장 MDX 구간 수 | 전체 등장 수 |", "|---|---:|---:|---:|", ] for kw in wide_keywords: md.append(f"| `{kw}` | {frame_count[kw]} / {n_frames} | {mdx_count.get(kw, 0)} / {n_mdx} | {freq[kw]} |") md += [ "", "### 등장 프레임 수 분포", "", "| 범위 | 키워드 수 |", "|---|---:|", ] for label in [ "특정 프레임에만 등장", "2~3개 프레임에 등장", "4~5개 프레임에 등장", "6~9개 프레임에 등장", "10개 이상 프레임에 등장", ]: md.append(f"| {label} | {df_hist.get(label, 0)} |") md += [ "", "---", "", f"## 5. 전체 키워드 목록 ({len(all_keywords):,}개, 펼침)", "", "정렬: 전체 등장 수가 많은 순서 → 등장 프레임 수가 많은 순서 → 가나다순", "", "
전체 키워드 테이블 펼치기", "", "| # | 키워드 | 전체 등장 수 | 등장 프레임 수 | 등장 MDX 구간 수 | 중요 표기 | 특정 프레임 전용 | 넓게 등장 |", "|---:|---|---:|---:|---:|---|---|---|", ] for idx, kw in enumerate(all_keywords, 1): fd = frame_count.get(kw, 0) mdv = mdx_count.get(kw, 0) important = "예" if kw in IMPORTANT_KEYWORDS else "" unique = "예" if fd == 1 else "" wide = "예" if fd >= WIDE_FRAME_THRESHOLD else "" md.append(f"| {idx} | `{kw}` | {freq[kw]} | {fd} | {mdv} | {important} | {unique} | {wide} |") md += ["", "
", ""] md_text = "\n".join(md) (HERE / "KEYWORD_INVENTORY.md").write_text(md_text, encoding="utf-8") write_html(md_text) print("[KEYWORD_INVENTORY] 생성 완료") print(f" 전체 키워드: {len(all_keywords):,}") print(f" 중요 표기: {count_important}") print(f" 프레임 전용: {count_unique}") print(f" 일부 등장: {count_partial}") print(f" 넓게 등장: {count_wide}") print(f" md: {HERE / 'KEYWORD_INVENTORY.md'}") print(f" html: {HERE / 'KEYWORD_INVENTORY.html'}") if __name__ == "__main__": main()