- 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>
213 lines
9.0 KiB
Python
213 lines
9.0 KiB
Python
"""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"""<!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: 1050px; 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: 3em; background: #f0f4f8; padding: 0.6em 0.9em; border-left: 4px solid #0a6; }}
|
|
h3 {{ margin-top: 1.5em; color: #333; font-size: 1.05em; }}
|
|
table {{ border-collapse: collapse; margin: 0.35em 0 1em 0; font-size: 0.9em; }}
|
|
th, td {{ border: 1px solid #ddd; padding: 5px 10px; text-align: left; vertical-align: top; }}
|
|
th {{ background: #f4f4f4; }}
|
|
td:nth-child(2), td:nth-child(3), td:nth-child(4) {{ text-align: center; color: #555; }}
|
|
code {{ background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; }}
|
|
details {{ margin: 0.7em 0; background: #fafafa; padding: 0.7em 0.9em; border-radius: 4px; border: 1px solid #ddd; }}
|
|
summary {{ cursor: pointer; color: #555; font-size: 0.94em; font-weight: 600; }}
|
|
hr {{ border: 0; border-top: 2px dashed #ccc; margin: 2em 0; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{html_body}
|
|
</body>
|
|
</html>"""
|
|
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"<details><summary>함께 등장한 키워드 쌍 보기 ({len(pairs)}개)</summary>")
|
|
md.append("")
|
|
if pairs:
|
|
md.append(", ".join(f"{' + '.join(pair['tokens'])} ({pair['count']})" for pair in pairs))
|
|
else:
|
|
md.append("없음")
|
|
md.append("")
|
|
md.append("</details>")
|
|
md.append("")
|
|
|
|
all_keywords = sorted(keyword_map.keys(), key=lambda kw: (-keyword_map[kw]["local_count"], kw))
|
|
md.append(f"<details><summary>전체 키워드 보기 ({len(all_keywords)}개)</summary>")
|
|
md.append("")
|
|
md.append(", ".join(f"`{kw}`" for kw in all_keywords))
|
|
md.append("")
|
|
md.append("</details>")
|
|
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()
|