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>
This commit is contained in:
334
scripts/match_17_units_my_matcher.py
Normal file
334
scripts/match_17_units_my_matcher.py
Normal file
@@ -0,0 +1,334 @@
|
||||
"""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]:
|
||||
""" + 그 근처의 이탤릭 [그림 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").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())
|
||||
Reference in New Issue
Block a user