"""프레임별 텍스트 + 메타 추출기.
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