- 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>
289 lines
12 KiB
Python
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()
|