전체 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>
206 lines
6.4 KiB
Python
206 lines
6.4 KiB
Python
"""프레임별 텍스트 + 메타 추출기.
|
|
|
|
figma_to_html_agent/blocks/{frame_id}/texts.md를 파싱하여
|
|
TF-IDF 매칭용 데이터 구조를 만든다.
|
|
|
|
keywords 수동 생성 불필요 — texts.md의 원본 텍스트를 직접 사용.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def extract_frame_meta(texts_md_path: Path) -> dict:
|
|
"""texts.md에서 프레임 메타 추출.
|
|
|
|
Returns:
|
|
{
|
|
"frame_id": "1171281190",
|
|
"title_text": "필수조건",
|
|
"subtitle_texts": ["기술(디지털)", "사람(역량)", ...],
|
|
"body_texts": ["건설단계별 근본적인...", ...],
|
|
"all_text": "필수조건 기술 디지털 ...", ← TF-IDF용 전체 텍스트
|
|
"item_count": 3,
|
|
"rough_structure": "3col",
|
|
"sections": [{"heading": "타이틀", "lines": ["필수조건"]}, ...]
|
|
}
|
|
"""
|
|
if not texts_md_path.exists():
|
|
return {}
|
|
|
|
content = texts_md_path.read_text(encoding="utf-8")
|
|
frame_id = texts_md_path.parent.name
|
|
|
|
# 섹션별 파싱 (## 기준)
|
|
sections = []
|
|
current_heading = ""
|
|
current_lines = []
|
|
|
|
for line in content.split("\n"):
|
|
line = line.strip()
|
|
if line.startswith("## "):
|
|
if current_heading or current_lines:
|
|
sections.append({"heading": current_heading, "lines": current_lines})
|
|
current_heading = line.lstrip("# ").strip()
|
|
current_lines = []
|
|
elif line.startswith("### "):
|
|
# 서브섹션은 heading에 포함
|
|
current_lines.append(line.lstrip("# ").strip())
|
|
elif line.startswith("# "):
|
|
# 최상위 제목 (프레임 ID) — 건너뜀
|
|
continue
|
|
elif line.startswith(">"):
|
|
continue
|
|
elif line and not line.startswith("-"):
|
|
current_lines.append(line)
|
|
elif line.startswith("- "):
|
|
current_lines.append(line.lstrip("- ").strip())
|
|
|
|
if current_heading or current_lines:
|
|
sections.append({"heading": current_heading, "lines": current_lines})
|
|
|
|
# 층별 텍스트 분류
|
|
title_text = ""
|
|
subtitle_texts = []
|
|
body_texts = []
|
|
|
|
for sec in sections:
|
|
heading = sec["heading"].lower()
|
|
lines = sec["lines"]
|
|
|
|
if "타이틀" in heading or "제목" in heading:
|
|
title_text = " ".join(lines)
|
|
elif "서브" in heading or "헤더" in heading or "카테고리" in heading:
|
|
subtitle_texts.extend(lines)
|
|
elif "열" in heading or "col" in heading.lower():
|
|
# 열별 텍스트 → subtitle + body
|
|
for line in lines:
|
|
if len(line) < 20:
|
|
subtitle_texts.append(line)
|
|
else:
|
|
body_texts.append(line)
|
|
elif "행" in heading or "row" in heading.lower():
|
|
# 행별 텍스트
|
|
for line in lines:
|
|
if len(line) < 15:
|
|
subtitle_texts.append(line)
|
|
else:
|
|
body_texts.append(line)
|
|
elif "결론" in heading or "요약" in heading:
|
|
body_texts.extend(lines)
|
|
else:
|
|
# 기타 — 길이로 구분
|
|
for line in lines:
|
|
if len(line) < 20:
|
|
subtitle_texts.append(line)
|
|
else:
|
|
body_texts.append(line)
|
|
|
|
# rough_structure 추정
|
|
rough_structure = _guess_structure(sections, subtitle_texts)
|
|
|
|
# all_text: TF-IDF용 전체 텍스트 (전처리 적용)
|
|
all_parts = [title_text] + subtitle_texts + body_texts
|
|
all_text = " ".join(all_parts)
|
|
all_text = _preprocess_text(all_text)
|
|
|
|
return {
|
|
"frame_id": frame_id,
|
|
"title_text": title_text.strip(),
|
|
"subtitle_texts": subtitle_texts,
|
|
"body_texts": body_texts,
|
|
"all_text": all_text,
|
|
"item_count": len(subtitle_texts),
|
|
"rough_structure": rough_structure,
|
|
"sections": sections,
|
|
}
|
|
|
|
|
|
def _guess_structure(sections: list[dict], subtitles: list[str]) -> str:
|
|
"""섹션 구조에서 대략적인 블록 유형 추정."""
|
|
headings = [s["heading"].lower() for s in sections]
|
|
heading_text = " ".join(headings)
|
|
|
|
# 열 기반
|
|
col_count = sum(1 for h in headings if "열" in h or "col" in h)
|
|
if col_count >= 3:
|
|
return "3col"
|
|
if col_count >= 2:
|
|
return "2col"
|
|
|
|
# 행 기반
|
|
row_count = sum(1 for h in headings if "행" in h or "row" in h)
|
|
if row_count >= 2:
|
|
return "rows"
|
|
|
|
# 좌/우
|
|
if any("좌" in h or "left" in h for h in headings):
|
|
return "2col-compare"
|
|
|
|
# 표
|
|
if any("표" in h or "table" in h for h in headings):
|
|
return "table"
|
|
|
|
# 기본
|
|
if len(subtitles) >= 3:
|
|
return "list"
|
|
|
|
return "unknown"
|
|
|
|
|
|
def _preprocess_text(text: str) -> str:
|
|
"""TF-IDF용 텍스트 전처리.
|
|
|
|
- 표기 통일
|
|
- 괄호/특수문자 정리
|
|
- 중복 제거
|
|
"""
|
|
# 표기 통일
|
|
text = text.replace("S/W", "SW 소프트웨어")
|
|
text = text.replace("H/W", "HW 하드웨어")
|
|
text = re.sub(r'\bDX\b', 'DX 디지털전환', text)
|
|
text = re.sub(r'\bBIM\b', 'BIM 건설정보모델링', text)
|
|
|
|
# 괄호 내용 유지하되 괄호 제거
|
|
text = text.replace("(", " ").replace(")", " ")
|
|
text = text.replace("[", " ").replace("]", " ")
|
|
|
|
# 특수문자 정리
|
|
text = re.sub(r'[·•→←↔×+/]', ' ', text)
|
|
text = re.sub(r'\s+', ' ', text).strip()
|
|
|
|
return text
|
|
|
|
|
|
def extract_all_frames(
|
|
blocks_dir: str | Path = "figma_to_html_agent/blocks",
|
|
) -> list[dict]:
|
|
"""모든 프레임의 메타 추출.
|
|
|
|
Returns:
|
|
[{"frame_id": ..., "title_text": ..., "all_text": ..., ...}]
|
|
"""
|
|
blocks_dir = Path(blocks_dir)
|
|
if not blocks_dir.exists():
|
|
logger.warning(f"[extractor] blocks 폴더 없음: {blocks_dir}")
|
|
return []
|
|
|
|
frames = []
|
|
for frame_dir in sorted(blocks_dir.iterdir()):
|
|
if not frame_dir.is_dir():
|
|
continue
|
|
texts_md = frame_dir / "texts.md"
|
|
if texts_md.exists():
|
|
meta = extract_frame_meta(texts_md)
|
|
if meta:
|
|
frames.append(meta)
|
|
logger.debug(f"[extractor] {meta['frame_id']}: {meta['title_text']} ({meta['rough_structure']})")
|
|
|
|
logger.info(f"[extractor] {len(frames)}개 프레임 추출 완료")
|
|
return frames
|