Files
C.E.L_Slide_test2/tests/matching/generate_keyword_inventory.py
T
KyeongminandClaude Opus 4.8 b836e79ee1 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>
2026-07-02 17:03:42 +09:00

289 lines
12 KiB
Python

"""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"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>키워드집 생성 보고서</title>
<style>
body {{ font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1100px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; }}
h1 {{ border-bottom: 2px solid #333; padding-bottom: 0.25em; }}
h2 {{ margin-top: 2.5em; background: #f0f4f8; padding: 0.55em 0.8em; border-left: 4px solid #0a6; }}
h3 {{ margin-top: 1.8em; color: #333; border-bottom: 1px solid #ddd; padding-bottom: 0.3em; }}
table {{ border-collapse: collapse; margin: 0.4em 0 1em 0; font-size: 0.9em; }}
th, td {{ border: 1px solid #ddd; padding: 5px 9px; text-align: left; vertical-align: top; }}
th {{ background: #f4f4f4; }}
code {{ background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; }}
details {{ margin: 0.7em 0; background: #fafafa; padding: 1em; border-radius: 4px; border: 1px solid #ddd; }}
summary {{ cursor: pointer; color: #0a6; font-size: 1em; font-weight: bold; }}
hr {{ border: 0; border-top: 2px dashed #ccc; margin: 2em 0; }}
strong {{ color: #0a6; }}
</style>
</head>
<body>
{html_body}
</body>
</html>"""
(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):,}개, 펼침)",
"",
"정렬: 전체 등장 수가 많은 순서 → 등장 프레임 수가 많은 순서 → 가나다순",
"",
"<details><summary>전체 키워드 테이블 펼치기</summary>",
"",
"| # | 키워드 | 전체 등장 수 | 등장 프레임 수 | 등장 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 += ["", "</details>", ""]
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()