"""Generate FRAME_KEYWORD_REVIEW report. This report explains how the keyword dictionary is applied to each Figma frame. It intentionally avoids internal shorthand such as token/set/tag/bucket in the main report text. """ from __future__ import annotations from collections import defaultdict from pathlib import Path import markdown import yaml HERE = Path(__file__).parent INPUT = HERE / "auto_anchor_candidates.yaml" OUT_MD = HERE / "FRAME_KEYWORD_REVIEW.md" OUT_HTML = HERE / "FRAME_KEYWORD_REVIEW.html" def classify_keyword(frame_appearances: int, is_important: bool) -> tuple[str, str | None]: """Return keyword role and reason for per-frame review.""" if frame_appearances == 1: return "standalone", "특정 프레임에만 등장" if is_important and frame_appearances <= 2: return "standalone", "중요 표기이면서 드물게 등장" return "related", None def write_html(md_text: str) -> None: html_body = markdown.markdown(md_text, extensions=["tables"]) html = f""" 프레임별 키워드 적용 보고서 {html_body} """ OUT_HTML.write_text(html, encoding="utf-8") def main() -> None: data = yaml.safe_load(INPUT.read_text(encoding="utf-8")) frame_keyword_info: dict[str, dict[str, dict]] = defaultdict(dict) for set_id, info in data["source_text_sets"].items(): frame_id = info["frame_id"] for term in info["terms"]: keyword = term["token"] if keyword not in frame_keyword_info[frame_id]: frame_keyword_info[frame_id][keyword] = { "frame_appearances": term["frame_df"], "mdx_appearances": term["mdx_df"], "is_important": term["is_special"], "local_count": term["local_count_in_frame"], "source_set_ids": [], } frame_keyword_info[frame_id][keyword]["source_set_ids"].append(set_id) for frame_id, keyword_map in frame_keyword_info.items(): for keyword, info in keyword_map.items(): role, reason = classify_keyword(info["frame_appearances"], info["is_important"]) info["role"] = role info["reason"] = reason md: list[str] = [ "# 프레임별 키워드 적용 보고서", "", "이 문서는 앞 단계에서 만든 키워드집을 Figma 각 프레임에 적용한 결과입니다.", "", "각 프레임마다 다음 정보를 확인합니다.", "", "1. **단독 대표 키워드**: 그 프레임을 혼자서도 비교적 강하게 가리킬 수 있는 키워드", "2. **대표 키워드 묶음**: 같은 문장 안에서 함께 등장해 프레임의 의미를 강하게 설명하는 키워드 조합", "3. **연관 키워드**: 프레임에 등장하지만 혼자서는 대표성이 약한 참고 키워드", "", "단독 대표 키워드와 대표 키워드 묶음은 모두 매칭 단서입니다. 다만 넓게 쓰이는 키워드는 묶음 안에서 함께 볼 때 더 의미가 큽니다.", "", "### 판정 기준", "", "| 항목 | 기준 |", "|---|---|", "| 단독 대표 키워드 | 특정 프레임에만 등장하거나, 중요 표기 키워드가 1~2개 프레임에만 등장하는 경우 |", "| 대표 키워드 묶음 | 원문 한 줄에서 함께 추출된 키워드 조합 |", "| 연관 키워드 | 단독 대표 키워드가 아닌 나머지 키워드 |", "", "**주의:** 이 보고서는 후보를 보여주는 검토 자료입니다. 최종 대표 키워드 확정은 사람이 검수해야 합니다.", "", "---", "", ] for frame_id in sorted(data["frame_stats"].keys()): frame_stat = data["frame_stats"][frame_id] frame_number = frame_stat["frame_number"] keyword_map = frame_keyword_info[frame_id] standalone = sorted( [(kw, info) for kw, info in keyword_map.items() if info["role"] == "standalone"], key=lambda x: (x[1]["frame_appearances"], -x[1]["local_count"], x[0]), ) related = sorted( [(kw, info) for kw, info in keyword_map.items() if info["role"] == "related"], key=lambda x: (-x[1]["local_count"], x[1]["frame_appearances"], x[0]), ) frame_sets = sorted( [ (set_id, info) for set_id, info in data["source_text_sets"].items() if info["frame_id"] == frame_id ], key=lambda x: x[1]["source_text_index"], ) weak_sets = [(set_id, info) for set_id, info in frame_sets if info["candidate_strength"]["is_weak"]] md.append(f"## 프레임 {frame_number} / {frame_id}") md.append("") md.append(f"### 단독 대표 키워드 후보 — {len(standalone)}개") md.append("") if standalone: md.append("| 키워드 | 등장 프레임 수 | 등장 MDX 구간 수 | 중요 표기 | 근거 |") md.append("|---|---:|---:|---|---|") for keyword, info in standalone: important = "예" if info["is_important"] else "" md.append( f"| `{keyword}` | {info['frame_appearances']} / 32 | {info['mdx_appearances']} / 4 | {important} | {info['reason']} |" ) else: md.append("단독 대표 키워드 후보가 없습니다.") md.append("") md.append(f"### 대표 키워드 묶음 후보 — {len(frame_sets)}개") md.append("") md.append(f"약한 후보로 표시된 묶음: {len(weak_sets)}개") md.append("") md.append("| 원문 | 키워드 묶음 | 상태 | 중요 표기 포함 | 프레임 전용 포함 |") md.append("|---|---|---|---|---|") for set_id, info in frame_sets: raw = info["source_text_raw"] if len(raw) > 60: raw = raw[:59] + "..." terms = ", ".join(info["term_values"]) status = "약함" if info["candidate_strength"]["is_weak"] else "후보" has_important = "예" if info["stats"]["contains_special"] else "" has_unique = "예" if info["stats"]["contains_unique_to_frame"] else "" md.append(f"| {raw} | {terms} | {status} | {has_important} | {has_unique} |") md.append("") md.append(f"### 연관 키워드 — {len(related)}개") md.append("") if related: chunks = [ f"`{kw}` ({info['frame_appearances']}프레임 / {info['mdx_appearances']}MDX)" for kw, info in related ] md.append(", ".join(chunks)) else: md.append("없음") md.append("") pairs = data.get("cooccurrence_by_frame", {}).get(frame_id, {}).get("pairs", []) md.append(f"
함께 등장한 키워드 쌍 보기 ({len(pairs)}개)") md.append("") if pairs: md.append(", ".join(f"{' + '.join(pair['tokens'])} ({pair['count']})" for pair in pairs)) else: md.append("없음") md.append("") md.append("
") md.append("") all_keywords = sorted(keyword_map.keys(), key=lambda kw: (-keyword_map[kw]["local_count"], kw)) md.append(f"
전체 키워드 보기 ({len(all_keywords)}개)") md.append("") md.append(", ".join(f"`{kw}`" for kw in all_keywords)) md.append("") md.append("
") md.append("") md.append("---") md.append("") md_text = "\n".join(md) OUT_MD.write_text(md_text, encoding="utf-8") write_html(md_text) print("산출 완료:") print(f" md: {OUT_MD}") print(f" html: {OUT_HTML}") print(f" frames: {len(data['frame_stats'])}") if __name__ == "__main__": main()