"""17개 콘텐츠 단위를 각각 추출하여 내 매처(src/block_matcher_tfidf.py, 32프레임 IDF 고정)로 매칭. 단위: 1. MDX01-intro — 중목차 앞 본문 2. MDX01-intro-details — 팝업: 혼용 대표 사례 3. MDX01-1 — 중목차: 용어 정의 4. MDX01-2 — 중목차: 용어간 상호관계 (본문, 표 제외) 5. MDX01-2-image — 이미지 캡션: DX와 핵심기술간 상호관계 6. MDX01-2-details — 팝업+표: DX와 BIM 구분 12행 7. MDX02-1 — 중목차: DX의 궁극적 목표 8. MDX02-1-image — 이미지 캡션 9. MDX02-2 — 중목차(컨테이너): 타이틀 + 도입부만 10. MDX02-2.1 — 소목차: 업무 수행 과정의 변화 11. MDX02-2.2 — 소목차: 주체별 기대효과 (본문, 표 제외) 12. MDX02-2.2-table — 표: 발주자/시공자/설계자 13. MDX03-1 — 중목차: 필수 요건 14. MDX03-2 — 중목차(컨테이너) 15. MDX03-2.1 — 소목차: 과정의 혁신 (본문, 표 제외) 16. MDX03-2.1-table — 표: As-is/To-be 17. MDX03-2.2 — 소목차: 결과의 변화 """ from __future__ import annotations import json import re import sys from datetime import datetime from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from src.block_matcher_tfidf import TfidfBlockMatcher TOP_K = 3 PREVIEW_DIR = Path("data/figma_previews") _INDEX: dict[str, dict] = json.loads((PREVIEW_DIR / "index.json").read_text(encoding="utf-8")) FRAME_TO_NUM: dict[str, str] = {v["frame_id"]: k for k, v in _INDEX.items()} def num_of(fid: str) -> str: return FRAME_TO_NUM.get(fid, f"?({fid})") def ftitle(matcher: TfidfBlockMatcher, fid: str) -> str: for f in matcher.frames: if f["frame_id"] == fid: return (f.get("title_text") or "").replace("\n", " ")[:50] return "" def strip_tags(t: str) -> str: t = re.sub(r"<[^>]+>", " ", t) t = re.sub(r"\{/\*.*?\*/\}", " ", t, flags=re.DOTALL) t = re.sub(r"\{[^{}]*\}", " ", t) t = re.sub(r"\s+", " ", t).strip() return t def extract_details(raw: str) -> list[dict]: out = [] for m in re.finditer(r"]*>([\s\S]*?)", raw, re.IGNORECASE): body = m.group(1) sm = re.search(r"]*>([\s\S]*?)", body, re.IGNORECASE) summary = strip_tags(sm.group(1)) if sm else "" rest = body[sm.end():] if sm else body out.append({ "summary": summary, "body": strip_tags(rest), "start": m.start(), "end": m.end(), }) return out def split_h2(raw: str) -> list[dict]: """raw → [{'title', 'body', 'start', 'end'}] for each ## section.""" iters = list(re.finditer(r"^##\s+(.+?)$", raw, re.MULTILINE)) out = [] for i, m in enumerate(iters): end = iters[i+1].start() if i+1 < len(iters) else len(raw) out.append({ "title": m.group(1).strip(), "start": m.start(), "end": end, "body_raw": raw[m.end():end], }) return out def split_h3(raw: str) -> list[dict]: iters = list(re.finditer(r"^###\s+(.+?)$", raw, re.MULTILINE)) out = [] for i, m in enumerate(iters): end = iters[i+1].start() if i+1 < len(iters) else len(raw) out.append({ "title": m.group(1).strip(), "start": m.start(), "end": end, "body_raw": raw[m.end():end], }) return out def extract_tables(raw: str) -> list[str]: """Markdown 표 ( | ... | ... | ) 블록을 각각 문자열로 반환.""" lines = raw.splitlines() tables = [] cur = [] for ln in lines: if re.match(r"^\s*\|.*\|\s*$", ln): cur.append(ln.strip()) else: if len(cur) >= 2: tables.append("\n".join(cur)) cur = [] if len(cur) >= 2: tables.append("\n".join(cur)) return tables def extract_image_captions(raw: str) -> list[str]: """![alt](path) + 그 근처의 이탤릭 [그림 N] 캡션 모음.""" out = [] for m in re.finditer(r"!\[([^\]]*)\]\(([^)]+)\)", raw): alt = m.group(1).strip() path = m.group(2).strip() # 뒤 300자 안에 *[그림 ...]* 캡션 찾기 after = raw[m.end():m.end()+400] cap = re.search(r"\*\[그림[^\]]*\][^*]*\*", after) caption = cap.group(0).strip("*").strip() if cap else "" out.append(f"{alt} {path} {caption}".strip()) return out def remove_details_and_tables(raw: str) -> str: t = re.sub(r"", " ", raw, flags=re.IGNORECASE) # 표 라인 제거 t = "\n".join(ln for ln in t.splitlines() if not re.match(r"^\s*\|.*\|\s*$", ln)) return t def build_17_units() -> list[dict]: mdx_dir = Path("samples/mdx") raw01 = (mdx_dir / "01. 건설산업 DX의 올바른 이해(0127).mdx").read_text(encoding="utf-8") raw02 = (mdx_dir / "02. DX의 시행 목표 및 기대효과.mdx").read_text(encoding="utf-8") raw03 = (mdx_dir / "03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx").read_text(encoding="utf-8") units: list[dict] = [] # ─── MDX 01 ─── h2_01 = split_h2(raw01) intro_01 = raw01[: h2_01[0]["start"]] if h2_01 else raw01 details_01 = extract_details(raw01) images_01 = extract_image_captions(raw01) # 1. MDX01-intro: 첫 ## 전 본문(details 제외) intro_text = remove_details_and_tables(intro_01) units.append({"id": "MDX01-intro", "kind": "중목차 앞 본문", "label": "용어 혼용 문제 제기", "text": strip_tags(intro_text)}) # 2. MDX01-intro-details d0 = details_01[0] if details_01 else {"summary": "", "body": ""} units.append({"id": "MDX01-intro-details", "kind": "팝업", "label": "혼용 대표 사례", "text": f"{d0['summary']} {d0['body']}"}) # 3. MDX01-1 body_01_1 = h2_01[0]["body_raw"] units.append({"id": "MDX01-1", "kind": "중목차", "label": "용어 정의", "text": f"{h2_01[0]['title']} {strip_tags(remove_details_and_tables(body_01_1))}"}) # 4. MDX01-2 (본문만, details/표 제거) body_01_2 = h2_01[1]["body_raw"] units.append({"id": "MDX01-2", "kind": "중목차", "label": "용어간 상호관계", "text": f"{h2_01[1]['title']} {strip_tags(remove_details_and_tables(body_01_2))}"}) # 5. MDX01-2-image units.append({"id": "MDX01-2-image", "kind": "이미지", "label": "DX1.png", "text": images_01[0] if images_01 else ""}) # 6. MDX01-2-details d1 = details_01[1] if len(details_01) >= 2 else {"summary": "", "body": ""} units.append({"id": "MDX01-2-details", "kind": "팝업+표", "label": "DX와 BIM의 구분 12행 비교표", "text": f"{d1['summary']} {d1['body']}"}) # ─── MDX 02 ─── h2_02 = split_h2(raw02) images_02 = extract_image_captions(raw02) # 7. MDX02-1 body_02_1 = h2_02[0]["body_raw"] units.append({"id": "MDX02-1", "kind": "중목차", "label": "DX의 궁극적 목표", "text": f"{h2_02[0]['title']} {strip_tags(remove_details_and_tables(body_02_1))}"}) # 8. MDX02-1-image units.append({"id": "MDX02-1-image", "kind": "이미지", "label": "궁극적목표.png", "text": images_02[0] if images_02 else ""}) # 9. MDX02-2 컨테이너 (title + ### 이전 본문) body_02_2 = h2_02[1]["body_raw"] h3_02_2 = split_h3(body_02_2) pre_h3 = body_02_2[: h3_02_2[0]["start"]] if h3_02_2 else body_02_2 units.append({"id": "MDX02-2", "kind": "중목차(컨테이너)", "label": "DX 기반 Process 혁신 기대효과", "text": f"{h2_02[1]['title']} {strip_tags(pre_h3)}"}) # 10. MDX02-2.1 (표 제거) body_021 = h3_02_2[0]["body_raw"] units.append({"id": "MDX02-2.1", "kind": "소목차", "label": "업무 수행 과정의 변화", "text": f"{h3_02_2[0]['title']} {strip_tags(remove_details_and_tables(body_021))}"}) # 11. MDX02-2.2 (표 제거) body_022 = h3_02_2[1]["body_raw"] units.append({"id": "MDX02-2.2", "kind": "소목차", "label": "주체별 기대효과", "text": f"{h3_02_2[1]['title']} {strip_tags(remove_details_and_tables(body_022))}"}) # 12. MDX02-2.2-table tables_022 = extract_tables(body_022) units.append({"id": "MDX02-2.2-table", "kind": "표", "label": "발주자/시공자/설계자 4×3 표", "text": strip_tags(tables_022[0]) if tables_022 else ""}) # ─── MDX 03 ─── h2_03 = split_h2(raw03) # 13. MDX03-1 body_03_1 = h2_03[0]["body_raw"] units.append({"id": "MDX03-1", "kind": "중목차", "label": "필수 요건 (기술/사람/자연)", "text": f"{h2_03[0]['title']} {strip_tags(remove_details_and_tables(body_03_1))}"}) # 14. MDX03-2 컨테이너 body_03_2 = h2_03[1]["body_raw"] h3_03_2 = split_h3(body_03_2) pre_h3_2 = body_03_2[: h3_03_2[0]["start"]] if h3_03_2 else body_03_2 units.append({"id": "MDX03-2", "kind": "중목차(컨테이너)", "label": "Process/Product 혁신", "text": f"{h2_03[1]['title']} {strip_tags(pre_h3_2)}"}) # 15. MDX03-2.1 (표 제거) body_031 = h3_03_2[0]["body_raw"] units.append({"id": "MDX03-2.1", "kind": "소목차", "label": "과정(Process)의 혁신", "text": f"{h3_03_2[0]['title']} {strip_tags(remove_details_and_tables(body_031))}"}) # 16. MDX03-2.1-table tables_031 = extract_tables(body_031) units.append({"id": "MDX03-2.1-table", "kind": "표", "label": "As-is/To-be 3행 비교표", "text": strip_tags(tables_031[0]) if tables_031 else ""}) # 17. MDX03-2.2 body_032 = h3_03_2[1]["body_raw"] units.append({"id": "MDX03-2.2", "kind": "소목차", "label": "결과(Product)의 변화", "text": f"{h3_03_2[1]['title']} {strip_tags(remove_details_and_tables(body_032))}"}) return units def main() -> int: print("[init] TF-IDF 인덱스 로딩 (src/block_matcher_tfidf.py, 32프레임 IDF 고정)...") matcher = TfidfBlockMatcher() print(f"[init] 프레임 {len(matcher.frames)}개 인덱싱 완료") units = build_17_units() md_lines: list[str] = [ "# 17개 콘텐츠 단위별 매칭 (내 매처: 32프레임 IDF 고정)", "", "엔진: `src/block_matcher_tfidf.py` (프레임 32개만으로 IDF 사전 계산, 확장어 주입).", "각 단위의 텍스트를 쿼리로 주입하고 top-3 프레임을 출력.", "", "| # | 단위 ID | 종류 | 라벨 | 텍스트 길이 | 1위 | 2위 | 3위 |", "|---|---|---|---|---|---|---|---|", ] rel = Path("..") / ".." / ".." / PREVIEW_DIR details_sections: list[str] = [] for i, u in enumerate(units, start=1): q = u["text"] print(f"\n[{i:02d}] {u['id']} ({u['kind']}) — {u['label']} 텍스트 {len(q)}자") if not q.strip(): print(" (텍스트 비어 있음)") row = f"| {i} | {u['id']} | {u['kind']} | {u['label']} | 0 | — | — | — |" md_lines.append(row) continue top = matcher.match(q, sub_titles=None, d1_items=None, top_k=len(matcher.frames)) top3 = [r for r in top[:TOP_K] if r["score"] > 0] for rank, r in enumerate(top3, start=1): num = num_of(r["frame_id"]) print(f" {rank}. #{num} {r['score']*100:5.1f}% | frame {r['frame_id']} | {ftitle(matcher, r['frame_id'])}") cells = [] for slot in range(3): if slot < len(top3): r = top3[slot] n = num_of(r["frame_id"]) cells.append(f"**#{n}** {r['score']*100:.1f}%") else: cells.append("—") row = (f"| {i} | {u['id']} | {u['kind']} | {u['label']} | {len(q)} | " + " | ".join(cells) + " |") md_lines.append(row) # 상세 섹션 details_sections.append(f"\n### {i}. {u['id']} — {u['label']}") details_sections.append(f"- 종류: {u['kind']} · 텍스트 길이: {len(q)}자") preview = q[:120] + ("…" if len(q) > 120 else "") details_sections.append(f"- 쿼리 미리보기: _{preview}_\n") details_sections.append("| rank | # | preview | score | frame_id | title_text |") details_sections.append("|---|---|---|---|---|---|") for rank, r in enumerate(top3, start=1): n = num_of(r["frame_id"]) prev = f"![]({(rel / (n+'.png')).as_posix()})" details_sections.append( f"| {rank} | **#{n}** | {prev} | **{r['score']*100:.1f}%** | " f"`{r['frame_id']}` | {ftitle(matcher, r['frame_id'])} |" ) if not top3: details_sections.append("| — | — | — | 0% | — | (매칭 없음) |") md_lines.append("\n## 상세\n") md_lines.extend(details_sections) ts = datetime.now().strftime("%Y%m%d_%H%M%S") out_dir = Path("data/runs") / f"{ts}_17units_my_matcher" out_dir.mkdir(parents=True, exist_ok=True) out = out_dir / "match_report.md" out.write_text("\n".join(md_lines), encoding="utf-8") print(f"\n[saved] {out}") return 0 if __name__ == "__main__": raise SystemExit(main())