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