Files
C.E.L_Slide_test2/scripts/match_17_units_my_matcher.py
kyeongmin 85c680f02a docs + V4 catalog + samples + Phase Q legacy 보존
전체 26 files (20 추가 + 6 수정), 10507 insertions.

Phase Z 문서 :
- docs/architecture/PHASE-Z-CHANGE-LOG.md (신설) — axis-by-axis 의사결정 history
  (newest-on-top). Step 7-A 부터 6 entry 박힘 + 2026-05-08 / 2026-05-08 #2
  (compat 매트릭스 폐기 / 6-B 폐기 / F14 표현 정정 / label gate policy 분리).
- docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md (수정) — Step 5/6/9 Gap note
  append (구조 무변, append-only). 6-B 폐기 사실 + Refinement F.
- docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md (수정) — snapshot date
  2026-05-08 갱신. §3 핵심 missing item 5 (Step 5/6/9 boundary axis breakdown
  + 폐기 기록). §6 한 줄 갱신 — 다음 axis 후보 A~F.

Project root docs :
- PLAN.md / PROGRESS.md / README.md (수정) — 토큰 체계 / 폴더 구조 / 설계 문서 /
  역할 분리 반영.
- IMPROVEMENT-REDESIGN.md (신설) — Phase Z 설계 핵심 문서.
- PROCESS_OVERVIEW.html (신설) — 파이프라인 개요 시각.
- docs/tasks/* (신설) — Phase Z task 문서.

V4 catalog (Phase Z runtime 필수 의존성) :
- tests/matching/v4_full32_result.yaml (신설, 4888 줄) — V4 매칭 결과 32 frame
  × 10 MDX section. lookup_v4_match() / lookup_v4_candidates() 가 본 파일 read.
  Phase Z runtime 이 *없으면 즉시 abort* — clone 후 즉시 동작 가능 보장.

Samples :
- samples/mdx_batch/04.mdx (신설) — MDX04 기본 sample.
- samples/mdx/04. DX 지연 요인.mdx (신설) — MDX04 원본.

Phase Q legacy 보존 (별 axis "Phase Q audit & salvage" 영역) :
- src/block_matcher_tfidf.py / catalog_blocks.py / frame_extractor.py /
  pipeline_v2.py — Phase Q (옛 파이프라인) src 신규 untracked 파일들.
  Phase Z runtime 와 의존성 0. Phase Q audit axis 에서 검토 예정.
- scripts/eval_block_matcher.py / fetch_all_frame_screenshots.py /
  match_17_units_my_matcher.py / match_mdx_strict.py / match_mdx_to_frames_tfidf.py /
  ocr_augment_texts.py / run_pipeline_v2.py / previews/ — Phase Q 작업 시
  사용한 옛 script. 같이 보존.
- run_mdx03_pipeline.py (수정) — Phase Q 진입점 (no flag) + Phase Z 진입점
  (--phase-z2 flag) 동시 wrapper. Phase Z 만 사용 시 `python -m
  src.phase_z2_pipeline samples/mdx_batch/03.mdx <run_id>` 직접 호출.

비-scope :
- tests/matching/ (v4_full32_result.yaml 외 ~63MB) — V4 진화 history /
  reports / DECK / ATTACH. Phase Q audit axis 에서 검토.
- tests/pipeline/ (~15MB) — pipeline data. Phase Q audit 영역.
- templates/catalog/blocks.yaml — 옛 block catalog. Phase Q audit.
- templates/phase_z2/frames/ — 옛 frame partial 위치. Phase Q audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:47:58 +09:00

335 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"<details\b[^>]*>([\s\S]*?)</details>", raw, re.IGNORECASE):
body = m.group(1)
sm = re.search(r"<summary\b[^>]*>([\s\S]*?)</summary>", 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"<details[\s\S]*?</details>", " ", 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())