wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규 - Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가 - templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신 - tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분) - tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가 - docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서 - scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸 - .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외 미완성 작업의 보존용 스냅샷 커밋 (2026-07-02) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
# pipeline/ — 최종 코드 + 데이터 + 결과 + 보고서
|
||||
|
||||
`tests/matching/` 의 320 개 파일 중 **최종본만 복사**한 정리 폴더.
|
||||
`tests/matching/` 는 원본 그대로 보존 (이 폴더는 복사본).
|
||||
|
||||
상위 문서: [../PIPELINE.md](../PIPELINE.md) — V1~V4 시스템 통합 정리
|
||||
|
||||
## 폴더 구조
|
||||
|
||||
```
|
||||
pipeline/
|
||||
├── code/ — 모든 .py (메인 + 의존 모듈) 30 개
|
||||
├── data/ — 입력 데이터 + Frame DB + 키워드 사전 14 개
|
||||
├── results/ — V1~V4 매칭 결과 yaml 4 개
|
||||
└── reports/ — DECK 1~7 + 보조 보고서 14 개
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## code/ (30 개) — 매칭 시스템 코드
|
||||
|
||||
### 메인 pipeline (V1 ~ V4)
|
||||
|
||||
| 파일 | 단계 | 역할 |
|
||||
|---|---|---|
|
||||
| `pipeline_06_2_mdx_matching.py` | **V1** | 키워드 매칭 — 0.414 핵심 + 0.320 세트 + 0.265 연관 |
|
||||
| `pipeline_08_v2_semantic_rerank.py` | **V2** | 의미 매칭 — ko-sroberta cosine |
|
||||
| `pipeline_08_v3_r5_structure_rerank.py` | **V3** (최종) | 구조 매칭 — 0.40 layout + 0.35 aff + 0.25 intent |
|
||||
| `pipeline_17_v4_full32.py` | **V4** (최종) | 종합 판정 — 32 frame 전체 평가, 5축 가중합 |
|
||||
|
||||
### 사전 작업 (입력 + 키워드 정리)
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `pipeline_01_extract_nodes.py` | Figma + BEPS + MDX 텍스트 추출 (`MDX_SECTIONS`, `MDX_DIR` 정의) |
|
||||
| `pipeline_02_tokenize.py` | Kiwi 형태소 분석 |
|
||||
| `pipeline_02_5_special_forms.py` | 특수 형식 (DX/BIM 등) 보존 |
|
||||
| `pipeline_04_normalize.py` | 표기 통합 + 동의어 처리 |
|
||||
| `pipeline_05_anchor_candidates.py` | 핵심 키워드 후보 추출 |
|
||||
| `pipeline_06_anchor_sets.py` | 키워드 세트 생성 |
|
||||
| `pipeline_07_auto_anchor_candidates.py` | 자동 anchor 후보 |
|
||||
|
||||
### Frame 구조 라벨링 (templates_v2)
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `pipeline_12_generate_templates_v2.py` | r1 — 초기 ontology 생성 (`AFFINITY_KEYWORDS`, `INTENT_KEYWORDS` 정의) |
|
||||
| `pipeline_12_r2_generate_templates_v2.py` | r2 |
|
||||
| `pipeline_12_r3_generate_templates_v2.py` | r3 (최종 키워드 사전) |
|
||||
| `pipeline_12_finalize_v2.py` | final ontology 마무리 |
|
||||
|
||||
### V3 의존 라운드 (r5 가 r4, r3, r2 의존)
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `pipeline_08_v3_r2_structure_rerank.py` | r2 — `MDX_LAYOUT_STRUCTURE` 정의 |
|
||||
| `pipeline_08_v3_r3_structure_rerank.py` | r3 — STRONG 패턴 검증 도입 |
|
||||
| `pipeline_08_v3_r4_structure_rerank.py` | r4 — `detect_mdx_v2_profile_r4`, `v3_r4_score` 정의 |
|
||||
|
||||
### 검증 / Ablation
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `pipeline_15_logistic_regression.py` | V1 가중치 학습 + LOOCV 검증 (4/4) |
|
||||
| `pipeline_15_bm25_with_sets.py` | BM25 비교 (현 방식 우위 확인) |
|
||||
| `pipeline_18_slot_axis_ablation.py` | V4 slot 축 ablation (Top-1 7/7 동일 확인) |
|
||||
|
||||
### 보고서 생성
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `pipeline_16_deck_4pages.py` | DECK 1~7 HTML 생성 |
|
||||
|
||||
### 의존 모듈
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `detect_mdx.py` | MDX 본문 분석 (`detect_mdx_analysis`, `slots_from_table` 등) |
|
||||
| `template_fit.py` | V4 confidence 계산 (`compute_template_fit`, `route`, 5축 가중치) |
|
||||
| `structure_v3.py` | V3 score 함수 (`structural_match_v3`) |
|
||||
| `phase_common.py` | 공통 유틸 (`detect_mdx_layout_v2`, `_COMPAT`, 표기 통합) |
|
||||
| `methods.py` | Kiwi 캐싱, content token 추출, layout 감지 |
|
||||
| `embeddings.py` | ko-sroberta 임베딩 + cosine |
|
||||
| `common.py` | 단순 토크나이저, char_ngrams |
|
||||
| `keyword_normalizer.py` | 키워드 정규화 |
|
||||
|
||||
---
|
||||
|
||||
## data/ (14 개) — 입력 데이터 + Frame DB + 키워드 사전
|
||||
|
||||
### Frame 구조 DB
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `structure_ontology_v2_final_r2.yaml` | **32 Frame 구조 DB** (layout, family, cardinality, content_affinity, structure_intent, slots, alternative_patterns) |
|
||||
|
||||
### 키워드 사전
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `auto_anchor_candidates.yaml` | **키워드 sets** (frame 별 source_text_sets, terms, frame_stats) |
|
||||
| `anchor_sets_input.yaml` | anchor sets 입력 |
|
||||
| `keyword_base.yaml` | 기본 키워드 사전 |
|
||||
| `domain_terms.yaml` | 도메인 용어 |
|
||||
| `synonyms_production.yaml` | 동의어 (운영용) |
|
||||
| `synonyms.yaml` | 동의어 후보 |
|
||||
|
||||
### 정답 / 검증 라벨
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `ground_truth.yaml` | 정답 매칭 라벨 |
|
||||
| `holdout_labels.yaml` | Holdout 평가 라벨 |
|
||||
|
||||
### 중간 데이터 (전처리 결과)
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `text_canonical.yaml` | 표준화된 텍스트 |
|
||||
| `normalized_text_tokens.yaml` | 정규화된 토큰 |
|
||||
| `actual_text_nodes.yaml` | 추출된 텍스트 노드 |
|
||||
| `actual_text_tokens.yaml` | 추출된 토큰 |
|
||||
| `special_forms_inventory.yaml` | 특수 형식 인벤토리 |
|
||||
|
||||
---
|
||||
|
||||
## results/ (4 개) — V1~V4 최종 결과
|
||||
|
||||
| 파일 | 단계 | 핵심 정보 |
|
||||
|---|---|---|
|
||||
| `mdx_matching_result.yaml` | **V1** | per_frame_detail (standalone/keyword_group/related 점수, hit/total), rank_by_matching_score |
|
||||
| `v2_semantic_rerank_result.yaml` | **V2** | v2_rerank (semantic_score, V2 순위) |
|
||||
| `v3_structure_rerank_r5_result.yaml` | **V3** | v3_r5_rerank (total, breakdown — layout_compat/content_affinity/structure_intent), mdx_profile |
|
||||
| `v4_full32_result.yaml` | **V4** | judgments_full32 (32 frame 전체 confidence + label), axes (5축 점수) |
|
||||
|
||||
---
|
||||
|
||||
## reports/ (14 개) — 임원 보고용 + 보조 보고서
|
||||
|
||||
### 임원 보고용 7 페이지
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `DECK_01_TARGET_01_2.html` | TARGET MDX 01-2 × V1~V4 Top-3 매칭 결과 (이미지 + 점수) |
|
||||
| `DECK_02_HOLDOUT_02_2_1.html` | Holdout MDX 02-2.1 × V1~V4 Top-3 |
|
||||
| `DECK_03_METHODS.html` | V1~V4 방법 설명 |
|
||||
| `DECK_04_DB_STRUCTURE.html` | MDX 1 vs Top-3 frame 5 컬럼 비교표 — 키워드/구조/종합 매칭 |
|
||||
| `DECK_05_STRUCTURE_KEYWORD.html` | 구조 매칭 4 항목 상세 (콘텐츠 성격/시각 의도/편집 슬롯/항목수·관계) + 키워드 사전 |
|
||||
| `DECK_06_FIGMA_TO_DB.html` | Frame 18 의 DB 저장 형태 샘플 (YAML) |
|
||||
| `DECK_07_KEYWORD_PIPELINE.html` | 키워드집 정리 과정 + Frame 18 샘플 |
|
||||
|
||||
> ※ DECK 1, 3, 4, 5, 7 은 **사용자가 직접 편집한 (최종) 버전** 복사. matching/ 의 `DECK_XX(최종).html` 이 원본.
|
||||
|
||||
### 보조 보고서
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `KEYWORD_INVENTORY.html` / `.md` | 키워드집 생성 보고서 (1,713 고유 + 13 중요 표기 + 451/1238/24 분포) |
|
||||
| `ATTACH_07_WEIGHT_VALIDATION.html` | V1 가중치 Logistic Regression + LOOCV 검증 |
|
||||
| `V4_SLOT_ABLATION.md` | V4 slot 축 ablation (Top-1 7/7 동일 — 영향 없음) |
|
||||
| `FIGMA_KEYWORDS_REPORT.html` / `.md` | Frame 별 키워드 인벤토리 |
|
||||
| `FRAME_KEYWORD_REVIEW.html` / `.md` | Frame 별 키워드 검토 |
|
||||
|
||||
---
|
||||
|
||||
## 사용 시나리오
|
||||
|
||||
### 새 작업자 (또는 새 Claude) 가 시스템을 처음 보는 경우
|
||||
1. [../PIPELINE.md](../PIPELINE.md) 읽기 — V1~V4 통합 정리 (산식 / 가중치 / 도출 과정 / 결과 / 약점)
|
||||
2. `reports/DECK_03_METHODS.html` 보기 — V1~V4 방법 시각화
|
||||
3. `reports/DECK_04_DB_STRUCTURE.html` 보기 — 실제 매칭 비교 사례
|
||||
4. `code/pipeline_17_v4_full32.py` 읽기 — V4 최종 매칭 로직
|
||||
|
||||
### 매칭 결과 분석
|
||||
- `results/mdx_matching_result.yaml` (V1)
|
||||
- `results/v3_structure_rerank_r5_result.yaml` (V3)
|
||||
- `results/v4_full32_result.yaml` (V4)
|
||||
|
||||
### Frame DB 확인
|
||||
- `data/structure_ontology_v2_final_r2.yaml`
|
||||
- `data/auto_anchor_candidates.yaml`
|
||||
|
||||
### 검증 / Ablation 결과
|
||||
- `reports/ATTACH_07_WEIGHT_VALIDATION.html` (V1 가중치)
|
||||
- `reports/V4_SLOT_ABLATION.md` (slot 축)
|
||||
|
||||
---
|
||||
|
||||
## 주의
|
||||
|
||||
- **이 폴더는 복사본**. `tests/matching/` 가 원본.
|
||||
- 코드 수정 시 두 곳 동기화 필요 (또는 matching/ 만 수정 후 다시 복사).
|
||||
- 코드 실행 시 import 가 같은 디렉토리 가정 (`from detect_mdx import ...`) — `code/` 디렉토리 안에서 실행 필요.
|
||||
@@ -0,0 +1,190 @@
|
||||
"""MDX ↔ Figma frame matching — 공통 유틸리티"""
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
|
||||
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
|
||||
MDX_DIR = ROOT / "samples" / "mdx_batch"
|
||||
GT_PATH = ROOT / "tests" / "matching" / "ground_truth.yaml"
|
||||
|
||||
|
||||
def load_ground_truth():
|
||||
with open(GT_PATH, encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)["sections"]
|
||||
|
||||
|
||||
def _strip_figma_meta(text):
|
||||
"""texts.md의 메타 헤더/구조 라벨 제거 + 타이틀 3회 반복(가중치).
|
||||
- '# Frame 1171281XXX — 텍스트 목록' 파일 최상위 제목 제거
|
||||
- '> ...' blockquote 메타 제거
|
||||
- '## 타이틀', '## 서브헤더', '## 열1', '### 세로 라벨' 구조 라벨 제거
|
||||
- '## 타이틀' 아래 bullet 또는 plain 라인을 3회 반복
|
||||
"""
|
||||
lines = text.split("\n")
|
||||
cleaned = []
|
||||
title_lines = []
|
||||
in_title_section = False
|
||||
for ln in lines:
|
||||
if re.match(r"^#\s+Frame\s+\d+", ln):
|
||||
continue
|
||||
if ln.startswith("> "):
|
||||
continue
|
||||
# ## 타이틀 시작
|
||||
if re.match(r"^##\s+타이틀", ln):
|
||||
in_title_section = True
|
||||
continue
|
||||
# 다른 ## 나 ### 헤더 → 타이틀 구역 종료 (헤더 자체는 제거)
|
||||
if re.match(r"^#{2,}\s", ln):
|
||||
in_title_section = False
|
||||
continue
|
||||
# 타이틀 구역 안의 비어있지 않은 라인은 수집 (bullet이든 plain이든)
|
||||
if in_title_section:
|
||||
s = ln.strip()
|
||||
if s:
|
||||
# 앞의 '- ' 제거해서 깔끔하게
|
||||
clean_s = re.sub(r"^-\s*", "", s)
|
||||
if clean_s:
|
||||
title_lines.append(clean_s)
|
||||
cleaned.append(ln)
|
||||
continue
|
||||
cleaned.append(ln)
|
||||
out = "\n".join(cleaned).strip()
|
||||
if title_lines:
|
||||
boost = "\n".join(title_lines)
|
||||
out = boost + "\n" + boost + "\n" + out
|
||||
return out
|
||||
|
||||
|
||||
def load_figma_texts(clean_meta=True):
|
||||
"""32개 프레임의 texts.md 읽기. {frame_id: text}
|
||||
clean_meta=True: 메타 헤더/라벨 제거 + 타이틀 가중치 적용 (권장)
|
||||
"""
|
||||
out = {}
|
||||
for d in sorted(os.listdir(BLOCKS_DIR)):
|
||||
p = BLOCKS_DIR / d / "texts.md"
|
||||
if p.is_file():
|
||||
with open(p, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
if clean_meta:
|
||||
text = _strip_figma_meta(text)
|
||||
out[d] = text
|
||||
return out
|
||||
|
||||
|
||||
def load_mdx_sections():
|
||||
"""MDX 01~03을 ## 중목차 단위로 분리.
|
||||
{section_id: section_text}. section_id는 GT의 id와 매칭."""
|
||||
sections = {}
|
||||
for fname in ["01.mdx", "02.mdx", "03.mdx"]:
|
||||
mdx_num = fname.replace(".mdx", "")
|
||||
with open(MDX_DIR / fname, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# frontmatter 제거
|
||||
content = re.sub(r"^---.*?---", "", content, flags=re.DOTALL).strip()
|
||||
# import 문 제거
|
||||
content = re.sub(r"^import .*$", "", content, flags=re.MULTILINE)
|
||||
# ## 기준 분리
|
||||
parts = re.split(r"(?=^## )", content, flags=re.MULTILINE)
|
||||
intro_collected = False
|
||||
sec_counter = 0
|
||||
for part in parts:
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
if part.startswith("## "):
|
||||
sec_counter += 1
|
||||
key = f"MDX{mdx_num}-{sec_counter}"
|
||||
else:
|
||||
key = f"MDX{mdx_num}-intro"
|
||||
intro_collected = True
|
||||
sections[key] = part
|
||||
return sections
|
||||
|
||||
|
||||
def tokenize_simple(text):
|
||||
"""단순 공백/특수문자 기준 토큰화."""
|
||||
# HTML/MDX 태그 제거
|
||||
text = re.sub(r"<[^>]+>", " ", text)
|
||||
text = re.sub(r"\{[^}]+\}", " ", text)
|
||||
text = re.sub(r"[|*#>\-\[\](){}:;,.!?/\\=\"'`~]", " ", text)
|
||||
# 2글자 이상 토큰만
|
||||
words = [w for w in text.split() if len(w) >= 2]
|
||||
return words
|
||||
|
||||
|
||||
def clean_text(text):
|
||||
"""기본 정리 (HTML/MDX 제거)."""
|
||||
text = re.sub(r"<[^>]+>", " ", text)
|
||||
text = re.sub(r"\{[^}]+\}", " ", text)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
return text
|
||||
|
||||
|
||||
def char_ngrams(text, n):
|
||||
"""겹치는 n글자 집합. 공백 제거 후."""
|
||||
text = re.sub(r"\s+", "", clean_text(text))
|
||||
return set(text[i:i + n] for i in range(max(0, len(text) - n + 1)))
|
||||
|
||||
|
||||
def evaluate_ranking(gt_entry, ranked_frames):
|
||||
"""한 섹션의 평가.
|
||||
ranked_frames: [(frame_id, score), ...] top부터 내림차순.
|
||||
returns: dict with hit@1, hit@3, mrr
|
||||
"""
|
||||
primary = gt_entry["primary"]
|
||||
secondary = gt_entry.get("secondary", []) or []
|
||||
|
||||
if primary is None:
|
||||
# null 정답: top-1 점수가 낮으면 OK (즉 "없음"을 잘 맞춤)
|
||||
# 여기선 hit@1 = 1 if ranked_frames[0][1] < 0.2 else 0 같은 규칙
|
||||
# 복잡하니 일단 null은 "모든 방법이 약한 점수 내야 성공"으로 단순 처리
|
||||
# → hit 체크에서 제외, 별도 분석용
|
||||
return {"hit@1": None, "hit@3": None, "mrr": None, "gt": None}
|
||||
|
||||
gt_set = {str(primary)} | {str(s) for s in secondary}
|
||||
rank_ids = [str(fid) for fid, _ in ranked_frames]
|
||||
|
||||
hit1 = 1 if rank_ids and rank_ids[0] == str(primary) else 0
|
||||
hit3 = 1 if any(r in gt_set for r in rank_ids[:3]) else 0
|
||||
|
||||
# MRR: primary의 역순위 (secondary는 MRR에서 제외 — 엄격)
|
||||
mrr = 0.0
|
||||
for i, r in enumerate(rank_ids, 1):
|
||||
if r == str(primary):
|
||||
mrr = 1.0 / i
|
||||
break
|
||||
|
||||
return {"hit@1": hit1, "hit@3": hit3, "mrr": mrr, "gt": str(primary)}
|
||||
|
||||
|
||||
def run_method(method_name, method_fn, mdx_sections, figma_texts, gt_list):
|
||||
"""방법을 실행하고 결과 dict 반환.
|
||||
method_fn(mdx_text, figma_texts_dict) -> [(frame_id, score), ...] top-k desc
|
||||
"""
|
||||
gt_by_id = {g["id"]: g for g in gt_list}
|
||||
results = {}
|
||||
for sec_id, sec_text in mdx_sections.items():
|
||||
if sec_id not in gt_by_id:
|
||||
continue
|
||||
ranked = method_fn(sec_text, figma_texts)
|
||||
results[sec_id] = {
|
||||
"ranked": ranked[:5],
|
||||
"eval": evaluate_ranking(gt_by_id[sec_id], ranked),
|
||||
}
|
||||
return {"method": method_name, "results": results}
|
||||
|
||||
|
||||
def aggregate_metrics(method_result):
|
||||
"""hit@1, hit@3, mrr 평균 (null GT 제외)"""
|
||||
vals = {"hit@1": [], "hit@3": [], "mrr": []}
|
||||
for sec_id, r in method_result["results"].items():
|
||||
e = r["eval"]
|
||||
if e["gt"] is None:
|
||||
continue
|
||||
for k in vals:
|
||||
vals[k].append(e[k])
|
||||
return {
|
||||
k: (sum(v) / len(v) if v else 0.0) for k, v in vals.items()
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
"""MDX 분석 — template-fit-v1 매칭용 코드 기반 추출.
|
||||
|
||||
Spec: tests/matching/TEMPLATE_FIT_V1.md §3
|
||||
|
||||
출력:
|
||||
{
|
||||
title, summary, detected_terms,
|
||||
item_count: {detected, source},
|
||||
relation_type: {value, confidence},
|
||||
content_shape: {has_table, has_subsections, has_bullets},
|
||||
slot_candidates: [{label, body}, ...]
|
||||
}
|
||||
|
||||
원칙: LLM 호출 없음. 모든 필드는 규칙 기반 코드로 추출.
|
||||
"""
|
||||
import re
|
||||
from phase_common import normalize_with_synonyms, load_synonyms
|
||||
from methods import _get_kiwi
|
||||
|
||||
# ─── 패턴 ────────────────────────────────────────────
|
||||
RE_TABLE_ROW = re.compile(r'^\|.*\|')
|
||||
RE_TABLE_SEP = re.compile(r'^\|[\s\-:|]+\|')
|
||||
RE_SUBSECTION = re.compile(r'^###\s+(\d+\.\d+(?:\.\d+)?)\s+(.+)')
|
||||
RE_TOP_BULLET = re.compile(r'^[-*]\s+\*\*([^*]+)\*\*')
|
||||
RE_HTML_TAG = re.compile(r'<[^>]+>')
|
||||
|
||||
AXIS_LABEL = '구분' # 표 헤더 중 이 라벨이 있으면 해당 컬럼은 axis 로 제외
|
||||
NOUN_TAGS = {'NNG', 'NNP', 'NR', 'SL', 'SN', 'SH'}
|
||||
|
||||
# relation_type 보정 키워드
|
||||
COMPARE_HINTS = ['과정', '결과', 'Process', 'Product', 'AS-IS', 'TO-BE',
|
||||
'Analogue', 'Digital', 'vs', 'VS', '대비']
|
||||
SEQUENCE_HINTS = ['단계', 'Step', '순서', '차례']
|
||||
|
||||
|
||||
# ═══ 표 파싱 ═══
|
||||
def parse_first_table(text):
|
||||
"""첫 markdown 테이블 반환 (header_cells, body_rows). 없으면 None."""
|
||||
lines = text.split('\n')
|
||||
tbl_start = None
|
||||
for i in range(len(lines) - 1):
|
||||
if RE_TABLE_ROW.match(lines[i].strip()) and RE_TABLE_SEP.match(lines[i+1].strip()):
|
||||
tbl_start = i
|
||||
break
|
||||
if tbl_start is None:
|
||||
return None
|
||||
rows = []
|
||||
for j in range(tbl_start, len(lines)):
|
||||
s = lines[j].strip()
|
||||
if not s.startswith('|'):
|
||||
break
|
||||
rows.append(s)
|
||||
if len(rows) < 2:
|
||||
return None
|
||||
def cells(line):
|
||||
return [c.strip().replace('**', '').strip() for c in line.strip('|').split('|')]
|
||||
header = cells(rows[0])
|
||||
body = [cells(r) for r in rows[2:]]
|
||||
return header, body
|
||||
|
||||
|
||||
def find_subsections(text):
|
||||
"""### X.Y TITLE 라인 목록 → [(number, title), ...]"""
|
||||
out = []
|
||||
for ln in text.split('\n'):
|
||||
m = RE_SUBSECTION.match(ln.strip())
|
||||
if m:
|
||||
out.append((m.group(1), m.group(2).strip()))
|
||||
return out
|
||||
|
||||
|
||||
def find_top_bullets(text):
|
||||
"""최상위 '* **LABEL**' 블릿 목록 → [full_line, ...]"""
|
||||
return [ln for ln in text.split('\n') if RE_TOP_BULLET.match(ln)]
|
||||
|
||||
|
||||
# ═══ 슬롯 후보 추출 ═══
|
||||
def slots_from_table(header, body):
|
||||
"""축(구분) 제외 컬럼 → label. 첫 데이터 행의 해당 cell → body."""
|
||||
axis_idx = next((i for i, h in enumerate(header) if AXIS_LABEL in h), None)
|
||||
first_row = body[0] if body else []
|
||||
result = []
|
||||
for i, h in enumerate(header):
|
||||
if i == axis_idx or not h:
|
||||
continue
|
||||
body_cell = first_row[i] if i < len(first_row) else ''
|
||||
body_clean = RE_HTML_TAG.sub(' ', body_cell).strip()[:120]
|
||||
result.append({'label': h, 'body': body_clean})
|
||||
return result
|
||||
|
||||
|
||||
def slots_from_subsections(subs):
|
||||
"""### TITLE → label (괄호 앞부분), body (전체 타이틀)."""
|
||||
out = []
|
||||
for num, title in subs:
|
||||
main = re.match(r'^([^\(]+)', title)
|
||||
label = main.group(1).strip() if main else title
|
||||
out.append({'label': label, 'body': title})
|
||||
return out
|
||||
|
||||
|
||||
def slots_from_bullets(bullets):
|
||||
out = []
|
||||
for b in bullets:
|
||||
m = RE_TOP_BULLET.match(b)
|
||||
if not m:
|
||||
continue
|
||||
full_label = m.group(1).strip()
|
||||
main = re.match(r'^([^\(]+)', full_label)
|
||||
label = main.group(1).strip() if main else full_label
|
||||
out.append({'label': label, 'body': full_label})
|
||||
return out
|
||||
|
||||
|
||||
# ═══ detected_terms ═══
|
||||
def clean_for_kiwi(text):
|
||||
"""Kiwi 입력 전 HTML/JSX/MDX 노이즈 제거.
|
||||
구조 파싱에는 원본 사용, Kiwi 명사 추출에만 이 함수 사용."""
|
||||
# HTML 태그 (open/close/self-closing)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
# JSX/MDX 속성 블록 {...} (style, eventHandler 등)
|
||||
text = re.sub(r'\{[^}]*\}', ' ', text)
|
||||
# HTML entity
|
||||
text = re.sub(r'&[a-zA-Z]+;', ' ', text)
|
||||
# 단위/수치 노이즈 (10px, 0.9rem 등)
|
||||
text = re.sub(r'\b\d+(?:\.\d+)?(?:px|rem|em|%)\b', ' ', text)
|
||||
# 공백 정규화
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
return text
|
||||
|
||||
|
||||
def extract_detected_terms(normalized_text, anchor_vocab):
|
||||
"""Kiwi 명사/외국어/숫자 + anchor_vocab substring.
|
||||
- HTML/JSX 제거 후 Kiwi noun 추출
|
||||
- anchor_vocab substring 은 원본(normalized) 에 대해 수행 (compound term 커버)
|
||||
"""
|
||||
cleaned = clean_for_kiwi(normalized_text)
|
||||
kiwi = _get_kiwi()
|
||||
tokens = kiwi.tokenize(cleaned)
|
||||
noun_tokens = [t.form for t in tokens
|
||||
if t.tag in NOUN_TAGS and len(t.form) >= 2]
|
||||
|
||||
# anchor_vocab substring (compound terms — Kiwi 가 분해하는 단어 커버)
|
||||
for term in (anchor_vocab or []):
|
||||
if len(term) >= 2 and term in normalized_text:
|
||||
if term not in noun_tokens:
|
||||
noun_tokens.append(term)
|
||||
|
||||
seen = set()
|
||||
out = []
|
||||
for t in noun_tokens:
|
||||
if t not in seen:
|
||||
seen.add(t)
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
|
||||
# ═══ summary ═══
|
||||
def build_summary(title, text, slot_candidates):
|
||||
"""title + 첫 일반 문단(있으면) + 슬롯 라벨 조인."""
|
||||
parts = [title]
|
||||
for ln in text.split('\n'):
|
||||
s = ln.strip()
|
||||
if not s: continue
|
||||
if s.startswith(('|', '#', '<', '*', '-', ':', '{', '```', '---')):
|
||||
continue
|
||||
if re.match(r'^\d+\.\s', s) or re.match(r'^<br', s):
|
||||
continue
|
||||
parts.append(s[:120])
|
||||
break
|
||||
if slot_candidates:
|
||||
parts.append(' / '.join(sc.get('label', '') for sc in slot_candidates[:5]))
|
||||
return '. '.join(p for p in parts if p)
|
||||
|
||||
|
||||
# ═══ relation_type ═══
|
||||
def infer_relation_type(item_count, item_source, text, subsections):
|
||||
# subsection 에서 과정/결과 등 compare hint 있으면 compare
|
||||
if subsections:
|
||||
joined = ' '.join(t for _, t in subsections)
|
||||
if any(kw in joined for kw in COMPARE_HINTS):
|
||||
return ('compare', 'high')
|
||||
if any(kw in joined for kw in SEQUENCE_HINTS):
|
||||
return ('sequence', 'high')
|
||||
|
||||
if item_count == 2:
|
||||
return ('compare', 'high')
|
||||
if item_count >= 3:
|
||||
return ('parallel', 'high')
|
||||
return ('definition', 'low')
|
||||
|
||||
|
||||
# ═══ structure_intent 추론 ═══
|
||||
def infer_structure_intent(text, title, detected_terms, item_count, relation_type):
|
||||
"""MDX 의 structure_intent 추론 (list, 여러 개 가능).
|
||||
|
||||
설계 원칙:
|
||||
- Layer 1 (subject-defined): process_product, transformation, persona, industry 등
|
||||
명확한 subject signal. 이들은 MDX 의 primary intent.
|
||||
- Layer 2 (mutually exclusive): concept_comparison 은 Layer 1 의 특정 intent
|
||||
(process_product/transformation) 와 배타적. 둘 다 설정되면 재구성 모호해짐.
|
||||
- Layer 3 (secondary): multi_attribute / category — 보조 intent.
|
||||
- Polarity (title 기반): requirement / problem 은 **title** 이나 구체 단어로만
|
||||
감지 (body noise 방지).
|
||||
"""
|
||||
intents = []
|
||||
terms_set = set(detected_terms)
|
||||
|
||||
# Layer 1: subject-defined primary intents
|
||||
# process_product: terms_set 또는 text 패턴
|
||||
has_pp_pattern = (
|
||||
('과정' in text and ('혁신' in text or 'Process' in text)) and
|
||||
('결과' in text and ('변화' in text or 'Product' in text))
|
||||
)
|
||||
if ('과정혁신' in terms_set and '결과혁신' in terms_set) or has_pp_pattern:
|
||||
intents.append('process_product_split')
|
||||
if ('AS-IS' in terms_set and 'TO-BE' in terms_set) or \
|
||||
('Analogue' in terms_set and 'Digital' in terms_set):
|
||||
if 'transformation_story' not in intents:
|
||||
intents.append('transformation_story')
|
||||
if {'발주자', '시공자', '설계자'} <= terms_set:
|
||||
intents.append('persona_benefit')
|
||||
if sum(1 for t in ['제조업', '건축', '토목'] if t in terms_set) >= 2:
|
||||
intents.append('industry_comparison')
|
||||
|
||||
# Layer 2: concept_comparison vs category_comparison — exclusive
|
||||
# (subject 가 개념 대조 vs 유형 분류 — 동시에 primary 될 수 없음)
|
||||
concept_fired = False
|
||||
if 'process_product_split' not in intents and 'transformation_story' not in intents:
|
||||
concept_pairs = [
|
||||
({'BIM', 'DX'}, ['BIM과 DX', 'DX와 BIM', 'BIM · DX', 'BIM vs DX', 'BIM/DX']),
|
||||
]
|
||||
for pair, markers in concept_pairs:
|
||||
if pair <= terms_set and any(m in text or m in title for m in markers):
|
||||
intents.append('concept_comparison')
|
||||
concept_fired = True
|
||||
break
|
||||
|
||||
# Layer 3: multi_attribute_comparison (secondary, co-occurring OK)
|
||||
multi_markers = ['관점별', '행별', '기준별']
|
||||
aspect_terms = {'범위', '성과품', '확장성', '수행개념', '수행주체', '프로세스', '활용'}
|
||||
has_multi_marker = any(m in text for m in multi_markers)
|
||||
aspect_hits = len(aspect_terms & terms_set)
|
||||
if (has_multi_marker or aspect_hits >= 3) and relation_type == 'compare':
|
||||
if 'multi_attribute_comparison' not in intents:
|
||||
intents.append('multi_attribute_comparison')
|
||||
|
||||
# Layer 2 (secondary branch): category_comparison — only if concept didn't fire
|
||||
# 명시 markers 필요 (title/text 에 분류 주제 언급)
|
||||
if not concept_fired and 'process_product_split' not in intents and \
|
||||
'transformation_story' not in intents:
|
||||
cat_pair_markers = [
|
||||
({'Package', 'Solution'}, ['Application S/W', 'S/W 구분', 'S/W의 구분', 'Package', 'Solution']),
|
||||
({'상용', '전용'}, ['상용 Engn', '전용 S/W', '3rd Party', '3rdParty']),
|
||||
]
|
||||
for pair, markers in cat_pair_markers:
|
||||
# pair 전체 + title/text 에 명시 marker 존재
|
||||
if pair <= terms_set:
|
||||
# title 에 marker 있으면 강한 신호
|
||||
has_title_marker = any(m in title for m in markers)
|
||||
if has_title_marker:
|
||||
intents.append('category_comparison')
|
||||
break
|
||||
|
||||
# Polarity: requirement_list (title 기반)
|
||||
if any(s in title for s in ['필수', '요건', '3대', '필수조건']) and relation_type == 'parallel':
|
||||
intents.append('requirement_list')
|
||||
|
||||
# Polarity: problem_diagnosis (title or specific problem-only terms)
|
||||
problem_title_markers = ['문제', '문제점', '한계', '왜곡', '실정', '부재']
|
||||
problem_specific = {'개념부재', '전문성부족', '비효율', '전제조건오류', '4대문제'}
|
||||
has_problem_title = any(s in title for s in problem_title_markers)
|
||||
has_problem_specific = bool(problem_specific & terms_set)
|
||||
if has_problem_title or has_problem_specific:
|
||||
intents.append('problem_diagnosis')
|
||||
|
||||
# Fallback: parallel N 이면 일반 requirement_or_pillar
|
||||
if not intents and relation_type == 'parallel' and item_count in (3, 4, 5):
|
||||
intents.append('requirement_or_pillar')
|
||||
|
||||
return intents
|
||||
|
||||
|
||||
# ═══ 엔트리 포인트 ═══
|
||||
def detect_mdx_analysis(text, title, anchor_vocab=None):
|
||||
synonyms = load_synonyms()
|
||||
normalized = normalize_with_synonyms(text, synonyms)
|
||||
|
||||
subs = find_subsections(text)
|
||||
table = parse_first_table(text)
|
||||
bullets = find_top_bullets(text)
|
||||
|
||||
# item_count 우선순위: subsections > table > bullets
|
||||
if subs:
|
||||
item_count = len(subs)
|
||||
item_source = 'subsections'
|
||||
slot_candidates = slots_from_subsections(subs)
|
||||
elif table:
|
||||
header, body = table
|
||||
non_axis = [h for h in header if AXIS_LABEL not in h and h]
|
||||
item_count = len(non_axis)
|
||||
item_source = 'table_columns'
|
||||
slot_candidates = slots_from_table(header, body)
|
||||
elif bullets:
|
||||
item_count = len(bullets)
|
||||
item_source = 'bullets'
|
||||
slot_candidates = slots_from_bullets(bullets)
|
||||
else:
|
||||
item_count = 1
|
||||
item_source = 'none'
|
||||
slot_candidates = []
|
||||
|
||||
rel_value, rel_conf = infer_relation_type(item_count, item_source, text, subs)
|
||||
|
||||
detected_terms = extract_detected_terms(normalized, anchor_vocab)
|
||||
|
||||
summary = build_summary(title, text, slot_candidates)
|
||||
|
||||
# structure_intent 추론 (Phase 24/25 의 구조 축 용)
|
||||
intents = infer_structure_intent(text, title, detected_terms, item_count, rel_value)
|
||||
|
||||
return {
|
||||
'title': title,
|
||||
'summary': summary,
|
||||
'detected_terms': detected_terms,
|
||||
'item_count': {'detected': item_count, 'source': item_source},
|
||||
'relation_type': {'value': rel_value, 'confidence': rel_conf},
|
||||
'content_shape': {
|
||||
'has_table': table is not None,
|
||||
'has_subsections': bool(subs),
|
||||
'has_bullets': bool(bullets),
|
||||
},
|
||||
'slot_candidates': slot_candidates,
|
||||
'structure_intent': intents,
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""ko-sroberta embeddings for template-fit content similarity.
|
||||
|
||||
Model: jhgan/ko-sroberta-multitask (HuggingFace)
|
||||
용도: MDX summary ↔ frame description 코사인 유사도 계산
|
||||
32 frame 규모 — 벡터 DB 없이 numpy 만으로 충분
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
_model = None
|
||||
|
||||
|
||||
def _get_model():
|
||||
global _model
|
||||
if _model is None:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
_model = SentenceTransformer('jhgan/ko-sroberta-multitask')
|
||||
return _model
|
||||
|
||||
|
||||
def embed_texts(texts):
|
||||
"""Batch embed. Returns numpy (N, 768)."""
|
||||
model = _get_model()
|
||||
return model.encode(list(texts), show_progress_bar=False, convert_to_numpy=True)
|
||||
|
||||
|
||||
def cosine(a, b):
|
||||
"""Single-vector cosine similarity."""
|
||||
an = a / (np.linalg.norm(a) + 1e-12)
|
||||
bn = b / (np.linalg.norm(b) + 1e-12)
|
||||
return float(np.dot(an, bn))
|
||||
|
||||
|
||||
def embed_map(id_to_text):
|
||||
"""{id: text} → {id: vec(768,)}"""
|
||||
ids = list(id_to_text.keys())
|
||||
vecs = embed_texts([id_to_text[i] for i in ids])
|
||||
return {ids[i]: vecs[i] for i in range(len(ids))}
|
||||
@@ -0,0 +1,297 @@
|
||||
"""공통 키워드 정규화 모듈.
|
||||
|
||||
Step 4, 5, 7 등 여러 파이프라인이 공유하는 정규화 로직을 한 파일로 집중.
|
||||
이 모듈은 **AI 호출을 수행하지 않음** — 순수 결정론적 규칙만.
|
||||
|
||||
제공 기능:
|
||||
- PRE_COLLAPSE regex (디지털 전환(DX) 계열 collapse)
|
||||
- 괄호 확장 (A(B)C → AC + B)
|
||||
- phrase_variants 치환
|
||||
- Kiwi user_dict 토큰화
|
||||
- 허용 태그 필터 (NNG/NNP/SL/SN + 1글자/순수숫자 제외)
|
||||
|
||||
사용 pipeline:
|
||||
- pipeline_04_normalize.py (정규화 + 저장)
|
||||
- pipeline_07_auto_anchor_candidates.py (source_text 기반 후보)
|
||||
(미래) 기타 step
|
||||
|
||||
설계 제약:
|
||||
- 상수/함수는 재진입 가능해야 함 (counter 를 None 허용)
|
||||
- Kiwi 객체는 build_kiwi() 로 생성 (global state 금지)
|
||||
- 모든 함수는 deterministic (random seed 없음)
|
||||
"""
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from kiwipiepy import Kiwi
|
||||
|
||||
# ============================================================
|
||||
# 상수
|
||||
# ============================================================
|
||||
|
||||
# Kiwi user_dict — compound (SL)
|
||||
USER_DICT_SL = ['S/W', 'H/W', '2D', '3D', 'DX', 'BIM', 'As-is', 'To-Be']
|
||||
# Kiwi user_dict — phrase canonical (NNG)
|
||||
USER_DICT_NNG = ['결과혁신', '과정혁신', '필수조건', '의사소통', '시행착오']
|
||||
|
||||
# Kiwi 허용 태그
|
||||
ALLOWED_TAGS = {'NNG', 'NNP', 'SL', 'SN'}
|
||||
|
||||
# PRE_COLLAPSE: "디지털 전환(DX...)" / "DX(디지털전환)" / "DX(Digital Transformation)" / "DX(DX...)" → DX
|
||||
# 순서 중요: 긴 → 짧은, 원본 → 치환 부산물.
|
||||
PRE_COLLAPSE = [
|
||||
(
|
||||
re.compile(
|
||||
r'디지털\s*전환\s*\(\s*DX(?:\s*[,/]\s*Digital\s+Transformation)?\s*\)',
|
||||
re.IGNORECASE,
|
||||
),
|
||||
'DX',
|
||||
'digital_transition_with_dx',
|
||||
),
|
||||
(
|
||||
re.compile(r'DX\s*\(\s*디지털\s*전환\s*\)', re.IGNORECASE),
|
||||
'DX',
|
||||
'dx_with_digital_transition',
|
||||
),
|
||||
(
|
||||
re.compile(r'DX\s*\(\s*Digital\s+Transformation\s*\)', re.IGNORECASE),
|
||||
'DX',
|
||||
'dx_with_english_fullname',
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
r'DX\s*\(\s*DX(?:\s*[,/]\s*Digital\s+Transformation)?\s*\)',
|
||||
re.IGNORECASE,
|
||||
),
|
||||
'DX',
|
||||
'dx_duplicate',
|
||||
),
|
||||
# 5. SW (단독 word) → S/W — 정규화 누락 보정 (word boundary 중요: SWOT 등 보호)
|
||||
(
|
||||
re.compile(r'(?<![A-Za-z])SW(?![A-Za-z])'),
|
||||
'S/W',
|
||||
'sw_to_sw_slash',
|
||||
),
|
||||
]
|
||||
|
||||
# 괄호 확장
|
||||
MAX_PAREN_LEN = 40
|
||||
PAREN_PATTERN = re.compile(r'\(([^()]{1,' + str(MAX_PAREN_LEN) + r'})\)')
|
||||
# 숫자/기호-only 괄호 내용 배제 (예: (1), (2), (※))
|
||||
PAREN_SKIP_PATTERN = re.compile(r'[\d\s\-.,:;※]+')
|
||||
PAREN_SAMPLE_MAX = 20
|
||||
|
||||
# ============================================================
|
||||
# 필터 규칙 (사용자 확정 — 제거 후보 3그룹 + 제품 whitelist)
|
||||
# ============================================================
|
||||
|
||||
# 의문사 (대소문자 무관 제거)
|
||||
INTERROGATIVES = {
|
||||
'how', 'what', 'when', 'why', 'who', 'where', 'which',
|
||||
}
|
||||
|
||||
# 완전 소문자 일반 기능어 제거
|
||||
SOFT_STOPWORDS = {
|
||||
'or', 'for', 'and', 'the', 'of', 'to', 'at', 'by', 'in', 'on', 'up',
|
||||
'as', 'a', 'an', 'with', 'from', 'into',
|
||||
'is', 'be', 'are', 'was', 'were', 'been', 'being',
|
||||
'it', 'we', 'he', 'she', 'they', 'i', 'you',
|
||||
'also', 'only', 'all', 'but', 'not', 'so', 'too', 'very',
|
||||
'this', 'that', 'these', 'those', 'etc',
|
||||
}
|
||||
|
||||
FILTER_NUMBER_COMMA_PATTERN = re.compile(r'^[\d,]+$')
|
||||
FILTER_SHORT_LOWERCASE_PATTERN = re.compile(r'^[a-z]{1,3}$')
|
||||
FILTER_UPPER_ABBREV_PATTERN = re.compile(r'^[A-Z][A-Z0-9]+$')
|
||||
|
||||
# 제품명/고유명사 수동 whitelist (사용자 승인, 자동 규칙 대신 수동)
|
||||
PRODUCT_WHITELIST = {
|
||||
# 상용 S/W 제품명
|
||||
'Revit', 'AutoCAD', 'Autocad', 'AutoCad', 'Auto CAD',
|
||||
'SketchUp', 'Sketchup',
|
||||
'Navisworks', 'Naviswork',
|
||||
'Infraworks',
|
||||
'Rhino',
|
||||
'ArchiCAD',
|
||||
'ArcGIS', 'QGIS',
|
||||
'Blender',
|
||||
'WatchBIM',
|
||||
'Domainer',
|
||||
'BCMF',
|
||||
# 회사/벤더명
|
||||
'Bentley', 'Bently',
|
||||
'Autodesk', 'AutoDesk',
|
||||
'Nemetschek',
|
||||
'ESRI',
|
||||
# 국제기구 약어 (대문자 약어 규칙으로 자동 보호되지만 안전하게 명시)
|
||||
'ADB', 'IBRD',
|
||||
}
|
||||
|
||||
# 화이트리스트 (user_dict + 제품명) — 필터 규칙에 걸려도 절대 제거 안 함
|
||||
FILTER_WHITELIST = set(USER_DICT_SL) | set(USER_DICT_NNG) | PRODUCT_WHITELIST
|
||||
|
||||
|
||||
def should_filter_token(token):
|
||||
"""필터 규칙 판정. (제거_여부, 이유) 반환.
|
||||
|
||||
우선순위:
|
||||
0. 화이트리스트 → 절대 제거 안 함
|
||||
1. 전부 대문자 약어 (IT/AI/ERP 등) → 보존
|
||||
2. 의문사 (대소문자 무관) → 제거
|
||||
3. 숫자+쉼표 → 제거
|
||||
4. 완전 소문자 stopword → 제거
|
||||
5. 소문자 1-3자 영어 → 제거
|
||||
"""
|
||||
# 0. 화이트리스트 우선
|
||||
if token in FILTER_WHITELIST:
|
||||
return False, None
|
||||
# 1. 의문사 (대소문자 무관) — 대문자 약어 검사보다 먼저 (HOW/WHEN 등)
|
||||
if token.lower() in INTERROGATIVES:
|
||||
return True, 'interrogative'
|
||||
# 2. 대문자 약어 보존 (IT, AI, ERP, CCTV 등)
|
||||
if FILTER_UPPER_ABBREV_PATTERN.fullmatch(token):
|
||||
return False, None
|
||||
# 3. 숫자+쉼표 패턴
|
||||
if FILTER_NUMBER_COMMA_PATTERN.fullmatch(token):
|
||||
return True, 'number_pattern'
|
||||
# 4. 완전 소문자 stopword
|
||||
if token in SOFT_STOPWORDS:
|
||||
return True, 'stopword'
|
||||
# 5. 소문자 1-3자 영어
|
||||
if FILTER_SHORT_LOWERCASE_PATTERN.fullmatch(token):
|
||||
return True, 'short_lowercase'
|
||||
return False, None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 함수
|
||||
# ============================================================
|
||||
|
||||
def load_phrase_variants(synonyms_path):
|
||||
"""synonyms.yaml 에서 phrase_variants 섹션 로드."""
|
||||
data = yaml.safe_load(Path(synonyms_path).read_text(encoding='utf-8'))
|
||||
pv = data.get('phrase_variants')
|
||||
if not pv:
|
||||
raise RuntimeError(f"{synonyms_path} 에 phrase_variants 섹션이 없음.")
|
||||
return pv
|
||||
|
||||
|
||||
def build_substitutions(phrase_variants):
|
||||
"""(variant, canonical) 리스트. canonical 자기 자신 재치환 금지. 긴 variant 먼저."""
|
||||
subs = []
|
||||
seen = set()
|
||||
for canonical, variants in phrase_variants.items():
|
||||
for v in (variants or []):
|
||||
if v == canonical:
|
||||
continue
|
||||
if (v, canonical) in seen:
|
||||
continue
|
||||
seen.add((v, canonical))
|
||||
subs.append((v, canonical))
|
||||
subs.sort(key=lambda x: (-len(x[0]), x[0]))
|
||||
return subs
|
||||
|
||||
|
||||
def expand_parentheses(text, paren_counter=None, paren_samples=None):
|
||||
"""A(B)C → 'AC B' 로 펼침.
|
||||
|
||||
- 괄호 밖: 빈 문자열로 제거 (조사 결합 유지)
|
||||
- 괄호 안: 뒤에 공백 연결
|
||||
- 길이 > MAX_PAREN_LEN 은 펼치지 않음
|
||||
- 숫자/기호-only 괄호 내용 제외 ((1), (※) 등)
|
||||
|
||||
paren_counter / paren_samples 는 None 허용 — 카운팅 안 할 때는 None.
|
||||
"""
|
||||
contents = PAREN_PATTERN.findall(text)
|
||||
if not contents:
|
||||
return text
|
||||
meaningful = []
|
||||
for c in contents:
|
||||
stripped = c.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if PAREN_SKIP_PATTERN.fullmatch(stripped):
|
||||
continue
|
||||
meaningful.append(stripped)
|
||||
if not meaningful:
|
||||
return text
|
||||
before = text
|
||||
text_out = PAREN_PATTERN.sub('', text)
|
||||
text_out = re.sub(r'\s+', ' ', text_out).strip()
|
||||
for c in meaningful:
|
||||
text_out = text_out + ' ' + c
|
||||
if paren_counter is not None:
|
||||
paren_counter['expansions'] += 1
|
||||
if paren_samples is not None and before != text_out and len(paren_samples) < PAREN_SAMPLE_MAX:
|
||||
paren_samples.append({'before': before, 'after': text_out})
|
||||
return text_out
|
||||
|
||||
|
||||
def apply_substitutions(text, subs, replacement_counter, pre_collapse_counter,
|
||||
paren_counter=None, paren_samples=None):
|
||||
"""PRE_COLLAPSE → 괄호 확장 → phrase_variants 순서 적용."""
|
||||
# 1) PRE_COLLAPSE (regex)
|
||||
for pattern, replacement, rule_id in PRE_COLLAPSE:
|
||||
new_text, n = pattern.subn(replacement, text)
|
||||
if n > 0:
|
||||
if pre_collapse_counter is not None:
|
||||
pre_collapse_counter[rule_id] += n
|
||||
text = new_text
|
||||
# 1.5) 괄호 확장
|
||||
text = expand_parentheses(text, paren_counter, paren_samples)
|
||||
# 2) phrase_variants (literal)
|
||||
for variant, canonical in subs:
|
||||
new_text, n = re.subn(re.escape(variant), canonical, text)
|
||||
if n > 0:
|
||||
if replacement_counter is not None:
|
||||
replacement_counter[canonical] += n
|
||||
text = new_text
|
||||
return text
|
||||
|
||||
|
||||
def build_kiwi():
|
||||
"""user_dict 포함 Kiwi 객체 생성."""
|
||||
kiwi = Kiwi()
|
||||
for w in USER_DICT_SL:
|
||||
kiwi.add_user_word(w, 'SL', 9.0)
|
||||
for w in USER_DICT_NNG:
|
||||
kiwi.add_user_word(w, 'NNG', 9.0)
|
||||
return kiwi
|
||||
|
||||
|
||||
def extract_tokens(kiwi, text, removal_log=None):
|
||||
"""Kiwi tokenize + 허용 태그/1글자/순수숫자 필터 + 사용자 확정 필터 규칙.
|
||||
|
||||
removal_log: dict[str, list[str]] 또는 None — 제거된 토큰 + 이유 기록 (추적용).
|
||||
예: {'interrogative': ['How', 'WHEN'], 'stopword': ['the', 'to'], ...}
|
||||
"""
|
||||
out = []
|
||||
for tok in kiwi.tokenize(text):
|
||||
if tok.tag not in ALLOWED_TAGS:
|
||||
continue
|
||||
f = tok.form
|
||||
if len(f) < 2:
|
||||
continue
|
||||
if re.fullmatch(r'\d+', f):
|
||||
continue
|
||||
# 사용자 확정 필터 (stopword / number / short_lowercase / interrogative)
|
||||
remove, reason = should_filter_token(f)
|
||||
if remove:
|
||||
if removal_log is not None:
|
||||
removal_log.setdefault(reason, []).append(f)
|
||||
continue
|
||||
out.append(f)
|
||||
return out
|
||||
|
||||
|
||||
def normalize_and_tokenize(text, subs, kiwi,
|
||||
replacement_counter=None, pre_collapse_counter=None,
|
||||
paren_counter=None, paren_samples=None):
|
||||
"""text → (normalized_text, tokens). 복합 파이프라인 단일 진입점."""
|
||||
normalized = apply_substitutions(
|
||||
text, subs, replacement_counter, pre_collapse_counter,
|
||||
paren_counter, paren_samples,
|
||||
)
|
||||
tokens = extract_tokens(kiwi, normalized)
|
||||
return normalized, tokens
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,326 @@
|
||||
"""Phase 18~22 공통 모듈
|
||||
- analysis.md 로드
|
||||
- 동의어 사전 로드 + normalize
|
||||
- 키워드 추출 / df 분류
|
||||
- 점수 계산 (옵션별 축 포함/제외)
|
||||
"""
|
||||
import re
|
||||
import math
|
||||
import collections
|
||||
import json
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
from methods import _get_kiwi, _extract_content_tokens, _detect_mdx_layout, _get_cross_encoder
|
||||
|
||||
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
|
||||
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
|
||||
PREVIEW_DIR = ROOT / "data" / "figma_previews"
|
||||
|
||||
|
||||
# ═══ 4개 테스트 유닛 ═══
|
||||
TARGET_UNITS = [
|
||||
("MDX01-2-details", "1. (MDX 1) 팝업 — DX와 BIM의 구분", "18", "01.mdx", "2. 용어간 상호관계", None),
|
||||
("MDX02-2.2-table", "2. (MDX 2) 2.2 DX 시행 주체별 기대효과", "14", "02.mdx", "2. DX 기반 Process 혁신에 따른 주체별 기대효과", "2.2 DX 시행 주체별 기대효과"),
|
||||
("MDX03-1", "3. (MDX 03) 1. DX 시행을 위한 필수요건", "13", "03.mdx", "1. DX 시행을 위한 필수 요건", None),
|
||||
("MDX03-2", "4. (MDX 03) 2. Process 혁신과 Product 변화", "29", "03.mdx", "2. Process의 혁신과 Product의 변화", None),
|
||||
]
|
||||
|
||||
|
||||
def load_synonyms():
|
||||
p = Path(__file__).parent / "synonyms.yaml"
|
||||
with open(p, encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)["synonyms"]
|
||||
|
||||
|
||||
def normalize_with_synonyms(text, synonyms):
|
||||
"""variants를 canonical 표기로 치환. 긴 variant 먼저."""
|
||||
replacements = []
|
||||
for canonical, variants in synonyms.items():
|
||||
for v in variants:
|
||||
replacements.append((len(v), v, canonical))
|
||||
replacements.sort(reverse=True) # 긴 것부터
|
||||
for _, variant, canonical in replacements:
|
||||
text = text.replace(variant, canonical)
|
||||
return text
|
||||
|
||||
|
||||
def load_keyword_base():
|
||||
"""keyword_base.yaml 로드. normalize=false 는 MDX 치환 대상에서 제외.
|
||||
반환: {canonical: [variants]} - synonyms.yaml 구조와 호환."""
|
||||
p = Path(__file__).parent / "keyword_base.yaml"
|
||||
if not p.exists():
|
||||
return {}
|
||||
with open(p, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
out = {}
|
||||
for canonical, entry in data.get('keywords', {}).items():
|
||||
if entry.get('normalize', True) is False:
|
||||
continue # anchor-only, MDX 치환 비활성
|
||||
variants = entry.get('variants', [])
|
||||
if variants:
|
||||
out[canonical] = variants
|
||||
# variants 없으면 치환할 게 없으니 dict 에 추가 안 함
|
||||
return out
|
||||
|
||||
|
||||
def normalize_with_keyword_base(text, kb):
|
||||
"""keyword_base 로 MDX 정규화. synonyms 와 동일 로직."""
|
||||
return normalize_with_synonyms(text, kb)
|
||||
|
||||
|
||||
def parse_analysis_md(path):
|
||||
"""analysis.md 파싱. Legacy ('구조', '내용') + Milestone 2 mirror ('구조 매칭 정보', '내용 설명') 둘 다 지원."""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
result = {"keywords": [], "layout": "", "detail": "", "content": ""}
|
||||
sections = re.split(r"\n## ", text)
|
||||
for sec in sections[1:]:
|
||||
lines = sec.split("\n", 1)
|
||||
heading = lines[0].strip()
|
||||
body = lines[1].strip() if len(lines) > 1 else ""
|
||||
if heading in ("구조", "구조 매칭 정보"):
|
||||
for ln in body.split("\n"):
|
||||
m = re.match(r"- \*\*layout\*\*:\s*(.+)", ln)
|
||||
if m: result["layout"] = m.group(1).strip()
|
||||
m = re.match(r"- \*\*detail\*\*:\s*(.+)", ln)
|
||||
if m: result["detail"] = m.group(1).strip()
|
||||
elif heading in ("내용", "내용 설명"):
|
||||
result["content"] = body.split("\n\n")[0].strip()
|
||||
elif heading == "후보 키워드":
|
||||
raw = body.split("\n\n")[0]
|
||||
result["keywords"] = [k.strip() for k in raw.split(",") if k.strip()]
|
||||
return result
|
||||
|
||||
|
||||
def load_32_frames():
|
||||
frames = {}
|
||||
for d in sorted(BLOCKS_DIR.iterdir()):
|
||||
if not d.is_dir(): continue
|
||||
fid = d.name
|
||||
if not fid.startswith("1171"): continue
|
||||
a = d / "analysis.md"
|
||||
if a.exists():
|
||||
frames[fid] = parse_analysis_md(a)
|
||||
return frames
|
||||
|
||||
|
||||
def compute_df_idf_tier(frames):
|
||||
df = collections.Counter()
|
||||
N = len(frames)
|
||||
for v in frames.values():
|
||||
for kw in set(v["keywords"]):
|
||||
df[kw] += 1
|
||||
tier = {}
|
||||
for kw, cnt in df.items():
|
||||
ratio = cnt / N
|
||||
if ratio <= 0.10: tier[kw] = "core"
|
||||
elif ratio >= 0.30: tier[kw] = "general"
|
||||
else: tier[kw] = "mid"
|
||||
idf = {kw: math.log(N / cnt) if cnt > 0 else 0 for kw, cnt in df.items()}
|
||||
return df, idf, tier, N
|
||||
|
||||
|
||||
def extract_mdx_keywords(mdx_text, vocabulary, synonyms=None, keyword_base=None):
|
||||
"""MDX 텍스트 → 정규화 → direct canonical hit + Kiwi token hit → union.
|
||||
|
||||
보강 이유: compound canonical (설계Data, 공사비절감, 3D모델 등) 은
|
||||
Kiwi 가 분해하여 원형이 tokens 에 없음. vocabulary 에서 직접 substring 매칭
|
||||
으로 별도 검사 → Kiwi hit 와 union.
|
||||
|
||||
Args:
|
||||
mdx_text: 원본 MDX 텍스트
|
||||
vocabulary: Figma frame 의 canonical keyword set (비교 대상)
|
||||
synonyms: legacy synonyms.yaml 사전 (하위호환)
|
||||
keyword_base: keyword_base.yaml 로드 결과 (우선 적용)
|
||||
"""
|
||||
if keyword_base is not None:
|
||||
mdx_text = normalize_with_keyword_base(mdx_text, keyword_base)
|
||||
elif synonyms is not None:
|
||||
mdx_text = normalize_with_synonyms(mdx_text, synonyms)
|
||||
|
||||
# ① direct canonical hit (compound 보호)
|
||||
direct = {t for t in vocabulary if len(t) >= 2 and t in mdx_text}
|
||||
|
||||
# ② Kiwi token hit
|
||||
kiwi = _get_kiwi()
|
||||
tokens = _extract_content_tokens(mdx_text, kiwi)
|
||||
kiwi_hit = set(tokens) & vocabulary
|
||||
|
||||
return direct | kiwi_hit
|
||||
|
||||
|
||||
def keyword_score(mdx_kws, fig_kws, idf, tier):
|
||||
"""IDF × tier 가중 Jaccard"""
|
||||
tier_weight = {"core": 1.0, "mid": 0.5, "general": 0.1}
|
||||
inter = mdx_kws & fig_kws
|
||||
union = mdx_kws | fig_kws
|
||||
num = sum(idf.get(c, 0.5) * tier_weight.get(tier.get(c, "mid"), 0.5) for c in inter)
|
||||
den = sum(idf.get(c, 0.5) * tier_weight.get(tier.get(c, "mid"), 0.5) for c in union)
|
||||
return num / den if den > 0 else 0, inter
|
||||
|
||||
|
||||
def content_scores_batch(mdx_content, frames):
|
||||
"""Cross-encoder 일괄 채점 → {fid: [0,1]}"""
|
||||
model = _get_cross_encoder()
|
||||
fids = list(frames.keys())
|
||||
pairs = [[mdx_content, frames[fid]["content"]] for fid in fids]
|
||||
raw = model.predict(pairs, show_progress_bar=False)
|
||||
return {fids[i]: 1 / (1 + math.exp(-float(raw[i]))) for i in range(len(fids))}
|
||||
|
||||
|
||||
# ═══ 구조 매칭 v3 — 우선순위 수정 + 호환도 점수(graded) ═══
|
||||
def detect_mdx_layout_v2(text):
|
||||
"""MDX 본문 정밀 구조 감지.
|
||||
우선순위: ### 서브섹션 > 표 > 블릿 (이전엔 표 먼저였음)"""
|
||||
lines = text.split("\n")
|
||||
|
||||
# 1. ### 서브섹션 우선 (### X.Y 패턴)
|
||||
subs = [ln for ln in lines if re.match(r"^###\s+\d+\.\d+", ln)]
|
||||
if len(subs) == 2:
|
||||
sub_text = " ".join(subs).lower()
|
||||
if any(kw in sub_text for kw in ["과정", "결과", "process", "product", "as-is", "to-be"]):
|
||||
return "compare-2banner"
|
||||
return "compare-2col"
|
||||
if len(subs) >= 3:
|
||||
return "multi-section"
|
||||
|
||||
# 2. 표 감지 (서브섹션 없을 때)
|
||||
table_header = None
|
||||
for ln in lines:
|
||||
stripped = ln.strip()
|
||||
if re.match(r"^\|.*\|.*\|", stripped) and not re.match(r"^\|[\s\-:]+\|", stripped):
|
||||
table_header = stripped
|
||||
break
|
||||
if table_header:
|
||||
cols = [c.strip().replace("*", "").lower() for c in table_header.strip("|").split("|") if c.strip()]
|
||||
col_text = " ".join(cols)
|
||||
if any(kw in col_text for kw in ["발주자", "시공자", "설계자"]):
|
||||
return "persona-3col"
|
||||
if any(kw in col_text for kw in ["제조업", "건축", "토목"]) and "토목" in col_text:
|
||||
return "table-3col"
|
||||
if any(kw in col_text for kw in ["bim", "dx"]) and ("bim" in col_text and "dx" in col_text):
|
||||
return "compare-rows"
|
||||
n_cols = len(cols) - 1
|
||||
if n_cols == 2: return "table-2col"
|
||||
if n_cols >= 3: return "table-3col"
|
||||
return "compare-rows"
|
||||
|
||||
# 3. 최상위 볼드 블릿
|
||||
top_bullets = [ln for ln in lines if re.match(r"^[-*]\s+\*\*", ln)]
|
||||
n = len(top_bullets)
|
||||
if n == 3: return "3col-parallel"
|
||||
if n == 2: return "compare-2col"
|
||||
if n == 4: return "cards-4"
|
||||
if n >= 5: return "multi-parallel"
|
||||
return "single-column"
|
||||
|
||||
|
||||
# ═══ 호환도 매트릭스 (compatibility) ═══
|
||||
# MDX 구조 × Figma layout = 0.0~1.0
|
||||
# "이 MDX 내용을 이 Figma 레이아웃으로 렌더링 시 얼마나 적합한가"
|
||||
_COMPAT = {
|
||||
# 2-column 계열 (서로 호환성 높음)
|
||||
"compare-2col": {
|
||||
"compare-2col": 1.0, "compare-2banner-top-2col-bottom": 0.9,
|
||||
"table-2col": 0.9, "2col-paired": 0.85, "2-boxes": 0.75,
|
||||
"central-split": 0.7, "paired-rows": 0.6,
|
||||
"compare-rows": 0.55, "table-3col": 0.3, "persona-3col": 0.3,
|
||||
"3col-parallel": 0.3, "single-column": 0.5,
|
||||
},
|
||||
"compare-2banner": {
|
||||
"compare-2banner-top-2col-bottom": 1.0, "compare-2col": 0.9,
|
||||
"table-2col": 0.85, "2col-paired": 0.8, "2-boxes": 0.7,
|
||||
"paired-rows": 0.7, "central-split": 0.7,
|
||||
"compare-rows": 0.5, "table-3col": 0.3, "persona-3col": 0.3,
|
||||
"3col-parallel": 0.3, "single-column": 0.5,
|
||||
},
|
||||
"table-2col": {
|
||||
"table-2col": 1.0, "compare-2col": 0.9,
|
||||
"compare-2banner-top-2col-bottom": 0.85,
|
||||
"compare-rows": 0.75, "2col-paired": 0.75,
|
||||
"table-3col": 0.5, "persona-3col": 0.4, "3col-parallel": 0.3,
|
||||
"single-column": 0.5,
|
||||
},
|
||||
|
||||
# 3-column 계열
|
||||
"persona-3col": {
|
||||
"persona-3col": 1.0, "3col-parallel": 0.75,
|
||||
"3col-cards": 0.75, "table-3col": 0.7, "3col-compare": 0.65,
|
||||
"cards-4plus5": 0.4, "compare-rows": 0.35,
|
||||
"compare-2col": 0.3, "table-2col": 0.4, "single-column": 0.5,
|
||||
},
|
||||
"3col-parallel": {
|
||||
"3col-parallel": 1.0, "3col-cards": 0.9, "3col-compare": 0.9,
|
||||
"persona-3col": 0.75, "table-3col": 0.6,
|
||||
"3-emphasis": 0.6, "3-category": 0.65, "3-section": 0.65,
|
||||
"cards-4": 0.5, "cards-4plus5": 0.4,
|
||||
"compare-2col": 0.3, "single-column": 0.5,
|
||||
},
|
||||
"table-3col": {
|
||||
"table-3col": 1.0, "3col-parallel": 0.65,
|
||||
"3col-cards": 0.65, "persona-3col": 0.7,
|
||||
"compare-rows": 0.7, "3col-compare": 0.75,
|
||||
"table-2col": 0.5, "single-column": 0.45,
|
||||
},
|
||||
"compare-rows": {
|
||||
"compare-rows": 1.0, "table-2col": 0.75,
|
||||
"table-3col": 0.7, "paired-rows": 0.75,
|
||||
"compare-2col": 0.55, "compare-2banner-top-2col-bottom": 0.5,
|
||||
"2col-paired": 0.5, "single-column": 0.45,
|
||||
},
|
||||
|
||||
# 4+ / 다중
|
||||
"cards-4": {
|
||||
"cards-4": 1.0, "cards-4plus5": 0.9,
|
||||
"policy-4card-plus-list": 0.85, "quadrant-issues": 0.85,
|
||||
"3col-parallel": 0.5, "table-3col": 0.4, "single-column": 0.4,
|
||||
},
|
||||
"multi-parallel": {
|
||||
"cards-4": 0.85, "cards-4plus5": 0.9,
|
||||
"policy-4card-plus-list": 0.8, "quadrant-issues": 0.75,
|
||||
"3col-parallel": 0.55, "persona-3col": 0.45,
|
||||
"table-3col": 0.4, "single-column": 0.4,
|
||||
},
|
||||
"multi-section": {
|
||||
"3-section": 1.0, "3-category": 0.9, "3-emphasis": 0.85,
|
||||
"3col-parallel": 0.6, "cards-4": 0.5, "single-column": 0.5,
|
||||
},
|
||||
|
||||
# 단일
|
||||
"single-column": {
|
||||
"bullet-cards": 0.85, "list-numbered": 0.85, "list-stacked": 0.85,
|
||||
"side-card": 0.75, "split-panel-diagram": 0.65,
|
||||
"split-panel-numbered": 0.65, "central-split": 0.55,
|
||||
"diagram-labels": 0.55, "diagram-5": 0.55, "central-5-goals": 0.5,
|
||||
"circular-nodes": 0.5, "cycle-3way": 0.5,
|
||||
"intro": 0.6, "definition-list": 0.7,
|
||||
"compare-2col": 0.5, "compare-rows": 0.5, "3col-parallel": 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def structural_match_v2(mdx_layout, fig_layout):
|
||||
"""구조 호환도 매칭 (0~1 graded). 정확일치=1.0, 유사=0.5~0.9, 무관=0.1~0.3."""
|
||||
if not mdx_layout or not fig_layout: return 0.0
|
||||
if mdx_layout == fig_layout: return 1.0
|
||||
row = _COMPAT.get(mdx_layout, {})
|
||||
return row.get(fig_layout, 0.15) # 명시 안 됐으면 매우 약한 기본 호환도
|
||||
|
||||
|
||||
# ═══ 유닛 로드 ═══
|
||||
def load_target_units():
|
||||
"""4개 타겟 유닛의 MDX 본문 + 제목 hierarchy 반환"""
|
||||
from extract_units import extract_units
|
||||
from phase10 import extract_titles_only_mdx
|
||||
units_full = extract_units()
|
||||
units_title = {}
|
||||
for uid, _, _, fname, mid, sub in TARGET_UNITS:
|
||||
units_title[uid] = extract_titles_only_mdx(fname, mid, sub)
|
||||
return units_full, units_title
|
||||
|
||||
|
||||
def load_frame_index():
|
||||
with open(PREVIEW_DIR / "index.json", encoding="utf-8") as f:
|
||||
idx_data = json.load(f)
|
||||
frame_to_short = {info["frame_id"]: sid for sid, info in idx_data.items()}
|
||||
return idx_data, frame_to_short
|
||||
@@ -0,0 +1,433 @@
|
||||
"""Step 1: texts.md / MDX 에서 실제 text node 만 추출.
|
||||
|
||||
원칙:
|
||||
- Figma texts.md: `- xxx` list item 만 포함 (heading/blockquote 제외)
|
||||
- MDX: heading + 일반 문단 + list item + table cell 포함
|
||||
(frontmatter / code block / :::note / standalone HTML tag 제외)
|
||||
- HTML tag / markdown bold / markdown italic 제거, HTML entity unescape
|
||||
- 단일 기호 / 1자 / 순수 숫자 제외 (30%, 2D, 3D 등은 보존)
|
||||
- 각 프레임별 + 전체 corpus 카운트
|
||||
- text_nodes 리스트는 중복 제거
|
||||
|
||||
산출: actual_text_nodes.yaml
|
||||
"""
|
||||
import html as html_lib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
|
||||
BLOCKS_DIR = ROOT / "figma_to_html_agent" / "blocks"
|
||||
MDX_DIR = ROOT / "samples" / "mdx_batch"
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
BEPS_ID = "1171281171"
|
||||
FRAME_IDS = sorted([
|
||||
d.name for d in BLOCKS_DIR.iterdir()
|
||||
if d.is_dir() and d.name.startswith("1171") and d.name != BEPS_ID
|
||||
])
|
||||
|
||||
|
||||
def clean_text(raw):
|
||||
"""HTML tag, markdown bold/italic, HTML entity 정리."""
|
||||
clean = re.sub(r'<[^>]+>', ' ', raw)
|
||||
clean = re.sub(r'\*\*([^*]+)\*\*', r'\1', clean)
|
||||
clean = re.sub(r'(?<!\w)_([^_]+)_(?!\w)', r'\1', clean)
|
||||
clean = html_lib.unescape(clean)
|
||||
clean = re.sub(r'\s+', ' ', clean).strip()
|
||||
return clean
|
||||
|
||||
|
||||
def is_trivial(clean):
|
||||
"""사소한 텍스트(빈 문자열/단일 기호/1자/순수 숫자) 여부."""
|
||||
if not clean or len(clean) < 2:
|
||||
return True
|
||||
if clean in {'-', '–', '—', '(', ')', '/', '*', '**', '.', '|', '~'}:
|
||||
return True
|
||||
# 순수 숫자 배제 (30%, 40% 감소, 2D, 3D 는 보존됨)
|
||||
if re.fullmatch(r'\d+', clean):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_figma_nodes(path):
|
||||
"""Figma texts.md 에서 text node 추출.
|
||||
|
||||
포함:
|
||||
- list item (`- ...` / `* ...`)
|
||||
- plain paragraph line (heading/blockquote 가 아닌 모든 비어있지 않은 줄)
|
||||
|
||||
제외:
|
||||
- heading (#/##/###/####) — 구조 라벨 (예: ## 타이틀, ### 라벨1)
|
||||
- blockquote (>) — meta 주석 (예: > 패턴, > 원본)
|
||||
- 코드블록 (```...```)
|
||||
- standalone HTML tag (<tag>, </tag>, <tag/>)
|
||||
- markdown table separator (|---|---|)
|
||||
- 빈 줄, 단일 기호, 1자, 순수 숫자 (is_trivial 필터)
|
||||
"""
|
||||
raw_nodes = []
|
||||
clean_nodes = []
|
||||
in_codeblock = False
|
||||
|
||||
for line in path.read_text(encoding='utf-8').split('\n'):
|
||||
stripped = line.strip()
|
||||
|
||||
# 코드블록 토글
|
||||
if stripped.startswith('```'):
|
||||
in_codeblock = not in_codeblock
|
||||
continue
|
||||
if in_codeblock:
|
||||
continue
|
||||
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
# heading 제외 (구조 라벨)
|
||||
if stripped.startswith('#'):
|
||||
continue
|
||||
|
||||
# blockquote 제외 (meta 주석)
|
||||
if stripped.startswith('>'):
|
||||
continue
|
||||
|
||||
# standalone HTML tag 제외 (opening/closing/self-closing 모두)
|
||||
if re.fullmatch(r'</?[^>]+>', stripped):
|
||||
continue
|
||||
|
||||
# markdown table separator 제외 (|---|---|)
|
||||
if re.fullmatch(r'\|[\s\-:|]+\|?', stripped):
|
||||
continue
|
||||
|
||||
# list item → prefix 제거
|
||||
m_list = re.match(r'^[-*]\s+(.+)$', stripped)
|
||||
if m_list:
|
||||
raw = m_list.group(1).strip()
|
||||
else:
|
||||
# plain line
|
||||
raw = stripped
|
||||
|
||||
clean = clean_text(raw)
|
||||
if is_trivial(clean):
|
||||
continue
|
||||
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
|
||||
return raw_nodes, clean_nodes
|
||||
|
||||
|
||||
def extract_mdx_content(path):
|
||||
"""MDX 에서 시각 가능한 텍스트 추출.
|
||||
|
||||
포함:
|
||||
- heading (#, ##, ### ...)
|
||||
- 일반 문단 (paragraph)
|
||||
- list item (-, *, 숫자. )
|
||||
- table cell (| a | b | c |)
|
||||
제외:
|
||||
- frontmatter (--- ... ---)
|
||||
- code block (``` ... ```)
|
||||
- admonition 개행 (:::note, :::)
|
||||
- standalone HTML tag 줄 (예: <Callout>)
|
||||
"""
|
||||
raw_nodes = []
|
||||
clean_nodes = []
|
||||
|
||||
lines = path.read_text(encoding='utf-8').split('\n')
|
||||
return _extract_mdx_from_lines(lines, handle_frontmatter=True)
|
||||
|
||||
|
||||
def _extract_mdx_from_lines(lines, handle_frontmatter=False):
|
||||
"""extract_mdx_content 의 내부 구현 (section 추출 재사용)."""
|
||||
raw_nodes = []
|
||||
clean_nodes = []
|
||||
in_codeblock = False
|
||||
in_admonition = False
|
||||
|
||||
start_idx = 0
|
||||
if handle_frontmatter and lines and lines[0].strip() == '---':
|
||||
for i in range(1, len(lines)):
|
||||
if lines[i].strip() == '---':
|
||||
start_idx = i + 1
|
||||
break
|
||||
|
||||
for line in lines[start_idx:]:
|
||||
stripped = line.strip()
|
||||
|
||||
# code block 토글
|
||||
if stripped.startswith('```'):
|
||||
in_codeblock = not in_codeblock
|
||||
continue
|
||||
if in_codeblock:
|
||||
continue
|
||||
|
||||
# admonition 토글 (:::note ... :::)
|
||||
if stripped.startswith(':::'):
|
||||
in_admonition = not in_admonition if stripped == ':::' else True
|
||||
if stripped == ':::':
|
||||
in_admonition = False
|
||||
else:
|
||||
in_admonition = True
|
||||
continue
|
||||
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
# table separator 배제 (|---|---|)
|
||||
if re.fullmatch(r'\|?[\s\-:|]+\|?', stripped) and '|' in stripped:
|
||||
continue
|
||||
|
||||
# table row 처리
|
||||
if stripped.startswith('|') and stripped.endswith('|'):
|
||||
cells = [c.strip() for c in stripped.strip('|').split('|')]
|
||||
for cell in cells:
|
||||
if not cell:
|
||||
continue
|
||||
raw = cell
|
||||
clean = clean_text(raw)
|
||||
if is_trivial(clean):
|
||||
continue
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
continue
|
||||
|
||||
# list item
|
||||
m_list = re.match(r'^[-*]\s+(.+)$', stripped)
|
||||
if m_list:
|
||||
raw = m_list.group(1).strip()
|
||||
clean = clean_text(raw)
|
||||
if not is_trivial(clean):
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
continue
|
||||
m_ol = re.match(r'^\d+\.\s+(.+)$', stripped)
|
||||
if m_ol:
|
||||
raw = m_ol.group(1).strip()
|
||||
clean = clean_text(raw)
|
||||
if not is_trivial(clean):
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
continue
|
||||
|
||||
# heading
|
||||
m_h = re.match(r'^#+\s+(.+)$', stripped)
|
||||
if m_h:
|
||||
raw = m_h.group(1).strip()
|
||||
clean = clean_text(raw)
|
||||
if not is_trivial(clean):
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
continue
|
||||
|
||||
# blockquote 는 일반 문단으로 취급 (단, > 만 있는 줄은 제외)
|
||||
if stripped.startswith('>'):
|
||||
rest = stripped.lstrip('>').strip()
|
||||
if not rest:
|
||||
continue
|
||||
raw = rest
|
||||
clean = clean_text(raw)
|
||||
if not is_trivial(clean):
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
continue
|
||||
|
||||
# standalone HTML tag 줄 배제 (<Callout> 같은)
|
||||
if re.fullmatch(r'<[^>]+>', stripped):
|
||||
continue
|
||||
# HTML 닫는 태그만 있는 줄
|
||||
if re.fullmatch(r'</[^>]+>', stripped):
|
||||
continue
|
||||
|
||||
# 일반 문단
|
||||
raw = stripped
|
||||
clean = clean_text(raw)
|
||||
if not is_trivial(clean):
|
||||
raw_nodes.append(raw)
|
||||
clean_nodes.append(clean)
|
||||
|
||||
return raw_nodes, clean_nodes
|
||||
|
||||
|
||||
# MDX section 정의
|
||||
# - TARGET (검증용): 01-2 / 02-2.2 / 03-1 / 03-2 (ANSWER_MAP 매핑 있음)
|
||||
# - 홀드아웃 (블라인드 검증): 01-1 / 02-1 / 02-2.1 (기대 프레임 미지정)
|
||||
MDX_SECTIONS = {
|
||||
# --- TARGET (ANSWER_MAP 매핑 있음) ---
|
||||
'01-2': {'file': '01.mdx', 'start': '## 2. 용어간 상호관계', 'end_prefix': None},
|
||||
'02-2.2': {'file': '02.mdx', 'start': '### 2.2 DX 시행 주체별 기대효과', 'end_prefix': None},
|
||||
'03-1': {'file': '03.mdx', 'start': '## 1. DX 시행을 위한 필수 요건', 'end_prefix': '## 2.'},
|
||||
'03-2': {'file': '03.mdx', 'start': '## 2. Process의 혁신과 Product의 변화', 'end_prefix': None},
|
||||
# --- 홀드아웃 (블라인드, 기대 프레임 없음) ---
|
||||
'01-1': {'file': '01.mdx', 'start': '## 1. 용어 정의', 'end_prefix': '## 2.'},
|
||||
'02-1': {'file': '02.mdx', 'start': '## 1. DX의 궁극적 목표', 'end_prefix': '## 2.'},
|
||||
'02-2.1': {'file': '02.mdx', 'start': '### 2.1 업무 수행 과정(Process)의 변화', 'end_prefix': '### 2.2'},
|
||||
}
|
||||
|
||||
|
||||
def extract_mdx_section(path, start_heading, end_prefix=None):
|
||||
"""MDX 파일에서 start_heading 줄부터 end_prefix 직전까지의 section 을 추출."""
|
||||
lines = path.read_text(encoding='utf-8').split('\n')
|
||||
start_idx = None
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == start_heading.strip():
|
||||
start_idx = i
|
||||
break
|
||||
if start_idx is None:
|
||||
raise ValueError(f"start_heading 찾지 못함: {start_heading!r} in {path}")
|
||||
|
||||
end_idx = len(lines)
|
||||
if end_prefix:
|
||||
for i in range(start_idx + 1, len(lines)):
|
||||
if lines[i].strip().startswith(end_prefix):
|
||||
end_idx = i
|
||||
break
|
||||
|
||||
section_lines = lines[start_idx:end_idx]
|
||||
return _extract_mdx_from_lines(section_lines, handle_frontmatter=False)
|
||||
|
||||
|
||||
def build_entry(raw, clean):
|
||||
"""한 소스의 카운트 + dedup 결과 패키징."""
|
||||
# dedup: 순서 유지
|
||||
seen = set()
|
||||
unique = []
|
||||
for c in clean:
|
||||
if c in seen:
|
||||
continue
|
||||
seen.add(c)
|
||||
unique.append(c)
|
||||
return {
|
||||
'raw_nodes_count': len(raw),
|
||||
'clean_nodes_count': len(clean),
|
||||
'unique_clean_nodes_count': len(unique),
|
||||
'duplicate_removed_count': len(clean) - len(unique),
|
||||
'text_nodes': unique,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
output = {
|
||||
'meta': {
|
||||
'pipeline_step': 1,
|
||||
'description': 'texts.md / MDX 에서 실제 text node 추출, HTML/markdown 정리, 중복 제거',
|
||||
'sources': {},
|
||||
},
|
||||
'frames': {},
|
||||
'beps': {},
|
||||
'mdx': {},
|
||||
}
|
||||
|
||||
# BEPS
|
||||
p = BLOCKS_DIR / BEPS_ID / "texts.md"
|
||||
if p.exists():
|
||||
raw, clean = extract_figma_nodes(p)
|
||||
entry = build_entry(raw, clean)
|
||||
entry['frame_id'] = BEPS_ID
|
||||
# frame_id를 맨 앞으로
|
||||
output['beps'] = {'frame_id': BEPS_ID, **{k: v for k, v in entry.items() if k != 'frame_id'}}
|
||||
|
||||
# 32 frames
|
||||
for fid in FRAME_IDS:
|
||||
p = BLOCKS_DIR / fid / "texts.md"
|
||||
if not p.exists():
|
||||
continue
|
||||
raw, clean = extract_figma_nodes(p)
|
||||
output['frames'][fid] = build_entry(raw, clean)
|
||||
|
||||
# MDX section 단위 (TARGET 4개)
|
||||
for section_id, cfg in MDX_SECTIONS.items():
|
||||
p = MDX_DIR / cfg['file']
|
||||
if not p.exists():
|
||||
continue
|
||||
raw, clean = extract_mdx_section(p, cfg['start'], cfg.get('end_prefix'))
|
||||
entry = build_entry(raw, clean)
|
||||
entry['section_id'] = section_id
|
||||
entry['source_file'] = cfg['file']
|
||||
entry['section_heading'] = cfg['start']
|
||||
output['mdx'][section_id] = entry
|
||||
|
||||
# 총계
|
||||
def _sum(key, grp):
|
||||
return sum(g[key] for g in grp.values())
|
||||
|
||||
beps = output['beps']
|
||||
frames = output['frames']
|
||||
mdx = output['mdx']
|
||||
|
||||
totals = {
|
||||
'beps_raw': beps.get('raw_nodes_count', 0),
|
||||
'beps_clean': beps.get('clean_nodes_count', 0),
|
||||
'beps_unique': beps.get('unique_clean_nodes_count', 0),
|
||||
'beps_duplicate_removed': beps.get('duplicate_removed_count', 0),
|
||||
'frames_raw': _sum('raw_nodes_count', frames),
|
||||
'frames_clean': _sum('clean_nodes_count', frames),
|
||||
'frames_unique': _sum('unique_clean_nodes_count', frames),
|
||||
'frames_duplicate_removed': _sum('duplicate_removed_count', frames),
|
||||
'mdx_raw': _sum('raw_nodes_count', mdx),
|
||||
'mdx_clean': _sum('clean_nodes_count', mdx),
|
||||
'mdx_unique': _sum('unique_clean_nodes_count', mdx),
|
||||
'mdx_duplicate_removed': _sum('duplicate_removed_count', mdx),
|
||||
}
|
||||
totals['total_raw'] = totals['beps_raw'] + totals['frames_raw'] + totals['mdx_raw']
|
||||
totals['total_clean'] = totals['beps_clean'] + totals['frames_clean'] + totals['mdx_clean']
|
||||
totals['total_unique'] = totals['beps_unique'] + totals['frames_unique'] + totals['mdx_unique']
|
||||
totals['total_duplicate_removed'] = (
|
||||
totals['beps_duplicate_removed']
|
||||
+ totals['frames_duplicate_removed']
|
||||
+ totals['mdx_duplicate_removed']
|
||||
)
|
||||
|
||||
output['meta']['sources'] = {
|
||||
'beps_count': 1 if beps else 0,
|
||||
'frame_count': len(frames),
|
||||
'mdx_count': len(mdx),
|
||||
}
|
||||
output['meta']['totals'] = totals
|
||||
|
||||
# 저장
|
||||
out = HERE / "actual_text_nodes.yaml"
|
||||
with open(out, 'w', encoding='utf-8') as f:
|
||||
yaml.safe_dump(output, f, allow_unicode=True, sort_keys=False, width=200)
|
||||
|
||||
# 화면 요약
|
||||
print(f"[Step 1] text node 추출 완료")
|
||||
print(f" BEPS: raw={totals['beps_raw']}, clean={totals['beps_clean']}, "
|
||||
f"unique={totals['beps_unique']}, dedup_removed={totals['beps_duplicate_removed']}")
|
||||
print(f" Frames: raw={totals['frames_raw']}, clean={totals['frames_clean']}, "
|
||||
f"unique={totals['frames_unique']}, dedup_removed={totals['frames_duplicate_removed']} "
|
||||
f"({len(frames)}개 frame)")
|
||||
print(f" MDX: raw={totals['mdx_raw']}, clean={totals['mdx_clean']}, "
|
||||
f"unique={totals['mdx_unique']}, dedup_removed={totals['mdx_duplicate_removed']}")
|
||||
print(f" TOTAL: raw={totals['total_raw']}, clean={totals['total_clean']}, "
|
||||
f"unique={totals['total_unique']}, dedup_removed={totals['total_duplicate_removed']}")
|
||||
print()
|
||||
print(f"산출: {out}")
|
||||
|
||||
# Frame 18/29/14/13 샘플 미리보기
|
||||
print()
|
||||
print("─" * 60)
|
||||
print("샘플 프레임 (정답 TARGET 4개)")
|
||||
print("─" * 60)
|
||||
for fnum in ['18', '29', '14', '13']:
|
||||
# frame_id 는 BEPS 제외 frame 중에서 frame number 로 접근 어려움 → FRAME_IDS 순 찾기
|
||||
# 사용자는 Frame 번호(순번)로 표현했으므로 FRAME_IDS[fnum-1] 로 추정
|
||||
idx = int(fnum) - 1
|
||||
if idx < 0 or idx >= len(FRAME_IDS):
|
||||
continue
|
||||
fid = FRAME_IDS[idx]
|
||||
info = frames.get(fid)
|
||||
if not info:
|
||||
continue
|
||||
print(f"\n[Frame {fnum}] id={fid}")
|
||||
print(f" raw={info['raw_nodes_count']}, clean={info['clean_nodes_count']}, "
|
||||
f"unique={info['unique_clean_nodes_count']}, dedup_removed={info['duplicate_removed_count']}")
|
||||
print(f" sample text_nodes (최대 10개):")
|
||||
for t in info['text_nodes'][:10]:
|
||||
print(f" - {t}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Step 2.5: special form inventory.
|
||||
|
||||
목적:
|
||||
- Step 4 synonym/canonical 룰 작성 전에 실제 표기형만 관찰 (아직 mapping 안 함).
|
||||
- exact 타겟: S/W, H/W, 2D, 3D, DX, BIM
|
||||
- variant 타겟: AS-IS, TO-BE (case-insensitive + 하이픈/공백/언더스코어 tolerant)
|
||||
- general_candidates threshold 8 로 재계산 (Step 2 결과 재활용)
|
||||
|
||||
count 는 occurrences(매치 횟수) + nodes(매치된 text_node 수) 둘 다.
|
||||
examples 는 source id(beps:<id> / frame:<id> / mdx:<n>) 포함.
|
||||
|
||||
산출: special_forms_inventory.yaml
|
||||
"""
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
INPUT_NODES = HERE / "actual_text_nodes.yaml"
|
||||
INPUT_TOKENS = HERE / "actual_text_tokens.yaml"
|
||||
OUTPUT = HERE / "special_forms_inventory.yaml"
|
||||
|
||||
EXACT_TARGETS = {
|
||||
'S/W': re.compile(r'S/W'),
|
||||
'H/W': re.compile(r'H/W'),
|
||||
'2D': re.compile(r'(?<![A-Za-z0-9])2D(?![A-Za-z0-9])'),
|
||||
'3D': re.compile(r'(?<![A-Za-z0-9])3D(?![A-Za-z0-9])'),
|
||||
'DX': re.compile(r'(?<![A-Za-z0-9])DX(?![A-Za-z0-9])'),
|
||||
'BIM': re.compile(r'(?<![A-Za-z0-9])BIM(?![A-Za-z0-9])'),
|
||||
}
|
||||
|
||||
VARIANT_TARGETS = {
|
||||
'AS-IS': re.compile(r'(?<![A-Za-z])as[-\s_]?is(?![A-Za-z])', re.IGNORECASE),
|
||||
'TO-BE': re.compile(r'(?<![A-Za-z])to[-\s_]?be(?![A-Za-z])', re.IGNORECASE),
|
||||
}
|
||||
|
||||
GROUPS = ['beps', 'frames', 'mdx']
|
||||
|
||||
|
||||
def collect_sources(nodes_data):
|
||||
"""[{'group', 'source', 'text'}] 평면 리스트."""
|
||||
out = []
|
||||
beps = nodes_data.get('beps') or {}
|
||||
if beps.get('text_nodes'):
|
||||
sid = f"beps:{beps.get('frame_id', '?')}"
|
||||
for t in beps['text_nodes']:
|
||||
out.append({'group': 'beps', 'source': sid, 'text': t})
|
||||
for fid, info in nodes_data.get('frames', {}).items():
|
||||
sid = f"frame:{fid}"
|
||||
for t in info.get('text_nodes', []):
|
||||
out.append({'group': 'frames', 'source': sid, 'text': t})
|
||||
for n, info in nodes_data.get('mdx', {}).items():
|
||||
sid = f"mdx:{n}"
|
||||
for t in info.get('text_nodes', []):
|
||||
out.append({'group': 'mdx', 'source': sid, 'text': t})
|
||||
return out
|
||||
|
||||
|
||||
def _empty_breakdown():
|
||||
return {'total': 0, 'beps': 0, 'frames': 0, 'mdx': 0}
|
||||
|
||||
|
||||
def _sum_total(d):
|
||||
d['total'] = d['beps'] + d['frames'] + d['mdx']
|
||||
return d
|
||||
|
||||
|
||||
def _collect_examples(rows_with_match, limit=10):
|
||||
"""group 별로 앞쪽 몇 개씩 섞어 최대 limit 개. text 기준 dedup."""
|
||||
bucket = {g: [] for g in GROUPS}
|
||||
per_group_cap = 5
|
||||
for r in rows_with_match:
|
||||
if len(bucket[r['group']]) < per_group_cap:
|
||||
bucket[r['group']].append(r)
|
||||
out = []
|
||||
seen = set()
|
||||
for g in GROUPS:
|
||||
for r in bucket[g]:
|
||||
if r['text'] in seen:
|
||||
continue
|
||||
seen.add(r['text'])
|
||||
out.append({'source': r['source'], 'text': r['text']})
|
||||
if len(out) >= limit:
|
||||
return out
|
||||
return out
|
||||
|
||||
|
||||
def inventory_exact(pattern, sources):
|
||||
occ = _empty_breakdown()
|
||||
nodes = _empty_breakdown()
|
||||
hits = []
|
||||
for r in sources:
|
||||
matches = pattern.findall(r['text'])
|
||||
if not matches:
|
||||
continue
|
||||
occ[r['group']] += len(matches)
|
||||
nodes[r['group']] += 1
|
||||
hits.append(r)
|
||||
return {
|
||||
'count': {
|
||||
'occurrences': _sum_total(occ),
|
||||
'nodes': _sum_total(nodes),
|
||||
},
|
||||
'examples': _collect_examples(hits, limit=10),
|
||||
}
|
||||
|
||||
|
||||
def inventory_variant(pattern, sources):
|
||||
occ = _empty_breakdown()
|
||||
nodes = _empty_breakdown()
|
||||
forms = Counter()
|
||||
hits = []
|
||||
for r in sources:
|
||||
matches = pattern.findall(r['text'])
|
||||
if not matches:
|
||||
continue
|
||||
for m in matches:
|
||||
forms[m] += 1
|
||||
occ[r['group']] += len(matches)
|
||||
nodes[r['group']] += 1
|
||||
hits.append(r)
|
||||
observed = [{'form': f, 'count': c} for f, c in forms.most_common()]
|
||||
return {
|
||||
'count': {
|
||||
'occurrences': _sum_total(occ),
|
||||
'nodes': _sum_total(nodes),
|
||||
},
|
||||
'observed_forms': observed,
|
||||
'examples': _collect_examples(hits, limit=10),
|
||||
}
|
||||
|
||||
|
||||
def rerun_general_candidates(tokens_data, threshold=8):
|
||||
frames = tokens_data.get('frames', {})
|
||||
mdx = tokens_data.get('mdx', {})
|
||||
total_frames = len(frames)
|
||||
|
||||
frame_count = Counter()
|
||||
mdx_count = Counter()
|
||||
for fid, info in frames.items():
|
||||
for tok in info.get('unique_tokens', []):
|
||||
frame_count[tok] += 1
|
||||
for n, info in mdx.items():
|
||||
for tok in info.get('unique_tokens', []):
|
||||
mdx_count[tok] += 1
|
||||
|
||||
candidates = sorted(
|
||||
[
|
||||
{'token': tok, 'frame_count': fc, 'mdx_count': mdx_count.get(tok, 0)}
|
||||
for tok, fc in frame_count.items() if fc >= threshold
|
||||
],
|
||||
key=lambda x: (-x['frame_count'], -x['mdx_count'], x['token']),
|
||||
)
|
||||
return {
|
||||
'threshold': threshold,
|
||||
'total_frames': total_frames,
|
||||
'count': len(candidates),
|
||||
'note': '삭제 기준 아님. 검토 후보. BIM/건설/기술 등 도메인어 포함 가능.',
|
||||
'candidates': candidates,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
nodes_data = yaml.safe_load(INPUT_NODES.read_text(encoding='utf-8'))
|
||||
tokens_data = yaml.safe_load(INPUT_TOKENS.read_text(encoding='utf-8'))
|
||||
sources = collect_sources(nodes_data)
|
||||
|
||||
output = {
|
||||
'meta': {
|
||||
'pipeline_step': 2.5,
|
||||
'description': (
|
||||
'Special form inventory. synonym/canonical 적용 전 실제 표기형 관찰. '
|
||||
'count 는 occurrences + nodes 둘 다. examples 는 source id 포함.'
|
||||
),
|
||||
},
|
||||
'exact_targets': {t: inventory_exact(p, sources) for t, p in EXACT_TARGETS.items()},
|
||||
'variant_targets': {t: inventory_variant(p, sources) for t, p in VARIANT_TARGETS.items()},
|
||||
'general_candidates_threshold_8': rerun_general_candidates(tokens_data, threshold=8),
|
||||
'general_candidates_threshold_4': rerun_general_candidates(tokens_data, threshold=4),
|
||||
}
|
||||
|
||||
OUTPUT.write_text(
|
||||
yaml.safe_dump(output, allow_unicode=True, sort_keys=False, width=200),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# 콘솔 요약
|
||||
print("[Step 2.5] Special form inventory\n")
|
||||
print("[exact_targets] (occ = occurrences, n = nodes)")
|
||||
for t, info in output['exact_targets'].items():
|
||||
o = info['count']['occurrences']
|
||||
n = info['count']['nodes']
|
||||
print(f" {t:6s} occ={o['total']:4d} (b{o['beps']:3d}/f{o['frames']:3d}/m{o['mdx']:3d}) "
|
||||
f"nodes={n['total']:4d} (b{n['beps']:3d}/f{n['frames']:3d}/m{n['mdx']:3d})")
|
||||
for ex in info['examples'][:3]:
|
||||
print(f" └ [{ex['source']}] {ex['text']}")
|
||||
print()
|
||||
print("[variant_targets]")
|
||||
for t, info in output['variant_targets'].items():
|
||||
o = info['count']['occurrences']
|
||||
n = info['count']['nodes']
|
||||
print(f" {t:6s} occ={o['total']:3d} (b{o['beps']:3d}/f{o['frames']:3d}/m{o['mdx']:3d}) "
|
||||
f"nodes={n['total']:3d} (b{n['beps']:3d}/f{n['frames']:3d}/m{n['mdx']:3d})")
|
||||
print(f" observed_forms:")
|
||||
for f in info['observed_forms']:
|
||||
print(f" {f['form']!r}: {f['count']}")
|
||||
for ex in info['examples'][:3]:
|
||||
print(f" └ [{ex['source']}] {ex['text']}")
|
||||
print()
|
||||
for key in ['general_candidates_threshold_8', 'general_candidates_threshold_4']:
|
||||
gc = output[key]
|
||||
print(f"[{key}] threshold={gc['threshold']} count={gc['count']} (상위 30개)")
|
||||
for c in gc['candidates'][:30]:
|
||||
print(f" {c['token']:12s} frame_count={c['frame_count']:2d}/{gc['total_frames']} mdx_count={c['mdx_count']}")
|
||||
print()
|
||||
print(f"산출: {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Step 2: actual_text_nodes.yaml → Kiwi 형태소 토큰 추출.
|
||||
|
||||
원칙:
|
||||
- Kiwi 품사 NNG/NNP/SL/SN 만 (NNB 제외)
|
||||
- 1글자 제거
|
||||
- 순수 숫자 제거
|
||||
- synonym/canonical 합침 없음 (Step 4 에서 처리)
|
||||
- frame별 unique tokens
|
||||
- corpus top_50_by_frequency + top_50_by_frame_df
|
||||
- general_candidates 는 삭제 기준 아님, 검토 후보
|
||||
- special_token_samples: 2D/3D/S/W/H/W/DX/BIM/AS-IS/TO-BE Kiwi 쪼개짐 관찰
|
||||
|
||||
산출: actual_text_tokens.yaml
|
||||
"""
|
||||
import re
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
from kiwipiepy import Kiwi
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
INPUT = HERE / "actual_text_nodes.yaml"
|
||||
OUTPUT = HERE / "actual_text_tokens.yaml"
|
||||
|
||||
ALLOWED_TAGS = {'NNG', 'NNP', 'SL', 'SN'}
|
||||
SPECIAL_TOKEN_TARGETS = ['2D', '3D', 'S/W', 'H/W', 'DX', 'BIM', 'AS-IS', 'TO-BE']
|
||||
|
||||
kiwi = Kiwi()
|
||||
|
||||
|
||||
def extract_tokens(texts):
|
||||
"""text 리스트에서 허용 태그 token 만 추출."""
|
||||
raw = []
|
||||
for t in texts:
|
||||
for tok in kiwi.tokenize(t):
|
||||
if tok.tag not in ALLOWED_TAGS:
|
||||
continue
|
||||
form = tok.form
|
||||
if len(form) < 2:
|
||||
continue
|
||||
if re.fullmatch(r'\d+', form):
|
||||
continue
|
||||
raw.append(form)
|
||||
return raw
|
||||
|
||||
|
||||
def dedup_keep_order(seq):
|
||||
seen = set()
|
||||
out = []
|
||||
for x in seq:
|
||||
if x in seen:
|
||||
continue
|
||||
seen.add(x)
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
|
||||
def tokenize_to_pairs(text):
|
||||
"""tokenize 결과를 [[form, tag], ...] 로 변환."""
|
||||
return [[tok.form, tok.tag] for tok in kiwi.tokenize(text)]
|
||||
|
||||
|
||||
def process_source(source_key, text_nodes, token_freq, token_to_sources):
|
||||
raw = extract_tokens(text_nodes)
|
||||
unique = dedup_keep_order(raw)
|
||||
for tok in raw:
|
||||
token_freq[tok] += 1
|
||||
for tok in set(raw):
|
||||
token_to_sources[tok].add(source_key)
|
||||
return {
|
||||
'raw_token_count': len(raw),
|
||||
'unique_token_count': len(unique),
|
||||
'unique_tokens': unique,
|
||||
}
|
||||
|
||||
|
||||
def build_special_samples(data):
|
||||
"""특수 토큰이 코퍼스에서 Kiwi 에 의해 어떻게 분해되는지 + source breakdown."""
|
||||
# source 별 text_nodes 수집
|
||||
beps_texts = (data.get('beps') or {}).get('text_nodes', [])
|
||||
frame_texts = []
|
||||
for info in data.get('frames', {}).values():
|
||||
frame_texts.extend(info.get('text_nodes', []))
|
||||
mdx_texts = []
|
||||
for info in data.get('mdx', {}).values():
|
||||
mdx_texts.extend(info.get('text_nodes', []))
|
||||
all_texts = beps_texts + frame_texts + mdx_texts
|
||||
|
||||
samples = {}
|
||||
for target in SPECIAL_TOKEN_TARGETS:
|
||||
beps_hits = [t for t in beps_texts if target in t]
|
||||
frame_hits = [t for t in frame_texts if target in t]
|
||||
mdx_hits = [t for t in mdx_texts if target in t]
|
||||
all_hits = [t for t in all_texts if target in t]
|
||||
samples[target] = {
|
||||
'tokenize_alone': tokenize_to_pairs(target),
|
||||
'found_in': {
|
||||
'total': len(all_hits),
|
||||
'beps': len(beps_hits),
|
||||
'frames': len(frame_hits),
|
||||
'mdx': len(mdx_hits),
|
||||
},
|
||||
'in_context': [
|
||||
{'text': t, 'tokens': tokenize_to_pairs(t)}
|
||||
for t in all_hits[:5]
|
||||
],
|
||||
}
|
||||
return samples
|
||||
|
||||
|
||||
def main():
|
||||
data = yaml.safe_load(INPUT.read_text(encoding='utf-8'))
|
||||
|
||||
output = {
|
||||
'meta': {
|
||||
'pipeline_step': 2,
|
||||
'description': 'Kiwi 형태소 추출 (명사/외국어/숫자). 1글자/순수숫자 제외. synonym 미적용.',
|
||||
'filters': {
|
||||
'allowed_tags': sorted(ALLOWED_TAGS),
|
||||
'exclude_1char': True,
|
||||
'exclude_pure_number': True,
|
||||
},
|
||||
},
|
||||
'beps': {},
|
||||
'frames': {},
|
||||
'mdx': {},
|
||||
'corpus': {},
|
||||
}
|
||||
|
||||
token_freq = Counter()
|
||||
token_to_sources = defaultdict(set)
|
||||
|
||||
# BEPS
|
||||
beps = data.get('beps') or {}
|
||||
if beps.get('text_nodes'):
|
||||
key = f"beps:{beps['frame_id']}"
|
||||
entry = process_source(key, beps['text_nodes'], token_freq, token_to_sources)
|
||||
output['beps'] = {'frame_id': beps['frame_id'], **entry}
|
||||
|
||||
# Frames
|
||||
for fid, info in data.get('frames', {}).items():
|
||||
key = f"frame:{fid}"
|
||||
entry = process_source(key, info.get('text_nodes', []), token_freq, token_to_sources)
|
||||
output['frames'][fid] = entry
|
||||
|
||||
# MDX
|
||||
for n, info in data.get('mdx', {}).items():
|
||||
key = f"mdx:{n}"
|
||||
entry = process_source(key, info.get('text_nodes', []), token_freq, token_to_sources)
|
||||
output['mdx'][n] = entry
|
||||
|
||||
# corpus 통계
|
||||
total_frames = len(output['frames'])
|
||||
frame_count_of = {
|
||||
t: sum(1 for s in token_to_sources[t] if s.startswith('frame:'))
|
||||
for t in token_freq
|
||||
}
|
||||
mdx_count_of = {
|
||||
t: sum(1 for s in token_to_sources[t] if s.startswith('mdx:'))
|
||||
for t in token_freq
|
||||
}
|
||||
|
||||
top50_by_freq = [
|
||||
{'token': t, 'count': c,
|
||||
'frame_count': frame_count_of[t], 'mdx_count': mdx_count_of[t]}
|
||||
for t, c in token_freq.most_common(50)
|
||||
]
|
||||
top50_by_frame_df = sorted(
|
||||
[
|
||||
{'token': t, 'frame_count': frame_count_of[t],
|
||||
'count': token_freq[t], 'mdx_count': mdx_count_of[t]}
|
||||
for t in token_freq
|
||||
],
|
||||
key=lambda x: (-x['frame_count'], -x['count'], x['token']),
|
||||
)[:50]
|
||||
|
||||
threshold = max(3, total_frames // 2)
|
||||
general = sorted(
|
||||
[
|
||||
{'token': t, 'frame_count': frame_count_of[t],
|
||||
'total_count': token_freq[t], 'mdx_count': mdx_count_of[t]}
|
||||
for t in token_freq if frame_count_of[t] >= threshold
|
||||
],
|
||||
key=lambda x: (-x['frame_count'], -x['total_count'], x['token']),
|
||||
)
|
||||
|
||||
special_samples = build_special_samples(data)
|
||||
|
||||
output['corpus'] = {
|
||||
'unique_token_count': len(token_freq),
|
||||
'total_occurrences': sum(token_freq.values()),
|
||||
'top_50_by_frequency': top50_by_freq,
|
||||
'top_50_by_frame_df': top50_by_frame_df,
|
||||
'general_candidates_note': (
|
||||
'제거 기준 아님. 32개 frame 중 threshold 이상에 등장한 토큰 = 검토 후보. '
|
||||
'BIM/건설/기술 등 도메인어가 포함될 수 있으므로 삭제하지 말 것.'
|
||||
),
|
||||
'general_candidates_threshold': threshold,
|
||||
'general_candidates_count': len(general),
|
||||
'general_candidates': general,
|
||||
'special_token_samples_note': (
|
||||
'Step 4 synonym/canonical 룰을 잡기 전에 Kiwi 가 '
|
||||
'2D/3D/S/W/H/W/DX/BIM/AS-IS/TO-BE 를 어떻게 분해하는지 관찰용. '
|
||||
'found_in 에 beps/frames/mdx breakdown 포함.'
|
||||
),
|
||||
'special_token_samples': special_samples,
|
||||
}
|
||||
|
||||
output['meta']['totals'] = {
|
||||
'beps_raw': output['beps'].get('raw_token_count', 0),
|
||||
'beps_unique': output['beps'].get('unique_token_count', 0),
|
||||
'frames_raw': sum(f['raw_token_count'] for f in output['frames'].values()),
|
||||
'frames_unique_avg': (
|
||||
sum(f['unique_token_count'] for f in output['frames'].values()) / total_frames
|
||||
if total_frames else 0
|
||||
),
|
||||
'mdx_raw': sum(m['raw_token_count'] for m in output['mdx'].values()),
|
||||
'mdx_unique_avg': (
|
||||
sum(m['unique_token_count'] for m in output['mdx'].values()) / len(output['mdx'])
|
||||
if output['mdx'] else 0
|
||||
),
|
||||
'corpus_unique': len(token_freq),
|
||||
'corpus_total_occurrences': sum(token_freq.values()),
|
||||
}
|
||||
|
||||
OUTPUT.write_text(
|
||||
yaml.safe_dump(output, allow_unicode=True, sort_keys=False, width=200),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# 화면 요약
|
||||
t = output['meta']['totals']
|
||||
print(f"[Step 2] Kiwi 토큰화 완료")
|
||||
print(f" BEPS: raw={t['beps_raw']}, unique={t['beps_unique']}")
|
||||
print(f" Frames: raw={t['frames_raw']}, unique_avg={t['frames_unique_avg']:.1f} "
|
||||
f"({total_frames}개 frame)")
|
||||
print(f" MDX: raw={t['mdx_raw']}, unique_avg={t['mdx_unique_avg']:.1f}")
|
||||
print(f" Corpus: unique={t['corpus_unique']}, occurrences={t['corpus_total_occurrences']}")
|
||||
print()
|
||||
print(f" 일반 token 후보 (frame_count >= {threshold}, 검토용): {len(general)}개")
|
||||
print(f" 상위 15개:")
|
||||
for g in general[:15]:
|
||||
print(f" {g['token']:12s} frames={g['frame_count']:2d}/{total_frames} "
|
||||
f"count={g['total_count']:3d} mdx={g['mdx_count']}")
|
||||
print()
|
||||
print(f" special_token_samples (Kiwi 쪼개짐 + source breakdown):")
|
||||
for target, info in output['corpus']['special_token_samples'].items():
|
||||
tokens_alone = ' '.join(f"{f}({tag})" for f, tag in info['tokenize_alone'])
|
||||
fi = info['found_in']
|
||||
print(f" {target:7s} alone=[{tokens_alone:35s}] "
|
||||
f"total={fi['total']:3d} beps={fi['beps']:3d} "
|
||||
f"frames={fi['frames']:3d} mdx={fi['mdx']:3d}")
|
||||
print()
|
||||
print(f"산출: {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,266 @@
|
||||
"""Step 4 (4.1 반영): pre-Kiwi substitution + Kiwi user_dict 기반 canonical 토큰 정규화.
|
||||
|
||||
파이프라인:
|
||||
text_node
|
||||
→ 4a-pre. PRE_COLLAPSE (regex): 디지털 전환(DX) / DX(DX) 계열 collapse
|
||||
→ 4a. phrase_variants 치환 (긴 variant 먼저, canonical 재치환 금지)
|
||||
→ 4b. Kiwi tokenize (user_dict: SL 8 + NNG 5)
|
||||
→ 4c. NNG/NNP/SL/SN + 1글자/순수숫자 필터
|
||||
→ canonical token list
|
||||
|
||||
설계 제약:
|
||||
- 긴 variant 먼저 치환
|
||||
- canonical 자기 자신 재치환 금지
|
||||
- DX canonical 하나로 통일 (디지털전환 canonical 없음)
|
||||
- 3D모델/2D도면 은 Step 4 에서 제외 (3D/2D 일반 차원 토큰 보존)
|
||||
- applied_replacements / pre_collapse_applied 카운트 분리 출력
|
||||
- replacement samples: per-group quota (beps 8 / frames 6 / mdx 6 = 20)
|
||||
- corpus 에 full token_frequency / token_frame_df / token_mdx_df 저장
|
||||
|
||||
산출:
|
||||
- normalized_text_tokens.yaml
|
||||
- replacement_report.yaml
|
||||
"""
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
# 정규화 로직은 keyword_normalizer 모듈에서 import (공유)
|
||||
from keyword_normalizer import (
|
||||
USER_DICT_SL, USER_DICT_NNG, ALLOWED_TAGS,
|
||||
PRE_COLLAPSE, MAX_PAREN_LEN, PAREN_PATTERN, PAREN_SKIP_PATTERN, PAREN_SAMPLE_MAX,
|
||||
load_phrase_variants as _load_phrase_variants,
|
||||
build_substitutions, expand_parentheses, apply_substitutions,
|
||||
build_kiwi, extract_tokens,
|
||||
)
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
INPUT_NODES = HERE / "actual_text_nodes.yaml"
|
||||
SYNONYMS = HERE / "synonyms.yaml"
|
||||
OUTPUT_TOKENS = HERE / "normalized_text_tokens.yaml"
|
||||
OUTPUT_REPORT = HERE / "replacement_report.yaml"
|
||||
|
||||
# 로컬 wrapper (기존 호출 형식 유지: load_phrase_variants())
|
||||
def load_phrase_variants():
|
||||
return _load_phrase_variants(SYNONYMS)
|
||||
|
||||
|
||||
# replacement samples per-group quota (합 20)
|
||||
SAMPLE_QUOTA = {'beps': 8, 'frames': 6, 'mdx': 6}
|
||||
|
||||
|
||||
def dedup_keep_order(seq):
|
||||
seen = set()
|
||||
out = []
|
||||
for x in seq:
|
||||
if x in seen:
|
||||
continue
|
||||
seen.add(x)
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
|
||||
def process_source(kiwi, subs, source_group, source_key, text_nodes,
|
||||
replacement_counter, pre_collapse_counter,
|
||||
replacement_samples, sample_count_by_group,
|
||||
corpus_freq, paren_counter, paren_samples):
|
||||
raw = []
|
||||
for t in text_nodes:
|
||||
before = t
|
||||
after = apply_substitutions(t, subs, replacement_counter, pre_collapse_counter,
|
||||
paren_counter, paren_samples)
|
||||
if before != after and sample_count_by_group[source_group] < SAMPLE_QUOTA[source_group]:
|
||||
replacement_samples.append({
|
||||
'group': source_group,
|
||||
'source': source_key,
|
||||
'before': before,
|
||||
'after': after,
|
||||
})
|
||||
sample_count_by_group[source_group] += 1
|
||||
tokens = extract_tokens(kiwi, after)
|
||||
for tk in tokens:
|
||||
corpus_freq[tk] += 1
|
||||
raw.extend(tokens)
|
||||
unique = dedup_keep_order(raw)
|
||||
return {
|
||||
'raw_token_count': len(raw),
|
||||
'unique_token_count': len(unique),
|
||||
'unique_tokens': unique,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
data = yaml.safe_load(INPUT_NODES.read_text(encoding='utf-8'))
|
||||
phrase_variants = load_phrase_variants()
|
||||
subs = build_substitutions(phrase_variants)
|
||||
kiwi = build_kiwi()
|
||||
|
||||
replacement_counter = Counter()
|
||||
pre_collapse_counter = Counter()
|
||||
replacement_samples = []
|
||||
sample_count_by_group = {'beps': 0, 'frames': 0, 'mdx': 0}
|
||||
corpus_freq = Counter()
|
||||
paren_counter = Counter()
|
||||
paren_samples = []
|
||||
|
||||
output = {
|
||||
'meta': {
|
||||
'pipeline_step': 4.2,
|
||||
'description': 'PRE_COLLAPSE + pre-Kiwi substitution + user_dict. phrase_variants 기준.',
|
||||
'user_dict': {'SL': USER_DICT_SL, 'NNG': USER_DICT_NNG},
|
||||
'phrase_variants_count': len(phrase_variants),
|
||||
'substitution_rule_count': len(subs),
|
||||
'pre_collapse_rule_count': len(PRE_COLLAPSE),
|
||||
'sample_quota': SAMPLE_QUOTA,
|
||||
'notes': [
|
||||
'DX canonical 유지 (디지털전환은 DX variant 로 통합)',
|
||||
'3D모델/2D도면 canonical 은 Step 4 에서 제외 (3D/2D 일반 차원 토큰 보존)',
|
||||
'PRE_COLLAPSE 는 regex 로 "디지털 전환(DX)" / "DX(DX)" 계열 통합',
|
||||
],
|
||||
},
|
||||
'beps': {}, 'frames': {}, 'mdx': {}, 'corpus': {},
|
||||
}
|
||||
|
||||
beps = data.get('beps') or {}
|
||||
if beps.get('text_nodes'):
|
||||
key = f"beps:{beps['frame_id']}"
|
||||
entry = process_source(kiwi, subs, 'beps', key, beps['text_nodes'],
|
||||
replacement_counter, pre_collapse_counter,
|
||||
replacement_samples, sample_count_by_group, corpus_freq,
|
||||
paren_counter, paren_samples)
|
||||
output['beps'] = {'frame_id': beps['frame_id'], **entry}
|
||||
|
||||
for fid, info in data.get('frames', {}).items():
|
||||
key = f"frame:{fid}"
|
||||
entry = process_source(kiwi, subs, 'frames', key, info.get('text_nodes', []),
|
||||
replacement_counter, pre_collapse_counter,
|
||||
replacement_samples, sample_count_by_group, corpus_freq,
|
||||
paren_counter, paren_samples)
|
||||
output['frames'][fid] = entry
|
||||
|
||||
for n, info in data.get('mdx', {}).items():
|
||||
key = f"mdx:{n}"
|
||||
entry = process_source(kiwi, subs, 'mdx', key, info.get('text_nodes', []),
|
||||
replacement_counter, pre_collapse_counter,
|
||||
replacement_samples, sample_count_by_group, corpus_freq,
|
||||
paren_counter, paren_samples)
|
||||
output['mdx'][n] = entry
|
||||
|
||||
# corpus 통계
|
||||
total_frames = len(output['frames'])
|
||||
frame_df = Counter()
|
||||
mdx_df = Counter()
|
||||
for fid, info in output['frames'].items():
|
||||
for tk in info['unique_tokens']:
|
||||
frame_df[tk] += 1
|
||||
for n, info in output['mdx'].items():
|
||||
for tk in info['unique_tokens']:
|
||||
mdx_df[tk] += 1
|
||||
|
||||
top50_by_freq = [
|
||||
{'token': t, 'count': c,
|
||||
'frame_count': frame_df.get(t, 0), 'mdx_count': mdx_df.get(t, 0)}
|
||||
for t, c in corpus_freq.most_common(50)
|
||||
]
|
||||
top50_by_frame_df = sorted(
|
||||
[{'token': t, 'frame_count': frame_df[t],
|
||||
'count': corpus_freq[t], 'mdx_count': mdx_df.get(t, 0)}
|
||||
for t in frame_df],
|
||||
key=lambda x: (-x['frame_count'], -x['count'], x['token']),
|
||||
)[:50]
|
||||
|
||||
output['corpus'] = {
|
||||
'unique_token_count': len(corpus_freq),
|
||||
'total_occurrences': sum(corpus_freq.values()),
|
||||
'top_50_by_frequency': top50_by_freq,
|
||||
'top_50_by_frame_df': top50_by_frame_df,
|
||||
'token_frequency': dict(sorted(corpus_freq.items(), key=lambda x: (-x[1], x[0]))),
|
||||
'token_frame_df': dict(sorted(frame_df.items(), key=lambda x: (-x[1], x[0]))),
|
||||
'token_mdx_df': dict(sorted(mdx_df.items(), key=lambda x: (-x[1], x[0]))),
|
||||
}
|
||||
|
||||
output['meta']['totals'] = {
|
||||
'beps_raw': output['beps'].get('raw_token_count', 0),
|
||||
'beps_unique': output['beps'].get('unique_token_count', 0),
|
||||
'frames_raw': sum(f['raw_token_count'] for f in output['frames'].values()),
|
||||
'frames_unique_avg': (
|
||||
sum(f['unique_token_count'] for f in output['frames'].values()) / total_frames
|
||||
if total_frames else 0
|
||||
),
|
||||
'mdx_raw': sum(m['raw_token_count'] for m in output['mdx'].values()),
|
||||
'mdx_unique_avg': (
|
||||
sum(m['unique_token_count'] for m in output['mdx'].values()) / len(output['mdx'])
|
||||
if output['mdx'] else 0
|
||||
),
|
||||
'corpus_unique': len(corpus_freq),
|
||||
}
|
||||
|
||||
OUTPUT_TOKENS.write_text(
|
||||
yaml.safe_dump(output, allow_unicode=True, sort_keys=False, width=200),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
pre_collapse_total = sum(pre_collapse_counter.values())
|
||||
phrase_total = sum(replacement_counter.values())
|
||||
total_text_changes = pre_collapse_total + phrase_total
|
||||
|
||||
report = {
|
||||
'meta': {
|
||||
'pipeline_step': 4.2,
|
||||
'description': 'PRE_COLLAPSE + 괄호 확장 + phrase_variants 치환 카운트 + per-group 쿼터 샘플',
|
||||
'pre_collapse_rule_count': len(PRE_COLLAPSE),
|
||||
'substitution_rule_count': len(subs),
|
||||
'sample_quota': SAMPLE_QUOTA,
|
||||
'paren_expansion_max_len': MAX_PAREN_LEN,
|
||||
},
|
||||
'pre_collapse_applied': dict(
|
||||
sorted(pre_collapse_counter.items(), key=lambda x: -x[1])
|
||||
),
|
||||
'pre_collapse_total': pre_collapse_total,
|
||||
'paren_expanded': {
|
||||
'total': paren_counter.get('expansions', 0),
|
||||
'samples_count': len(paren_samples),
|
||||
'samples': paren_samples,
|
||||
},
|
||||
'applied_replacements': dict(
|
||||
sorted(replacement_counter.items(), key=lambda x: -x[1])
|
||||
),
|
||||
'total_replacements': phrase_total,
|
||||
'total_text_changes': total_text_changes,
|
||||
'sample_count_by_group': sample_count_by_group,
|
||||
'samples': replacement_samples,
|
||||
}
|
||||
OUTPUT_REPORT.write_text(
|
||||
yaml.safe_dump(report, allow_unicode=True, sort_keys=False, width=200),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# 콘솔 요약
|
||||
t = output['meta']['totals']
|
||||
print(f"[Step 4.2] PRE_COLLAPSE (4 rules) + pre-Kiwi substitution + user_dict")
|
||||
print(f" PRE_COLLAPSE: {len(PRE_COLLAPSE)} regex rules")
|
||||
print(f" phrase_variants: {len(phrase_variants)}개 canonical, {len(subs)}개 rule")
|
||||
print(f" user_dict: SL {len(USER_DICT_SL)}, NNG {len(USER_DICT_NNG)}")
|
||||
print()
|
||||
print(f" BEPS: raw={t['beps_raw']}, unique={t['beps_unique']}")
|
||||
print(f" Frames: raw={t['frames_raw']}, unique_avg={t['frames_unique_avg']:.1f} ({total_frames}개 frame)")
|
||||
print(f" MDX: raw={t['mdx_raw']}, unique_avg={t['mdx_unique_avg']:.1f}")
|
||||
print(f" Corpus: unique={t['corpus_unique']}, occurrences={sum(corpus_freq.values())}")
|
||||
print()
|
||||
print(f" pre_collapse_total: {pre_collapse_total}")
|
||||
for rid, cnt in report['pre_collapse_applied'].items():
|
||||
print(f" [{rid}] {cnt}")
|
||||
print(f" applied_replacements total: {phrase_total}")
|
||||
for canonical, cnt in report['applied_replacements'].items():
|
||||
print(f" {canonical:10s} {cnt}")
|
||||
print(f" total_text_changes: {total_text_changes}")
|
||||
print()
|
||||
print(f" samples per group: {sample_count_by_group} (quota={SAMPLE_QUOTA})")
|
||||
print()
|
||||
print(f"산출: {OUTPUT_TOKENS}")
|
||||
print(f" {OUTPUT_REPORT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,342 @@
|
||||
"""Step 5.1: anchor 후보 랭킹 — 3그룹 + Full candidate.
|
||||
|
||||
변경점 (5.0 → 5.1):
|
||||
- top 15 → top 20 (frequent 그룹만)
|
||||
- 후보를 3 그룹 + full 로 분리:
|
||||
1. frequent_candidates — frame_local_count 내림차순 top 20
|
||||
2. special_candidates — USER_DICT_SL/NNG 중 해당 frame 에 등장한 것 (local desc)
|
||||
3. unique_to_frame_candidates — frame_df=1 후보 (local desc)
|
||||
4. full_candidates — 전체 frame 토큰 (local desc)
|
||||
- score 계산 없음. raw 지표만.
|
||||
|
||||
원칙:
|
||||
- AI 자동 추천 없음. 검수용 후보 목록.
|
||||
- 3그룹은 서로 겹칠 수 있음 (예: BIM 은 frequent + special 둘 다).
|
||||
|
||||
산출:
|
||||
- anchor_candidates.yaml (4개 리스트 구조)
|
||||
- anchor_candidates_report.md
|
||||
- anchor_candidates_report.html (details/summary, TARGET frames open)
|
||||
"""
|
||||
import math
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
import markdown
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from pipeline_04_normalize import (
|
||||
apply_substitutions, build_substitutions, build_kiwi,
|
||||
extract_tokens, USER_DICT_SL, USER_DICT_NNG,
|
||||
load_phrase_variants,
|
||||
)
|
||||
|
||||
INPUT_NODES = HERE / "actual_text_nodes.yaml"
|
||||
INPUT_NORMALIZED = HERE / "normalized_text_tokens.yaml"
|
||||
OUTPUT_YAML = HERE / "anchor_candidates.yaml"
|
||||
OUTPUT_MD = HERE / "anchor_candidates_report.md"
|
||||
OUTPUT_HTML = HERE / "anchor_candidates_report.html"
|
||||
|
||||
SPECIAL_TOKENS = set(USER_DICT_SL) | set(USER_DICT_NNG)
|
||||
FREQUENT_TOP_N = 20
|
||||
EXAMPLE_COUNT = 3
|
||||
EXAMPLE_TRUNCATE = 80
|
||||
TARGET_FRAMES = {13, 14, 18, 29}
|
||||
TOTAL_FRAMES = 32
|
||||
|
||||
|
||||
def truncate(s, n):
|
||||
return s if len(s) <= n else s[:n - 1] + '…'
|
||||
|
||||
|
||||
def build_frame_token_counter(text_nodes, subs, kiwi):
|
||||
dummy_rep = Counter()
|
||||
dummy_pre = Counter()
|
||||
counter = Counter()
|
||||
normalized_texts = []
|
||||
for t in text_nodes:
|
||||
after = apply_substitutions(t, subs, dummy_rep, dummy_pre)
|
||||
normalized_texts.append((t, after))
|
||||
tokens = extract_tokens(kiwi, after)
|
||||
for tk in tokens:
|
||||
counter[tk] += 1
|
||||
return counter, normalized_texts
|
||||
|
||||
|
||||
def find_examples(token, normalized_texts, limit=EXAMPLE_COUNT):
|
||||
seen = set()
|
||||
exs = []
|
||||
for original, after in normalized_texts:
|
||||
if token in after and original not in seen:
|
||||
seen.add(original)
|
||||
exs.append(original)
|
||||
if len(exs) >= limit:
|
||||
break
|
||||
return exs
|
||||
|
||||
|
||||
def build_row(token, local, corpus_fd, corpus_md_df, normalized_texts):
|
||||
frame_df = corpus_fd.get(token, 0)
|
||||
mdx_df = corpus_md_df.get(token, 0)
|
||||
idf = round(math.log(TOTAL_FRAMES / frame_df), 3) if frame_df > 0 else 0.0
|
||||
flags = []
|
||||
if frame_df == 1:
|
||||
flags.append('unique_to_frame')
|
||||
return {
|
||||
'token': token,
|
||||
'frame_local_count': local,
|
||||
'frame_df': frame_df,
|
||||
'mdx_df': mdx_df,
|
||||
'mdx_hit': mdx_df > 0,
|
||||
'is_special': token in SPECIAL_TOKENS,
|
||||
'idf': idf,
|
||||
'flags': flags,
|
||||
'examples': find_examples(token, normalized_texts),
|
||||
}
|
||||
|
||||
|
||||
def rank_groups_for_frame(text_nodes, subs, kiwi, corpus_fd, corpus_md_df):
|
||||
frame_counter, normalized_texts = build_frame_token_counter(text_nodes, subs, kiwi)
|
||||
all_rows = [
|
||||
build_row(tok, local, corpus_fd, corpus_md_df, normalized_texts)
|
||||
for tok, local in frame_counter.items()
|
||||
]
|
||||
# 정렬: local desc, idf desc, token asc
|
||||
all_rows.sort(key=lambda x: (-x['frame_local_count'], -x['idf'], x['token']))
|
||||
|
||||
frequent = all_rows[:FREQUENT_TOP_N]
|
||||
special = [r for r in all_rows if r['is_special']]
|
||||
unique_to_frame = [r for r in all_rows if 'unique_to_frame' in r['flags']]
|
||||
full = all_rows
|
||||
return {
|
||||
'total_unique_tokens': len(all_rows),
|
||||
'frequent_candidates': frequent,
|
||||
'special_candidates': special,
|
||||
'unique_to_frame_candidates': unique_to_frame,
|
||||
'full_candidates': full,
|
||||
}
|
||||
|
||||
|
||||
# ---------- md / html 렌더 ----------
|
||||
|
||||
TABLE_HEADERS = ['token', 'local', 'df/32', 'mdx_df/3', 'mdx_hit',
|
||||
'special', 'idf', 'flags', 'examples']
|
||||
|
||||
|
||||
def format_examples_md(examples):
|
||||
if not examples:
|
||||
return '—'
|
||||
return '<br>'.join(truncate(e, EXAMPLE_TRUNCATE) for e in examples)
|
||||
|
||||
|
||||
def rows_to_md_table(rows):
|
||||
if not rows:
|
||||
return '_(없음)_'
|
||||
lines = ['| ' + ' | '.join(TABLE_HEADERS) + ' |',
|
||||
'|' + '|'.join(['---'] * len(TABLE_HEADERS)) + '|']
|
||||
for c in rows:
|
||||
lines.append('| ' + ' | '.join([
|
||||
c['token'],
|
||||
str(c['frame_local_count']),
|
||||
str(c['frame_df']),
|
||||
str(c['mdx_df']),
|
||||
'Y' if c['mdx_hit'] else '',
|
||||
'Y' if c['is_special'] else '',
|
||||
str(c['idf']),
|
||||
', '.join(c['flags']),
|
||||
format_examples_md(c['examples']),
|
||||
]) + ' |')
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def frame_section_md(frame_number, frame_id, groups):
|
||||
head = f"### Frame {frame_number} / {frame_id} (total_unique {groups['total_unique_tokens']})"
|
||||
sub1 = f"#### 1. Frequent candidates — top {FREQUENT_TOP_N}\n\n" + rows_to_md_table(groups['frequent_candidates'])
|
||||
sub2 = f"#### 2. Special / canonical — {len(groups['special_candidates'])}개\n\n" + rows_to_md_table(groups['special_candidates'])
|
||||
sub3 = f"#### 3. Unique-to-frame (df=1) — {len(groups['unique_to_frame_candidates'])}개\n\n" + rows_to_md_table(groups['unique_to_frame_candidates'])
|
||||
sub4 = f"#### 4. Full candidates — 전체 {len(groups['full_candidates'])}개\n\n" + rows_to_md_table(groups['full_candidates'])
|
||||
return '\n\n'.join([head, sub1, sub2, sub3, sub4])
|
||||
|
||||
|
||||
def frame_section_html(frame_number, frame_id, groups, open_by_default):
|
||||
head_text = f"Frame {frame_number} / {frame_id} (total_unique {groups['total_unique_tokens']})"
|
||||
|
||||
def sub(label, rows):
|
||||
body_md = rows_to_md_table(rows)
|
||||
body_html = markdown.markdown(body_md, extensions=['tables'])
|
||||
return f"<h4>{label}</h4>\n{body_html}"
|
||||
|
||||
sub1 = sub(f"1. Frequent candidates — top {FREQUENT_TOP_N}", groups['frequent_candidates'])
|
||||
sub2 = sub(f"2. Special / canonical — {len(groups['special_candidates'])}개",
|
||||
groups['special_candidates'])
|
||||
sub3 = sub(f"3. Unique-to-frame (df=1) — {len(groups['unique_to_frame_candidates'])}개",
|
||||
groups['unique_to_frame_candidates'])
|
||||
full_html = markdown.markdown(rows_to_md_table(groups['full_candidates']),
|
||||
extensions=['tables'])
|
||||
sub4 = (f"<details><summary><b>4. Full candidates — 전체 "
|
||||
f"{len(groups['full_candidates'])}개 (접기/펼치기)</b></summary>\n"
|
||||
f"{full_html}\n</details>")
|
||||
|
||||
opened = 'open' if open_by_default else ''
|
||||
return f"""<details {opened}>
|
||||
<summary><b>{head_text}</b></summary>
|
||||
{sub1}
|
||||
{sub2}
|
||||
{sub3}
|
||||
{sub4}
|
||||
</details>"""
|
||||
|
||||
|
||||
def main():
|
||||
nodes = yaml.safe_load(INPUT_NODES.read_text(encoding='utf-8'))
|
||||
normalized = yaml.safe_load(INPUT_NORMALIZED.read_text(encoding='utf-8'))
|
||||
phrase_variants = load_phrase_variants()
|
||||
subs = build_substitutions(phrase_variants)
|
||||
kiwi = build_kiwi()
|
||||
|
||||
corpus_fd = normalized['corpus']['token_frame_df']
|
||||
corpus_md_df = normalized['corpus']['token_mdx_df']
|
||||
|
||||
frame_ids = sorted(nodes['frames'].keys())
|
||||
|
||||
output_yaml = {
|
||||
'meta': {
|
||||
'pipeline_step': 5.1,
|
||||
'frequent_top_n': FREQUENT_TOP_N,
|
||||
'total_frames': len(frame_ids),
|
||||
'special_tokens': sorted(SPECIAL_TOKENS),
|
||||
'sort_order': ['frame_local_count desc', 'idf desc', 'token asc'],
|
||||
'groups': [
|
||||
'1. frequent_candidates (top 20)',
|
||||
'2. special_candidates (USER_DICT 중 해당 frame 등장)',
|
||||
'3. unique_to_frame_candidates (frame_df=1)',
|
||||
'4. full_candidates (전체 frame token)',
|
||||
],
|
||||
'note': (
|
||||
'score 없음. raw 지표 + flags + examples. '
|
||||
'3 그룹은 서로 겹칠 수 있음 (예: BIM 은 frequent + special 둘 다). '
|
||||
'AI 자동 추천 없음. 검수용 후보 목록.'
|
||||
),
|
||||
},
|
||||
'frames': {},
|
||||
}
|
||||
|
||||
md_sections = [
|
||||
"# Anchor Candidates Report — Step 5.1",
|
||||
"",
|
||||
f"- total frames: **{len(frame_ids)}**",
|
||||
f"- frequent top N: **{FREQUENT_TOP_N}**",
|
||||
f"- sort: frame_local_count desc → idf desc → token asc",
|
||||
f"- special tokens ({len(SPECIAL_TOKENS)}): {', '.join(sorted(SPECIAL_TOKENS))}",
|
||||
"",
|
||||
"## 그룹 설명",
|
||||
"1. **Frequent candidates** — frame 내 등장 횟수 top 20",
|
||||
"2. **Special / canonical** — USER_DICT_SL/NNG 중 해당 frame 에 등장한 것",
|
||||
"3. **Unique-to-frame** — frame_df=1 (그 frame 에만 등장)",
|
||||
"4. **Full candidates** — 전체 frame 토큰 (참조용)",
|
||||
"",
|
||||
"**score 없음. raw 지표만. 검수용 후보 목록.**",
|
||||
"",
|
||||
]
|
||||
|
||||
html_sections = []
|
||||
|
||||
for i, fid in enumerate(frame_ids, 1):
|
||||
info = nodes['frames'][fid]
|
||||
text_nodes = info.get('text_nodes', [])
|
||||
if not text_nodes:
|
||||
continue
|
||||
groups = rank_groups_for_frame(text_nodes, subs, kiwi, corpus_fd, corpus_md_df)
|
||||
output_yaml['frames'][fid] = {
|
||||
'frame_number': i,
|
||||
'total_unique_tokens': groups['total_unique_tokens'],
|
||||
'frequent_candidates': groups['frequent_candidates'],
|
||||
'special_candidates': groups['special_candidates'],
|
||||
'unique_to_frame_candidates': groups['unique_to_frame_candidates'],
|
||||
'full_candidates': groups['full_candidates'],
|
||||
}
|
||||
md_sections.append(frame_section_md(i, fid, groups))
|
||||
md_sections.append("")
|
||||
html_sections.append(
|
||||
frame_section_html(i, fid, groups, open_by_default=(i in TARGET_FRAMES))
|
||||
)
|
||||
|
||||
OUTPUT_YAML.write_text(
|
||||
yaml.safe_dump(output_yaml, allow_unicode=True, sort_keys=False, width=300),
|
||||
encoding='utf-8',
|
||||
)
|
||||
OUTPUT_MD.write_text('\n'.join(md_sections), encoding='utf-8')
|
||||
|
||||
html_header = f"""<h1>Anchor Candidates Report — Step 5.1</h1>
|
||||
<ul>
|
||||
<li>total frames: <b>{len(frame_ids)}</b></li>
|
||||
<li>frequent top N: <b>{FREQUENT_TOP_N}</b></li>
|
||||
<li>sort: frame_local_count desc → idf desc → token asc</li>
|
||||
<li>special tokens ({len(SPECIAL_TOKENS)}): {', '.join(sorted(SPECIAL_TOKENS))}</li>
|
||||
</ul>
|
||||
<h2>그룹 설명</h2>
|
||||
<ol>
|
||||
<li><b>Frequent candidates</b> — frame 내 등장 횟수 top {FREQUENT_TOP_N}</li>
|
||||
<li><b>Special / canonical</b> — USER_DICT 중 해당 frame 에 등장한 것</li>
|
||||
<li><b>Unique-to-frame</b> — frame_df=1 (그 frame 에만 등장)</li>
|
||||
<li><b>Full candidates</b> — 전체 frame 토큰 (참조용, 접기)</li>
|
||||
</ol>
|
||||
<p><b>score 없음. raw 지표만. AI 자동 추천 없음. 검수용 후보 목록.</b></p>
|
||||
<p>TARGET frames 13 / 14 / 18 / 29 는 펼쳐진 상태, 나머지는 접힘.</p>
|
||||
<hr>
|
||||
"""
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Anchor Candidates Report — Step 5.1</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1300px; margin: 2em auto; padding: 0 1em; line-height: 1.5; color: #222; }}
|
||||
h1 {{ border-bottom: 2px solid #333; padding-bottom: 0.2em; }}
|
||||
h2 {{ margin-top: 1.5em; }}
|
||||
h3 {{ margin: 0; color: #333; }}
|
||||
h4 {{ margin-top: 1.2em; margin-bottom: 0.4em; color: #555; border-bottom: 1px dashed #ccc; padding-bottom: 0.2em; }}
|
||||
details {{ margin: 0.6em 0; border: 1px solid #ddd; border-radius: 4px; padding: 0.5em 1em; }}
|
||||
details[open] > summary {{ border-bottom: 1px solid #eee; margin-bottom: 0.5em; padding-bottom: 0.3em; }}
|
||||
details details {{ border: 1px dashed #ccc; background: #fcfcfc; }}
|
||||
details[open] {{ background: #fafbfc; }}
|
||||
summary {{ cursor: pointer; font-size: 1.02em; padding: 0.3em 0; color: #0a6; }}
|
||||
summary b {{ color: #222; }}
|
||||
table {{ border-collapse: collapse; margin: 0.3em 0 0.8em 0; font-size: 0.85em; width: 100%; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: 4px 8px; text-align: left; vertical-align: top; }}
|
||||
th {{ background: #f4f4f4; }}
|
||||
code {{ background: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
||||
em {{ color: #888; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_header}
|
||||
{''.join(html_sections)}
|
||||
</body>
|
||||
</html>"""
|
||||
OUTPUT_HTML.write_text(html, encoding='utf-8')
|
||||
|
||||
print(f"[Step 5.1] 3그룹 + Full 구조 생성 완료")
|
||||
print(f" frames: {len(output_yaml['frames'])}")
|
||||
print(f" frequent_top_n: {FREQUENT_TOP_N}")
|
||||
print(f" special tokens: {len(SPECIAL_TOKENS)}")
|
||||
|
||||
# 그룹별 크기 통계
|
||||
special_sizes = [len(f['special_candidates']) for f in output_yaml['frames'].values()]
|
||||
uniq_sizes = [len(f['unique_to_frame_candidates']) for f in output_yaml['frames'].values()]
|
||||
full_sizes = [len(f['full_candidates']) for f in output_yaml['frames'].values()]
|
||||
print(f" special_candidates: avg={sum(special_sizes)/len(special_sizes):.1f} max={max(special_sizes)}")
|
||||
print(f" unique_to_frame: avg={sum(uniq_sizes)/len(uniq_sizes):.1f} max={max(uniq_sizes)}")
|
||||
print(f" full_candidates: avg={sum(full_sizes)/len(full_sizes):.1f} max={max(full_sizes)}")
|
||||
print()
|
||||
print(f" 산출:")
|
||||
print(f" yaml: {OUTPUT_YAML}")
|
||||
print(f" md: {OUTPUT_MD}")
|
||||
print(f" html: {OUTPUT_HTML}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,624 @@
|
||||
"""Generate MDX_MATCHING_REPORT.
|
||||
|
||||
The report compares MDX section keywords against frame keywords and shows the
|
||||
top matching frames with Korean labels that are readable outside the codebase.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
INPUT_AUTO = HERE / "auto_anchor_candidates.yaml"
|
||||
INPUT_NORMALIZED = HERE / "normalized_text_tokens.yaml"
|
||||
OUT_YAML = HERE / "mdx_matching_result.yaml"
|
||||
OUT_MD = HERE / "MDX_MATCHING_REPORT.md"
|
||||
OUT_HTML = HERE / "MDX_MATCHING_REPORT.html"
|
||||
|
||||
# Current review weights. These are intentionally shown in the report.
|
||||
WEIGHT_STANDALONE = 0.414 # Logistic Regression 학습 (TARGET 4, LOOCV 4/4)
|
||||
WEIGHT_GROUP = 0.320
|
||||
WEIGHT_RELATED = 0.265
|
||||
|
||||
# Review targets with known expected frames (ANSWER_MAP 잠금: 수정 금지).
|
||||
ANSWER_MAP = {
|
||||
"01-2": 18,
|
||||
"02-2.2": 14,
|
||||
"03-1": 13,
|
||||
"03-2": 29,
|
||||
}
|
||||
|
||||
# 홀드아웃 섹션 (블라인드 검증, 기대 프레임 미지정)
|
||||
HOLDOUT_SECTIONS = ['01-1', '02-1', '02-2.1']
|
||||
|
||||
# 기준 잠금 대상 파일 (홀드아웃 실행 시 sha256 기록)
|
||||
LOCK_SNAPSHOT_FILES = [
|
||||
'keyword_normalizer.py',
|
||||
'synonyms.yaml',
|
||||
'pipeline_01_extract_nodes.py',
|
||||
'pipeline_04_normalize.py',
|
||||
'pipeline_06_2_mdx_matching.py',
|
||||
'pipeline_07_auto_anchor_candidates.py',
|
||||
]
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def build_lock_snapshot() -> dict:
|
||||
"""홀드아웃 실행 시점의 기준 잠금 스냅샷."""
|
||||
return {
|
||||
'timestamp': datetime.now().isoformat(timespec='seconds'),
|
||||
'files': {
|
||||
name: sha256_file(HERE / name) if (HERE / name).exists() else None
|
||||
for name in LOCK_SNAPSHOT_FILES
|
||||
},
|
||||
'config': {
|
||||
'weights': {
|
||||
'W1_standalone': WEIGHT_STANDALONE,
|
||||
'W2_group': WEIGHT_GROUP,
|
||||
'W3_related': WEIGHT_RELATED,
|
||||
},
|
||||
'answer_map': dict(ANSWER_MAP),
|
||||
'holdout_sections': list(HOLDOUT_SECTIONS),
|
||||
},
|
||||
'principle': [
|
||||
'검증 대상 고정 — ANSWER_MAP 4개 유지',
|
||||
'기준 잠금 — 전처리/anchor/가중치 수정 금지',
|
||||
'블라인드 실행 — 홀드아웃 기대 프레임 미지정',
|
||||
'사후 평가 — 실행 후 사람이 결과 해석',
|
||||
'수정 규칙 제한 — 섹션 맞추기 보정 금지, 공통 규칙만',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def is_standalone_keyword(frame_appearances: int, is_important: bool) -> bool:
|
||||
return frame_appearances == 1 or (is_important and frame_appearances <= 2)
|
||||
|
||||
|
||||
def write_html(md_text: str) -> None:
|
||||
html_body = markdown.markdown(md_text, extensions=["tables"])
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>키워드 기반 MDX 매칭</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1400px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; }}
|
||||
h1 {{ border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; }}
|
||||
h2 {{ margin-top: 2.6em; background: #e0e7ff; padding: 0.6em 0.9em; border-left: 4px solid #0a6; border-radius: 4px; }}
|
||||
h3 {{ margin-top: 1.5em; color: #1a365d; font-size: 1.05em; }}
|
||||
table {{ border-collapse: collapse; margin: 0.35em 0 1em 0; font-size: 0.88em; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.05); }}
|
||||
th, td {{ border: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }}
|
||||
th {{ background: #1e293b; color: #fff; font-weight: 700; text-align: center; }}
|
||||
code {{ background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; }}
|
||||
details {{ margin: 0.7em 0; background: #fafafa; padding: 0.7em 0.9em; border-radius: 4px; border: 1px solid #ddd; }}
|
||||
summary {{ cursor: pointer; color: #555; font-size: 0.94em; font-weight: 600; }}
|
||||
hr {{ border: 0; border-top: 2px dashed #ccc; margin: 2em 0; }}
|
||||
strong {{ color: #0a6; }}
|
||||
|
||||
/* 매트릭스 셀 이미지 */
|
||||
.matrix-cell img {{ max-width: 240px; height: auto; display: block; margin: 0 auto 6px; border: 1px solid #cbd5e1; border-radius: 4px; }}
|
||||
.matrix-cell {{ min-width: 260px; vertical-align: top; }}
|
||||
.answer-badge {{ background: #fff3cd; border: 3px solid #dc2626; padding: 8px; border-radius: 6px; }}
|
||||
.holdout-top {{ background: #eef2ff; border: 2px solid #4338ca; padding: 8px; border-radius: 6px; }}
|
||||
.frame-label {{ display: block; margin-top: 4px; font-size: 0.9em; }}
|
||||
.frame-label strong {{ color: #2563eb; font-size: 1.05em; }}
|
||||
.frame-desc {{ display: block; color: #64748b; font-size: 0.82em; margin-top: 4px; }}
|
||||
.score {{ color: #059669; font-weight: 600; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_body}
|
||||
</body>
|
||||
</html>"""
|
||||
OUT_HTML.write_text(html, encoding="utf-8")
|
||||
|
||||
|
||||
def build_frame_layers(auto: dict) -> tuple[dict, dict]:
|
||||
frame_groups = defaultdict(list)
|
||||
frame_keyword_info = defaultdict(dict)
|
||||
|
||||
for set_id, info in auto["source_text_sets"].items():
|
||||
frame_id = info["frame_id"]
|
||||
frame_groups[frame_id].append(
|
||||
{
|
||||
"source_text_raw": info["source_text_raw"],
|
||||
"keywords": info["term_values"],
|
||||
}
|
||||
)
|
||||
for term in info["terms"]:
|
||||
keyword = term["token"]
|
||||
if keyword not in frame_keyword_info[frame_id]:
|
||||
frame_keyword_info[frame_id][keyword] = {
|
||||
"frame_appearances": term["frame_df"],
|
||||
"mdx_appearances": term["mdx_df"],
|
||||
"is_important": term["is_special"],
|
||||
}
|
||||
|
||||
frame_layers = {}
|
||||
for frame_id, keyword_map in frame_keyword_info.items():
|
||||
standalone = sorted(
|
||||
keyword
|
||||
for keyword, info in keyword_map.items()
|
||||
if is_standalone_keyword(info["frame_appearances"], info["is_important"])
|
||||
)
|
||||
related = sorted(
|
||||
keyword
|
||||
for keyword, info in keyword_map.items()
|
||||
if not is_standalone_keyword(info["frame_appearances"], info["is_important"])
|
||||
)
|
||||
frame_layers[frame_id] = {
|
||||
"standalone_keywords": standalone,
|
||||
"related_keywords": related,
|
||||
"keyword_groups": frame_groups[frame_id],
|
||||
}
|
||||
|
||||
frame_numbers = {
|
||||
frame_id: auto["frame_stats"][frame_id]["frame_number"]
|
||||
for frame_id in auto["frame_stats"]
|
||||
}
|
||||
return frame_layers, frame_numbers
|
||||
|
||||
|
||||
def score_frame(mdx_keywords: set[str], layers: dict) -> dict:
|
||||
standalone_total = layers["standalone_keywords"]
|
||||
standalone_hit = [kw for kw in standalone_total if kw in mdx_keywords]
|
||||
standalone_score = len(standalone_hit) / len(standalone_total) if standalone_total else 0.0
|
||||
|
||||
group_details = []
|
||||
for group in layers["keyword_groups"]:
|
||||
hit = [kw for kw in group["keywords"] if kw in mdx_keywords]
|
||||
coverage = len(hit) / len(group["keywords"]) if group["keywords"] else 0.0
|
||||
group_details.append(
|
||||
{
|
||||
"source_text_raw": group["source_text_raw"],
|
||||
"keywords": group["keywords"],
|
||||
"hit_keywords": hit,
|
||||
"coverage": round(coverage, 3),
|
||||
}
|
||||
)
|
||||
|
||||
group_score_avg = (
|
||||
sum(group["coverage"] for group in group_details) / len(group_details)
|
||||
if group_details
|
||||
else 0.0
|
||||
)
|
||||
group_score_max = max((group["coverage"] for group in group_details), default=0.0)
|
||||
|
||||
related_total = layers["related_keywords"]
|
||||
related_hit = [kw for kw in related_total if kw in mdx_keywords]
|
||||
related_score = len(related_hit) / len(related_total) if related_total else 0.0
|
||||
|
||||
total_score = (
|
||||
WEIGHT_STANDALONE * standalone_score
|
||||
+ WEIGHT_GROUP * group_score_avg
|
||||
+ WEIGHT_RELATED * related_score
|
||||
)
|
||||
|
||||
return {
|
||||
"standalone": {
|
||||
"total": standalone_total,
|
||||
"total_count": len(standalone_total),
|
||||
"hit": standalone_hit,
|
||||
"hit_count": len(standalone_hit),
|
||||
"score": round(standalone_score, 3),
|
||||
},
|
||||
"keyword_group": {
|
||||
"total_count": len(group_details),
|
||||
"score_avg": round(group_score_avg, 3),
|
||||
"score_max": round(group_score_max, 3),
|
||||
"groups": group_details,
|
||||
},
|
||||
"related": {
|
||||
"total_count": len(related_total),
|
||||
"hit": related_hit,
|
||||
"hit_count": len(related_hit),
|
||||
"score": round(related_score, 3),
|
||||
},
|
||||
"matching_score": round(total_score, 3),
|
||||
}
|
||||
|
||||
|
||||
def build_frame_descriptions(auto: dict) -> dict:
|
||||
"""Frame 번호 → 첫 source_text (설명용)."""
|
||||
out = {}
|
||||
for fid, info in auto["frame_stats"].items():
|
||||
fnum = info["frame_number"]
|
||||
# 해당 frame 의 source_text_sets 중 index=1 인 것
|
||||
for set_id, sinfo in auto["source_text_sets"].items():
|
||||
if sinfo["frame_id"] == fid and sinfo["source_text_index"] == 1:
|
||||
raw = sinfo["source_text_raw"]
|
||||
if len(raw) > 35:
|
||||
raw = raw[:34] + "…"
|
||||
out[fnum] = raw
|
||||
break
|
||||
if fnum not in out:
|
||||
out[fnum] = f"Frame {fnum}"
|
||||
return out
|
||||
|
||||
|
||||
def cell_html(rank_row: dict, descriptions: dict, is_answer: bool = False,
|
||||
is_holdout_top: bool = False) -> str:
|
||||
"""Top-N 매트릭스 한 칸의 HTML."""
|
||||
fn = rank_row["frame_number"]
|
||||
score = rank_row["matching_score"]
|
||||
desc = descriptions.get(fn, "")
|
||||
img_src = f"../../data/figma_previews/{fn:02d}.png"
|
||||
inner = (
|
||||
f'<img alt="{fn:02d}" src="{img_src}" />'
|
||||
f'<span class="frame-label"><strong>Frame {fn}</strong> '
|
||||
f'<span class="score">{score:.3f}</span></span>'
|
||||
f'<span class="frame-desc">{desc}</span>'
|
||||
)
|
||||
if is_answer:
|
||||
return f'<td class="matrix-cell"><div class="answer-badge">🎯 <b>정답</b><br>{inner}</div></td>'
|
||||
if is_holdout_top:
|
||||
return f'<td class="matrix-cell"><div class="holdout-top">{inner}</div></td>'
|
||||
return f'<td class="matrix-cell">{inner}</td>'
|
||||
|
||||
|
||||
def main() -> None:
|
||||
auto = yaml.safe_load(INPUT_AUTO.read_text(encoding="utf-8"))
|
||||
normalized = yaml.safe_load(INPUT_NORMALIZED.read_text(encoding="utf-8"))
|
||||
|
||||
frame_layers, frame_numbers = build_frame_layers(auto)
|
||||
frame_descriptions = build_frame_descriptions(auto)
|
||||
results = {}
|
||||
|
||||
# TARGET + 홀드아웃 모두 처리. ANSWER_MAP 없는 섹션은 answer 없이 Top 만.
|
||||
all_mdx_sections = list(ANSWER_MAP.keys()) + [
|
||||
s for s in HOLDOUT_SECTIONS if s not in ANSWER_MAP
|
||||
]
|
||||
|
||||
for mdx_id in all_mdx_sections:
|
||||
if mdx_id not in normalized["mdx"]:
|
||||
continue
|
||||
answer_frame_number = ANSWER_MAP.get(mdx_id) # 홀드아웃은 None
|
||||
mdx_keywords = set(normalized["mdx"][mdx_id]["unique_tokens"])
|
||||
per_frame = {}
|
||||
|
||||
for frame_id, layers in frame_layers.items():
|
||||
scored = score_frame(mdx_keywords, layers)
|
||||
scored["frame_number"] = frame_numbers[frame_id]
|
||||
per_frame[frame_id] = scored
|
||||
|
||||
def rank_by(key_fn):
|
||||
return sorted(per_frame.items(), key=lambda x: (-key_fn(x[1]), x[0]))
|
||||
|
||||
rank_total = rank_by(lambda d: d["matching_score"])
|
||||
rank_group = rank_by(lambda d: d["keyword_group"]["score_avg"])
|
||||
rank_standalone = rank_by(lambda d: d["standalone"]["score"])
|
||||
rank_related = rank_by(lambda d: d["related"]["score"])
|
||||
|
||||
def find_rank(ranked_list, frame_number: int) -> int | None:
|
||||
for idx, (_frame_id, info) in enumerate(ranked_list, start=1):
|
||||
if info["frame_number"] == frame_number:
|
||||
return idx
|
||||
return None
|
||||
|
||||
is_holdout = mdx_id in HOLDOUT_SECTIONS
|
||||
# 홀드아웃 섹션은 answer_frame_number 가 None 이라 answer_ranks 계산 불가 → None 기록
|
||||
answer_ranks_val = None
|
||||
if answer_frame_number is not None:
|
||||
answer_ranks_val = {
|
||||
"by_matching_score": find_rank(rank_total, answer_frame_number),
|
||||
"by_keyword_group_score": find_rank(rank_group, answer_frame_number),
|
||||
"by_standalone_keyword_score": find_rank(rank_standalone, answer_frame_number),
|
||||
"by_related_keyword_score": find_rank(rank_related, answer_frame_number),
|
||||
}
|
||||
|
||||
results[mdx_id] = {
|
||||
"mdx_keywords_count": len(mdx_keywords),
|
||||
"section_type": "holdout" if is_holdout else "target",
|
||||
"answer_frame_number": answer_frame_number,
|
||||
"answer_ranks": answer_ranks_val,
|
||||
"rank_by_matching_score": [
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"frame_number": info["frame_number"],
|
||||
"matching_score": info["matching_score"],
|
||||
"standalone": info["standalone"]["score"],
|
||||
"keyword_group": info["keyword_group"]["score_avg"],
|
||||
"related": info["related"]["score"],
|
||||
}
|
||||
for frame_id, info in rank_total
|
||||
],
|
||||
"rank_by_keyword_group_score": [
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"frame_number": info["frame_number"],
|
||||
"score_avg": info["keyword_group"]["score_avg"],
|
||||
"score_max": info["keyword_group"]["score_max"],
|
||||
}
|
||||
for frame_id, info in rank_group
|
||||
],
|
||||
"rank_by_standalone_keyword_score": [
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"frame_number": info["frame_number"],
|
||||
"score": info["standalone"]["score"],
|
||||
"hit": info["standalone"]["hit"],
|
||||
}
|
||||
for frame_id, info in rank_standalone
|
||||
],
|
||||
"rank_by_related_keyword_score": [
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"frame_number": info["frame_number"],
|
||||
"score": info["related"]["score"],
|
||||
"hit_count": info["related"]["hit_count"],
|
||||
}
|
||||
for frame_id, info in rank_related
|
||||
],
|
||||
"per_frame_detail": per_frame,
|
||||
}
|
||||
|
||||
output = {
|
||||
"meta": {
|
||||
"pipeline_step": "6.2",
|
||||
"description": "MDX 키워드와 프레임별 키워드 매칭 결과 (TARGET + 홀드아웃)",
|
||||
"score_parts": ["단독 대표 키워드", "대표 키워드 묶음", "연관 키워드"],
|
||||
"weights": {
|
||||
"standalone_keyword": WEIGHT_STANDALONE,
|
||||
"keyword_group": WEIGHT_GROUP,
|
||||
"related_keyword": WEIGHT_RELATED,
|
||||
},
|
||||
"formula": "matching_score = 0.30*standalone + 0.50*keyword_group + 0.20*related",
|
||||
"answer_map": ANSWER_MAP,
|
||||
"holdout_sections": HOLDOUT_SECTIONS,
|
||||
"source_mdx_keywords": "normalized_text_tokens.yaml",
|
||||
"source_frame_keywords": "auto_anchor_candidates.yaml",
|
||||
"lock_snapshot": build_lock_snapshot(),
|
||||
"note": "규칙 기반 점수입니다. 홀드아웃 섹션(01-1/02-1/02-2.1)은 기대 프레임 미지정 — 블라인드 검증용.",
|
||||
},
|
||||
"mdx_sections": results,
|
||||
}
|
||||
OUT_YAML.write_text(
|
||||
yaml.safe_dump(output, allow_unicode=True, sort_keys=False, width=300),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
md: list[str] = [
|
||||
"# 키워드 기반 MDX 매칭",
|
||||
"",
|
||||
"이 문서는 **키워드 수준** 의 매칭 결과입니다. MDX 구간에서 추출한 키워드와 각 프레임에 적용된 키워드를 비교해, 어떤 프레임이 가장 잘 맞는지 1~3순위로 보여줍니다.",
|
||||
"",
|
||||
"_향후 단계에서 의미(semantic) / 구조(structure) / 적용가능성(template fit) 매칭이 추가될 예정입니다._",
|
||||
"",
|
||||
"점수는 세 가지 단서를 합산합니다.",
|
||||
"",
|
||||
"| 단서 | 의미 | 가중치 |",
|
||||
"|---|---|---:|",
|
||||
f"| 단독 대표 키워드 | 특정 프레임을 혼자서도 강하게 가리키는 키워드가 MDX에 얼마나 맞는지 | {WEIGHT_STANDALONE} |",
|
||||
f"| 대표 키워드 묶음 | 프레임 원문에서 함께 등장한 키워드 조합이 MDX에 얼마나 같이 맞는지 | {WEIGHT_GROUP} |",
|
||||
f"| 연관 키워드 | 그 외 참고 키워드가 MDX와 얼마나 겹치는지 | {WEIGHT_RELATED} |",
|
||||
"",
|
||||
"```text",
|
||||
f"종합 매칭 점수 = {WEIGHT_STANDALONE} x 단독 대표 키워드 점수",
|
||||
f" + {WEIGHT_GROUP} x 대표 키워드 묶음 점수",
|
||||
f" + {WEIGHT_RELATED} x 연관 키워드 점수",
|
||||
"```",
|
||||
"",
|
||||
"### 검증용 정답 프레임",
|
||||
"",
|
||||
"| MDX 구간 | 기대 프레임 |",
|
||||
"|---|---:|",
|
||||
]
|
||||
for mdx_id, frame_number in ANSWER_MAP.items():
|
||||
md.append(f"| {mdx_id} | Frame {frame_number} |")
|
||||
md += ["", "---", ""]
|
||||
|
||||
# ============================================================
|
||||
# 🖼 Top-3 매트릭스 (이미지 포함, TARGET + 홀드아웃)
|
||||
# ============================================================
|
||||
md.append("## 🖼 Top-3 매칭 매트릭스")
|
||||
md.append("")
|
||||
md.append("**TARGET 섹션** (ANSWER_MAP 4개): 정답 프레임은 🎯 노란 박스. ")
|
||||
md.append("**홀드아웃 섹션** (기대 프레임 미지정): 1위는 파란 박스, 해석은 사용자 판단.")
|
||||
md.append("")
|
||||
|
||||
def _matrix_table(section_order, label_fn):
|
||||
rows = [
|
||||
'<table><thead><tr><th style="min-width:200px">콘텐츠</th>'
|
||||
'<th>1순위</th><th>2순위</th><th>3순위</th></tr></thead><tbody>'
|
||||
]
|
||||
for mdx_id in section_order:
|
||||
if mdx_id not in results:
|
||||
continue
|
||||
r = results[mdx_id]
|
||||
top3 = r["rank_by_matching_score"][:3]
|
||||
label = label_fn(mdx_id, r)
|
||||
tds = [f'<td style="background:#f1f5f9;text-align:center;font-weight:600"><br>{label}</td>']
|
||||
for idx, row in enumerate(top3):
|
||||
is_answer = (r["answer_frame_number"] is not None
|
||||
and row["frame_number"] == r["answer_frame_number"])
|
||||
is_hold_top = (r["answer_frame_number"] is None and idx == 0)
|
||||
tds.append(cell_html(row, frame_descriptions,
|
||||
is_answer=is_answer,
|
||||
is_holdout_top=is_hold_top))
|
||||
rows.append('<tr>' + ''.join(tds) + '</tr>')
|
||||
rows.append('</tbody></table>')
|
||||
return '\n'.join(rows)
|
||||
|
||||
target_order = ["01-2", "02-2.2", "03-1", "03-2"]
|
||||
holdout_order = HOLDOUT_SECTIONS
|
||||
|
||||
md.append("### TARGET 4 섹션")
|
||||
md.append("")
|
||||
md.append(_matrix_table(
|
||||
target_order,
|
||||
lambda mid, r: f'<strong>MDX {mid}</strong><br>정답 Frame <strong>{r["answer_frame_number"]}</strong>',
|
||||
))
|
||||
md.append("")
|
||||
md.append("### 🔒 홀드아웃 3 섹션 (블라인드)")
|
||||
md.append("")
|
||||
md.append(_matrix_table(
|
||||
holdout_order,
|
||||
lambda mid, r: f'<strong>MDX {mid}</strong><br><em>기대 프레임 미지정</em>',
|
||||
))
|
||||
md.append("")
|
||||
md.append("---")
|
||||
md.append("")
|
||||
|
||||
for mdx_id in ["01-2", "02-2.2", "03-1", "03-2"]:
|
||||
result = results[mdx_id]
|
||||
ranks = result["answer_ranks"]
|
||||
answer_frame = result["answer_frame_number"]
|
||||
md.append(f"## MDX {mdx_id} (기대 프레임: Frame {answer_frame})")
|
||||
md.append("")
|
||||
md.append("**기대 프레임의 순위**")
|
||||
md.append("")
|
||||
md.append(f"- 종합 매칭 점수 기준: **{ranks['by_matching_score']}위**")
|
||||
md.append(f"- 대표 키워드 묶음 기준: {ranks['by_keyword_group_score']}위")
|
||||
md.append(f"- 단독 대표 키워드 기준: {ranks['by_standalone_keyword_score']}위")
|
||||
md.append(f"- 연관 키워드 기준: {ranks['by_related_keyword_score']}위")
|
||||
md.append("")
|
||||
|
||||
md.append("**종합 매칭 점수 Top 3**")
|
||||
md.append("")
|
||||
md.append("| 순위 | 프레임 | 종합 점수 | 단독 대표 키워드 | 대표 키워드 묶음 | 연관 키워드 |")
|
||||
md.append("|---:|---|---:|---:|---:|---:|")
|
||||
for idx, row in enumerate(result["rank_by_matching_score"][:3], start=1):
|
||||
mark = " ✓" if row["frame_number"] == answer_frame else ""
|
||||
md.append(
|
||||
f"| **{idx}**{mark} | Frame {row['frame_number']} / {row['frame_id']} | "
|
||||
f"**{row['matching_score']}** | {row['standalone']} | {row['keyword_group']} | {row['related']} |"
|
||||
)
|
||||
md.append("")
|
||||
|
||||
md.append("<details><summary>Top 10 전체 보기</summary>")
|
||||
md.append("")
|
||||
md.append("| 순위 | 프레임 | 종합 점수 | 단독 대표 키워드 | 대표 키워드 묶음 | 연관 키워드 |")
|
||||
md.append("|---:|---|---:|---:|---:|---:|")
|
||||
for idx, row in enumerate(result["rank_by_matching_score"][:10], start=1):
|
||||
mark = " ✓" if row["frame_number"] == answer_frame else ""
|
||||
md.append(
|
||||
f"| {idx}{mark} | Frame {row['frame_number']} / {row['frame_id']} | "
|
||||
f"{row['matching_score']} | {row['standalone']} | {row['keyword_group']} | {row['related']} |"
|
||||
)
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
|
||||
answer_frame_id = [
|
||||
frame_id
|
||||
for frame_id, info in result["per_frame_detail"].items()
|
||||
if info["frame_number"] == answer_frame
|
||||
][0]
|
||||
detail = result["per_frame_detail"][answer_frame_id]
|
||||
md.append(f"<details><summary>기대 프레임 Frame {answer_frame} / {answer_frame_id} 상세 보기</summary>")
|
||||
md.append("")
|
||||
md.append(
|
||||
f"- 단독 대표 키워드: {detail['standalone']['hit_count']} / {detail['standalone']['total_count']} = {detail['standalone']['score']}"
|
||||
)
|
||||
md.append(f" - 맞은 키워드: {', '.join(detail['standalone']['hit']) or '없음'}")
|
||||
md.append(
|
||||
f"- 대표 키워드 묶음: 평균 {detail['keyword_group']['score_avg']} / 최고 {detail['keyword_group']['score_max']}"
|
||||
)
|
||||
for group in detail["keyword_group"]["groups"]:
|
||||
terms = ", ".join(group["keywords"])
|
||||
hits = ", ".join(group["hit_keywords"]) or "없음"
|
||||
md.append(f" - `{group['source_text_raw']}` → [{terms}] / 맞은 키워드 [{hits}] / 일치율 **{group['coverage']}**")
|
||||
md.append(
|
||||
f"- 연관 키워드: {detail['related']['hit_count']} / {detail['related']['total_count']} = {detail['related']['score']}"
|
||||
)
|
||||
md.append(f" - 맞은 키워드: {', '.join(detail['related']['hit']) or '없음'}")
|
||||
md.append(f"- 종합 매칭 점수: **{detail['matching_score']}**")
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
md.append("---")
|
||||
md.append("")
|
||||
|
||||
# ============================================================
|
||||
# 홀드아웃 섹션 (블라인드, 기대 프레임 없음)
|
||||
# ============================================================
|
||||
md.append("")
|
||||
md.append("# 🔒 홀드아웃 검증 섹션 (블라인드)")
|
||||
md.append("")
|
||||
md.append("아래 섹션들은 **기대 프레임을 사전에 지정하지 않은** 블라인드 검증 대상입니다.")
|
||||
md.append("현재 파이프라인으로 그대로 돌려 Top 1~3 결과와 점수를 raw 로 기록합니다.")
|
||||
md.append("결과 해석은 사람이 사후에 수행합니다.")
|
||||
md.append("")
|
||||
md.append("### 기준 잠금 스냅샷")
|
||||
md.append("")
|
||||
snap = output['meta']['lock_snapshot']
|
||||
md.append(f"- **실행 시각**: {snap['timestamp']}")
|
||||
md.append(f"- **가중치**: W1={snap['config']['weights']['W1_standalone']} / "
|
||||
f"W2={snap['config']['weights']['W2_group']} / "
|
||||
f"W3={snap['config']['weights']['W3_related']}")
|
||||
md.append("")
|
||||
md.append("**파일 지문 (sha256, 앞 16자)**:")
|
||||
md.append("")
|
||||
md.append("| 파일 | sha256 |")
|
||||
md.append("|---|---|")
|
||||
for name, h in snap['files'].items():
|
||||
h_short = h[:16] + '...' if h else '(없음)'
|
||||
md.append(f"| `{name}` | `{h_short}` |")
|
||||
md.append("")
|
||||
md.append("**원칙**:")
|
||||
md.append("")
|
||||
for p in snap['principle']:
|
||||
md.append(f"- {p}")
|
||||
md.append("")
|
||||
md.append("---")
|
||||
md.append("")
|
||||
|
||||
for mdx_id in HOLDOUT_SECTIONS:
|
||||
if mdx_id not in results:
|
||||
continue
|
||||
result = results[mdx_id]
|
||||
md.append(f"## 🔒 MDX {mdx_id} (홀드아웃, 기대 프레임 미지정)")
|
||||
md.append("")
|
||||
md.append(f"- MDX 키워드 수: {result['mdx_keywords_count']}")
|
||||
md.append("")
|
||||
md.append("**Top 3 (기대 프레임 없음, 해석은 사후)**")
|
||||
md.append("")
|
||||
md.append("| 순위 | 프레임 | 종합 점수 | 단독 대표 키워드 | 대표 키워드 묶음 | 연관 키워드 |")
|
||||
md.append("|---:|---|---:|---:|---:|---:|")
|
||||
for idx, row in enumerate(result["rank_by_matching_score"][:3], start=1):
|
||||
md.append(
|
||||
f"| **{idx}** | Frame {row['frame_number']} / {row['frame_id']} | "
|
||||
f"**{row['matching_score']}** | {row['standalone']} | {row['keyword_group']} | {row['related']} |"
|
||||
)
|
||||
md.append("")
|
||||
md.append("<details><summary>Top 10 전체 보기</summary>")
|
||||
md.append("")
|
||||
md.append("| 순위 | 프레임 | 종합 점수 | 단독 대표 키워드 | 대표 키워드 묶음 | 연관 키워드 |")
|
||||
md.append("|---:|---|---:|---:|---:|---:|")
|
||||
for idx, row in enumerate(result["rank_by_matching_score"][:10], start=1):
|
||||
md.append(
|
||||
f"| {idx} | Frame {row['frame_number']} / {row['frame_id']} | "
|
||||
f"{row['matching_score']} | {row['standalone']} | {row['keyword_group']} | {row['related']} |"
|
||||
)
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
md.append("---")
|
||||
md.append("")
|
||||
|
||||
md_text = "\n".join(md)
|
||||
OUT_MD.write_text(md_text, encoding="utf-8")
|
||||
write_html(md_text)
|
||||
|
||||
print("산출 완료:")
|
||||
print(f" yaml: {OUT_YAML}")
|
||||
print(f" md: {OUT_MD}")
|
||||
print(f" html: {OUT_HTML}")
|
||||
print()
|
||||
print("기준 잠금 스냅샷:")
|
||||
print(f" 실행시각: {output['meta']['lock_snapshot']['timestamp']}")
|
||||
for name, h in output['meta']['lock_snapshot']['files'].items():
|
||||
if h:
|
||||
print(f" {name:40s} {h[:16]}...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,344 @@
|
||||
"""Step 6: anchor_sets 검증 + 근거 첨부.
|
||||
|
||||
원칙:
|
||||
- AI 는 의미 묶음 판단 안 함. anchor_sets_input.yaml 이 사람이 편집한 source.
|
||||
- pipeline_06 은 term 존재 검증 + 근거(local/df/source) 첨부 + 보고서 생성만.
|
||||
- 없는 term 은 not_in_frame 플래그 (에러 아님, 사용자 재검토용).
|
||||
|
||||
입력:
|
||||
- anchor_sets_input.yaml (사용자 편집)
|
||||
- anchor_candidates.yaml (Step 5.1)
|
||||
- normalized_text_tokens.yaml (corpus 통계)
|
||||
|
||||
산출:
|
||||
- anchor_sets_draft.yaml
|
||||
- anchor_sets_report.md
|
||||
- anchor_sets_report.html
|
||||
"""
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
import markdown
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
INPUT = HERE / "anchor_sets_input.yaml"
|
||||
CANDIDATES = HERE / "anchor_candidates.yaml"
|
||||
NORMALIZED = HERE / "normalized_text_tokens.yaml"
|
||||
|
||||
OUT_YAML = HERE / "anchor_sets_draft.yaml"
|
||||
OUT_MD = HERE / "anchor_sets_report.md"
|
||||
OUT_HTML = HERE / "anchor_sets_report.html"
|
||||
|
||||
EXAMPLE_TRUNCATE = 80
|
||||
|
||||
|
||||
def truncate(s, n):
|
||||
return s if len(s) <= n else s[:n - 1] + '…'
|
||||
|
||||
|
||||
def build_candidate_index(frame_info):
|
||||
"""frame 의 full_candidates 를 token → candidate dict 로 인덱싱."""
|
||||
idx = {c['token']: c for c in frame_info['full_candidates']}
|
||||
frequent_set = {c['token'] for c in frame_info['frequent_candidates']}
|
||||
special_set = {c['token'] for c in frame_info['special_candidates']}
|
||||
unique_set = {c['token'] for c in frame_info['unique_to_frame_candidates']}
|
||||
return idx, frequent_set, special_set, unique_set
|
||||
|
||||
|
||||
def verify_term(token, cand_idx, freq_set, spec_set, uniq_set, corpus_fd, corpus_md_df):
|
||||
"""term 검증 + 근거 수집."""
|
||||
if token in cand_idx:
|
||||
c = cand_idx[token]
|
||||
sources = []
|
||||
if token in freq_set: sources.append('Frequent')
|
||||
if token in spec_set: sources.append('Special')
|
||||
if token in uniq_set: sources.append('Unique')
|
||||
if not sources: sources.append('Full')
|
||||
example = c['examples'][0] if c.get('examples') else None
|
||||
return {
|
||||
'token': token,
|
||||
'in_frame': True,
|
||||
'local_count': c['frame_local_count'],
|
||||
'frame_df': c['frame_df'],
|
||||
'mdx_df': c['mdx_df'],
|
||||
'mdx_hit': c['mdx_hit'],
|
||||
'is_special': c['is_special'],
|
||||
'source_groups': sources,
|
||||
'example': example,
|
||||
}
|
||||
else:
|
||||
# corpus 에 있는지 확인 (다른 frame 에만 있는 경우)
|
||||
in_corpus = token in corpus_fd
|
||||
return {
|
||||
'token': token,
|
||||
'in_frame': False,
|
||||
'in_corpus': in_corpus,
|
||||
'corpus_frame_df': corpus_fd.get(token, 0) if in_corpus else 0,
|
||||
'corpus_mdx_df': corpus_md_df.get(token, 0) if in_corpus else 0,
|
||||
'note': (
|
||||
'token 이 해당 frame 의 실제 후보에 없음. '
|
||||
'코퍼스에 있지만 다른 frame/source 에만 등장.'
|
||||
if in_corpus else
|
||||
'token 이 전체 코퍼스에 없음. 철자/정규화 확인 필요.'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def verify_flagged_terms(flagged_list, cand_idx, corpus_fd, corpus_md_df):
|
||||
"""flagged_terms 검증 + corpus 존재 여부 첨부."""
|
||||
out = []
|
||||
for ft in flagged_list or []:
|
||||
tok = ft.get('token')
|
||||
out.append({
|
||||
'token': tok,
|
||||
'reason': ft.get('reason', ''),
|
||||
'in_frame': tok in cand_idx,
|
||||
'in_corpus': tok in corpus_fd,
|
||||
'corpus_frame_df': corpus_fd.get(tok, 0),
|
||||
'corpus_mdx_df': corpus_md_df.get(tok, 0),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def verify_review_needed(review_list, cand_idx, corpus_fd, corpus_md_df):
|
||||
"""review_needed 검증 (frame 에는 있으나 의미상 확인 필요)."""
|
||||
out = []
|
||||
for rv in review_list or []:
|
||||
tok = rv.get('token')
|
||||
cand = cand_idx.get(tok)
|
||||
out.append({
|
||||
'token': tok,
|
||||
'reason': rv.get('reason', ''),
|
||||
'in_frame': tok in cand_idx,
|
||||
'local_count': cand['frame_local_count'] if cand else 0,
|
||||
'frame_df': cand['frame_df'] if cand else corpus_fd.get(tok, 0),
|
||||
'mdx_df': cand['mdx_df'] if cand else corpus_md_df.get(tok, 0),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def process_frame(fid, frame_input, candidates_data, corpus_fd, corpus_md_df):
|
||||
frame_info = candidates_data['frames'][fid]
|
||||
cand_idx, freq_set, spec_set, uniq_set = build_candidate_index(frame_info)
|
||||
|
||||
anchor_sets_out = []
|
||||
for aset in frame_input.get('anchor_sets', []):
|
||||
verified_terms = []
|
||||
for tok in aset.get('terms', []):
|
||||
verified_terms.append(
|
||||
verify_term(tok, cand_idx, freq_set, spec_set, uniq_set,
|
||||
corpus_fd, corpus_md_df)
|
||||
)
|
||||
in_frame_count = sum(1 for t in verified_terms if t['in_frame'])
|
||||
not_in_frame_count = len(verified_terms) - in_frame_count
|
||||
anchor_sets_out.append({
|
||||
'id': aset['id'],
|
||||
'term_count': len(verified_terms),
|
||||
'in_frame_count': in_frame_count,
|
||||
'not_in_frame_count': not_in_frame_count,
|
||||
'terms': verified_terms,
|
||||
})
|
||||
|
||||
result = {
|
||||
'frame_number': frame_info['frame_number'],
|
||||
'total_unique_tokens': frame_info['total_unique_tokens'],
|
||||
'anchor_sets': anchor_sets_out,
|
||||
}
|
||||
if 'notes' in frame_input:
|
||||
result['notes'] = frame_input['notes']
|
||||
if 'review_needed' in frame_input:
|
||||
result['review_needed'] = verify_review_needed(
|
||||
frame_input['review_needed'], cand_idx, corpus_fd, corpus_md_df
|
||||
)
|
||||
if 'flagged_terms' in frame_input:
|
||||
result['flagged_terms'] = verify_flagged_terms(
|
||||
frame_input['flagged_terms'], cand_idx, corpus_fd, corpus_md_df
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# ---------- 렌더 ----------
|
||||
|
||||
def term_row_md(t):
|
||||
if t['in_frame']:
|
||||
flags = []
|
||||
if t['is_special']: flags.append('S')
|
||||
if t['mdx_hit']: flags.append('M')
|
||||
flag_str = ''.join(flags) if flags else '—'
|
||||
ex = truncate(t['example'], EXAMPLE_TRUNCATE) if t.get('example') else '—'
|
||||
return (f"| ✓ {t['token']} | {t['local_count']} | {t['frame_df']} | "
|
||||
f"{t['mdx_df']} | {flag_str} | {'+'.join(t['source_groups'])} | {ex} |")
|
||||
else:
|
||||
note = t.get('note', '')
|
||||
return (f"| ⚠ {t['token']} | — | — | — | — | **not_in_frame** | {note} |")
|
||||
|
||||
|
||||
def anchor_set_md(aset):
|
||||
lines = [
|
||||
f"##### anchor_set: `{aset['id']}` "
|
||||
f"({aset['in_frame_count']}/{aset['term_count']} in_frame"
|
||||
+ (f", ⚠ {aset['not_in_frame_count']} not_in_frame" if aset['not_in_frame_count'] else "")
|
||||
+ ")",
|
||||
"",
|
||||
"| token | local | df/32 | mdx/3 | flags | source | example |",
|
||||
"|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for t in aset['terms']:
|
||||
lines.append(term_row_md(t))
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def frame_section_md(fid, frame_out):
|
||||
head = (f"### Frame {frame_out['frame_number']} / {fid} "
|
||||
f"(total_unique {frame_out['total_unique_tokens']})")
|
||||
sets = [anchor_set_md(a) for a in frame_out['anchor_sets']]
|
||||
return head + "\n\n" + "\n\n".join(sets)
|
||||
|
||||
|
||||
def main():
|
||||
input_data = yaml.safe_load(INPUT.read_text(encoding='utf-8'))
|
||||
candidates_data = yaml.safe_load(CANDIDATES.read_text(encoding='utf-8'))
|
||||
normalized = yaml.safe_load(NORMALIZED.read_text(encoding='utf-8'))
|
||||
corpus_fd = normalized['corpus']['token_frame_df']
|
||||
corpus_md_df = normalized['corpus']['token_mdx_df']
|
||||
|
||||
output = {
|
||||
'meta': {
|
||||
'pipeline_step': 6,
|
||||
'scope': 'TARGET_FRAMES (13, 14, 18, 29)',
|
||||
'note': (
|
||||
'AI 자동 묶음 없음. anchor_sets_input.yaml 이 사람 편집 source. '
|
||||
'이 파이프라인은 존재 검증 + 근거 첨부만 수행. '
|
||||
'not_in_frame 은 에러가 아니라 사용자 재검토 플래그.'
|
||||
),
|
||||
},
|
||||
'frames': {},
|
||||
}
|
||||
|
||||
total_sets = 0
|
||||
total_terms = 0
|
||||
total_in_frame = 0
|
||||
total_not_in_frame = 0
|
||||
not_in_frame_details = [] # (frame_id, set_id, token, note)
|
||||
|
||||
for fid, frame_input in input_data.get('frames', {}).items():
|
||||
fid_str = str(fid)
|
||||
if fid_str not in candidates_data['frames']:
|
||||
print(f"WARNING: frame {fid_str} 이 anchor_candidates.yaml 에 없음")
|
||||
continue
|
||||
frame_out = process_frame(fid_str, frame_input, candidates_data,
|
||||
corpus_fd, corpus_md_df)
|
||||
output['frames'][fid_str] = frame_out
|
||||
|
||||
for aset in frame_out['anchor_sets']:
|
||||
total_sets += 1
|
||||
total_terms += aset['term_count']
|
||||
total_in_frame += aset['in_frame_count']
|
||||
total_not_in_frame += aset['not_in_frame_count']
|
||||
for t in aset['terms']:
|
||||
if not t['in_frame']:
|
||||
not_in_frame_details.append({
|
||||
'frame_id': fid_str,
|
||||
'frame_number': frame_out['frame_number'],
|
||||
'set_id': aset['id'],
|
||||
'token': t['token'],
|
||||
'in_corpus': t.get('in_corpus', False),
|
||||
'note': t.get('note', ''),
|
||||
})
|
||||
|
||||
output['meta']['totals'] = {
|
||||
'frames': len(output['frames']),
|
||||
'anchor_sets': total_sets,
|
||||
'terms': total_terms,
|
||||
'in_frame': total_in_frame,
|
||||
'not_in_frame': total_not_in_frame,
|
||||
}
|
||||
output['meta']['not_in_frame_details'] = not_in_frame_details
|
||||
|
||||
OUT_YAML.write_text(
|
||||
yaml.safe_dump(output, allow_unicode=True, sort_keys=False, width=300),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# md 생성
|
||||
md_sections = [
|
||||
"# Anchor Sets Report — Step 6",
|
||||
"",
|
||||
f"- scope: TARGET_FRAMES 4개 (13, 14, 18, 29)",
|
||||
f"- frames: {len(output['frames'])}",
|
||||
f"- anchor_sets: {total_sets}",
|
||||
f"- terms: {total_terms} ({total_in_frame} in_frame, {total_not_in_frame} not_in_frame)",
|
||||
f"- source: `anchor_sets_input.yaml` (사람이 직접 편집)",
|
||||
f"- 이 보고서: `pipeline_06_anchor_sets.py` 가 검증 + 근거 첨부 후 생성",
|
||||
"",
|
||||
]
|
||||
if not_in_frame_details:
|
||||
md_sections.append("## ⚠ not_in_frame terms (사용자 재검토)")
|
||||
md_sections.append("")
|
||||
md_sections.append("| Frame | set_id | token | in_corpus | note |")
|
||||
md_sections.append("|---|---|---|---|---|")
|
||||
for d in not_in_frame_details:
|
||||
md_sections.append(
|
||||
f"| Frame {d['frame_number']} / {d['frame_id']} | {d['set_id']} | "
|
||||
f"{d['token']} | {'Y' if d['in_corpus'] else 'N'} | {d['note']} |"
|
||||
)
|
||||
md_sections.append("")
|
||||
|
||||
md_sections.append("## Frame 별 anchor_sets 상세")
|
||||
md_sections.append("")
|
||||
md_sections.append("**flags**: S = special token, M = mdx_hit")
|
||||
md_sections.append("")
|
||||
for fid, frame_out in output['frames'].items():
|
||||
md_sections.append(frame_section_md(fid, frame_out))
|
||||
md_sections.append("")
|
||||
|
||||
md_text = '\n'.join(md_sections)
|
||||
OUT_MD.write_text(md_text, encoding='utf-8')
|
||||
|
||||
# html
|
||||
html_body = markdown.markdown(md_text, extensions=['tables'])
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Anchor Sets Report — Step 6</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1200px; margin: 2em auto; padding: 0 1em; line-height: 1.5; color: #222; }}
|
||||
h1 {{ border-bottom: 2px solid #333; padding-bottom: 0.2em; }}
|
||||
h2 {{ margin-top: 2em; border-bottom: 1px solid #ccc; padding-bottom: 0.2em; }}
|
||||
h3 {{ margin-top: 1.8em; color: #333; }}
|
||||
h5 {{ margin-top: 1em; margin-bottom: 0.3em; color: #0a6; font-size: 0.95em; }}
|
||||
table {{ border-collapse: collapse; margin: 0.3em 0 1.2em 0; font-size: 0.9em; width: 100%; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: 5px 9px; text-align: left; vertical-align: top; }}
|
||||
th {{ background: #f4f4f4; }}
|
||||
code {{ background: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
||||
strong {{ color: #d33; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_body}
|
||||
</body>
|
||||
</html>"""
|
||||
OUT_HTML.write_text(html, encoding='utf-8')
|
||||
|
||||
print(f"[Step 6] anchor_sets 검증 + 근거 첨부 완료")
|
||||
print(f" frames: {len(output['frames'])}")
|
||||
print(f" anchor_sets: {total_sets}")
|
||||
print(f" terms: {total_terms}")
|
||||
print(f" in_frame: {total_in_frame}")
|
||||
print(f" not_in_frame: {total_not_in_frame}")
|
||||
if not_in_frame_details:
|
||||
print()
|
||||
print(f" [⚠ not_in_frame 상세]")
|
||||
for d in not_in_frame_details:
|
||||
print(f" Frame {d['frame_number']} [{d['set_id']}] {d['token']!r} "
|
||||
f"corpus={'Y' if d['in_corpus'] else 'N'} {d['note']}")
|
||||
print()
|
||||
print(f"산출:")
|
||||
print(f" yaml: {OUT_YAML}")
|
||||
print(f" md: {OUT_MD}")
|
||||
print(f" html: {OUT_HTML}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,498 @@
|
||||
"""Step 7: 통계 기반 자동 anchor 후보 증거집 생성.
|
||||
|
||||
Step 7 산출물은 anchor_set 확정본이 아니다.
|
||||
Step 7 산출물은 source_text 기반 anchor 후보 증거집이다.
|
||||
|
||||
이 보고서는 원문 텍스트 항목에서 정규화/형태소 분석으로 나온 후보만 보여준다.
|
||||
의미 이름 붙이기, 병합 확정, 없는 단어 추가는 수행하지 않는다.
|
||||
|
||||
AI 개입 없음:
|
||||
- LLM 호출 없음 (anthropic/openai/transformers 등 미사용)
|
||||
- 의미 이름 붙임 없음 (ID 는 자동 생성: auto_set_{frame_id}_{idx:04d})
|
||||
- 병합 자동 확정 없음 (status: candidate_only)
|
||||
- 단어 추가/수정 없음
|
||||
|
||||
Generation rules:
|
||||
- SOURCE_TEXT_TOKEN_SET_V1 — text_node 원문을 정규화 + Kiwi tokenize
|
||||
- COOCCURRENCE_PAIR_V1 — 같은 text_node 에 공존한 token pair
|
||||
- SHARED_TOKEN_MERGE_CANDIDATE_V1 — 공통 token 기반 병합 후보 (확정 아님)
|
||||
"""
|
||||
import hashlib
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
import markdown
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from keyword_normalizer import (
|
||||
USER_DICT_SL, USER_DICT_NNG,
|
||||
load_phrase_variants, build_substitutions,
|
||||
apply_substitutions, build_kiwi, extract_tokens,
|
||||
)
|
||||
|
||||
INPUT_NODES = HERE / "actual_text_nodes.yaml"
|
||||
INPUT_NORMALIZED = HERE / "normalized_text_tokens.yaml"
|
||||
SYNONYMS = HERE / "synonyms.yaml"
|
||||
NORMALIZER_MODULE = HERE / "keyword_normalizer.py"
|
||||
|
||||
OUT_YAML = HERE / "auto_anchor_candidates.yaml"
|
||||
OUT_MD = HERE / "auto_anchor_candidates_report.md"
|
||||
OUT_HTML = HERE / "auto_anchor_candidates_report.html"
|
||||
|
||||
BROAD_FRAME_DF_THRESHOLD = 10 # 32 frame 중 10 이상 = broad
|
||||
|
||||
SPECIAL_TOKENS = set(USER_DICT_SL) | set(USER_DICT_NNG)
|
||||
|
||||
|
||||
def sha256_file(p):
|
||||
return hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
nodes = yaml.safe_load(INPUT_NODES.read_text(encoding='utf-8'))
|
||||
normalized = yaml.safe_load(INPUT_NORMALIZED.read_text(encoding='utf-8'))
|
||||
phrase_variants = load_phrase_variants(SYNONYMS)
|
||||
subs = build_substitutions(phrase_variants)
|
||||
kiwi = build_kiwi()
|
||||
|
||||
corpus_fd = normalized['corpus']['token_frame_df']
|
||||
corpus_md_df = normalized['corpus']['token_mdx_df']
|
||||
|
||||
frame_ids = sorted(nodes['frames'].keys())
|
||||
|
||||
source_text_sets = {}
|
||||
cooccur_by_frame = {}
|
||||
merge_candidates = {}
|
||||
frame_stats = {}
|
||||
|
||||
for fnum, fid in enumerate(frame_ids, 1):
|
||||
finfo = nodes['frames'][fid]
|
||||
text_nodes = finfo.get('text_nodes', [])
|
||||
|
||||
per_text = [] # [(idx(1-base), raw, normalized_text, tokens)]
|
||||
for i, t in enumerate(text_nodes, 1):
|
||||
normalized_text = apply_substitutions(t, subs, None, None)
|
||||
tokens = extract_tokens(kiwi, normalized_text)
|
||||
per_text.append((i, t, normalized_text, tokens))
|
||||
|
||||
# frame 내 local count (같은 token 이 여러 text_node 에 나온 경우 포함)
|
||||
frame_local = Counter()
|
||||
for _, _, _, toks in per_text:
|
||||
for tk in toks:
|
||||
frame_local[tk] += 1
|
||||
|
||||
# source_text_sets (SOURCE_TEXT_TOKEN_SET_V1)
|
||||
frame_sets_ids = []
|
||||
for idx, raw, nt, tokens in per_text:
|
||||
if not tokens:
|
||||
continue
|
||||
set_id = f"auto_set_{fid}_{idx:04d}"
|
||||
frame_sets_ids.append(set_id)
|
||||
|
||||
terms = []
|
||||
for tok in tokens:
|
||||
terms.append({
|
||||
'token': tok,
|
||||
'from_source_text_index': idx,
|
||||
'local_count_in_frame': frame_local[tok],
|
||||
'frame_df': corpus_fd.get(tok, 0),
|
||||
'mdx_df': corpus_md_df.get(tok, 0),
|
||||
'is_special': tok in SPECIAL_TOKENS,
|
||||
})
|
||||
|
||||
avg_fd = round(sum(t['frame_df'] for t in terms) / len(terms), 2)
|
||||
|
||||
# weak_reason 계산 (자동 삭제 아님, 플래그만)
|
||||
weak_reason = []
|
||||
if len(tokens) < 2:
|
||||
weak_reason.append('single_token')
|
||||
if all(t['frame_df'] >= BROAD_FRAME_DF_THRESHOLD for t in terms):
|
||||
weak_reason.append('all_terms_broad')
|
||||
if all(t['mdx_df'] == 0 for t in terms):
|
||||
weak_reason.append('all_terms_mdx_zero')
|
||||
|
||||
source_text_sets[set_id] = {
|
||||
'generation_rule': 'SOURCE_TEXT_TOKEN_SET_V1',
|
||||
'source_type': 'figma_text_layer', # enum: figma_text_layer | image_text_verified | mdx_section
|
||||
'source_file': f"figma_to_html_agent/blocks/{fid}/texts.md",
|
||||
'frame_id': fid,
|
||||
'frame_number': fnum,
|
||||
'source_text_index': idx,
|
||||
'source_text_index_base': 1,
|
||||
'source_text_raw': raw,
|
||||
'source_text_normalized': nt,
|
||||
'term_values': tokens,
|
||||
'terms': terms,
|
||||
'stats': {
|
||||
'term_count': len(tokens),
|
||||
'contains_special': any(t['is_special'] for t in terms),
|
||||
'contains_unique_to_frame': any(t['frame_df'] == 1 for t in terms),
|
||||
'avg_frame_df': avg_fd,
|
||||
},
|
||||
'candidate_strength': {
|
||||
'weak_reason': weak_reason,
|
||||
'is_weak': bool(weak_reason),
|
||||
},
|
||||
}
|
||||
|
||||
# co-occurrence (COOCCURRENCE_PAIR_V1)
|
||||
pairs = Counter()
|
||||
for _, _, _, tokens in per_text:
|
||||
uniq_sorted = sorted(set(tokens))
|
||||
for i_a, a in enumerate(uniq_sorted):
|
||||
for b in uniq_sorted[i_a + 1:]:
|
||||
pairs[(a, b)] += 1
|
||||
if pairs:
|
||||
cooccur_by_frame[fid] = {
|
||||
'generation_rule': 'COOCCURRENCE_PAIR_V1',
|
||||
'frame_id': fid,
|
||||
'frame_number': fnum,
|
||||
'pair_count': len(pairs),
|
||||
'pairs': [
|
||||
{'tokens': list(p), 'count': c}
|
||||
for p, c in sorted(pairs.items(), key=lambda x: (-x[1], x[0]))
|
||||
],
|
||||
}
|
||||
|
||||
# merge_candidates (SHARED_TOKEN_MERGE_CANDIDATE_V1)
|
||||
shared_map = defaultdict(list)
|
||||
for sid in frame_sets_ids:
|
||||
for tok in source_text_sets[sid]['term_values']:
|
||||
shared_map[tok].append(sid)
|
||||
merge_idx = 0
|
||||
for shared_tok, sids in sorted(shared_map.items()):
|
||||
if len(sids) < 2:
|
||||
continue
|
||||
merge_idx += 1
|
||||
safe_tok = shared_tok.replace('/', '-').replace(' ', '_')
|
||||
merge_id = f"merge_{fid}_{safe_tok}_{merge_idx:04d}"
|
||||
merged = []
|
||||
seen = set()
|
||||
for sid in sids:
|
||||
for tok in source_text_sets[sid]['term_values']:
|
||||
if tok not in seen:
|
||||
seen.add(tok)
|
||||
merged.append(tok)
|
||||
shared_fd = corpus_fd.get(shared_tok, 0)
|
||||
merge_candidates[merge_id] = {
|
||||
'generation_rule': 'SHARED_TOKEN_MERGE_CANDIDATE_V1',
|
||||
'status': 'candidate_only',
|
||||
'frame_id': fid,
|
||||
'shared_token': shared_tok,
|
||||
'shared_token_frame_df': shared_fd,
|
||||
'shared_token_is_broad': shared_fd >= BROAD_FRAME_DF_THRESHOLD,
|
||||
'source_set_ids': sids,
|
||||
'source_set_count': len(sids),
|
||||
'merged_terms': merged,
|
||||
'merged_term_count': len(merged),
|
||||
}
|
||||
|
||||
# frame_stats
|
||||
unique_to_frame = sorted(
|
||||
t for t in frame_local if corpus_fd.get(t, 0) == 1
|
||||
)
|
||||
top_local = sorted(frame_local.items(), key=lambda x: (-x[1], x[0]))[:15]
|
||||
frame_stats[fid] = {
|
||||
'frame_number': fnum,
|
||||
'total_text_nodes': len(text_nodes),
|
||||
'total_source_sets': len(frame_sets_ids),
|
||||
'total_tokens_in_frame': sum(frame_local.values()),
|
||||
'unique_tokens_in_frame': len(frame_local),
|
||||
'unique_to_frame_tokens': unique_to_frame,
|
||||
'top_local_count': [{'token': t, 'local': c} for t, c in top_local],
|
||||
}
|
||||
|
||||
# manifest
|
||||
meta = {
|
||||
'pipeline_step': 7,
|
||||
'disclaimer': (
|
||||
'Step 7 산출물은 anchor_set 확정본이 아니다. '
|
||||
'Step 7 산출물은 source_text 기반 anchor 후보 증거집이다. '
|
||||
'이 보고서는 원문 텍스트 항목에서 정규화/형태소 분석으로 나온 후보만 보여준다. '
|
||||
'의미 이름 붙이기, 병합 확정, 없는 단어 추가는 수행하지 않는다.'
|
||||
),
|
||||
'generation_rules': [
|
||||
'SOURCE_TEXT_TOKEN_SET_V1',
|
||||
'COOCCURRENCE_PAIR_V1',
|
||||
'SHARED_TOKEN_MERGE_CANDIDATE_V1',
|
||||
],
|
||||
'ai_generation_used': False,
|
||||
'manual_grouping_used': False,
|
||||
'deterministic': True,
|
||||
'input_files': {
|
||||
'actual_text_nodes.yaml': {'sha256': sha256_file(INPUT_NODES)},
|
||||
'synonyms.yaml': {'sha256': sha256_file(SYNONYMS)},
|
||||
'normalized_text_tokens.yaml': {'sha256': sha256_file(INPUT_NORMALIZED)},
|
||||
'keyword_normalizer.py': {'sha256': sha256_file(NORMALIZER_MODULE)},
|
||||
},
|
||||
'totals': {
|
||||
'frames': len(frame_ids),
|
||||
'source_text_sets': len(source_text_sets),
|
||||
'cooccur_frames': len(cooccur_by_frame),
|
||||
'merge_candidates': len(merge_candidates),
|
||||
},
|
||||
'broad_frame_df_threshold': BROAD_FRAME_DF_THRESHOLD,
|
||||
'special_tokens': sorted(SPECIAL_TOKENS),
|
||||
'source_type_policy': {
|
||||
'current': 'figma_text_layer_default',
|
||||
'possible_values': [
|
||||
'figma_text_layer',
|
||||
'image_text_verified',
|
||||
'mdx_section',
|
||||
],
|
||||
'note': 'OCR 보강 텍스트가 늘어나면 heading 또는 별도 source map 으로 구분한다. 현재 모든 source_text 는 figma_text_layer 로 분류.',
|
||||
},
|
||||
}
|
||||
|
||||
output = {
|
||||
'meta': meta,
|
||||
'source_text_sets': source_text_sets,
|
||||
'cooccurrence_by_frame': cooccur_by_frame,
|
||||
'merge_candidates': merge_candidates,
|
||||
'frame_stats': frame_stats,
|
||||
}
|
||||
|
||||
OUT_YAML.write_text(
|
||||
yaml.safe_dump(output, allow_unicode=True, sort_keys=False, width=300),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# 약한 이유 한국어 매핑
|
||||
WEAK_REASON_KR = {
|
||||
'single_token': '키워드 1개',
|
||||
'all_terms_broad': '모든 키워드가 흔함',
|
||||
'all_terms_mdx_zero': 'MDX 매칭 없음',
|
||||
}
|
||||
|
||||
# md report — 상단 주의사항 + frame 별 키워드 세트 (한국어)
|
||||
md = [
|
||||
"# 자동 키워드 후보 증거집 — Step 7",
|
||||
"",
|
||||
"## ⚠ 주의 (AI 개입 없음)",
|
||||
"",
|
||||
"**이 보고서는 anchor 세트 확정본이 아닙니다.**",
|
||||
"**원문 텍스트 기반으로 자동 추출된 anchor 후보 증거집입니다.**",
|
||||
"",
|
||||
"원문 항목을 정규화/형태소 분석한 결과만 보여주며, 다음은 수행하지 않습니다:",
|
||||
"- 의미 이름 붙이기",
|
||||
"- 병합 확정",
|
||||
"- 원문에 없는 단어 추가",
|
||||
"",
|
||||
"### 생성 방식",
|
||||
"",
|
||||
"| 규칙 ID | 설명 |",
|
||||
"|---|---|",
|
||||
"| `SOURCE_TEXT_TOKEN_SET_V1` | 원문 한 줄을 형태소 분석한 결과 |",
|
||||
"| `COOCCURRENCE_PAIR_V1` | 같은 원문 항목에 같이 등장한 단어쌍 |",
|
||||
"| `SHARED_TOKEN_MERGE_CANDIDATE_V1` | 공통 단어로 묶을 수 있는 병합 후보 (확정 아님) |",
|
||||
"",
|
||||
"### AI 개입 없음 증거",
|
||||
"",
|
||||
f"- **LLM 호출 없음** (`ai_generation_used: false`)",
|
||||
f"- **의미 이름 붙이기 없음** (세트 ID 는 `auto_set_프레임ID_번호` 형식, 숫자만)",
|
||||
f"- **병합 자동 확정 없음** (모든 병합 후보는 `status: candidate_only`)",
|
||||
f"- **원문에 없는 단어 추가 없음** (키워드는 원문 형태소 분석 결과만)",
|
||||
"",
|
||||
"### 입력 파일 지문 (재현성)",
|
||||
"",
|
||||
"| 파일 | sha256 |",
|
||||
"|---|---|",
|
||||
]
|
||||
for k, v in meta['input_files'].items():
|
||||
md.append(f"| `{k}` | `{v['sha256']}` |")
|
||||
md += [
|
||||
"",
|
||||
"### 통계",
|
||||
"",
|
||||
f"- **프레임 수**: {meta['totals']['frames']}",
|
||||
f"- **키워드 세트 수**: {meta['totals']['source_text_sets']} (원문 항목 수와 동일)",
|
||||
f"- **병합 후보 수**: {meta['totals']['merge_candidates']} (모두 확정 아님)",
|
||||
f"- **흔한 단어 기준**: 32 프레임 중 **{meta['broad_frame_df_threshold']}개** 이상 등장하면 '흔함' 으로 표시",
|
||||
"",
|
||||
"### 용어",
|
||||
"",
|
||||
"| 용어 | 의미 |",
|
||||
"|---|---|",
|
||||
"| 원문 번호 | texts.md 의 몇 번째 줄인지 (1부터 시작) |",
|
||||
"| 키워드 목록 | 그 원문 한 줄을 형태소 분석해서 나온 단어들 |",
|
||||
"| 약한 이유 | 이 세트가 anchor 로 쓰기 약한 이유 (키워드 1개 / 모든 키워드 흔함 / MDX 매칭 없음) |",
|
||||
"| 특수 | 특수 토큰 포함 (S/W, H/W, 2D, 3D, DX, BIM, As-is, To-Be, 결과혁신, 과정혁신, 필수조건, 의사소통, 시행착오) |",
|
||||
"| 전용 | 이 프레임에만 등장하는 키워드 포함 |",
|
||||
"| 평균 분포 | 세트의 키워드들이 평균 몇 개 프레임에 등장하는가 (낮을수록 이 프레임 특화) |",
|
||||
"| 공통 단어 | 여러 세트를 묶을 수 있는 공통 단어 (병합 후보의 근거) |",
|
||||
"| 공통어 흔함 (⚠) | 공통 단어가 10개 이상 프레임에 등장 — 묶으면 너무 넓어질 수 있음 |",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"## 프레임별 키워드 후보",
|
||||
"",
|
||||
]
|
||||
|
||||
for fnum, fid in enumerate(frame_ids, 1):
|
||||
fstat = frame_stats[fid]
|
||||
# 해당 frame 의 source_text_sets
|
||||
frame_sets = [
|
||||
(sid, info) for sid, info in source_text_sets.items()
|
||||
if info['frame_id'] == fid
|
||||
]
|
||||
frame_sets.sort(key=lambda x: x[1]['source_text_index'])
|
||||
|
||||
# strong / weak 분류
|
||||
strong_sets = [(s, i) for s, i in frame_sets if not i['candidate_strength']['is_weak']]
|
||||
weak_sets = [(s, i) for s, i in frame_sets if i['candidate_strength']['is_weak']]
|
||||
|
||||
# merge_candidates 중 해당 frame
|
||||
frame_merges = [
|
||||
(mid, minfo) for mid, minfo in merge_candidates.items()
|
||||
if minfo['frame_id'] == fid
|
||||
]
|
||||
non_broad_merges = [(m, i) for m, i in frame_merges if not i['shared_token_is_broad']]
|
||||
broad_merges = [(m, i) for m, i in frame_merges if i['shared_token_is_broad']]
|
||||
|
||||
# special_in_frame 계산
|
||||
special_in_frame = sorted({
|
||||
t for _, info in frame_sets for t in info['term_values']
|
||||
if t in SPECIAL_TOKENS
|
||||
})
|
||||
|
||||
md.append(f"### 프레임 {fnum} / {fid}")
|
||||
md.append("")
|
||||
md.append("**요약**:")
|
||||
md.append("")
|
||||
md.append(f"- 🎯 **프레임 전용 키워드** ({len(fstat['unique_to_frame_tokens'])}개): "
|
||||
f"{', '.join('`'+t+'`' for t in fstat['unique_to_frame_tokens']) if fstat['unique_to_frame_tokens'] else '_(없음)_'}")
|
||||
md.append(f"- ⭐ **특수 토큰** ({len(special_in_frame)}개): "
|
||||
f"{', '.join('`'+t+'`' for t in special_in_frame) if special_in_frame else '_(없음)_'}")
|
||||
md.append(f"- 📄 **키워드 세트**: 쓸 만함 **{len(strong_sets)}** / 약함 **{len(weak_sets)}** / 전체 **{len(frame_sets)}**")
|
||||
md.append(f"- 🔗 **병합 후보**: 공통어 흔하지 않음 **{len(non_broad_merges)}** / 공통어 흔함 ⚠ **{len(broad_merges)}** / 전체 **{len(frame_merges)}**")
|
||||
md.append("")
|
||||
|
||||
# 1) 키워드 세트 상세
|
||||
md.append(f"<details><summary>📄 키워드 세트 상세 ({len(frame_sets)}개)</summary>")
|
||||
md.append("")
|
||||
md.append("| 번호 | 원문 | 키워드 목록 | 약한 이유 | 특수 | 전용 | 평균 분포 |")
|
||||
md.append("|---|---|---|---|---|---|---|")
|
||||
for sid, info in frame_sets:
|
||||
raw_short = info['source_text_raw']
|
||||
if len(raw_short) > 60:
|
||||
raw_short = raw_short[:59] + '…'
|
||||
terms_str = ', '.join(info['term_values'])
|
||||
weak_reasons_kr = [WEAK_REASON_KR.get(r, r) for r in info['candidate_strength']['weak_reason']]
|
||||
weak = ', '.join(weak_reasons_kr) or '—'
|
||||
sp = '✓' if info['stats']['contains_special'] else ''
|
||||
uq = '✓' if info['stats']['contains_unique_to_frame'] else ''
|
||||
idx_num = info['source_text_index']
|
||||
md.append(f"| {idx_num} | {raw_short} | {terms_str} | {weak} | {sp} | {uq} | {info['stats']['avg_frame_df']} |")
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
|
||||
# 2) 병합 후보 non-broad
|
||||
md.append(f"<details><summary>🔗 병합 후보 · 공통어 흔하지 않음 ({len(non_broad_merges)}개)</summary>")
|
||||
md.append("")
|
||||
if non_broad_merges:
|
||||
md.append("| 공통 단어 | 공통어 프레임 분포 | 병합 대상 번호 | 합친 키워드 |")
|
||||
md.append("|---|---|---|---|")
|
||||
for mid, minfo in sorted(non_broad_merges, key=lambda x: (-x[1]['source_set_count'], x[0])):
|
||||
src_ids_short = ', '.join(str(int(sid.split('_')[-1])) for sid in minfo['source_set_ids'])
|
||||
merged_short = ', '.join(minfo['merged_terms'])
|
||||
if len(merged_short) > 80:
|
||||
merged_short = merged_short[:79] + '…'
|
||||
md.append(f"| `{minfo['shared_token']}` | {minfo['shared_token_frame_df']} / 32 | {src_ids_short} | {merged_short} |")
|
||||
else:
|
||||
md.append("_(없음)_")
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
|
||||
# 3) 병합 후보 broad
|
||||
md.append(f"<details><summary>🔗 병합 후보 · 공통어 흔함 ⚠ 주의 ({len(broad_merges)}개)</summary>")
|
||||
md.append("")
|
||||
if broad_merges:
|
||||
md.append("| 공통 단어 | 공통어 프레임 분포 | 병합 대상 번호 | 합친 키워드 |")
|
||||
md.append("|---|---|---|---|")
|
||||
for mid, minfo in sorted(broad_merges, key=lambda x: (-x[1]['source_set_count'], x[0])):
|
||||
src_ids_short = ', '.join(str(int(sid.split('_')[-1])) for sid in minfo['source_set_ids'])
|
||||
merged_short = ', '.join(minfo['merged_terms'])
|
||||
if len(merged_short) > 80:
|
||||
merged_short = merged_short[:79] + '…'
|
||||
md.append(f"| `{minfo['shared_token']}` | ⚠ {minfo['shared_token_frame_df']} / 32 | {src_ids_short} | {merged_short} |")
|
||||
else:
|
||||
md.append("_(없음)_")
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
|
||||
# 4) cooccurrence
|
||||
pairs = cooccur_by_frame.get(fid, {}).get('pairs', [])
|
||||
md.append(f"<details><summary>📐 같이 나온 단어쌍 ({len(pairs)}개)</summary>")
|
||||
md.append("")
|
||||
if pairs:
|
||||
md.append("| 단어쌍 | 빈도 |")
|
||||
md.append("|---|---|")
|
||||
for p in pairs:
|
||||
md.append(f"| {', '.join(p['tokens'])} | {p['count']} |")
|
||||
else:
|
||||
md.append("_(없음)_")
|
||||
md.append("")
|
||||
md.append("</details>")
|
||||
md.append("")
|
||||
|
||||
md_text = '\n'.join(md)
|
||||
OUT_MD.write_text(md_text, encoding='utf-8')
|
||||
|
||||
html_body = markdown.markdown(md_text, extensions=['tables'])
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Auto Anchor Candidates Report — Step 7</title>
|
||||
<style>
|
||||
body {{ font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1300px; margin: 2em auto; padding: 0 1em 4em; line-height: 1.5; color: #222; }}
|
||||
h1 {{ border-bottom: 2px solid #333; padding-bottom: 0.2em; }}
|
||||
h2 {{ margin-top: 2em; border-bottom: 1px solid #ccc; padding-bottom: 0.3em; }}
|
||||
h3 {{ margin-top: 1.8em; color: #333; }}
|
||||
table {{ border-collapse: collapse; margin: 0.3em 0 1em 0; font-size: 0.82em; width: 100%; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: 4px 8px; text-align: left; vertical-align: top; word-break: break-word; }}
|
||||
th {{ background: #f4f4f4; }}
|
||||
code {{ background: #f4f4f4; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }}
|
||||
strong {{ color: #0a6; }}
|
||||
h2:first-of-type + p + p, h2:first-of-type ~ p {{ background: #fffbdc; padding: 0.5em 0.8em; border-left: 4px solid #d33; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_body}
|
||||
</body>
|
||||
</html>"""
|
||||
OUT_HTML.write_text(html, encoding='utf-8')
|
||||
|
||||
output_sha = sha256_file(OUT_YAML)
|
||||
print("[Step 7] auto_anchor_candidates 생성 완료")
|
||||
print()
|
||||
print(" AI 개입 없음 증거:")
|
||||
print(f" ai_generation_used: {meta['ai_generation_used']}")
|
||||
print(f" manual_grouping_used: {meta['manual_grouping_used']}")
|
||||
print(f" deterministic: {meta['deterministic']}")
|
||||
print()
|
||||
print(" Input sha256:")
|
||||
for k, v in meta['input_files'].items():
|
||||
print(f" {k}: {v['sha256'][:16]}...")
|
||||
print()
|
||||
print(" Output sha256:")
|
||||
print(f" auto_anchor_candidates.yaml: {output_sha[:16]}...")
|
||||
print()
|
||||
print(f" Totals:")
|
||||
for k, v in meta['totals'].items():
|
||||
print(f" {k}: {v}")
|
||||
print()
|
||||
print(f"산출:")
|
||||
print(f" yaml: {OUT_YAML}")
|
||||
print(f" md: {OUT_MD}")
|
||||
print(f" html: {OUT_HTML}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,215 @@
|
||||
"""Pipeline Step 8 — V2: V1 Top-K semantic rerank
|
||||
|
||||
파이프라인 위치:
|
||||
V1 (pipeline_06_2_mdx_matching.py) → **V2 (이 스크립트)** → V3 → V4
|
||||
|
||||
설계 원칙:
|
||||
- V1 결과(mdx_matching_result.yaml) 는 **고정 baseline**. 건드리지 않음.
|
||||
- V1 rank_by_matching_score Top-K 만 rerank 대상 — K=5 (기준 잠금).
|
||||
- 가중합 아님. **순수 rerank** — V1 점수는 저장만, 재정렬 키는 의미 유사도뿐.
|
||||
- AI 판단 없음. ko-sroberta 임베딩은 결정론적 (seed 고정 가정).
|
||||
|
||||
입력:
|
||||
- mdx_matching_result.yaml (V1)
|
||||
- MDX_SECTIONS 원문 (pipeline_01 의 MDX_DIR + section config)
|
||||
- Figma frame content (phase_common.load_32_frames() → analysis.md '내용')
|
||||
|
||||
처리:
|
||||
1. 각 MDX 섹션(TARGET 4 + Holdout 3)별로:
|
||||
- V1 Top-5 frame_id 추출
|
||||
- MDX summary = detect_mdx.build_summary (title + 첫 문단 + slot labels)
|
||||
- Top-5 frame.content ↔ summary 의 ko-sroberta cosine 계산
|
||||
- cosine 내림차순으로 재정렬
|
||||
2. lock_snapshot 기록 (V1 yaml + 이 스크립트 + 의존 모듈 sha256)
|
||||
|
||||
출력:
|
||||
- v2_semantic_rerank_result.yaml
|
||||
"""
|
||||
import hashlib
|
||||
import sys
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from phase_common import load_32_frames, load_frame_index
|
||||
from detect_mdx import detect_mdx_analysis
|
||||
from embeddings import embed_texts, cosine
|
||||
from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR
|
||||
|
||||
TOP_K = 5
|
||||
MODEL_ID = 'jhgan/ko-sroberta-multitask'
|
||||
|
||||
# ============================================================
|
||||
# 상수 — lock_snapshot 대상
|
||||
# ============================================================
|
||||
LOCK_SNAPSHOT_FILES = [
|
||||
'pipeline_08_v2_semantic_rerank.py',
|
||||
'embeddings.py',
|
||||
'detect_mdx.py',
|
||||
'phase_common.py',
|
||||
'pipeline_01_extract_nodes.py',
|
||||
'synonyms.yaml',
|
||||
'mdx_matching_result.yaml',
|
||||
]
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def extract_mdx_raw_section(section_id: str) -> tuple[str, str]:
|
||||
"""(title, body_text) 반환. title = 첫 줄 heading 마커 제거, body_text = 원문 그대로."""
|
||||
cfg = MDX_SECTIONS[section_id]
|
||||
p = MDX_DIR / cfg['file']
|
||||
lines = p.read_text(encoding='utf-8').split('\n')
|
||||
start_idx = None
|
||||
for i, ln in enumerate(lines):
|
||||
if ln.strip() == cfg['start'].strip():
|
||||
start_idx = i
|
||||
break
|
||||
if start_idx is None:
|
||||
raise ValueError(f"start_heading 찾지 못함: {cfg['start']!r} in {p}")
|
||||
end_idx = len(lines)
|
||||
if cfg.get('end_prefix'):
|
||||
for i in range(start_idx + 1, len(lines)):
|
||||
if lines[i].strip().startswith(cfg['end_prefix']):
|
||||
end_idx = i
|
||||
break
|
||||
section_lines = lines[start_idx:end_idx]
|
||||
raw = '\n'.join(section_lines)
|
||||
# title = heading 마커(#, ##, ###) 제거한 첫 줄
|
||||
title_line = section_lines[0].lstrip('#').strip()
|
||||
return title_line, raw
|
||||
|
||||
|
||||
def main():
|
||||
# 1. V1 결과 로드
|
||||
v1_path = HERE / 'mdx_matching_result.yaml'
|
||||
v1 = yaml.safe_load(v1_path.read_text(encoding='utf-8'))
|
||||
|
||||
# 2. Figma frame 로드 + content 임베딩 (한 번만)
|
||||
frames = load_32_frames()
|
||||
idx_data, frame_to_short = load_frame_index()
|
||||
fids = list(frames.keys())
|
||||
frame_contents = [frames[fid].get('content', '') for fid in fids]
|
||||
fid_to_idx = {fid: i for i, fid in enumerate(fids)}
|
||||
|
||||
print(f"[V2] ko-sroberta 모델 로드 + 32 frame content 임베딩...")
|
||||
frame_vecs = embed_texts(frame_contents)
|
||||
|
||||
# 3. 섹션별 처리
|
||||
out_sections = {}
|
||||
for sid, sec in v1['mdx_sections'].items():
|
||||
top = sec['rank_by_matching_score'][:TOP_K]
|
||||
top_fids = [r['frame_id'] for r in top]
|
||||
|
||||
# MDX summary 생성
|
||||
title, raw_text = extract_mdx_raw_section(sid)
|
||||
analysis = detect_mdx_analysis(raw_text, title, anchor_vocab=None)
|
||||
summary = analysis['summary']
|
||||
|
||||
# summary 임베딩
|
||||
mdx_vec = embed_texts([summary])[0]
|
||||
|
||||
# Top-K 에 대해서만 cosine 계산
|
||||
rerank = []
|
||||
for v1_rank_idx, r in enumerate(top, start=1):
|
||||
fid = r['frame_id']
|
||||
frame_idx = fid_to_idx[fid]
|
||||
sem = cosine(mdx_vec, frame_vecs[frame_idx])
|
||||
rerank.append({
|
||||
'frame_id': fid,
|
||||
'frame_number': r['frame_number'],
|
||||
'v1_rank': v1_rank_idx,
|
||||
'v1_score': r['matching_score'],
|
||||
'semantic_score': round(float(sem), 4),
|
||||
})
|
||||
|
||||
# semantic_score 내림차순 재정렬
|
||||
rerank.sort(key=lambda x: -x['semantic_score'])
|
||||
for new_rank, item in enumerate(rerank, start=1):
|
||||
item['v2_rank'] = new_rank
|
||||
|
||||
out_sections[sid] = {
|
||||
'section_type': sec.get('section_type'),
|
||||
'answer_frame_number': sec.get('answer_frame_number'),
|
||||
'mdx_title': title,
|
||||
'mdx_summary': summary,
|
||||
'top_k': TOP_K,
|
||||
'v1_top_k': [
|
||||
{
|
||||
'rank': i + 1,
|
||||
'frame_id': r['frame_id'],
|
||||
'frame_number': r['frame_number'],
|
||||
'v1_score': r['matching_score'],
|
||||
}
|
||||
for i, r in enumerate(top)
|
||||
],
|
||||
'v2_rerank': rerank,
|
||||
}
|
||||
|
||||
# 4. lock_snapshot
|
||||
lock = {
|
||||
'timestamp': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'model': MODEL_ID,
|
||||
'top_k': TOP_K,
|
||||
'files': {
|
||||
name: sha256_file(HERE / name) for name in LOCK_SNAPSHOT_FILES
|
||||
},
|
||||
'principle': [
|
||||
'V1 baseline 고정 — 재정렬 대상만 Top-K 로 한정',
|
||||
'가중합 금지 — 순수 semantic rerank',
|
||||
'Holdout 성적을 설계 근거로 사용하지 않음',
|
||||
],
|
||||
}
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'pipeline_step': '8.v2',
|
||||
'description': 'V1 Top-K 후보를 ko-sroberta cosine 으로 재정렬 (캐스케이드 rerank 1단)',
|
||||
'model': MODEL_ID,
|
||||
'similarity': 'cosine',
|
||||
'top_k': TOP_K,
|
||||
'mdx_summary_spec': 'title + 첫 일반 문단(≤120자) + slot label Top-5 join (detect_mdx.build_summary)',
|
||||
'frame_source': 'analysis.md "내용" 섹션 (phase_common.load_32_frames)',
|
||||
'v1_source': 'mdx_matching_result.yaml rank_by_matching_score',
|
||||
'holdout_sections': ['01-1', '02-1', '02-2.1'],
|
||||
'answer_map': v1['meta']['answer_map'],
|
||||
'lock_snapshot': lock,
|
||||
},
|
||||
'mdx_sections': out_sections,
|
||||
}
|
||||
|
||||
out_path = HERE / 'v2_semantic_rerank_result.yaml'
|
||||
out_path.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# 콘솔 요약
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(f"V2 재정렬 완료: {out_path}")
|
||||
print("=" * 70)
|
||||
answer_map = v1['meta']['answer_map']
|
||||
for sid, s in out_sections.items():
|
||||
answer_num = answer_map.get(sid)
|
||||
v1_top1 = s['v1_top_k'][0]['frame_number']
|
||||
v2_top1 = s['v2_rerank'][0]['frame_number']
|
||||
v2_top1_v1rank = s['v2_rerank'][0]['v1_rank']
|
||||
mark = ''
|
||||
if answer_num is not None:
|
||||
v1_ok = '✓' if v1_top1 == answer_num else '✗'
|
||||
v2_ok = '✓' if v2_top1 == answer_num else '✗'
|
||||
mark = f" 정답={answer_num} V1{v1_ok} V2{v2_ok}"
|
||||
else:
|
||||
mark = f" (holdout)"
|
||||
print(f" [{sid:8}] V1 top1=Frame {v1_top1:>2} → V2 top1=Frame {v2_top1:>2} (V1 rank {v2_top1_v1rank}){mark}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,368 @@
|
||||
"""Pipeline Step 8 — V3 r2: v2 스키마 기반 구조 rerank.
|
||||
|
||||
기존 V3 (pipeline_08_v3_structure_rerank.py) 의 `_COMPAT` 수작업 테이블 대신
|
||||
`structure_ontology_v2_final.yaml` 의 content_affinity + structure_intent +
|
||||
alternative_patterns 로 자동 파생 매칭.
|
||||
|
||||
매칭 공식 (스키마 초안 §4 반영):
|
||||
v3_score = 0.40 × layout_compat_v2
|
||||
+ 0.35 × affinity_match(mdx, frame)
|
||||
+ 0.25 × intent_match(mdx, frame)
|
||||
|
||||
layout_compat_v2 = 0.25 family + 0.20 relation_type + 0.15 cardinality
|
||||
+ 0.20 alternative_patterns + 0.20 exact layout match
|
||||
|
||||
affinity_match / intent_match:
|
||||
primary == primary → 1.0
|
||||
primary ∈ other secondary → 0.6
|
||||
secondary 교집합 ≥ 1 → 0.3
|
||||
else → 0.0
|
||||
|
||||
입력:
|
||||
- v2_semantic_rerank_result.yaml (V2 Top-K 유지)
|
||||
- structure_ontology_v2_final.yaml (프레임 v2 라벨)
|
||||
- MDX 섹션 원문 (pipeline_01)
|
||||
|
||||
출력:
|
||||
- v3_structure_rerank_r2_result.yaml
|
||||
"""
|
||||
import datetime
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from detect_mdx import detect_mdx_analysis
|
||||
from phase_common import detect_mdx_layout_v2
|
||||
from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR
|
||||
|
||||
# r3 에서 튜닝된 AFFINITY_KEYWORDS + INTENT_KEYWORDS 재사용
|
||||
import pipeline_12_r3_generate_templates_v2 as r3mod
|
||||
|
||||
OUT_PATH = HERE / 'v3_structure_rerank_r2_result.yaml'
|
||||
FINAL_ONTOLOGY = HERE / 'structure_ontology_v2_final.yaml'
|
||||
V2_RESULT = HERE / 'v2_semantic_rerank_result.yaml'
|
||||
|
||||
LOCK_SNAPSHOT_FILES = [
|
||||
'pipeline_08_v3_r2_structure_rerank.py',
|
||||
'structure_ontology_v2_final.yaml',
|
||||
'v2_semantic_rerank_result.yaml',
|
||||
'pipeline_12_r3_generate_templates_v2.py',
|
||||
'pipeline_12_r2_generate_templates_v2.py',
|
||||
'pipeline_12_generate_templates_v2.py',
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# MDX layout → family / relation_type 추론 (frame 쪽과 맞추기 위해)
|
||||
# ============================================================
|
||||
MDX_LAYOUT_STRUCTURE = {
|
||||
'3col-parallel': {'family': 'card', 'relation_type': 'parallel', 'cardinality_ideal': 3},
|
||||
'compare-rows': {'family': 'table', 'relation_type': 'compare', 'cardinality_ideal': 2},
|
||||
'compare-2banner': {'family': 'diagram', 'relation_type': 'compare', 'cardinality_ideal': 2},
|
||||
'compare-2col': {'family': 'card', 'relation_type': 'compare', 'cardinality_ideal': 2},
|
||||
'persona-3col': {'family': 'card', 'relation_type': 'parallel', 'cardinality_ideal': 3},
|
||||
'table-2col': {'family': 'table', 'relation_type': 'compare', 'cardinality_ideal': 2},
|
||||
'table-3col': {'family': 'table', 'relation_type': 'parallel', 'cardinality_ideal': 3},
|
||||
'cards-4': {'family': 'card', 'relation_type': 'parallel', 'cardinality_ideal': 4},
|
||||
'multi-parallel': {'family': 'card', 'relation_type': 'parallel', 'cardinality_ideal': 5},
|
||||
'multi-section': {'family': 'list', 'relation_type': 'parallel', 'cardinality_ideal': 3},
|
||||
'single-column': {'family': 'list', 'relation_type': 'parallel', 'cardinality_ideal': 1},
|
||||
}
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def extract_mdx_raw(sid: str) -> tuple[str, str]:
|
||||
cfg = MDX_SECTIONS[sid]
|
||||
p = MDX_DIR / cfg['file']
|
||||
lines = p.read_text(encoding='utf-8').split('\n')
|
||||
start = None
|
||||
for i, ln in enumerate(lines):
|
||||
if ln.strip() == cfg['start'].strip():
|
||||
start = i
|
||||
break
|
||||
end = len(lines)
|
||||
if cfg.get('end_prefix'):
|
||||
for i in range(start + 1, len(lines)):
|
||||
if lines[i].strip().startswith(cfg['end_prefix']):
|
||||
end = i
|
||||
break
|
||||
section = lines[start:end]
|
||||
title = section[0].lstrip('#').strip()
|
||||
return title, '\n'.join(section)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# MDX v2 profile 추출
|
||||
# ============================================================
|
||||
|
||||
def find_keyword_hits(text: str, keyword_map: dict):
|
||||
hits = []
|
||||
for label, kws in keyword_map.items():
|
||||
found = [kw for kw in kws if kw in text]
|
||||
if found:
|
||||
hits.append((label, found))
|
||||
return hits
|
||||
|
||||
|
||||
def detect_mdx_v2_profile(sid: str) -> dict:
|
||||
"""MDX 섹션 → v2 프로필 추출."""
|
||||
title, raw_text = extract_mdx_raw(sid)
|
||||
layout = detect_mdx_layout_v2(raw_text)
|
||||
struct = MDX_LAYOUT_STRUCTURE.get(layout, {})
|
||||
|
||||
# content_affinity: content(title + raw) 키워드 매칭
|
||||
all_text = title + ' ' + raw_text
|
||||
aff_hits = find_keyword_hits(all_text, r3mod.AFFINITY_KEYWORDS_R3)
|
||||
aff_hits.sort(key=lambda x: -len(x[1]))
|
||||
if aff_hits:
|
||||
aff_primary = aff_hits[0][0]
|
||||
aff_secondary = [l for l, _ in aff_hits[1:3]]
|
||||
else:
|
||||
aff_primary = 'concept_definition' # safe default
|
||||
aff_secondary = []
|
||||
|
||||
# structure_intent: 키워드 + 구조 신호
|
||||
import pipeline_12_generate_templates_v2 as r1mod
|
||||
int_hits = list(find_keyword_hits(raw_text, r1mod.INTENT_KEYWORDS))
|
||||
|
||||
rel = struct.get('relation_type')
|
||||
ideal = struct.get('cardinality_ideal')
|
||||
if rel == 'compare' and ideal == 2:
|
||||
if any(k in raw_text for k in ('AS-IS', 'TO-BE', '혁신', '전환', '이중')):
|
||||
int_hits.append(('state_transition', ['AS-IS/TO-BE/혁신']))
|
||||
else:
|
||||
int_hits.append(('binary_compare', ['compare+2']))
|
||||
elif rel == 'parallel' and ideal and ideal >= 3:
|
||||
int_hits.append(('multi_parallel', [f'parallel+{ideal}']))
|
||||
|
||||
int_hits.sort(key=lambda x: -len(x[1]))
|
||||
if int_hits:
|
||||
int_primary = int_hits[0][0]
|
||||
int_secondary = [l for l, _ in int_hits[1:3] if l != int_primary]
|
||||
else:
|
||||
int_primary = 'multi_parallel'
|
||||
int_secondary = []
|
||||
|
||||
return {
|
||||
'section_id': sid,
|
||||
'title': title,
|
||||
'layout': layout,
|
||||
'family': struct.get('family'),
|
||||
'relation_type': rel,
|
||||
'cardinality': {'ideal': ideal} if ideal else {},
|
||||
'content_affinity': {'primary': aff_primary, 'secondary': aff_secondary},
|
||||
'structure_intent': {'primary': int_primary, 'secondary': int_secondary},
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 매칭 함수
|
||||
# ============================================================
|
||||
|
||||
def profile_match(mdx_axis: dict, frame_axis: dict) -> tuple[float, str]:
|
||||
"""primary == primary → 1.0, 교차 → 0.6, secondary 교집합 → 0.3"""
|
||||
a_p = mdx_axis.get('primary')
|
||||
b_p = frame_axis.get('primary')
|
||||
a_sec = set(mdx_axis.get('secondary') or [])
|
||||
b_sec = set(frame_axis.get('secondary') or [])
|
||||
|
||||
if a_p and a_p == b_p:
|
||||
return 1.0, 'primary_match'
|
||||
if (a_p and a_p in b_sec) or (b_p and b_p in a_sec):
|
||||
return 0.6, 'cross_primary_secondary'
|
||||
if a_sec & b_sec:
|
||||
return 0.3, 'secondary_overlap'
|
||||
return 0.0, 'no_match'
|
||||
|
||||
|
||||
def layout_compat_v2(mdx_profile: dict, frame_tpl: dict) -> tuple[float, dict]:
|
||||
vp = frame_tpl['visual_pattern']
|
||||
score = 0.0
|
||||
bd = {}
|
||||
|
||||
# family (0.25)
|
||||
if mdx_profile.get('family') and mdx_profile['family'] == vp.get('family'):
|
||||
score += 0.25
|
||||
bd['family'] = 1.0
|
||||
else:
|
||||
bd['family'] = 0.0
|
||||
|
||||
# relation_type (0.20)
|
||||
if mdx_profile.get('relation_type') and mdx_profile['relation_type'] == vp.get('relation_type'):
|
||||
score += 0.20
|
||||
bd['relation_type'] = 1.0
|
||||
else:
|
||||
bd['relation_type'] = 0.0
|
||||
|
||||
# cardinality overlap (0.15)
|
||||
mdx_card = mdx_profile.get('cardinality', {}).get('ideal')
|
||||
f_card = vp.get('cardinality', {})
|
||||
if mdx_card and f_card:
|
||||
mn = f_card.get('min', 0)
|
||||
mx = f_card.get('max', 999)
|
||||
if mn <= mdx_card <= mx:
|
||||
score += 0.15
|
||||
bd['cardinality'] = 1.0
|
||||
else:
|
||||
bd['cardinality'] = 0.0
|
||||
else:
|
||||
bd['cardinality'] = 0.0
|
||||
|
||||
# layout 일치 또는 alternative_patterns (0.40)
|
||||
mdx_layout = mdx_profile.get('layout')
|
||||
frame_orig = frame_tpl['source'].get('original_layout')
|
||||
alts = {a['pattern']: a['confidence'] for a in frame_tpl.get('alternative_patterns', [])}
|
||||
if mdx_layout and mdx_layout == frame_orig:
|
||||
score += 0.40
|
||||
bd['layout_match'] = 1.0
|
||||
bd['layout_match_source'] = 'exact'
|
||||
elif mdx_layout and mdx_layout in alts:
|
||||
c = alts[mdx_layout]
|
||||
score += 0.40 * c
|
||||
bd['layout_match'] = c
|
||||
bd['layout_match_source'] = 'alternative'
|
||||
else:
|
||||
bd['layout_match'] = 0.0
|
||||
bd['layout_match_source'] = 'none'
|
||||
|
||||
return round(score, 4), bd
|
||||
|
||||
|
||||
def v3_r2_score(mdx_profile: dict, frame_tpl: dict) -> dict:
|
||||
layout_s, layout_bd = layout_compat_v2(mdx_profile, frame_tpl)
|
||||
aff_s, aff_src = profile_match(
|
||||
mdx_profile['content_affinity'],
|
||||
frame_tpl['content_affinity'],
|
||||
)
|
||||
int_s, int_src = profile_match(
|
||||
mdx_profile['structure_intent'],
|
||||
frame_tpl['structure_intent_v2'],
|
||||
)
|
||||
|
||||
total = 0.40 * layout_s + 0.35 * aff_s + 0.25 * int_s
|
||||
return {
|
||||
'total': round(total, 4),
|
||||
'layout_compat': layout_s,
|
||||
'layout_breakdown': layout_bd,
|
||||
'content_affinity': round(aff_s, 4),
|
||||
'content_affinity_source': aff_src,
|
||||
'structure_intent': round(int_s, 4),
|
||||
'structure_intent_source': int_src,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 메인
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
v2 = yaml.safe_load(V2_RESULT.read_text(encoding='utf-8'))
|
||||
ontology = yaml.safe_load(FINAL_ONTOLOGY.read_text(encoding='utf-8'))
|
||||
templates = ontology['templates_v2']
|
||||
|
||||
out_sections = {}
|
||||
for sid, v2_sec in v2['mdx_sections'].items():
|
||||
mdx_profile = detect_mdx_v2_profile(sid)
|
||||
|
||||
rerank = []
|
||||
for cand in v2_sec['v2_rerank']:
|
||||
fid = cand['frame_id']
|
||||
if fid not in templates:
|
||||
continue
|
||||
tpl = templates[fid]
|
||||
s = v3_r2_score(mdx_profile, tpl)
|
||||
|
||||
rerank.append({
|
||||
'frame_id': fid,
|
||||
'frame_number': cand['frame_number'],
|
||||
'v1_rank': cand['v1_rank'],
|
||||
'v2_rank': cand['v2_rank'],
|
||||
'v1_score': cand['v1_score'],
|
||||
'semantic_score': cand['semantic_score'],
|
||||
'v3_r2_total': s['total'],
|
||||
'v3_r2_breakdown': {
|
||||
'layout_compat': s['layout_compat'],
|
||||
'layout_detail': s['layout_breakdown'],
|
||||
'content_affinity': s['content_affinity'],
|
||||
'content_affinity_source': s['content_affinity_source'],
|
||||
'structure_intent': s['structure_intent'],
|
||||
'structure_intent_source': s['structure_intent_source'],
|
||||
},
|
||||
'fig_layout': tpl['source'].get('original_layout'),
|
||||
'fig_content_affinity': tpl['content_affinity']['primary'],
|
||||
'fig_structure_intent': tpl['structure_intent_v2']['primary'],
|
||||
})
|
||||
|
||||
rerank.sort(key=lambda x: (-x['v3_r2_total'], x['v2_rank']))
|
||||
for new_rank, item in enumerate(rerank, start=1):
|
||||
item['v3_r2_rank'] = new_rank
|
||||
|
||||
out_sections[sid] = {
|
||||
'section_type': v2_sec.get('section_type'),
|
||||
'answer_frame_number': v2_sec.get('answer_frame_number'),
|
||||
'mdx_title': v2_sec['mdx_title'],
|
||||
'mdx_profile': mdx_profile,
|
||||
'v3_r2_rerank': rerank,
|
||||
}
|
||||
|
||||
lock = {
|
||||
'timestamp': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'figma_profile_source': 'structure_ontology_v2_final.yaml',
|
||||
'matching_formula': '0.40 × layout_compat_v2 + 0.35 × affinity_match + 0.25 × intent_match',
|
||||
'files': {name: sha256_file(HERE / name) for name in LOCK_SNAPSHOT_FILES},
|
||||
'principle': [
|
||||
'v2 스키마 기반 — content_affinity + structure_intent + alternative_patterns',
|
||||
'_COMPAT 수작업 테이블 대신 자동 파생',
|
||||
'V1→V2→V3 캐스케이드 유지, rerank 범위는 V2 Top-K 내부',
|
||||
],
|
||||
}
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'pipeline_step': '8.v3.r2',
|
||||
'description': 'v2 스키마 기반 V3 구조 rerank — _COMPAT 대신 content_affinity + intent + alt',
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'holdout_sections': v2['meta']['holdout_sections'],
|
||||
'answer_map': v2['meta']['answer_map'],
|
||||
'lock_snapshot': lock,
|
||||
},
|
||||
'mdx_sections': out_sections,
|
||||
}
|
||||
|
||||
OUT_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
print('=' * 70)
|
||||
print('V3 r2 (v2 스키마 기반) 재정렬 완료')
|
||||
print('=' * 70)
|
||||
print(f' {OUT_PATH}')
|
||||
print()
|
||||
answer_map = v2['meta']['answer_map']
|
||||
for sid, s in out_sections.items():
|
||||
top = s['v3_r2_rerank'][0]
|
||||
mp = s['mdx_profile']
|
||||
ans = answer_map.get(sid)
|
||||
mark = ''
|
||||
if ans is not None:
|
||||
ok = '✓' if top['frame_number'] == ans else '✗'
|
||||
mark = f" 정답={ans} {ok}"
|
||||
else:
|
||||
mark = " (holdout)"
|
||||
print(f" [{sid:8}] mdx[layout={mp['layout']:14} aff={mp['content_affinity']['primary']:24} intent={mp['structure_intent']['primary']}]")
|
||||
print(f" → V3 r2 top1 Frame {top['frame_number']:>2} "
|
||||
f"(total={top['v3_r2_total']:.3f}, layout={top['v3_r2_breakdown']['layout_compat']:.2f}, "
|
||||
f"aff={top['v3_r2_breakdown']['content_affinity']:.2f}, "
|
||||
f"intent={top['v3_r2_breakdown']['structure_intent']:.2f}){mark}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,293 @@
|
||||
"""Pipeline Step 8 — V3 r3: MDX 입력 하네스 안정화 (B).
|
||||
|
||||
r2 에서 드러난 퇴행 (TARGET 4/4 → 2/4) 의 원인은 **MDX 쪽 affinity/intent 추출 과민**.
|
||||
"전환", "혁신", "변화" 같은 광범위 단어 단독이 거의 모든 MDX 에 before_after_change /
|
||||
state_transition 을 뿌렸음. DX = "디지털 전환" 이므로 "전환" 은 평범한 배경 단어.
|
||||
|
||||
r3 변경 (입력 하네스):
|
||||
1. STRONG_AFFINITY_PATTERNS / STRONG_INTENT_PATTERNS 정의 — 구절 단위 강한 신호
|
||||
2. Validator 도입: before_after_change / state_transition 은 STRONG 매치 1개 이상 필요
|
||||
(WEAK 키워드 단독이면 해당 label 은 primary/secondary 후보에서 제외)
|
||||
3. r2 의 layout_compat_v2 + profile_match 공식은 그대로 유지 (점수식은 안 건드림)
|
||||
|
||||
V4 는 C.4 완결 전까지 재실행 보류.
|
||||
|
||||
출력:
|
||||
- v3_structure_rerank_r3_result.yaml
|
||||
"""
|
||||
import datetime
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from detect_mdx import detect_mdx_analysis
|
||||
from phase_common import detect_mdx_layout_v2
|
||||
from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR
|
||||
|
||||
# r3 키워드 테이블 + r2 점수식 재사용
|
||||
import pipeline_12_r3_generate_templates_v2 as r3mod
|
||||
import pipeline_12_generate_templates_v2 as r1mod
|
||||
from pipeline_08_v3_r2_structure_rerank import (
|
||||
MDX_LAYOUT_STRUCTURE,
|
||||
find_keyword_hits,
|
||||
profile_match,
|
||||
layout_compat_v2,
|
||||
sha256_file,
|
||||
extract_mdx_raw,
|
||||
)
|
||||
|
||||
OUT_PATH = HERE / 'v3_structure_rerank_r3_result.yaml'
|
||||
FINAL_ONTOLOGY = HERE / 'structure_ontology_v2_final.yaml'
|
||||
V2_RESULT = HERE / 'v2_semantic_rerank_result.yaml'
|
||||
|
||||
LOCK_SNAPSHOT_FILES = [
|
||||
'pipeline_08_v3_r3_structure_rerank.py',
|
||||
'pipeline_08_v3_r2_structure_rerank.py',
|
||||
'structure_ontology_v2_final.yaml',
|
||||
'v2_semantic_rerank_result.yaml',
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# STRONG 신호 패턴 (r3 신규)
|
||||
# ============================================================
|
||||
# 이 label 을 활성화하려면 STRONG 패턴 1개 이상 필요.
|
||||
# WEAK (기존 AFFINITY_KEYWORDS_R3) 만으로는 후보에서 제외.
|
||||
STRONG_AFFINITY_PATTERNS = {
|
||||
'before_after_change': [
|
||||
'AS-IS', 'TO-BE', 'as-is', 'to-be',
|
||||
'이중 변환', '이중 Transformation',
|
||||
'과정 혁신', '과정의 혁신',
|
||||
'Process 혁신', 'Process의 혁신',
|
||||
'Product 변화', 'Product/Process',
|
||||
'Analogue', '2D→3D', '2D -> 3D',
|
||||
'전후 비교', '전후 대비', 'before/after', 'Before/After',
|
||||
],
|
||||
}
|
||||
|
||||
STRONG_INTENT_PATTERNS = {
|
||||
'state_transition': [
|
||||
'AS-IS', 'TO-BE', 'as-is', 'to-be',
|
||||
'이중 변환', '이중 Transformation',
|
||||
'Process 혁신', 'Process의 혁신',
|
||||
'과정 혁신', '과정의 혁신',
|
||||
'AS-IS ↔ TO-BE', 'AS-IS vs TO-BE',
|
||||
'전후 비교', '전후 대비', 'before/after',
|
||||
],
|
||||
}
|
||||
|
||||
# 이 label 들은 STRONG validator 통과 필수
|
||||
STRONG_REQUIRED_AFFINITY = set(STRONG_AFFINITY_PATTERNS.keys())
|
||||
STRONG_REQUIRED_INTENT = set(STRONG_INTENT_PATTERNS.keys())
|
||||
|
||||
|
||||
def has_strong_pattern(text: str, patterns: list[str]) -> list[str]:
|
||||
return [p for p in patterns if p in text]
|
||||
|
||||
|
||||
def detect_mdx_v2_profile_r3(sid: str) -> dict:
|
||||
"""MDX 섹션 → v2 profile. r3: STRONG validator 적용."""
|
||||
title, raw_text = extract_mdx_raw(sid)
|
||||
layout = detect_mdx_layout_v2(raw_text)
|
||||
struct = MDX_LAYOUT_STRUCTURE.get(layout, {})
|
||||
|
||||
all_text = title + ' ' + raw_text
|
||||
|
||||
# ─── content_affinity ───
|
||||
weak_hits = find_keyword_hits(all_text, r3mod.AFFINITY_KEYWORDS_R3)
|
||||
|
||||
# STRONG required label 들은 validator 통과 필요
|
||||
validated_aff = []
|
||||
strong_matches_aff = {}
|
||||
for lab, kws in weak_hits:
|
||||
if lab in STRONG_REQUIRED_AFFINITY:
|
||||
strongs = has_strong_pattern(all_text, STRONG_AFFINITY_PATTERNS[lab])
|
||||
if not strongs:
|
||||
# WEAK only → 제외
|
||||
continue
|
||||
strong_matches_aff[lab] = strongs
|
||||
validated_aff.append((lab, kws))
|
||||
|
||||
# primary 결정: validated 중 score 가장 높은 (len 기준)
|
||||
validated_aff.sort(key=lambda x: -len(x[1]))
|
||||
if validated_aff:
|
||||
aff_primary = validated_aff[0][0]
|
||||
aff_secondary = [l for l, _ in validated_aff[1:3] if l != aff_primary]
|
||||
else:
|
||||
# 모두 필터됨 → layout-default
|
||||
aff_primary = 'concept_definition'
|
||||
aff_secondary = []
|
||||
|
||||
# ─── structure_intent ───
|
||||
int_weak_hits = find_keyword_hits(raw_text, r1mod.INTENT_KEYWORDS)
|
||||
|
||||
# relation_type/cardinality 기반 구조 신호 (STRONG-aware)
|
||||
rel = struct.get('relation_type')
|
||||
ideal = struct.get('cardinality_ideal')
|
||||
extra_int_hits = []
|
||||
if rel == 'compare' and ideal == 2:
|
||||
st_strongs = has_strong_pattern(raw_text, STRONG_INTENT_PATTERNS['state_transition'])
|
||||
if st_strongs:
|
||||
extra_int_hits.append(('state_transition', st_strongs))
|
||||
else:
|
||||
extra_int_hits.append(('binary_compare', ['compare+cardinality=2']))
|
||||
elif rel == 'parallel' and ideal and ideal >= 3:
|
||||
extra_int_hits.append(('multi_parallel', [f'parallel+cardinality={ideal}']))
|
||||
elif rel == 'sequence':
|
||||
extra_int_hits.append(('sequence', ['relation_type=sequence']))
|
||||
|
||||
int_all = list(int_weak_hits) + extra_int_hits
|
||||
|
||||
# STRONG required intent 들 validator
|
||||
validated_int = []
|
||||
strong_matches_int = {}
|
||||
for lab, kws in int_all:
|
||||
if lab in STRONG_REQUIRED_INTENT:
|
||||
strongs = has_strong_pattern(all_text, STRONG_INTENT_PATTERNS[lab])
|
||||
if not strongs:
|
||||
continue
|
||||
strong_matches_int[lab] = strongs
|
||||
validated_int.append((lab, kws))
|
||||
|
||||
validated_int.sort(key=lambda x: -len(x[1]))
|
||||
if validated_int:
|
||||
int_primary = validated_int[0][0]
|
||||
int_secondary = [l for l, _ in validated_int[1:3] if l != int_primary]
|
||||
else:
|
||||
int_primary = 'multi_parallel'
|
||||
int_secondary = []
|
||||
|
||||
return {
|
||||
'section_id': sid,
|
||||
'title': title,
|
||||
'layout': layout,
|
||||
'family': struct.get('family'),
|
||||
'relation_type': rel,
|
||||
'cardinality': {'ideal': ideal} if ideal else {},
|
||||
'content_affinity': {'primary': aff_primary, 'secondary': aff_secondary},
|
||||
'structure_intent': {'primary': int_primary, 'secondary': int_secondary},
|
||||
'validator_log': {
|
||||
'strong_aff_matches': strong_matches_aff,
|
||||
'strong_int_matches': strong_matches_int,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def v3_r3_score(mdx_profile: dict, frame_tpl: dict) -> dict:
|
||||
layout_s, layout_bd = layout_compat_v2(mdx_profile, frame_tpl)
|
||||
aff_s, aff_src = profile_match(
|
||||
mdx_profile['content_affinity'],
|
||||
frame_tpl['content_affinity'],
|
||||
)
|
||||
int_s, int_src = profile_match(
|
||||
mdx_profile['structure_intent'],
|
||||
frame_tpl['structure_intent_v2'],
|
||||
)
|
||||
total = 0.40 * layout_s + 0.35 * aff_s + 0.25 * int_s
|
||||
return {
|
||||
'total': round(total, 4),
|
||||
'layout_compat': layout_s,
|
||||
'layout_breakdown': layout_bd,
|
||||
'content_affinity': round(aff_s, 4),
|
||||
'content_affinity_source': aff_src,
|
||||
'structure_intent': round(int_s, 4),
|
||||
'structure_intent_source': int_src,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
v2 = yaml.safe_load(V2_RESULT.read_text(encoding='utf-8'))
|
||||
ontology = yaml.safe_load(FINAL_ONTOLOGY.read_text(encoding='utf-8'))
|
||||
templates = ontology['templates_v2']
|
||||
|
||||
out_sections = {}
|
||||
for sid, v2_sec in v2['mdx_sections'].items():
|
||||
mdx_profile = detect_mdx_v2_profile_r3(sid)
|
||||
|
||||
rerank = []
|
||||
for cand in v2_sec['v2_rerank']:
|
||||
fid = cand['frame_id']
|
||||
if fid not in templates:
|
||||
continue
|
||||
tpl = templates[fid]
|
||||
s = v3_r3_score(mdx_profile, tpl)
|
||||
rerank.append({
|
||||
'frame_id': fid,
|
||||
'frame_number': cand['frame_number'],
|
||||
'v1_rank': cand['v1_rank'],
|
||||
'v2_rank': cand['v2_rank'],
|
||||
'v3_r3_total': s['total'],
|
||||
'v3_r3_breakdown': s,
|
||||
'fig_layout': tpl['source'].get('original_layout'),
|
||||
'fig_content_affinity': tpl['content_affinity']['primary'],
|
||||
'fig_structure_intent': tpl['structure_intent_v2']['primary'],
|
||||
})
|
||||
rerank.sort(key=lambda x: (-x['v3_r3_total'], x['v2_rank']))
|
||||
for new_rank, item in enumerate(rerank, start=1):
|
||||
item['v3_r3_rank'] = new_rank
|
||||
|
||||
out_sections[sid] = {
|
||||
'answer_frame_number': v2_sec.get('answer_frame_number'),
|
||||
'mdx_title': v2_sec['mdx_title'],
|
||||
'mdx_profile': mdx_profile,
|
||||
'v3_r3_rerank': rerank,
|
||||
}
|
||||
|
||||
lock = {
|
||||
'timestamp': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'files': {name: sha256_file(HERE / name) for name in LOCK_SNAPSHOT_FILES},
|
||||
'r3_changes': [
|
||||
'STRONG_AFFINITY_PATTERNS / STRONG_INTENT_PATTERNS 도입',
|
||||
'before_after_change / state_transition 은 STRONG 매치 필수',
|
||||
'"전환" 같은 일반 단어 단독은 후보에서 제외',
|
||||
],
|
||||
}
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'pipeline_step': '8.v3.r3',
|
||||
'description': 'MDX 입력 하네스 안정화 — STRONG 패턴 validator 도입',
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'holdout_sections': v2['meta']['holdout_sections'],
|
||||
'answer_map': v2['meta']['answer_map'],
|
||||
'lock_snapshot': lock,
|
||||
},
|
||||
'mdx_sections': out_sections,
|
||||
}
|
||||
OUT_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
print('=' * 70)
|
||||
print('V3 r3 (STRONG validator) 재정렬 완료')
|
||||
print('=' * 70)
|
||||
print(f' {OUT_PATH}')
|
||||
print()
|
||||
answer_map = v2['meta']['answer_map']
|
||||
for sid, s in out_sections.items():
|
||||
top = s['v3_r3_rerank'][0]
|
||||
mp = s['mdx_profile']
|
||||
ans = answer_map.get(sid)
|
||||
mark = ''
|
||||
if ans is not None:
|
||||
ok = '✓' if top['frame_number'] == ans else '✗'
|
||||
mark = f" 정답={ans} {ok}"
|
||||
else:
|
||||
mark = " (holdout)"
|
||||
validator = mp.get('validator_log', {})
|
||||
strong = (list(validator.get('strong_aff_matches', {}).keys())
|
||||
+ list(validator.get('strong_int_matches', {}).keys()))
|
||||
strong_str = f" [STRONG: {strong}]" if strong else ""
|
||||
print(f" [{sid:8}] mdx[aff={mp['content_affinity']['primary']:24} "
|
||||
f"int={mp['structure_intent']['primary']:20}]{strong_str}")
|
||||
print(f" → top1 Frame {top['frame_number']:>2} "
|
||||
f"(total={top['v3_r3_total']:.3f}){mark}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,287 @@
|
||||
"""Pipeline Step 8 — V3 r4: MDX 입력 하네스 추가 안정화 (B-1 + B-2).
|
||||
|
||||
r3 에서도 남은 문제:
|
||||
- 01-2 "용어간 상호관계" 에서 aff=concept_definition, intent=hierarchy 오판
|
||||
- 원인: 본문 "개념" 빈도 / "상위" 단독 매칭
|
||||
|
||||
r4 변경 (B-1 + B-2 만. B-3 title weighting 은 미적용 — 사용자 지시):
|
||||
B-1. AFFINITY_KEYWORDS['concept_definition'] 에서 "개념" **제거**
|
||||
(concept_definition = ['정의', '이란', '분류', '구분'] 만)
|
||||
B-2. INTENT_KEYWORDS['hierarchy'] 단일 키워드 **제거** + STRONG pair 요구
|
||||
(hierarchy 활성화는 "상위 하위"/"상하위"/"계층"/"포함 관계" 같은 구절 패턴 필요)
|
||||
|
||||
나머지 매칭 공식은 r3 그대로 유지.
|
||||
|
||||
출력:
|
||||
- v3_structure_rerank_r4_result.yaml
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from phase_common import detect_mdx_layout_v2
|
||||
import pipeline_12_r3_generate_templates_v2 as r3mod
|
||||
import pipeline_12_generate_templates_v2 as r1mod
|
||||
from pipeline_08_v3_r2_structure_rerank import (
|
||||
MDX_LAYOUT_STRUCTURE, find_keyword_hits, profile_match, layout_compat_v2,
|
||||
sha256_file, extract_mdx_raw,
|
||||
)
|
||||
from pipeline_08_v3_r3_structure_rerank import (
|
||||
STRONG_AFFINITY_PATTERNS,
|
||||
has_strong_pattern,
|
||||
)
|
||||
|
||||
OUT_PATH = HERE / 'v3_structure_rerank_r4_result.yaml'
|
||||
FINAL_ONTOLOGY = HERE / 'structure_ontology_v2_final.yaml'
|
||||
V2_RESULT = HERE / 'v2_semantic_rerank_result.yaml'
|
||||
|
||||
LOCK_SNAPSHOT_FILES = [
|
||||
'pipeline_08_v3_r4_structure_rerank.py',
|
||||
'pipeline_08_v3_r3_structure_rerank.py',
|
||||
'pipeline_08_v3_r2_structure_rerank.py',
|
||||
'structure_ontology_v2_final.yaml',
|
||||
'v2_semantic_rerank_result.yaml',
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# r4 키워드 튜닝
|
||||
# ============================================================
|
||||
# B-1: concept_definition 에서 '개념' 제거
|
||||
AFFINITY_KEYWORDS_R4 = dict(r3mod.AFFINITY_KEYWORDS_R3)
|
||||
AFFINITY_KEYWORDS_R4['concept_definition'] = ['정의', '이란', '분류', '구분']
|
||||
|
||||
# B-2: hierarchy 단일 키워드 제거 → 구절 패턴만
|
||||
INTENT_KEYWORDS_R4 = dict(r1mod.INTENT_KEYWORDS)
|
||||
INTENT_KEYWORDS_R4['hierarchy'] = [] # 단일 키워드 매칭 금지
|
||||
|
||||
# STRONG patterns (r3 것 확장)
|
||||
STRONG_AFFINITY_PATTERNS_R4 = dict(STRONG_AFFINITY_PATTERNS)
|
||||
|
||||
STRONG_INTENT_PATTERNS_R4 = {
|
||||
'state_transition': [
|
||||
'AS-IS', 'TO-BE', 'as-is', 'to-be',
|
||||
'이중 변환', '이중 Transformation',
|
||||
'Process 혁신', 'Process의 혁신',
|
||||
'과정 혁신', '과정의 혁신',
|
||||
'전후 비교', '전후 대비', 'before/after',
|
||||
],
|
||||
# B-2: hierarchy 는 구절 패턴 필수
|
||||
'hierarchy': [
|
||||
'상위 하위', '상하위', '상위-하위', '상위, 하위',
|
||||
'상위 개념 하위', '상위개념 하위개념',
|
||||
'계층', '포함 관계', '트리 구조', 'hierarchy',
|
||||
],
|
||||
}
|
||||
|
||||
STRONG_REQUIRED_AFFINITY_R4 = set(STRONG_AFFINITY_PATTERNS_R4.keys())
|
||||
STRONG_REQUIRED_INTENT_R4 = set(STRONG_INTENT_PATTERNS_R4.keys()) # state_transition + hierarchy
|
||||
|
||||
|
||||
def detect_mdx_v2_profile_r4(sid: str) -> dict:
|
||||
title, raw_text = extract_mdx_raw(sid)
|
||||
layout = detect_mdx_layout_v2(raw_text)
|
||||
struct = MDX_LAYOUT_STRUCTURE.get(layout, {})
|
||||
|
||||
all_text = title + ' ' + raw_text
|
||||
|
||||
# ─── affinity ───
|
||||
weak_hits = find_keyword_hits(all_text, AFFINITY_KEYWORDS_R4)
|
||||
validated_aff = []
|
||||
strong_matches_aff = {}
|
||||
for lab, kws in weak_hits:
|
||||
if lab in STRONG_REQUIRED_AFFINITY_R4:
|
||||
strongs = has_strong_pattern(all_text, STRONG_AFFINITY_PATTERNS_R4[lab])
|
||||
if not strongs:
|
||||
continue
|
||||
strong_matches_aff[lab] = strongs
|
||||
validated_aff.append((lab, kws))
|
||||
validated_aff.sort(key=lambda x: -len(x[1]))
|
||||
|
||||
if validated_aff:
|
||||
aff_primary = validated_aff[0][0]
|
||||
aff_secondary = [l for l, _ in validated_aff[1:3] if l != aff_primary]
|
||||
else:
|
||||
aff_primary = 'concept_definition'
|
||||
aff_secondary = []
|
||||
|
||||
# ─── intent ───
|
||||
int_weak_hits = find_keyword_hits(raw_text, INTENT_KEYWORDS_R4)
|
||||
|
||||
rel = struct.get('relation_type')
|
||||
ideal = struct.get('cardinality_ideal')
|
||||
extra_int_hits = []
|
||||
if rel == 'compare' and ideal == 2:
|
||||
st_strongs = has_strong_pattern(all_text, STRONG_INTENT_PATTERNS_R4['state_transition'])
|
||||
if st_strongs:
|
||||
extra_int_hits.append(('state_transition', st_strongs))
|
||||
else:
|
||||
extra_int_hits.append(('binary_compare', ['compare+cardinality=2']))
|
||||
elif rel == 'parallel' and ideal and ideal >= 3:
|
||||
extra_int_hits.append(('multi_parallel', [f'parallel+cardinality={ideal}']))
|
||||
elif rel == 'sequence':
|
||||
extra_int_hits.append(('sequence', ['relation_type=sequence']))
|
||||
|
||||
# STRONG hierarchy pattern 있으면 추가
|
||||
h_strongs = has_strong_pattern(all_text, STRONG_INTENT_PATTERNS_R4['hierarchy'])
|
||||
if h_strongs:
|
||||
extra_int_hits.append(('hierarchy', h_strongs))
|
||||
|
||||
int_all = list(int_weak_hits) + extra_int_hits
|
||||
validated_int = []
|
||||
strong_matches_int = {}
|
||||
for lab, kws in int_all:
|
||||
if lab in STRONG_REQUIRED_INTENT_R4:
|
||||
strongs = has_strong_pattern(all_text, STRONG_INTENT_PATTERNS_R4[lab])
|
||||
if not strongs:
|
||||
continue
|
||||
strong_matches_int[lab] = strongs
|
||||
validated_int.append((lab, kws))
|
||||
validated_int.sort(key=lambda x: -len(x[1]))
|
||||
|
||||
if validated_int:
|
||||
int_primary = validated_int[0][0]
|
||||
int_secondary = [l for l, _ in validated_int[1:3] if l != int_primary]
|
||||
else:
|
||||
int_primary = 'multi_parallel'
|
||||
int_secondary = []
|
||||
|
||||
return {
|
||||
'section_id': sid,
|
||||
'title': title,
|
||||
'layout': layout,
|
||||
'family': struct.get('family'),
|
||||
'relation_type': rel,
|
||||
'cardinality': {'ideal': ideal} if ideal else {},
|
||||
'content_affinity': {'primary': aff_primary, 'secondary': aff_secondary},
|
||||
'structure_intent': {'primary': int_primary, 'secondary': int_secondary},
|
||||
'validator_log': {
|
||||
'strong_aff_matches': strong_matches_aff,
|
||||
'strong_int_matches': strong_matches_int,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def v3_r4_score(mdx_profile: dict, frame_tpl: dict) -> dict:
|
||||
layout_s, layout_bd = layout_compat_v2(mdx_profile, frame_tpl)
|
||||
aff_s, aff_src = profile_match(
|
||||
mdx_profile['content_affinity'],
|
||||
frame_tpl['content_affinity'],
|
||||
)
|
||||
int_s, int_src = profile_match(
|
||||
mdx_profile['structure_intent'],
|
||||
frame_tpl['structure_intent_v2'],
|
||||
)
|
||||
total = 0.40 * layout_s + 0.35 * aff_s + 0.25 * int_s
|
||||
return {
|
||||
'total': round(total, 4),
|
||||
'layout_compat': layout_s,
|
||||
'layout_breakdown': layout_bd,
|
||||
'content_affinity': round(aff_s, 4),
|
||||
'content_affinity_source': aff_src,
|
||||
'structure_intent': round(int_s, 4),
|
||||
'structure_intent_source': int_src,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
v2 = yaml.safe_load(V2_RESULT.read_text(encoding='utf-8'))
|
||||
ontology = yaml.safe_load(FINAL_ONTOLOGY.read_text(encoding='utf-8'))
|
||||
templates = ontology['templates_v2']
|
||||
|
||||
out_sections = {}
|
||||
for sid, v2_sec in v2['mdx_sections'].items():
|
||||
mdx_profile = detect_mdx_v2_profile_r4(sid)
|
||||
|
||||
rerank = []
|
||||
for cand in v2_sec['v2_rerank']:
|
||||
fid = cand['frame_id']
|
||||
if fid not in templates:
|
||||
continue
|
||||
tpl = templates[fid]
|
||||
s = v3_r4_score(mdx_profile, tpl)
|
||||
rerank.append({
|
||||
'frame_id': fid,
|
||||
'frame_number': cand['frame_number'],
|
||||
'v1_rank': cand['v1_rank'],
|
||||
'v2_rank': cand['v2_rank'],
|
||||
'v3_r4_total': s['total'],
|
||||
'v3_r4_breakdown': s,
|
||||
'fig_layout': tpl['source'].get('original_layout'),
|
||||
'fig_content_affinity': tpl['content_affinity']['primary'],
|
||||
'fig_structure_intent': tpl['structure_intent_v2']['primary'],
|
||||
})
|
||||
rerank.sort(key=lambda x: (-x['v3_r4_total'], x['v2_rank']))
|
||||
for new_rank, item in enumerate(rerank, start=1):
|
||||
item['v3_r4_rank'] = new_rank
|
||||
|
||||
out_sections[sid] = {
|
||||
'answer_frame_number': v2_sec.get('answer_frame_number'),
|
||||
'mdx_title': v2_sec['mdx_title'],
|
||||
'mdx_profile': mdx_profile,
|
||||
'v3_r4_rerank': rerank,
|
||||
}
|
||||
|
||||
lock = {
|
||||
'timestamp': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'files': {name: sha256_file(HERE / name) for name in LOCK_SNAPSHOT_FILES},
|
||||
'r4_changes': [
|
||||
'B-1: concept_definition 에서 "개념" 제거',
|
||||
'B-2: hierarchy 단일 키워드 제거 — STRONG 구절 패턴 필수',
|
||||
'B-3 (title weighting) 은 미적용 (사용자 지시)',
|
||||
],
|
||||
}
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'pipeline_step': '8.v3.r4',
|
||||
'description': 'MDX 입력 하네스 B-1 + B-2 적용',
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'answer_map': v2['meta']['answer_map'],
|
||||
'holdout_sections': v2['meta']['holdout_sections'],
|
||||
'lock_snapshot': lock,
|
||||
},
|
||||
'mdx_sections': out_sections,
|
||||
}
|
||||
OUT_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
print('=' * 70)
|
||||
print('V3 r4 (B-1 + B-2) 재정렬 완료')
|
||||
print('=' * 70)
|
||||
print(f' {OUT_PATH}')
|
||||
print()
|
||||
answer_map = v2['meta']['answer_map']
|
||||
hits_r4 = 0
|
||||
for sid, s in out_sections.items():
|
||||
top = s['v3_r4_rerank'][0]
|
||||
mp = s['mdx_profile']
|
||||
ans = answer_map.get(sid)
|
||||
mark = ''
|
||||
if ans is not None:
|
||||
ok = '✓' if top['frame_number'] == ans else '✗'
|
||||
mark = f" 정답={ans} {ok}"
|
||||
if top['frame_number'] == ans:
|
||||
hits_r4 += 1
|
||||
else:
|
||||
mark = " (holdout)"
|
||||
validator = mp.get('validator_log', {})
|
||||
strong = (list(validator.get('strong_aff_matches', {}).keys())
|
||||
+ list(validator.get('strong_int_matches', {}).keys()))
|
||||
strong_str = f" [STRONG: {strong}]" if strong else ""
|
||||
print(f" [{sid:8}] mdx[aff={mp['content_affinity']['primary']:24} "
|
||||
f"int={mp['structure_intent']['primary']:20}]{strong_str}")
|
||||
print(f" → top1 Frame {top['frame_number']:>2} "
|
||||
f"(total={top['v3_r4_total']:.3f}){mark}")
|
||||
print()
|
||||
print(f'TARGET 정답률: {hits_r4}/4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Pipeline Step 8 — V3 r5: r4 MDX 하네스 + final_r2 ontology.
|
||||
|
||||
변경:
|
||||
- ontology 입력을 structure_ontology_v2_final.yaml → structure_ontology_v2_final_r2.yaml
|
||||
(Frame 13 review_queue 해제 반영)
|
||||
- MDX 하네스는 r4 (B-1 + B-2) 그대로
|
||||
|
||||
목표:
|
||||
03-1 정답률 교정 확인 — Frame 20 → Frame 13 으로 바뀌는지.
|
||||
TARGET 4/4 달성 시 C.5 (V4 재실행) 진입 가능.
|
||||
|
||||
출력: v3_structure_rerank_r5_result.yaml
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from pipeline_08_v3_r2_structure_rerank import (
|
||||
sha256_file,
|
||||
)
|
||||
from pipeline_08_v3_r4_structure_rerank import (
|
||||
detect_mdx_v2_profile_r4, v3_r4_score,
|
||||
)
|
||||
|
||||
OUT_PATH = HERE / 'v3_structure_rerank_r5_result.yaml'
|
||||
FINAL_R2 = HERE / 'structure_ontology_v2_final_r2.yaml'
|
||||
V2_RESULT = HERE / 'v2_semantic_rerank_result.yaml'
|
||||
|
||||
LOCK_SNAPSHOT_FILES = [
|
||||
'pipeline_08_v3_r5_structure_rerank.py',
|
||||
'pipeline_08_v3_r4_structure_rerank.py',
|
||||
'structure_ontology_v2_final_r2.yaml',
|
||||
'v2_semantic_rerank_result.yaml',
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
v2 = yaml.safe_load(V2_RESULT.read_text(encoding='utf-8'))
|
||||
ontology = yaml.safe_load(FINAL_R2.read_text(encoding='utf-8'))
|
||||
templates = ontology['templates_v2']
|
||||
|
||||
out_sections = {}
|
||||
for sid, v2_sec in v2['mdx_sections'].items():
|
||||
mdx_profile = detect_mdx_v2_profile_r4(sid)
|
||||
|
||||
rerank = []
|
||||
for cand in v2_sec['v2_rerank']:
|
||||
fid = cand['frame_id']
|
||||
if fid not in templates:
|
||||
continue
|
||||
tpl = templates[fid]
|
||||
s = v3_r4_score(mdx_profile, tpl)
|
||||
rerank.append({
|
||||
'frame_id': fid,
|
||||
'frame_number': cand['frame_number'],
|
||||
'v1_rank': cand['v1_rank'],
|
||||
'v2_rank': cand['v2_rank'],
|
||||
'v3_r5_total': s['total'],
|
||||
'v3_r5_breakdown': s,
|
||||
'fig_layout': tpl['source'].get('original_layout'),
|
||||
'fig_content_affinity': tpl['content_affinity']['primary'],
|
||||
'fig_structure_intent': tpl['structure_intent_v2']['primary'],
|
||||
})
|
||||
rerank.sort(key=lambda x: (-x['v3_r5_total'], x['v2_rank']))
|
||||
for new_rank, item in enumerate(rerank, start=1):
|
||||
item['v3_r5_rank'] = new_rank
|
||||
|
||||
out_sections[sid] = {
|
||||
'answer_frame_number': v2_sec.get('answer_frame_number'),
|
||||
'mdx_title': v2_sec['mdx_title'],
|
||||
'mdx_profile': mdx_profile,
|
||||
'v3_r5_rerank': rerank,
|
||||
}
|
||||
|
||||
lock = {
|
||||
'timestamp': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'files': {name: sha256_file(HERE / name) for name in LOCK_SNAPSHOT_FILES},
|
||||
'r5_changes': [
|
||||
'ontology 입력: final.yaml → final_r2.yaml (Frame 13 review_queue 해제 반영)',
|
||||
'MDX 하네스: r4 (B-1 + B-2) 그대로',
|
||||
],
|
||||
}
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'pipeline_step': '8.v3.r5',
|
||||
'description': 'r4 MDX 하네스 + Frame 13 SSOT 확정 ontology',
|
||||
'top_k': v2['meta']['top_k'],
|
||||
'answer_map': v2['meta']['answer_map'],
|
||||
'holdout_sections': v2['meta']['holdout_sections'],
|
||||
'lock_snapshot': lock,
|
||||
},
|
||||
'mdx_sections': out_sections,
|
||||
}
|
||||
OUT_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
print('=' * 70)
|
||||
print('V3 r5 재정렬 완료 (final_r2 ontology + B-1/B-2 하네스)')
|
||||
print('=' * 70)
|
||||
print(f' {OUT_PATH}')
|
||||
print()
|
||||
answer_map = v2['meta']['answer_map']
|
||||
hits = 0
|
||||
for sid, s in out_sections.items():
|
||||
top = s['v3_r5_rerank'][0]
|
||||
mp = s['mdx_profile']
|
||||
ans = answer_map.get(sid)
|
||||
mark = ''
|
||||
if ans is not None:
|
||||
ok = '✓' if top['frame_number'] == ans else '✗'
|
||||
mark = f" 정답={ans} {ok}"
|
||||
if top['frame_number'] == ans:
|
||||
hits += 1
|
||||
else:
|
||||
mark = " (holdout)"
|
||||
print(f" [{sid:8}] mdx[aff={mp['content_affinity']['primary']:24} "
|
||||
f"int={mp['structure_intent']['primary']:20}]")
|
||||
print(f" → top1 Frame {top['frame_number']:>2} "
|
||||
f"(total={top['v3_r5_total']:.3f}){mark}")
|
||||
print()
|
||||
print(f'TARGET 정답률: {hits}/4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Pipeline Step 12-final — r3 에 최소 수동 오버라이드 적용하고 V3 용 최종 v2 고정.
|
||||
|
||||
사용자 승인 사항:
|
||||
- Frame 04 → interrelation (관계도 기반)
|
||||
- Frame 05 → concept_definition (보상현황은 정책 현황이 아님)
|
||||
- Frame 31 → comparative_matrix (산업별 3열 비교)
|
||||
- Frame 13, 19, 22, 28 은 **review_queue** 에 남김 (수동 하드코딩 최소화)
|
||||
|
||||
출력:
|
||||
- structure_ontology_v2_final.yaml ← C.4 (V3 매칭 로직 v2) 에서 사용
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
R3_PATH = HERE / 'structure_ontology_v2_r3.yaml'
|
||||
FINAL_PATH = HERE / 'structure_ontology_v2_final.yaml'
|
||||
|
||||
# ============================================================
|
||||
# 수동 오버라이드 (사용자 승인, 3건)
|
||||
# ============================================================
|
||||
MANUAL_OVERRIDES = {
|
||||
'04': {
|
||||
'content_affinity_primary': 'interrelation',
|
||||
'reason': '"관계도" 가 타이틀/내용에 있어 의미상 interrelation 이 더 적합',
|
||||
},
|
||||
'05': {
|
||||
'content_affinity_primary': 'concept_definition',
|
||||
'reason': '"보상현황" 의 "현황" 은 정책 현황 아님 — 민원 관리 방식 정의',
|
||||
},
|
||||
'31': {
|
||||
'content_affinity_primary': 'comparative_matrix',
|
||||
'reason': '산업별 3열 table 구조는 다축 비교 매트릭스',
|
||||
},
|
||||
}
|
||||
|
||||
# 재검토 대기 목록 (수동 오버라이드 안 함)
|
||||
REVIEW_QUEUE = [
|
||||
{'frame': '13', 'reason': '"필수조건" 이 comparative_matrix 로 잡힘 — capability_requirements 여야 할 가능성'},
|
||||
{'frame': '19', 'reason': '"설계방식 왜곡" 이 capability_requirements 로 잡힘 — stakeholder_roles 또는 problem_diagnosis 여부 검토'},
|
||||
{'frame': '22', 'reason': '"Model 특화 S/W" 가 process_steps 로 잡힘 — concept_definition 또는 capability_requirements 여부 검토'},
|
||||
{'frame': '28', 'reason': '"현존 S/W 의 현실" 이 goal_axes 로 잡힘 — singleton_emphasis 또는 problem_diagnosis 여부 검토'},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
r3 = yaml.safe_load(R3_PATH.read_text(encoding='utf-8'))
|
||||
templates = r3['templates_v2']
|
||||
|
||||
# short_id → fid 매핑
|
||||
short_to_fid = {v['short_id']: k for k, v in templates.items()}
|
||||
|
||||
# 오버라이드 적용
|
||||
override_log = []
|
||||
for short_id, override in MANUAL_OVERRIDES.items():
|
||||
fid = short_to_fid.get(short_id)
|
||||
if not fid:
|
||||
continue
|
||||
entry = templates[fid]
|
||||
old_primary = entry['content_affinity']['primary']
|
||||
new_primary = override['content_affinity_primary']
|
||||
|
||||
# primary 교체 (secondary 는 그대로 유지)
|
||||
entry['content_affinity']['primary'] = new_primary
|
||||
# evidence 에 오버라이드 기록
|
||||
entry['content_affinity']['evidence']['primary'] = {
|
||||
'source': 'manual_override',
|
||||
'prior_primary': old_primary,
|
||||
'reason': override['reason'],
|
||||
'confidence': 1.0,
|
||||
}
|
||||
entry.setdefault('v2_meta', {})['manually_overridden'] = True
|
||||
|
||||
override_log.append({
|
||||
'frame': short_id,
|
||||
'frame_id': fid,
|
||||
'field': 'content_affinity.primary',
|
||||
'from': old_primary,
|
||||
'to': new_primary,
|
||||
'reason': override['reason'],
|
||||
})
|
||||
|
||||
# 메타 업데이트
|
||||
meta = dict(r3['meta'])
|
||||
meta['schema_version'] = 'template-fit-v2-final'
|
||||
meta['generated_at'] = datetime.datetime.now().isoformat(timespec='seconds')
|
||||
meta['generator'] = 'pipeline_12_finalize_v2.py'
|
||||
meta['predecessor_drafts'] = meta.get('predecessor_drafts', []) + ['structure_ontology_v2_r3.yaml (r3)']
|
||||
meta['manual_overrides'] = override_log
|
||||
meta['review_queue'] = REVIEW_QUEUE
|
||||
meta['status'] = 'locked_for_v3_matching'
|
||||
meta['r4_note'] = (
|
||||
'3건 수동 오버라이드 (Frame 04/05/31) 적용. '
|
||||
'재검토 대기 4건 (13/19/22/28) 은 review_queue 로 남김 — '
|
||||
'하드코딩 최소화, 추후 샘플 확장 시 재평가.'
|
||||
)
|
||||
|
||||
out = {'meta': meta, 'templates_v2': templates}
|
||||
FINAL_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
print('=' * 70)
|
||||
print('v2 final 고정 완료 (C.4 입력)')
|
||||
print('=' * 70)
|
||||
print(f' {FINAL_PATH}')
|
||||
print()
|
||||
print('수동 오버라이드:')
|
||||
for log in override_log:
|
||||
print(f" Frame {log['frame']}: {log['from']} → {log['to']}")
|
||||
print()
|
||||
print(f"review_queue ({len(REVIEW_QUEUE)} 건):")
|
||||
for r in REVIEW_QUEUE:
|
||||
print(f" Frame {r['frame']}: {r['reason']}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,695 @@
|
||||
"""Pipeline Step 12 — templates_v2 draft 생성 (C.2-b AI 일괄 라벨링).
|
||||
|
||||
사용자 지침:
|
||||
1. 원본 structure_ontology.yaml 는 건드리지 않음. structure_ontology_v2.yaml 별도 파일.
|
||||
2. 각 프레임에 content_affinity / structure_intent / alternative_patterns + evidence/reason.
|
||||
3. 32개 전부 자동 확정하지 말고 우선 검토 5개 (12, 20, 29, 3, 11) 별도 리포트.
|
||||
4. 결과 3개 산출물:
|
||||
(a) structure_ontology_v2.yaml
|
||||
(b) PRIORITY_5_REVIEW.md/html (우선 5개 상세)
|
||||
(c) TEMPLATES_V2_DIFF.md (v1 대비 무엇이 달라졌나)
|
||||
|
||||
라벨링 전략:
|
||||
- Layout-default heuristic (fig_layout → 기본 affinity/intent)
|
||||
- 내용 설명 키워드 refinement (override primary)
|
||||
- suits/not_suits 에서 교차 검증 신호 추출
|
||||
- alternative_patterns: _COMPAT 에서 파생 + layout family 유사도
|
||||
- 각 라벨에 evidence: {field, quote, confidence} 구조
|
||||
|
||||
AI 라벨링이지만 결정론적 (재현 가능) — LLM 호출 없이 규칙+키워드로 생성.
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
BLOCKS_DIR = ROOT / 'figma_to_html_agent' / 'blocks'
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from phase_common import _COMPAT
|
||||
|
||||
ONTOLOGY_V1_PATH = HERE / 'structure_ontology.yaml'
|
||||
OUT_V2_PATH = HERE / 'structure_ontology_v2.yaml'
|
||||
PRIORITY_MD = HERE / 'PRIORITY_5_REVIEW.md'
|
||||
PRIORITY_HTML = HERE / 'PRIORITY_5_REVIEW.html'
|
||||
DIFF_MD = HERE / 'TEMPLATES_V2_DIFF.md'
|
||||
|
||||
PRIORITY_FRAME_NUMBERS = {3, 11, 12, 20, 29}
|
||||
|
||||
# ============================================================
|
||||
# Vocabularies (schema v2)
|
||||
# ============================================================
|
||||
CONTENT_AFFINITY_ENUM = [
|
||||
'concept_definition', 'concept_comparison', 'goal_axes',
|
||||
'persona_benefit', 'process_steps', 'before_after_change',
|
||||
'capability_requirements', 'comparative_matrix',
|
||||
'stakeholder_roles', 'policy_requirements',
|
||||
'tool_ecosystem', 'interrelation',
|
||||
]
|
||||
STRUCTURE_INTENT_ENUM = [
|
||||
'binary_compare', 'multi_parallel', 'hierarchy',
|
||||
'sequence', 'state_transition', 'cycle_interrelation',
|
||||
'matrix_coverage', 'persona_mapping', 'singleton_emphasis',
|
||||
]
|
||||
|
||||
# ============================================================
|
||||
# Layout → default content_affinity (primary 먼저)
|
||||
# ============================================================
|
||||
# Key 는 original_layout (visual_pattern.layout 또는 source.original_layout)
|
||||
LAYOUT_DEFAULT_AFFINITY = {
|
||||
# Compare family
|
||||
'compare-rows': ['concept_comparison', 'comparative_matrix'],
|
||||
'compare-2banner-top-2col-bottom': ['before_after_change', 'concept_comparison'],
|
||||
'compare-2col': ['concept_comparison'],
|
||||
'compare-2banner': ['before_after_change', 'concept_comparison'],
|
||||
'banner-top-2col-bottom': ['before_after_change', 'concept_comparison'],
|
||||
|
||||
# Table
|
||||
'table-2col': ['concept_comparison', 'comparative_matrix'],
|
||||
'table-3col': ['comparative_matrix'],
|
||||
|
||||
# Persona / 3-column
|
||||
'persona-3col': ['persona_benefit', 'stakeholder_roles'],
|
||||
'3col-parallel': ['goal_axes'],
|
||||
'3col-cards': ['capability_requirements'],
|
||||
'3col-compare': ['concept_comparison', 'comparative_matrix'],
|
||||
'3-column': ['goal_axes'],
|
||||
'3-category': ['concept_definition'],
|
||||
'cards-3-category': ['concept_definition'],
|
||||
'cards-3-compare': ['concept_comparison'],
|
||||
'cards-3-header': ['capability_requirements'],
|
||||
|
||||
# Cards 4+
|
||||
'cards-4': ['capability_requirements'],
|
||||
'cards-4-grid': ['capability_requirements'],
|
||||
'policy-4card-plus-list': ['policy_requirements'],
|
||||
|
||||
# Cycle / interrelation
|
||||
'cycle-3way': ['interrelation', 'goal_axes'],
|
||||
'cycle-3way-intersection': ['interrelation', 'goal_axes'],
|
||||
'circular-nodes': ['interrelation'],
|
||||
'circular-nodes-6': ['interrelation'],
|
||||
'quadrilateral-relations': ['interrelation'],
|
||||
|
||||
# Lists
|
||||
'list-numbered': ['policy_requirements', 'capability_requirements'],
|
||||
'list-numbered-4': ['policy_requirements', 'capability_requirements'],
|
||||
'list-stacked': ['policy_requirements'],
|
||||
'list-stacked-vertical': ['policy_requirements'],
|
||||
'bullet-cards': ['capability_requirements'],
|
||||
'bullet-cards-4-plus-center': ['capability_requirements', 'goal_axes'],
|
||||
|
||||
# Paired / Quadrant
|
||||
'paired-rows': ['comparative_matrix'],
|
||||
'paired-rows-2x2': ['comparative_matrix'],
|
||||
'2col-paired': ['persona_benefit'],
|
||||
'2col-paired-list': ['persona_benefit', 'stakeholder_roles'],
|
||||
'2-boxes': ['concept_comparison'],
|
||||
'quadrant-issues': ['policy_requirements', 'singleton_emphasis'],
|
||||
'quadrant-4': ['comparative_matrix'],
|
||||
|
||||
# Diagram / radial
|
||||
'diagram-5': ['goal_axes'],
|
||||
'radial-diagram-5': ['goal_axes'],
|
||||
'diagram-labels': ['concept_definition'],
|
||||
'central-5-goals': ['goal_axes'],
|
||||
'central-split': ['concept_comparison'],
|
||||
'central-split-synthesis': ['concept_comparison'],
|
||||
|
||||
# Split panel
|
||||
'split-panel-diagram': ['concept_comparison'],
|
||||
'split-panel-numbered': ['process_steps'],
|
||||
|
||||
# Side / Sections
|
||||
'side-card': ['concept_definition'],
|
||||
'side-card-with-list': ['concept_definition', 'capability_requirements'],
|
||||
'3-section': ['goal_axes'],
|
||||
'3-section-framework': ['goal_axes', 'policy_requirements'],
|
||||
'3-emphasis': ['goal_axes', 'singleton_emphasis'],
|
||||
'title-plus-3-emphasis': ['goal_axes', 'singleton_emphasis'],
|
||||
|
||||
# Full page
|
||||
'full-page-map': ['policy_requirements', 'stakeholder_roles'],
|
||||
'full-page-map-banner': ['policy_requirements'],
|
||||
}
|
||||
|
||||
LAYOUT_DEFAULT_INTENT = {
|
||||
'compare-rows': ['matrix_coverage', 'binary_compare'],
|
||||
'compare-2banner-top-2col-bottom': ['state_transition', 'binary_compare'],
|
||||
'compare-2col': ['binary_compare'],
|
||||
'compare-2banner': ['state_transition', 'binary_compare'],
|
||||
'banner-top-2col-bottom': ['state_transition', 'binary_compare'],
|
||||
'table-2col': ['matrix_coverage'],
|
||||
'table-3col': ['matrix_coverage'],
|
||||
'persona-3col': ['persona_mapping', 'multi_parallel'],
|
||||
'3col-parallel': ['multi_parallel'],
|
||||
'3col-cards': ['multi_parallel'],
|
||||
'3col-compare': ['binary_compare', 'matrix_coverage'],
|
||||
'3-column': ['multi_parallel'],
|
||||
'3-category': ['multi_parallel'],
|
||||
'cards-3-category': ['multi_parallel'],
|
||||
'cards-3-compare': ['binary_compare'],
|
||||
'cards-3-header': ['multi_parallel'],
|
||||
'cards-4': ['multi_parallel'],
|
||||
'cards-4-grid': ['multi_parallel'],
|
||||
'policy-4card-plus-list': ['multi_parallel'],
|
||||
'cycle-3way': ['cycle_interrelation', 'multi_parallel'],
|
||||
'cycle-3way-intersection': ['cycle_interrelation', 'multi_parallel'],
|
||||
'circular-nodes': ['cycle_interrelation'],
|
||||
'circular-nodes-6': ['cycle_interrelation'],
|
||||
'quadrilateral-relations': ['cycle_interrelation'],
|
||||
'list-numbered': ['sequence'],
|
||||
'list-numbered-4': ['sequence', 'multi_parallel'],
|
||||
'list-stacked': ['multi_parallel'],
|
||||
'list-stacked-vertical': ['multi_parallel'],
|
||||
'bullet-cards': ['multi_parallel'],
|
||||
'bullet-cards-4-plus-center': ['multi_parallel', 'hierarchy'],
|
||||
'paired-rows': ['matrix_coverage'],
|
||||
'paired-rows-2x2': ['matrix_coverage'],
|
||||
'2col-paired': ['persona_mapping', 'binary_compare'],
|
||||
'2col-paired-list': ['persona_mapping'],
|
||||
'2-boxes': ['binary_compare'],
|
||||
'quadrant-issues': ['matrix_coverage', 'multi_parallel'],
|
||||
'quadrant-4': ['matrix_coverage'],
|
||||
'diagram-5': ['hierarchy'],
|
||||
'radial-diagram-5': ['hierarchy', 'multi_parallel'],
|
||||
'diagram-labels': ['singleton_emphasis'],
|
||||
'central-5-goals': ['hierarchy', 'multi_parallel'],
|
||||
'central-split': ['binary_compare'],
|
||||
'central-split-synthesis': ['binary_compare'],
|
||||
'split-panel-diagram': ['binary_compare'],
|
||||
'split-panel-numbered': ['sequence'],
|
||||
'side-card': ['singleton_emphasis'],
|
||||
'side-card-with-list': ['singleton_emphasis', 'multi_parallel'],
|
||||
'3-section': ['multi_parallel'],
|
||||
'3-section-framework': ['multi_parallel', 'hierarchy'],
|
||||
'3-emphasis': ['multi_parallel', 'singleton_emphasis'],
|
||||
'title-plus-3-emphasis': ['multi_parallel', 'singleton_emphasis'],
|
||||
'full-page-map': ['singleton_emphasis', 'matrix_coverage'],
|
||||
'full-page-map-banner': ['singleton_emphasis'],
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 키워드 → affinity 보강 (내용 설명에서 발견 시 primary override)
|
||||
# ============================================================
|
||||
AFFINITY_KEYWORDS = {
|
||||
'concept_definition': ['정의', '이란', '개념', '분류', '구분'],
|
||||
'concept_comparison': ['비교', '대조', '차이', 'vs', 'VS', '상호관계'],
|
||||
'goal_axes': ['목표', '궁극적', '비전', '목적', '지향'],
|
||||
'persona_benefit': ['발주자', '설계자', '시공자', '기대효과', '혜택', '이익'],
|
||||
'process_steps': ['단계', '순서', 'Step', '1단계', '2단계', '흐름'],
|
||||
'before_after_change': ['AS-IS', 'TO-BE', '전환', '혁신', '변화', '이중 변환', '과정'],
|
||||
'capability_requirements': ['필수', '요건', '필요', '역량', 'S/W', '도구'],
|
||||
'comparative_matrix': ['다면', '다축', '관점별', '여러 관점', '축'],
|
||||
'stakeholder_roles': ['역할', '책임', '주도', '수행'],
|
||||
'policy_requirements': ['정책', '제도', '도입', '거버넌스', '전면 도입', '국외'],
|
||||
'tool_ecosystem': ['Revit', 'Navisworks', 'SketchUp', '소프트웨어', 'S/W 생태'],
|
||||
'interrelation': ['상호관계', '순환', '조화', '교차', '수렴', '3원'],
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 키워드 → intent 보강
|
||||
# ============================================================
|
||||
INTENT_KEYWORDS = {
|
||||
'binary_compare': ['2개 비교', '2개 개념', '대조', '양분'],
|
||||
'state_transition': ['AS-IS', 'TO-BE', '전환', '혁신', '이중 Transformation', '과정'],
|
||||
'cycle_interrelation': ['상호관계', '순환', '조화', '교차', '3원'],
|
||||
'multi_parallel': ['3개 병렬', '4개 병렬', '카드 3열', '3관점'],
|
||||
'hierarchy': ['중앙', '상위', '하위', '포함'],
|
||||
'sequence': ['단계', '순서', '흐름', 'step'],
|
||||
'matrix_coverage': ['다면', '관점별', '여러 관점', '축별'],
|
||||
'persona_mapping': ['주체별', '발주자/설계자/시공자', '역할별'],
|
||||
'singleton_emphasis': ['강조', '문제', '진단', '약점'],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 핵심 함수
|
||||
# ============================================================
|
||||
|
||||
def parse_analysis_md(path: Path) -> dict:
|
||||
"""analysis.md 의 주요 섹션 추출."""
|
||||
text = path.read_text(encoding='utf-8')
|
||||
# 내용 설명
|
||||
m = re.search(r'##\s*내용 설명\s*\n+([\s\S]+?)(?=\n##\s|\Z)', text)
|
||||
content = m.group(1).strip() if m else ''
|
||||
|
||||
# suits
|
||||
m_s = re.search(r'###\s*suits\s*\n([\s\S]+?)(?=\n###|\n##\s|\Z)', text)
|
||||
suits = []
|
||||
if m_s:
|
||||
for ln in m_s.group(1).strip().split('\n'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('-'):
|
||||
suits.append(ln.lstrip('-').strip())
|
||||
|
||||
# not_suits
|
||||
m_ns = re.search(r'###\s*not_suits\s*\n([\s\S]+?)(?=\n###|\n##\s|\Z)', text)
|
||||
not_suits = []
|
||||
if m_ns:
|
||||
for ln in m_ns.group(1).strip().split('\n'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('-'):
|
||||
not_suits.append(ln.lstrip('-').strip())
|
||||
|
||||
return {'content': content, 'suits': suits, 'not_suits': not_suits}
|
||||
|
||||
|
||||
def find_keyword_hits(text: str, keyword_map: dict) -> list[tuple[str, list[str]]]:
|
||||
"""text 안에서 각 label 의 키워드 찾기."""
|
||||
hits = []
|
||||
for label, kws in keyword_map.items():
|
||||
found = [kw for kw in kws if kw in text]
|
||||
if found:
|
||||
hits.append((label, found))
|
||||
return hits
|
||||
|
||||
|
||||
def label_content_affinity(layout: str, original_layout: str, content: str,
|
||||
suits: list, not_suits: list) -> dict:
|
||||
"""content_affinity primary + secondary + evidence."""
|
||||
# 1. default
|
||||
defaults = (LAYOUT_DEFAULT_AFFINITY.get(original_layout)
|
||||
or LAYOUT_DEFAULT_AFFINITY.get(layout)
|
||||
or ['concept_definition'])
|
||||
default_primary = defaults[0]
|
||||
default_secondary = defaults[1:]
|
||||
|
||||
# 2. 키워드 hit
|
||||
all_text = content + ' ' + ' '.join(suits)
|
||||
kw_hits = find_keyword_hits(all_text, AFFINITY_KEYWORDS)
|
||||
|
||||
# 3. primary 결정: 키워드 가장 강한 것 (개수 기준), 없으면 default
|
||||
if kw_hits:
|
||||
kw_hits.sort(key=lambda x: -len(x[1]))
|
||||
primary = kw_hits[0][0]
|
||||
primary_kws = kw_hits[0][1]
|
||||
primary_source = 'keyword_match'
|
||||
else:
|
||||
primary = default_primary
|
||||
primary_kws = []
|
||||
primary_source = 'layout_default'
|
||||
|
||||
# 4. secondary: primary 제외 keyword hit + default secondary
|
||||
sec_candidates = [l for l, kws in kw_hits[1:] if l != primary]
|
||||
for d in default_secondary:
|
||||
if d not in sec_candidates and d != primary:
|
||||
sec_candidates.append(d)
|
||||
secondary = sec_candidates[:2]
|
||||
|
||||
# 5. evidence 생성
|
||||
evidence = {}
|
||||
if primary_source == 'keyword_match':
|
||||
evidence['primary'] = {
|
||||
'source': 'keyword_match',
|
||||
'field': '내용 설명 / suits',
|
||||
'keywords': primary_kws,
|
||||
'rule': f"'{primary_kws[0]}' 등 키워드가 {primary} 를 가리킴",
|
||||
'confidence': min(0.95, 0.6 + 0.1 * len(primary_kws)),
|
||||
}
|
||||
else:
|
||||
evidence['primary'] = {
|
||||
'source': 'layout_default',
|
||||
'field': f'original_layout = {original_layout or layout}',
|
||||
'rule': f'layout → 기본 affinity 매핑',
|
||||
'confidence': 0.6,
|
||||
}
|
||||
for i, s in enumerate(secondary, start=1):
|
||||
hits_for_s = next((kws for l, kws in kw_hits if l == s), None)
|
||||
if hits_for_s:
|
||||
evidence[f'secondary_{i}'] = {
|
||||
'source': 'keyword_match',
|
||||
'keywords': hits_for_s,
|
||||
'confidence': 0.65,
|
||||
}
|
||||
else:
|
||||
evidence[f'secondary_{i}'] = {
|
||||
'source': 'layout_default',
|
||||
'confidence': 0.5,
|
||||
}
|
||||
|
||||
return {
|
||||
'primary': primary,
|
||||
'secondary': secondary,
|
||||
'evidence': evidence,
|
||||
}
|
||||
|
||||
|
||||
def label_structure_intent(layout: str, original_layout: str, content: str,
|
||||
relation_type: str, cardinality: dict) -> dict:
|
||||
"""structure_intent primary + secondary + evidence."""
|
||||
defaults = (LAYOUT_DEFAULT_INTENT.get(original_layout)
|
||||
or LAYOUT_DEFAULT_INTENT.get(layout)
|
||||
or ['multi_parallel'])
|
||||
default_primary = defaults[0]
|
||||
default_secondary = defaults[1:]
|
||||
|
||||
kw_hits = find_keyword_hits(content, INTENT_KEYWORDS)
|
||||
|
||||
# relation_type + cardinality 보강
|
||||
ideal = cardinality.get('ideal') if cardinality else None
|
||||
if relation_type == 'compare' and ideal == 2:
|
||||
# state_transition 우선 (내용에 전환/AS-IS 가 있으면)
|
||||
if 'AS-IS' in content or 'TO-BE' in content or '혁신' in content or '전환' in content:
|
||||
kw_hits.append(('state_transition', ['AS-IS/TO-BE/혁신/전환']))
|
||||
else:
|
||||
kw_hits.append(('binary_compare', ['compare + cardinality.ideal=2']))
|
||||
elif relation_type == 'parallel' and ideal and ideal >= 3:
|
||||
kw_hits.append(('multi_parallel', [f'parallel + cardinality.ideal={ideal}']))
|
||||
elif relation_type == 'sequence':
|
||||
kw_hits.append(('sequence', ['relation_type=sequence']))
|
||||
|
||||
if kw_hits:
|
||||
kw_hits.sort(key=lambda x: -len(x[1]))
|
||||
primary = kw_hits[0][0]
|
||||
primary_kws = kw_hits[0][1]
|
||||
primary_source = 'keyword_or_structure_match'
|
||||
else:
|
||||
primary = default_primary
|
||||
primary_kws = []
|
||||
primary_source = 'layout_default'
|
||||
|
||||
sec_candidates = []
|
||||
for l, kws in kw_hits[1:]:
|
||||
if l != primary and l not in sec_candidates:
|
||||
sec_candidates.append(l)
|
||||
for d in default_secondary:
|
||||
if d not in sec_candidates and d != primary:
|
||||
sec_candidates.append(d)
|
||||
secondary = sec_candidates[:2]
|
||||
|
||||
evidence = {
|
||||
'primary': {
|
||||
'source': primary_source,
|
||||
'rule': primary_kws[0] if primary_kws else f'layout default → {primary}',
|
||||
'confidence': 0.7 if primary_source == 'keyword_or_structure_match' else 0.6,
|
||||
}
|
||||
}
|
||||
for i, s in enumerate(secondary, start=1):
|
||||
evidence[f'secondary_{i}'] = {
|
||||
'source': 'layout_default' if s in default_secondary else 'keyword_match',
|
||||
'confidence': 0.55,
|
||||
}
|
||||
|
||||
return {
|
||||
'primary': primary,
|
||||
'secondary': secondary,
|
||||
'evidence': evidence,
|
||||
}
|
||||
|
||||
|
||||
def derive_alternative_patterns(fig_layout: str) -> list[dict]:
|
||||
"""_COMPAT 에서 파생 — 이 fig_layout 과 의미적으로 호환 가능한 다른 layout."""
|
||||
alternatives = {}
|
||||
# 이 fig_layout 을 높게 평가하는 mdx_layout 들
|
||||
for mdx_l, fig_dict in _COMPAT.items():
|
||||
my_compat = fig_dict.get(fig_layout, 0)
|
||||
if my_compat >= 0.6:
|
||||
# 같은 mdx_l 에서 compat >= 0.7 인 다른 fig_layout 들 → 대안
|
||||
for other_fig, c in fig_dict.items():
|
||||
if other_fig == fig_layout:
|
||||
continue
|
||||
if c >= 0.7:
|
||||
# 가중 합산: 공통으로 호환되는 mdx_l 이 많을수록 강한 대안
|
||||
alternatives[other_fig] = alternatives.get(other_fig, 0) + my_compat * c
|
||||
|
||||
# 정규화 (0~1)
|
||||
if not alternatives:
|
||||
return []
|
||||
max_v = max(alternatives.values())
|
||||
alts_list = []
|
||||
for fig_l, v in sorted(alternatives.items(), key=lambda x: -x[1])[:5]:
|
||||
conf = round(v / max_v, 2)
|
||||
alts_list.append({
|
||||
'pattern': fig_l,
|
||||
'reason': f'_COMPAT 공통 호환 mdx_layout 집합 기반 파생 (정규화 {conf})',
|
||||
'confidence': conf,
|
||||
})
|
||||
return alts_list
|
||||
|
||||
|
||||
def generate_v2_entry(fid: str, tpl_v1: dict) -> dict:
|
||||
"""v1 엔트리에 v2 필드 추가."""
|
||||
layout = tpl_v1['visual_pattern']['layout']
|
||||
original_layout = tpl_v1['source'].get('original_layout', layout)
|
||||
relation_type = tpl_v1['visual_pattern'].get('relation_type')
|
||||
cardinality = tpl_v1['visual_pattern'].get('cardinality', {})
|
||||
|
||||
# analysis.md 읽기
|
||||
analysis = parse_analysis_md(BLOCKS_DIR / fid / 'analysis.md')
|
||||
|
||||
# v2 필드 생성
|
||||
affinity = label_content_affinity(
|
||||
layout, original_layout,
|
||||
analysis['content'], analysis['suits'], analysis['not_suits'],
|
||||
)
|
||||
intent = label_structure_intent(
|
||||
layout, original_layout, analysis['content'],
|
||||
relation_type, cardinality,
|
||||
)
|
||||
alternatives = derive_alternative_patterns(original_layout)
|
||||
|
||||
# v1 엔트리 그대로 복제 + v2 필드 추가
|
||||
entry = dict(tpl_v1)
|
||||
entry['content_affinity'] = affinity
|
||||
entry['structure_intent_v2'] = intent # v1 의 structure_intent 와 구분 위해 이름 변경
|
||||
entry['alternative_patterns'] = alternatives
|
||||
entry['v2_meta'] = {
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'source_analysis': f'{fid}/analysis.md',
|
||||
'needs_review': tpl_v1.get('short_id') in {'03', '11', '12', '20', '29'},
|
||||
}
|
||||
return entry
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 메인
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
v1 = yaml.safe_load(ONTOLOGY_V1_PATH.read_text(encoding='utf-8'))
|
||||
templates_v1 = v1['templates_v1']
|
||||
|
||||
templates_v2 = {}
|
||||
for fid, tpl in templates_v1.items():
|
||||
templates_v2[fid] = generate_v2_entry(fid, tpl)
|
||||
|
||||
# 산출물 1: structure_ontology_v2.yaml
|
||||
out = {
|
||||
'meta': {
|
||||
'schema_version': 'template-fit-v2-draft',
|
||||
'generated_from': 'structure_ontology.yaml (templates_v1)',
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'generator': 'pipeline_12_generate_templates_v2.py',
|
||||
'status': 'draft_pending_user_review',
|
||||
'priority_review_frames': sorted(PRIORITY_FRAME_NUMBERS),
|
||||
'vocabularies': {
|
||||
'content_affinity': CONTENT_AFFINITY_ENUM,
|
||||
'structure_intent': STRUCTURE_INTENT_ENUM,
|
||||
},
|
||||
'matching_weights_initial': {
|
||||
'layout_compat': 0.40,
|
||||
'content_affinity': 0.35,
|
||||
'structure_intent': 0.25,
|
||||
},
|
||||
'note': (
|
||||
'AI 초안. 원본 structure_ontology.yaml 는 유지. '
|
||||
'사용자 검토 우선순위: Frame 12, 20, 29, 3, 11 → 2차 (3col/cycle/table/process 계열) → 3차 (나머지).'
|
||||
),
|
||||
},
|
||||
'templates_v2': templates_v2,
|
||||
}
|
||||
OUT_V2_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 산출물 2: PRIORITY_5_REVIEW
|
||||
# ============================================================
|
||||
pr_md = []
|
||||
pr_md.append('# Priority 5 프레임 검토 리포트 (C.2-b AI 초안)')
|
||||
pr_md.append('')
|
||||
pr_md.append(
|
||||
'사용자 검토 우선순위 1순위 — Holdout 평가에서 V3 문제가 드러난 프레임.'
|
||||
)
|
||||
pr_md.append('')
|
||||
pr_md.append(
|
||||
'각 프레임마다: (a) v1 대비 추가된 v2 필드, (b) 라벨 근거(evidence), '
|
||||
'(c) 검토 포인트.'
|
||||
)
|
||||
pr_md.append('')
|
||||
|
||||
short_to_fid = {v['short_id']: k for k, v in templates_v1.items()}
|
||||
for fn in ['03', '11', '12', '20', '29']:
|
||||
fid = short_to_fid.get(fn)
|
||||
if not fid:
|
||||
continue
|
||||
v2_entry = templates_v2[fid]
|
||||
v1_entry = templates_v1[fid]
|
||||
title = v1_entry['source']['title']
|
||||
original_layout = v1_entry['source'].get('original_layout')
|
||||
|
||||
pr_md.append(f"## Frame {fn} — {title}")
|
||||
pr_md.append('')
|
||||
pr_md.append(f"- **frame_id**: `{fid}`")
|
||||
pr_md.append(f"- **layout**: `{v1_entry['visual_pattern']['layout']}` (original: `{original_layout}`)")
|
||||
pr_md.append(f"- **family / relation_type / cardinality**: "
|
||||
f"`{v1_entry['visual_pattern']['family']}` / "
|
||||
f"`{v1_entry['visual_pattern'].get('relation_type')}` / "
|
||||
f"ideal={v1_entry['visual_pattern'].get('cardinality', {}).get('ideal')}")
|
||||
pr_md.append('')
|
||||
pr_md.append('### content_affinity')
|
||||
aff = v2_entry['content_affinity']
|
||||
pr_md.append(f"- **primary**: `{aff['primary']}`")
|
||||
if aff['secondary']:
|
||||
pr_md.append(f"- **secondary**: {', '.join(f'`{s}`' for s in aff['secondary'])}")
|
||||
pr_md.append('- **evidence**:')
|
||||
for k, v in aff['evidence'].items():
|
||||
extras = []
|
||||
if 'keywords' in v and v['keywords']:
|
||||
extras.append(f"keywords={v['keywords']}")
|
||||
if 'rule' in v:
|
||||
extras.append(f"rule={v['rule']}")
|
||||
extras.append(f"conf={v['confidence']}")
|
||||
pr_md.append(f" - `{k}` (source: {v['source']}) — {' / '.join(extras)}")
|
||||
pr_md.append('')
|
||||
pr_md.append('### structure_intent (v2)')
|
||||
si = v2_entry['structure_intent_v2']
|
||||
pr_md.append(f"- **primary**: `{si['primary']}`")
|
||||
if si['secondary']:
|
||||
pr_md.append(f"- **secondary**: {', '.join(f'`{s}`' for s in si['secondary'])}")
|
||||
pr_md.append('- **evidence**:')
|
||||
for k, v in si['evidence'].items():
|
||||
pr_md.append(f" - `{k}` (source: {v['source']}) — rule={v.get('rule', '—')} / conf={v['confidence']}")
|
||||
pr_md.append('')
|
||||
pr_md.append('### alternative_patterns (파생)')
|
||||
if v2_entry['alternative_patterns']:
|
||||
pr_md.append('| 대안 layout | confidence | 근거 |')
|
||||
pr_md.append('|---|---:|---|')
|
||||
for a in v2_entry['alternative_patterns']:
|
||||
pr_md.append(f"| `{a['pattern']}` | {a['confidence']} | {a['reason']} |")
|
||||
else:
|
||||
pr_md.append('(파생된 대안 없음 — _COMPAT 기반 공통 호환 부재)')
|
||||
pr_md.append('')
|
||||
pr_md.append('### 검토 포인트')
|
||||
pr_md.append(f"- primary content_affinity 가 Frame 의 실제 의도에 맞나?")
|
||||
pr_md.append(f"- structure_intent primary/secondary 가 layout 의 시각적 메시지를 정확히 기술하나?")
|
||||
pr_md.append(f"- alternative_patterns 에 **빠진** 의미적 대안이 있나? (예: Frame 12 라면 `3col-parallel` 이 포함되어 있나)")
|
||||
pr_md.append('')
|
||||
pr_md.append('---')
|
||||
pr_md.append('')
|
||||
|
||||
pr_text = '\n'.join(pr_md)
|
||||
PRIORITY_MD.write_text(pr_text, encoding='utf-8')
|
||||
|
||||
style = """
|
||||
body { font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1100px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; }
|
||||
h1 { border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; }
|
||||
h2 { margin-top: 2.5em; background: #e0e7ff; padding: 0.6em 0.9em; border-left: 4px solid #0a6; border-radius: 4px; }
|
||||
h3 { margin-top: 1.3em; color: #1a365d; }
|
||||
table { border-collapse: collapse; background: #fff; margin: 0.5em 0 1em; }
|
||||
th, td { border: 1px solid #e2e8f0; padding: 8px 10px; text-align: left; vertical-align: top; }
|
||||
th { background: #1e293b; color: #fff; }
|
||||
code { background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; }
|
||||
strong { color: #0a6; }
|
||||
"""
|
||||
html_body = markdown.markdown(pr_text, extensions=['tables'])
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><title>Priority 5 검토</title><style>{style}</style></head>
|
||||
<body>{html_body}</body></html>"""
|
||||
PRIORITY_HTML.write_text(html, encoding='utf-8')
|
||||
|
||||
# ============================================================
|
||||
# 산출물 3: TEMPLATES_V2_DIFF.md (v1 대비 변경 요약)
|
||||
# ============================================================
|
||||
d_md = []
|
||||
d_md.append('# templates_v1 → templates_v2 변경 요약')
|
||||
d_md.append('')
|
||||
d_md.append('_v2 는 v1 엔트리를 **복제 + 필드 추가** 방식으로 생성. 기존 필드는 변경 없음._')
|
||||
d_md.append('')
|
||||
d_md.append('## 추가된 필드')
|
||||
d_md.append('')
|
||||
d_md.append('| 필드 | 타입 | 역할 |')
|
||||
d_md.append('|---|---|---|')
|
||||
d_md.append('| `content_affinity` | `{primary, secondary[], evidence{}}` | 프레임이 선호하는 콘텐츠 성격 |')
|
||||
d_md.append('| `structure_intent_v2` | `{primary, secondary[], evidence{}}` | 레이아웃의 시각 의도 (v1 의 structure_intent 와 별도 축) |')
|
||||
d_md.append('| `alternative_patterns` | `[{pattern, reason, confidence}]` | 의미적 대안 layout 목록 |')
|
||||
d_md.append('| `v2_meta` | `{generated_at, source_analysis, needs_review}` | 생성 메타 |')
|
||||
d_md.append('')
|
||||
d_md.append('## 유지된 필드 (변경 없음)')
|
||||
d_md.append('')
|
||||
d_md.append('- `short_id` / `template_id` / `schema_version` / `source`')
|
||||
d_md.append('- `description` / `visual_pattern` (전체)')
|
||||
d_md.append('- `slots` / `suits` / `not_suits` / `adaptation_allowed`')
|
||||
d_md.append('')
|
||||
d_md.append('## 통계')
|
||||
d_md.append('')
|
||||
|
||||
# affinity 분포
|
||||
aff_counter = Counter()
|
||||
intent_counter = Counter()
|
||||
alt_counts = []
|
||||
for fid, e in templates_v2.items():
|
||||
aff_counter[e['content_affinity']['primary']] += 1
|
||||
intent_counter[e['structure_intent_v2']['primary']] += 1
|
||||
alt_counts.append(len(e['alternative_patterns']))
|
||||
|
||||
d_md.append('### content_affinity primary 분포')
|
||||
d_md.append('')
|
||||
d_md.append('| primary | 프레임 수 |')
|
||||
d_md.append('|---|---:|')
|
||||
for lab, c in aff_counter.most_common():
|
||||
d_md.append(f"| `{lab}` | {c} |")
|
||||
d_md.append('')
|
||||
|
||||
d_md.append('### structure_intent (v2) primary 분포')
|
||||
d_md.append('')
|
||||
d_md.append('| primary | 프레임 수 |')
|
||||
d_md.append('|---|---:|')
|
||||
for lab, c in intent_counter.most_common():
|
||||
d_md.append(f"| `{lab}` | {c} |")
|
||||
d_md.append('')
|
||||
|
||||
d_md.append('### alternative_patterns 수')
|
||||
d_md.append('')
|
||||
d_md.append(f"- 평균 대안 수: {round(sum(alt_counts)/len(alt_counts), 1)}")
|
||||
d_md.append(f"- 대안 0개 프레임: {sum(1 for c in alt_counts if c == 0)}")
|
||||
d_md.append(f"- 대안 5개 프레임: {sum(1 for c in alt_counts if c == 5)}")
|
||||
d_md.append('')
|
||||
|
||||
d_md.append('## v1 과의 관계')
|
||||
d_md.append('')
|
||||
d_md.append(
|
||||
'- `structure_ontology.yaml` 원본 **유지** — v2 는 `structure_ontology_v2.yaml` 별도 파일.'
|
||||
)
|
||||
d_md.append(
|
||||
'- v2 매칭 로직 (pipeline_08_v3_structure_rerank.py 재구현) 에서 `_COMPAT` 대신 '
|
||||
'`content_affinity + structure_intent_v2 + alternative_patterns` 조합 사용 예정.'
|
||||
)
|
||||
d_md.append(
|
||||
'- v2 최종 승인 전까지 V3 재구현 보류 — 사용자 검토 (priority 5) 후 확정.'
|
||||
)
|
||||
d_md.append('')
|
||||
DIFF_MD.write_text('\n'.join(d_md), encoding='utf-8')
|
||||
|
||||
print('=' * 70)
|
||||
print('templates_v2 draft 생성 완료')
|
||||
print('=' * 70)
|
||||
print(f' 1. structure_ontology_v2.yaml: {OUT_V2_PATH}')
|
||||
print(f' 2. PRIORITY_5_REVIEW.md/html: {PRIORITY_MD}')
|
||||
print(f' 3. TEMPLATES_V2_DIFF.md: {DIFF_MD}')
|
||||
print()
|
||||
print(f' affinity primary 분포: {dict(aff_counter.most_common(5))}')
|
||||
print(f' intent primary 분포: {dict(intent_counter.most_common(5))}')
|
||||
print(f' 평균 대안 수: {round(sum(alt_counts)/len(alt_counts), 1)}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,567 @@
|
||||
"""Pipeline Step 12-r2 — templates_v2 재라벨링 (규칙 버그 수정 버전).
|
||||
|
||||
r1 (`pipeline_12_generate_templates_v2.py`) 의 버그:
|
||||
- 내용 설명 + suits + not_suits 를 **모두** 긍정 신호로 스캔
|
||||
- 결과: not_suits 의 "시간 순서 단계", "BIM vs DX 비교" 등에서 긍정 키워드 매칭 → 오판 4건
|
||||
|
||||
r2 수정:
|
||||
1. **내용 설명** = 주 신호 (positive)
|
||||
2. **suits** = 보너스 긍정 신호 (존재 시 confidence +)
|
||||
3. **not_suits** = **반대 신호** — 해당 affinity/intent 가 primary 로 오르지 못하게 **차단**
|
||||
4. Frame 12 (cycle-3way) 의 `3col-parallel` 대안 — 자동 파생 불가 항목 **수동 시드** 등록
|
||||
|
||||
산출물 (기존과 별도):
|
||||
- structure_ontology_v2_r2.yaml
|
||||
- PRIORITY_5_REVIEW_r2.md/html
|
||||
- TEMPLATES_V2_DIFF_r2.md
|
||||
|
||||
원본 r1 결과는 **유지** — 재현성 / 디버그용.
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
BLOCKS_DIR = ROOT / 'figma_to_html_agent' / 'blocks'
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from phase_common import _COMPAT
|
||||
|
||||
# r1 의 상수 재사용 (heuristic 테이블은 그대로)
|
||||
from pipeline_12_generate_templates_v2 import (
|
||||
CONTENT_AFFINITY_ENUM,
|
||||
STRUCTURE_INTENT_ENUM,
|
||||
LAYOUT_DEFAULT_AFFINITY,
|
||||
LAYOUT_DEFAULT_INTENT,
|
||||
AFFINITY_KEYWORDS,
|
||||
INTENT_KEYWORDS,
|
||||
parse_analysis_md,
|
||||
find_keyword_hits,
|
||||
derive_alternative_patterns as r1_derive_alternatives,
|
||||
)
|
||||
|
||||
ONTOLOGY_V1_PATH = HERE / 'structure_ontology.yaml'
|
||||
OUT_V2_PATH = HERE / 'structure_ontology_v2_r2.yaml'
|
||||
PRIORITY_MD = HERE / 'PRIORITY_5_REVIEW_r2.md'
|
||||
PRIORITY_HTML = HERE / 'PRIORITY_5_REVIEW_r2.html'
|
||||
DIFF_MD = HERE / 'TEMPLATES_V2_DIFF_r2.md'
|
||||
|
||||
PRIORITY_FRAME_NUMBERS = {3, 11, 12, 20, 29}
|
||||
|
||||
# ============================================================
|
||||
# 수동 시드 (자동 파생 한계 보완 — 사용자 승인 항목)
|
||||
# ============================================================
|
||||
MANUAL_SEED_ALTERNATIVES = {
|
||||
# Frame 12 (cycle-3way): _COMPAT 에 MDX 쪽 매핑 없어 자동 파생 불가
|
||||
'1171281189': [
|
||||
{
|
||||
'pattern': '3col-parallel',
|
||||
'reason': '3개 요소를 순환 대신 평면 병렬 나열로 표현하는 대안',
|
||||
'confidence': 0.7,
|
||||
'source': 'manual_seed',
|
||||
},
|
||||
{
|
||||
'pattern': 'circular-nodes',
|
||||
'reason': 'N개 요소를 radial 로 배치하는 대안',
|
||||
'confidence': 0.6,
|
||||
'source': 'manual_seed',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 수정된 라벨 함수 — not_suits 반대 신호 처리
|
||||
# ============================================================
|
||||
|
||||
def label_content_affinity_r2(layout, original_layout, content, suits, not_suits):
|
||||
"""r2: 긍정/반대 신호 분리.
|
||||
|
||||
1. content(내용 설명) 에서 키워드 매칭 → 주 긍정 신호
|
||||
2. suits 에서 키워드 매칭 → 보너스 긍정 (confidence +)
|
||||
3. not_suits 에서 키워드 매칭 → **반대 신호** (해당 affinity 는 primary 후보에서 제거)
|
||||
"""
|
||||
defaults = (LAYOUT_DEFAULT_AFFINITY.get(original_layout)
|
||||
or LAYOUT_DEFAULT_AFFINITY.get(layout)
|
||||
or ['concept_definition'])
|
||||
default_primary = defaults[0]
|
||||
default_secondary = defaults[1:]
|
||||
|
||||
content_hits = find_keyword_hits(content, AFFINITY_KEYWORDS)
|
||||
suits_text = ' '.join(suits)
|
||||
suits_hits = find_keyword_hits(suits_text, AFFINITY_KEYWORDS)
|
||||
not_suits_text = ' '.join(not_suits)
|
||||
not_suits_hits_dict = {lab: kws for lab, kws in find_keyword_hits(not_suits_text, AFFINITY_KEYWORDS)}
|
||||
|
||||
# 점수 계산: content 가중치 1.0, suits 보너스 0.5, not_suits 매칭 시 차단 (강한 penalty)
|
||||
score = defaultdict(float)
|
||||
kw_source = defaultdict(dict)
|
||||
|
||||
for lab, kws in content_hits:
|
||||
score[lab] += 1.0 * len(kws)
|
||||
kw_source[lab]['content'] = kws
|
||||
for lab, kws in suits_hits:
|
||||
score[lab] += 0.5 * len(kws)
|
||||
kw_source[lab]['suits'] = kws
|
||||
|
||||
# not_suits 차단 — score 0 으로 강제
|
||||
blocked_labels = set()
|
||||
for lab in list(score.keys()):
|
||||
if lab in not_suits_hits_dict:
|
||||
blocked_labels.add(lab)
|
||||
# 완전 제거 (primary 후보에서 빠지도록)
|
||||
del score[lab]
|
||||
kw_source[lab]['blocked_by_not_suits'] = not_suits_hits_dict[lab]
|
||||
|
||||
# primary 결정
|
||||
if score:
|
||||
primary = max(score.keys(), key=lambda k: score[k])
|
||||
primary_source = 'keyword_match'
|
||||
primary_kws = []
|
||||
for field in ('content', 'suits'):
|
||||
primary_kws.extend(kw_source[primary].get(field, []))
|
||||
confidence = min(0.95, 0.55 + 0.10 * score[primary])
|
||||
else:
|
||||
primary = default_primary
|
||||
primary_source = 'layout_default'
|
||||
primary_kws = []
|
||||
confidence = 0.6
|
||||
|
||||
# secondary: 나머지 score 상위 + default
|
||||
sec_candidates = [k for k in sorted(score.keys(), key=lambda k: -score[k]) if k != primary]
|
||||
for d in default_secondary:
|
||||
if d not in sec_candidates and d != primary and d not in blocked_labels:
|
||||
sec_candidates.append(d)
|
||||
secondary = sec_candidates[:2]
|
||||
|
||||
# evidence
|
||||
evidence = {}
|
||||
if primary_source == 'keyword_match':
|
||||
evidence['primary'] = {
|
||||
'source': 'keyword_match',
|
||||
'field': '내용 설명 / suits',
|
||||
'keywords': primary_kws,
|
||||
'rule': f"'{primary_kws[0] if primary_kws else '?'}' 등 키워드가 {primary} 를 가리킴",
|
||||
'confidence': round(confidence, 2),
|
||||
}
|
||||
else:
|
||||
evidence['primary'] = {
|
||||
'source': 'layout_default',
|
||||
'field': f'original_layout = {original_layout or layout}',
|
||||
'rule': 'layout → 기본 affinity',
|
||||
'confidence': 0.6,
|
||||
}
|
||||
for i, s in enumerate(secondary, start=1):
|
||||
hits_for_s = list(kw_source[s].get('content', [])) + list(kw_source[s].get('suits', []))
|
||||
if hits_for_s:
|
||||
evidence[f'secondary_{i}'] = {
|
||||
'source': 'keyword_match',
|
||||
'keywords': hits_for_s,
|
||||
'confidence': 0.65,
|
||||
}
|
||||
else:
|
||||
evidence[f'secondary_{i}'] = {
|
||||
'source': 'layout_default',
|
||||
'confidence': 0.5,
|
||||
}
|
||||
if blocked_labels:
|
||||
evidence['blocked_by_not_suits'] = {
|
||||
lab: kws for lab, kws in
|
||||
((lab, not_suits_hits_dict[lab]) for lab in blocked_labels)
|
||||
}
|
||||
|
||||
return {
|
||||
'primary': primary,
|
||||
'secondary': secondary,
|
||||
'evidence': evidence,
|
||||
}
|
||||
|
||||
|
||||
def label_structure_intent_r2(layout, original_layout, content, suits, not_suits,
|
||||
relation_type, cardinality):
|
||||
"""r2: intent 도 not_suits 차단 적용."""
|
||||
defaults = (LAYOUT_DEFAULT_INTENT.get(original_layout)
|
||||
or LAYOUT_DEFAULT_INTENT.get(layout)
|
||||
or ['multi_parallel'])
|
||||
default_primary = defaults[0]
|
||||
default_secondary = defaults[1:]
|
||||
|
||||
content_hits = find_keyword_hits(content, INTENT_KEYWORDS)
|
||||
suits_hits = find_keyword_hits(' '.join(suits), INTENT_KEYWORDS)
|
||||
not_suits_hits_dict = {lab: kws for lab, kws in
|
||||
find_keyword_hits(' '.join(not_suits), INTENT_KEYWORDS)}
|
||||
|
||||
score = defaultdict(float)
|
||||
kw_source = defaultdict(list)
|
||||
for lab, kws in content_hits:
|
||||
score[lab] += 1.0 * len(kws)
|
||||
kw_source[lab].extend(kws)
|
||||
for lab, kws in suits_hits:
|
||||
score[lab] += 0.5 * len(kws)
|
||||
kw_source[lab].extend(kws)
|
||||
|
||||
# relation_type + cardinality 구조 추가 신호 (content-based, 안전)
|
||||
ideal = cardinality.get('ideal') if cardinality else None
|
||||
if relation_type == 'compare' and ideal == 2:
|
||||
if any(s in content for s in ('AS-IS', 'TO-BE', '혁신', '전환', '이중')):
|
||||
score['state_transition'] += 1.0
|
||||
kw_source['state_transition'].append('AS-IS/TO-BE/혁신(content)')
|
||||
else:
|
||||
score['binary_compare'] += 0.8
|
||||
kw_source['binary_compare'].append('compare+cardinality=2')
|
||||
elif relation_type == 'parallel' and ideal and ideal >= 3:
|
||||
score['multi_parallel'] += 1.0
|
||||
kw_source['multi_parallel'].append(f'parallel+cardinality={ideal}')
|
||||
elif relation_type == 'sequence':
|
||||
score['sequence'] += 1.0
|
||||
kw_source['sequence'].append('relation_type=sequence')
|
||||
|
||||
# not_suits 차단
|
||||
blocked = set()
|
||||
for lab in list(score.keys()):
|
||||
if lab in not_suits_hits_dict:
|
||||
blocked.add(lab)
|
||||
del score[lab]
|
||||
|
||||
if score:
|
||||
primary = max(score.keys(), key=lambda k: score[k])
|
||||
primary_source = 'content_or_structure'
|
||||
primary_rule = (kw_source[primary][0] if kw_source[primary] else primary)
|
||||
confidence = min(0.95, 0.55 + 0.10 * score[primary])
|
||||
else:
|
||||
primary = default_primary
|
||||
primary_source = 'layout_default'
|
||||
primary_rule = f'default {primary}'
|
||||
confidence = 0.6
|
||||
|
||||
sec_candidates = [k for k in sorted(score.keys(), key=lambda k: -score[k]) if k != primary]
|
||||
for d in default_secondary:
|
||||
if d not in sec_candidates and d != primary and d not in blocked:
|
||||
sec_candidates.append(d)
|
||||
secondary = sec_candidates[:2]
|
||||
|
||||
evidence = {
|
||||
'primary': {
|
||||
'source': primary_source,
|
||||
'rule': primary_rule,
|
||||
'confidence': round(confidence, 2),
|
||||
}
|
||||
}
|
||||
for i, s in enumerate(secondary, start=1):
|
||||
evidence[f'secondary_{i}'] = {
|
||||
'source': 'layout_default' if s in default_secondary else 'keyword_match',
|
||||
'confidence': 0.55,
|
||||
}
|
||||
if blocked:
|
||||
evidence['blocked_by_not_suits'] = {lab: not_suits_hits_dict[lab] for lab in blocked}
|
||||
|
||||
return {
|
||||
'primary': primary,
|
||||
'secondary': secondary,
|
||||
'evidence': evidence,
|
||||
}
|
||||
|
||||
|
||||
def derive_alternatives_with_seed(fid, original_layout):
|
||||
"""r1 자동 파생 + 수동 시드 병합."""
|
||||
auto = r1_derive_alternatives(original_layout)
|
||||
seed = MANUAL_SEED_ALTERNATIVES.get(fid, [])
|
||||
# auto 결과에 source 필드 없으면 추가
|
||||
for a in auto:
|
||||
a.setdefault('source', 'derived_from_COMPAT')
|
||||
# 시드 병합 (중복 pattern 은 시드 우선)
|
||||
seen = {a['pattern'] for a in seed}
|
||||
for a in auto:
|
||||
if a['pattern'] not in seen:
|
||||
seed.append(a)
|
||||
seen.add(a['pattern'])
|
||||
return seed
|
||||
|
||||
|
||||
def generate_v2_entry_r2(fid, tpl_v1):
|
||||
layout = tpl_v1['visual_pattern']['layout']
|
||||
original_layout = tpl_v1['source'].get('original_layout', layout)
|
||||
relation_type = tpl_v1['visual_pattern'].get('relation_type')
|
||||
cardinality = tpl_v1['visual_pattern'].get('cardinality', {})
|
||||
|
||||
analysis = parse_analysis_md(BLOCKS_DIR / fid / 'analysis.md')
|
||||
|
||||
affinity = label_content_affinity_r2(
|
||||
layout, original_layout,
|
||||
analysis['content'], analysis['suits'], analysis['not_suits'],
|
||||
)
|
||||
intent = label_structure_intent_r2(
|
||||
layout, original_layout,
|
||||
analysis['content'], analysis['suits'], analysis['not_suits'],
|
||||
relation_type, cardinality,
|
||||
)
|
||||
alternatives = derive_alternatives_with_seed(fid, original_layout)
|
||||
|
||||
entry = dict(tpl_v1)
|
||||
entry['content_affinity'] = affinity
|
||||
entry['structure_intent_v2'] = intent
|
||||
entry['alternative_patterns'] = alternatives
|
||||
entry['v2_meta'] = {
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'source_analysis': f'{fid}/analysis.md',
|
||||
'generator_version': 'r2 (not_suits 차단 + 수동 시드)',
|
||||
'needs_review': tpl_v1.get('short_id') in {'03', '11', '12', '20', '29'},
|
||||
}
|
||||
return entry
|
||||
|
||||
|
||||
def main():
|
||||
v1 = yaml.safe_load(ONTOLOGY_V1_PATH.read_text(encoding='utf-8'))
|
||||
templates_v1 = v1['templates_v1']
|
||||
|
||||
templates_v2 = {}
|
||||
for fid, tpl in templates_v1.items():
|
||||
templates_v2[fid] = generate_v2_entry_r2(fid, tpl)
|
||||
|
||||
# structure_ontology_v2_r2.yaml
|
||||
out = {
|
||||
'meta': {
|
||||
'schema_version': 'template-fit-v2-r2-draft',
|
||||
'generated_from': 'structure_ontology.yaml (templates_v1)',
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'generator': 'pipeline_12_r2_generate_templates_v2.py',
|
||||
'predecessor_draft': 'structure_ontology_v2.yaml (r1, 유지)',
|
||||
'r2_changes': [
|
||||
'r1 버그 수정: not_suits 를 긍정 신호로 스캔하던 문제 해결',
|
||||
'suits = 보너스 긍정, not_suits = 반대 신호(primary 후보 차단)',
|
||||
'Frame 12 (cycle-3way) alternative_patterns 에 3col-parallel 수동 시드 추가',
|
||||
],
|
||||
'status': 'draft_pending_user_review',
|
||||
'priority_review_frames': sorted(PRIORITY_FRAME_NUMBERS),
|
||||
'vocabularies': {
|
||||
'content_affinity': CONTENT_AFFINITY_ENUM,
|
||||
'structure_intent': STRUCTURE_INTENT_ENUM,
|
||||
},
|
||||
'matching_weights_initial': {
|
||||
'layout_compat': 0.40,
|
||||
'content_affinity': 0.35,
|
||||
'structure_intent': 0.25,
|
||||
},
|
||||
},
|
||||
'templates_v2': templates_v2,
|
||||
}
|
||||
OUT_V2_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# PRIORITY_5_REVIEW_r2 — r1 포맷 재사용 + r1 vs r2 비교 추가
|
||||
r1 = yaml.safe_load((HERE / 'structure_ontology_v2.yaml').read_text(encoding='utf-8'))
|
||||
templates_v2_r1 = r1['templates_v2']
|
||||
|
||||
short_to_fid = {v['short_id']: k for k, v in templates_v1.items()}
|
||||
|
||||
pr_md = []
|
||||
pr_md.append('# Priority 5 프레임 검토 리포트 (C.2-b r2)')
|
||||
pr_md.append('')
|
||||
pr_md.append('r1 버그 수정 후 재라벨링. 각 프레임마다 **r1 → r2 변화** 를 함께 표시.')
|
||||
pr_md.append('')
|
||||
|
||||
for fn in ['03', '11', '12', '20', '29']:
|
||||
fid = short_to_fid.get(fn)
|
||||
if not fid:
|
||||
continue
|
||||
v2_r1 = templates_v2_r1[fid]
|
||||
v2_r2 = templates_v2[fid]
|
||||
v1_entry = templates_v1[fid]
|
||||
title = v1_entry['source']['title']
|
||||
original_layout = v1_entry['source'].get('original_layout')
|
||||
|
||||
pr_md.append(f"## Frame {fn} — {title}")
|
||||
pr_md.append('')
|
||||
pr_md.append(f"- layout: `{v1_entry['visual_pattern']['layout']}` "
|
||||
f"(original: `{original_layout}`)")
|
||||
pr_md.append(f"- relation_type / cardinality.ideal: "
|
||||
f"`{v1_entry['visual_pattern'].get('relation_type')}` / "
|
||||
f"{v1_entry['visual_pattern'].get('cardinality', {}).get('ideal')}")
|
||||
pr_md.append('')
|
||||
|
||||
# r1 → r2 변화
|
||||
r1_aff = v2_r1['content_affinity']
|
||||
r2_aff = v2_r2['content_affinity']
|
||||
r1_int = v2_r1['structure_intent_v2']
|
||||
r2_int = v2_r2['structure_intent_v2']
|
||||
|
||||
changed_aff = r1_aff['primary'] != r2_aff['primary']
|
||||
changed_int = r1_int['primary'] != r2_int['primary']
|
||||
|
||||
pr_md.append('### r1 → r2 변화 요약')
|
||||
pr_md.append('')
|
||||
pr_md.append('| 축 | r1 primary | r2 primary | 변경 |')
|
||||
pr_md.append('|---|---|---|---|')
|
||||
pr_md.append(
|
||||
f"| content_affinity | `{r1_aff['primary']}` | "
|
||||
f"**`{r2_aff['primary']}`** | {'✓' if changed_aff else '—'} |"
|
||||
)
|
||||
pr_md.append(
|
||||
f"| structure_intent | `{r1_int['primary']}` | "
|
||||
f"**`{r2_int['primary']}`** | {'✓' if changed_int else '—'} |"
|
||||
)
|
||||
pr_md.append('')
|
||||
|
||||
# r2 content_affinity
|
||||
pr_md.append('### r2 content_affinity')
|
||||
pr_md.append(f"- primary: **`{r2_aff['primary']}`**")
|
||||
if r2_aff['secondary']:
|
||||
pr_md.append(f"- secondary: {', '.join(f'`{s}`' for s in r2_aff['secondary'])}")
|
||||
pr_md.append('- evidence:')
|
||||
for k, v in r2_aff['evidence'].items():
|
||||
if k == 'blocked_by_not_suits':
|
||||
blocked_str = ', '.join(f'{lab}({kws})' for lab, kws in v.items())
|
||||
pr_md.append(f" - `{k}` — {blocked_str}")
|
||||
else:
|
||||
extras = []
|
||||
if 'keywords' in v and v['keywords']:
|
||||
extras.append(f"kw={v['keywords']}")
|
||||
if 'rule' in v:
|
||||
extras.append(f"rule={v['rule']}")
|
||||
extras.append(f"conf={v['confidence']}")
|
||||
pr_md.append(f" - `{k}` ({v['source']}) — {' / '.join(extras)}")
|
||||
pr_md.append('')
|
||||
|
||||
# r2 structure_intent
|
||||
pr_md.append('### r2 structure_intent')
|
||||
pr_md.append(f"- primary: **`{r2_int['primary']}`**")
|
||||
if r2_int['secondary']:
|
||||
pr_md.append(f"- secondary: {', '.join(f'`{s}`' for s in r2_int['secondary'])}")
|
||||
pr_md.append('- evidence:')
|
||||
for k, v in r2_int['evidence'].items():
|
||||
if k == 'blocked_by_not_suits':
|
||||
blocked_str = ', '.join(f'{lab}({kws})' for lab, kws in v.items())
|
||||
pr_md.append(f" - `{k}` — {blocked_str}")
|
||||
else:
|
||||
pr_md.append(f" - `{k}` ({v['source']}) — rule={v.get('rule', '—')} / conf={v['confidence']}")
|
||||
pr_md.append('')
|
||||
|
||||
# alternative_patterns
|
||||
pr_md.append('### alternative_patterns')
|
||||
if v2_r2['alternative_patterns']:
|
||||
pr_md.append('| 대안 | confidence | source | reason |')
|
||||
pr_md.append('|---|---:|---|---|')
|
||||
for a in v2_r2['alternative_patterns']:
|
||||
pr_md.append(f"| `{a['pattern']}` | {a['confidence']} | "
|
||||
f"{a.get('source', 'derived')} | {a['reason']} |")
|
||||
else:
|
||||
pr_md.append('(없음)')
|
||||
pr_md.append('')
|
||||
pr_md.append('---')
|
||||
pr_md.append('')
|
||||
|
||||
pr_text = '\n'.join(pr_md)
|
||||
PRIORITY_MD.write_text(pr_text, encoding='utf-8')
|
||||
|
||||
style = """
|
||||
body { font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1100px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; }
|
||||
h1 { border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; }
|
||||
h2 { margin-top: 2.5em; background: #e0e7ff; padding: 0.6em 0.9em; border-left: 4px solid #0a6; border-radius: 4px; }
|
||||
h3 { margin-top: 1.3em; color: #1a365d; }
|
||||
table { border-collapse: collapse; background: #fff; margin: 0.5em 0 1em; }
|
||||
th, td { border: 1px solid #e2e8f0; padding: 8px 10px; text-align: left; vertical-align: top; }
|
||||
th { background: #1e293b; color: #fff; }
|
||||
code { background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; }
|
||||
strong { color: #0a6; }
|
||||
"""
|
||||
html_body = markdown.markdown(pr_text, extensions=['tables'])
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><title>Priority 5 r2</title><style>{style}</style></head>
|
||||
<body>{html_body}</body></html>"""
|
||||
PRIORITY_HTML.write_text(html, encoding='utf-8')
|
||||
|
||||
# TEMPLATES_V2_DIFF_r2.md
|
||||
d_md = []
|
||||
d_md.append('# templates_v2 r1 → r2 변경 요약')
|
||||
d_md.append('')
|
||||
d_md.append('## 규칙 수정')
|
||||
d_md.append('- `suits` = 보너스 긍정 신호 (0.5 가중치)')
|
||||
d_md.append('- `not_suits` = **반대 신호** — 해당 affinity/intent 가 primary 후보에서 차단')
|
||||
d_md.append('- Frame 12 (cycle-3way) 에 `3col-parallel` / `circular-nodes` 수동 시드 추가')
|
||||
d_md.append('')
|
||||
|
||||
# primary 변경된 프레임 집계
|
||||
changed_affinity = []
|
||||
changed_intent = []
|
||||
for fid, r2_entry in templates_v2.items():
|
||||
r1_entry = templates_v2_r1[fid]
|
||||
short_id = templates_v1[fid]['short_id']
|
||||
if r1_entry['content_affinity']['primary'] != r2_entry['content_affinity']['primary']:
|
||||
changed_affinity.append((short_id, fid,
|
||||
r1_entry['content_affinity']['primary'],
|
||||
r2_entry['content_affinity']['primary']))
|
||||
if r1_entry['structure_intent_v2']['primary'] != r2_entry['structure_intent_v2']['primary']:
|
||||
changed_intent.append((short_id, fid,
|
||||
r1_entry['structure_intent_v2']['primary'],
|
||||
r2_entry['structure_intent_v2']['primary']))
|
||||
|
||||
d_md.append(f'## r1 → r2 primary 변경된 프레임')
|
||||
d_md.append('')
|
||||
d_md.append(f"- content_affinity primary 변경: **{len(changed_affinity)}/32 프레임**")
|
||||
d_md.append(f"- structure_intent primary 변경: **{len(changed_intent)}/32 프레임**")
|
||||
d_md.append('')
|
||||
|
||||
if changed_affinity:
|
||||
d_md.append('### content_affinity primary 변경')
|
||||
d_md.append('')
|
||||
d_md.append('| Frame | r1 | → | r2 |')
|
||||
d_md.append('|---|---|---|---|')
|
||||
for short_id, fid, r1_p, r2_p in sorted(changed_affinity):
|
||||
d_md.append(f"| {short_id} | `{r1_p}` | → | **`{r2_p}`** |")
|
||||
d_md.append('')
|
||||
|
||||
if changed_intent:
|
||||
d_md.append('### structure_intent primary 변경')
|
||||
d_md.append('')
|
||||
d_md.append('| Frame | r1 | → | r2 |')
|
||||
d_md.append('|---|---|---|---|')
|
||||
for short_id, fid, r1_p, r2_p in sorted(changed_intent):
|
||||
d_md.append(f"| {short_id} | `{r1_p}` | → | **`{r2_p}`** |")
|
||||
d_md.append('')
|
||||
|
||||
# r2 분포
|
||||
aff_counter = Counter()
|
||||
intent_counter = Counter()
|
||||
for e in templates_v2.values():
|
||||
aff_counter[e['content_affinity']['primary']] += 1
|
||||
intent_counter[e['structure_intent_v2']['primary']] += 1
|
||||
|
||||
d_md.append('## r2 primary 분포')
|
||||
d_md.append('')
|
||||
d_md.append('### content_affinity')
|
||||
d_md.append('| primary | 프레임 수 |')
|
||||
d_md.append('|---|---:|')
|
||||
for lab, c in aff_counter.most_common():
|
||||
d_md.append(f"| `{lab}` | {c} |")
|
||||
d_md.append('')
|
||||
d_md.append('### structure_intent')
|
||||
d_md.append('| primary | 프레임 수 |')
|
||||
d_md.append('|---|---:|')
|
||||
for lab, c in intent_counter.most_common():
|
||||
d_md.append(f"| `{lab}` | {c} |")
|
||||
d_md.append('')
|
||||
|
||||
DIFF_MD.write_text('\n'.join(d_md), encoding='utf-8')
|
||||
|
||||
print('=' * 70)
|
||||
print('r2 재라벨링 완료')
|
||||
print('=' * 70)
|
||||
print(f' 1. {OUT_V2_PATH}')
|
||||
print(f' 2. {PRIORITY_MD} / .html')
|
||||
print(f' 3. {DIFF_MD}')
|
||||
print()
|
||||
print(f' primary 변경: affinity {len(changed_affinity)}/32, intent {len(changed_intent)}/32')
|
||||
print(f' r2 affinity 분포 Top 5: {dict(aff_counter.most_common(5))}')
|
||||
print(f' r2 intent 분포 Top 5: {dict(intent_counter.most_common(5))}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,313 @@
|
||||
"""Pipeline Step 12-r3 — 키워드 튜닝 후 재라벨링.
|
||||
|
||||
r2 에서 확인된 남은 문제:
|
||||
- Frame 03/04 stakeholder_roles: "수행" 이 너무 약한 신호로 잡힘
|
||||
- Frame 01 comparative_matrix: "순환도" 는 interrelation 에 가까움
|
||||
- 일부 "국외/현황/사례" 가 policy_requirements 로 안 잡힘
|
||||
|
||||
r3 변경 (키워드 튜닝만):
|
||||
- `stakeholder_roles` 에서 "수행" 제거 — 너무 약한 단독 신호
|
||||
- `policy_requirements` 에 "국외", "선진", "현황", "사례" 추가
|
||||
- `interrelation` 에 "순환도", "관계도" 추가
|
||||
|
||||
r2 의 모든 로직(not_suits 차단, 수동 시드) 그대로 유지.
|
||||
|
||||
산출물:
|
||||
- structure_ontology_v2_r3.yaml
|
||||
- PRIORITY_5_REVIEW_r3.md (+ 추가 샘플 7개)
|
||||
- TEMPLATES_V2_DIFF_r3.md (r2 대비)
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
BLOCKS_DIR = ROOT / 'figma_to_html_agent' / 'blocks'
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from phase_common import _COMPAT
|
||||
|
||||
# r1/r2 의 상수 재사용. AFFINITY_KEYWORDS 는 **r3 에서 덮어쓸** 것.
|
||||
import pipeline_12_generate_templates_v2 as r1mod
|
||||
import pipeline_12_r2_generate_templates_v2 as r2mod
|
||||
from pipeline_12_r2_generate_templates_v2 import (
|
||||
MANUAL_SEED_ALTERNATIVES,
|
||||
label_content_affinity_r2,
|
||||
label_structure_intent_r2,
|
||||
derive_alternatives_with_seed,
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 키워드 튜닝 (r3)
|
||||
# ============================================================
|
||||
AFFINITY_KEYWORDS_R3 = dict(r1mod.AFFINITY_KEYWORDS)
|
||||
AFFINITY_KEYWORDS_R3['stakeholder_roles'] = ['역할', '책임', '주도'] # '수행' 제거
|
||||
AFFINITY_KEYWORDS_R3['policy_requirements'] = [
|
||||
'정책', '제도', '도입', '거버넌스', '전면 도입',
|
||||
'국외', '국내', '선진', '현황', '사례', # 추가
|
||||
]
|
||||
AFFINITY_KEYWORDS_R3['interrelation'] = [
|
||||
'상호관계', '순환', '조화', '교차', '수렴', '3원',
|
||||
'순환도', '관계도', # 추가
|
||||
]
|
||||
|
||||
# r1/r2 모듈 네임스페이스 모두에 tuned keywords 주입
|
||||
# (r2 가 `from r1 import AFFINITY_KEYWORDS` 했기 때문에 r2 네임스페이스에도 새 바인딩 필요)
|
||||
r1mod.AFFINITY_KEYWORDS = AFFINITY_KEYWORDS_R3
|
||||
r2mod.AFFINITY_KEYWORDS = AFFINITY_KEYWORDS_R3
|
||||
|
||||
ONTOLOGY_V1_PATH = HERE / 'structure_ontology.yaml'
|
||||
OUT_V2_PATH = HERE / 'structure_ontology_v2_r3.yaml'
|
||||
PRIORITY_MD = HERE / 'PRIORITY_5_REVIEW_r3.md'
|
||||
PRIORITY_HTML = HERE / 'PRIORITY_5_REVIEW_r3.html'
|
||||
DIFF_MD = HERE / 'TEMPLATES_V2_DIFF_r3.md'
|
||||
|
||||
PRIORITY_FRAMES = ['03', '11', '12', '20', '29']
|
||||
ADDITIONAL_SAMPLES = ['01', '04', '13', '14', '19', '22', '28'] # 다양한 카테고리 샘플
|
||||
|
||||
R2_PATH = HERE / 'structure_ontology_v2_r2.yaml'
|
||||
|
||||
|
||||
def generate_v2_entry_r3(fid, tpl_v1):
|
||||
layout = tpl_v1['visual_pattern']['layout']
|
||||
original_layout = tpl_v1['source'].get('original_layout', layout)
|
||||
relation_type = tpl_v1['visual_pattern'].get('relation_type')
|
||||
cardinality = tpl_v1['visual_pattern'].get('cardinality', {})
|
||||
|
||||
analysis = r1mod.parse_analysis_md(BLOCKS_DIR / fid / 'analysis.md')
|
||||
|
||||
affinity = label_content_affinity_r2(
|
||||
layout, original_layout,
|
||||
analysis['content'], analysis['suits'], analysis['not_suits'],
|
||||
)
|
||||
intent = label_structure_intent_r2(
|
||||
layout, original_layout,
|
||||
analysis['content'], analysis['suits'], analysis['not_suits'],
|
||||
relation_type, cardinality,
|
||||
)
|
||||
alternatives = derive_alternatives_with_seed(fid, original_layout)
|
||||
|
||||
entry = dict(tpl_v1)
|
||||
entry['content_affinity'] = affinity
|
||||
entry['structure_intent_v2'] = intent
|
||||
entry['alternative_patterns'] = alternatives
|
||||
entry['v2_meta'] = {
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'source_analysis': f'{fid}/analysis.md',
|
||||
'generator_version': 'r3 (r2 + 키워드 튜닝: 수행 제거, 국외/사례 추가, 순환도 추가)',
|
||||
'needs_review': tpl_v1.get('short_id') in set(PRIORITY_FRAMES),
|
||||
}
|
||||
return entry
|
||||
|
||||
|
||||
def main():
|
||||
v1 = yaml.safe_load(ONTOLOGY_V1_PATH.read_text(encoding='utf-8'))
|
||||
templates_v1 = v1['templates_v1']
|
||||
|
||||
templates_v2_r3 = {}
|
||||
for fid, tpl in templates_v1.items():
|
||||
templates_v2_r3[fid] = generate_v2_entry_r3(fid, tpl)
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'schema_version': 'template-fit-v2-r3-draft',
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'generator': 'pipeline_12_r3_generate_templates_v2.py',
|
||||
'predecessor_drafts': ['structure_ontology_v2.yaml (r1)', 'structure_ontology_v2_r2.yaml (r2)'],
|
||||
'r3_changes': [
|
||||
'stakeholder_roles 키워드에서 "수행" 제거 — 너무 약한 단독 신호',
|
||||
'policy_requirements 에 국외/국내/선진/현황/사례 추가',
|
||||
'interrelation 에 순환도/관계도 추가',
|
||||
'r2 의 not_suits 차단 + 수동 시드 로직 그대로 유지',
|
||||
],
|
||||
'status': 'draft_pending_user_review',
|
||||
'priority_review_frames': [int(x) for x in PRIORITY_FRAMES],
|
||||
'additional_sample_frames': [int(x) for x in ADDITIONAL_SAMPLES],
|
||||
'vocabularies': {
|
||||
'content_affinity': r1mod.CONTENT_AFFINITY_ENUM,
|
||||
'structure_intent': r1mod.STRUCTURE_INTENT_ENUM,
|
||||
},
|
||||
},
|
||||
'templates_v2': templates_v2_r3,
|
||||
}
|
||||
OUT_V2_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
# 비교 리포트
|
||||
r2 = yaml.safe_load(R2_PATH.read_text(encoding='utf-8'))
|
||||
templates_r2 = r2['templates_v2']
|
||||
|
||||
short_to_fid = {v['short_id']: k for k, v in templates_v1.items()}
|
||||
|
||||
# r2 → r3 변경 집계
|
||||
changed_aff = []
|
||||
changed_int = []
|
||||
for fid, r3_e in templates_v2_r3.items():
|
||||
r2_e = templates_r2[fid]
|
||||
s = templates_v1[fid]['short_id']
|
||||
if r2_e['content_affinity']['primary'] != r3_e['content_affinity']['primary']:
|
||||
changed_aff.append((s, r2_e['content_affinity']['primary'], r3_e['content_affinity']['primary']))
|
||||
if r2_e['structure_intent_v2']['primary'] != r3_e['structure_intent_v2']['primary']:
|
||||
changed_int.append((s, r2_e['structure_intent_v2']['primary'], r3_e['structure_intent_v2']['primary']))
|
||||
|
||||
# ============================================================
|
||||
# PRIORITY + 샘플 리포트
|
||||
# ============================================================
|
||||
pr_md = []
|
||||
pr_md.append('# Priority 5 + 추가 샘플 7개 검토 (r3)')
|
||||
pr_md.append('')
|
||||
pr_md.append(
|
||||
'키워드 튜닝 후 재라벨링. 각 프레임에 **r2 → r3 변화** 표시. '
|
||||
'추가 샘플은 r2 에서 의심스러웠던 카테고리 (stakeholder_roles, comparative_matrix, '
|
||||
'goal_axes, process_steps, persona_benefit) 에서 선별.'
|
||||
)
|
||||
pr_md.append('')
|
||||
|
||||
pr_md.append(f'## r2 → r3 primary 변경 요약')
|
||||
pr_md.append('')
|
||||
pr_md.append(f"- content_affinity primary 변경: **{len(changed_aff)}/32 프레임**")
|
||||
pr_md.append(f"- structure_intent primary 변경: **{len(changed_int)}/32 프레임**")
|
||||
pr_md.append('')
|
||||
if changed_aff:
|
||||
pr_md.append('### content_affinity primary 변경 (전체)')
|
||||
pr_md.append('| Frame | r2 | → | r3 |')
|
||||
pr_md.append('|---|---|---|---|')
|
||||
for s, o, n in sorted(changed_aff):
|
||||
pr_md.append(f"| {s} | `{o}` | → | **`{n}`** |")
|
||||
pr_md.append('')
|
||||
|
||||
# 프레임별 상세
|
||||
def render_frame(fn, category):
|
||||
fid = short_to_fid.get(fn)
|
||||
if not fid:
|
||||
return ''
|
||||
r3_e = templates_v2_r3[fid]
|
||||
r2_e = templates_r2[fid]
|
||||
v1_e = templates_v1[fid]
|
||||
lines = []
|
||||
lines.append(f"## [{category}] Frame {fn} — {v1_e['source']['title']}")
|
||||
lines.append('')
|
||||
lines.append(f"- layout: `{v1_e['visual_pattern']['layout']}` "
|
||||
f"(original: `{v1_e['source'].get('original_layout')}`)")
|
||||
lines.append(f"- relation_type / cardinality.ideal: "
|
||||
f"`{v1_e['visual_pattern'].get('relation_type')}` / "
|
||||
f"{v1_e['visual_pattern'].get('cardinality', {}).get('ideal')}")
|
||||
lines.append('')
|
||||
|
||||
# r2 vs r3 변화
|
||||
r2a = r2_e['content_affinity']['primary']
|
||||
r3a = r3_e['content_affinity']['primary']
|
||||
r2i = r2_e['structure_intent_v2']['primary']
|
||||
r3i = r3_e['structure_intent_v2']['primary']
|
||||
lines.append('### r2 → r3')
|
||||
lines.append('| 축 | r2 | r3 | 변경 |')
|
||||
lines.append('|---|---|---|---|')
|
||||
lines.append(f"| content_affinity | `{r2a}` | **`{r3a}`** | {'✓' if r2a != r3a else '—'} |")
|
||||
lines.append(f"| structure_intent | `{r2i}` | **`{r3i}`** | {'✓' if r2i != r3i else '—'} |")
|
||||
lines.append('')
|
||||
|
||||
# r3 상세
|
||||
aff = r3_e['content_affinity']
|
||||
intent = r3_e['structure_intent_v2']
|
||||
lines.append(f"### r3 content_affinity: **`{aff['primary']}`**")
|
||||
if aff['secondary']:
|
||||
lines.append(f"- secondary: {', '.join(f'`{s}`' for s in aff['secondary'])}")
|
||||
ev = aff['evidence'].get('primary', {})
|
||||
lines.append(f"- evidence: source={ev.get('source')}, "
|
||||
f"rule={ev.get('rule', '—')}, conf={ev.get('confidence')}")
|
||||
if 'blocked_by_not_suits' in aff['evidence']:
|
||||
blocked = list(aff['evidence']['blocked_by_not_suits'].keys())
|
||||
lines.append(f"- blocked: {blocked}")
|
||||
|
||||
lines.append('')
|
||||
lines.append(f"### r3 structure_intent: **`{intent['primary']}`**")
|
||||
if intent['secondary']:
|
||||
lines.append(f"- secondary: {', '.join(f'`{s}`' for s in intent['secondary'])}")
|
||||
ev = intent['evidence'].get('primary', {})
|
||||
lines.append(f"- evidence: source={ev.get('source')}, "
|
||||
f"rule={ev.get('rule', '—')}, conf={ev.get('confidence')}")
|
||||
lines.append('')
|
||||
|
||||
lines.append(f"### alternative_patterns ({len(r3_e['alternative_patterns'])})")
|
||||
for a in r3_e['alternative_patterns']:
|
||||
lines.append(f"- `{a['pattern']}` ({a['confidence']}, {a.get('source', 'derived')}): {a['reason']}")
|
||||
lines.append('')
|
||||
lines.append('---')
|
||||
lines.append('')
|
||||
return '\n'.join(lines)
|
||||
|
||||
pr_md.append('## Priority 5')
|
||||
pr_md.append('')
|
||||
for fn in PRIORITY_FRAMES:
|
||||
pr_md.append(render_frame(fn, 'Priority'))
|
||||
|
||||
pr_md.append('## 추가 샘플 (r2 의심 프레임)')
|
||||
pr_md.append('')
|
||||
for fn in ADDITIONAL_SAMPLES:
|
||||
pr_md.append(render_frame(fn, 'Sample'))
|
||||
|
||||
PRIORITY_MD.write_text('\n'.join(pr_md), encoding='utf-8')
|
||||
|
||||
style = """body { font-family: -apple-system, "Segoe UI", Pretendard, sans-serif; max-width: 1100px; margin: 2em auto; padding: 0 1.5em 4em; line-height: 1.65; color: #222; background: #f8fafc; }
|
||||
h1 { border-bottom: 3px solid #2563eb; padding-bottom: 0.25em; }
|
||||
h2 { margin-top: 2em; background: #e0e7ff; padding: 0.5em 0.8em; border-left: 4px solid #0a6; border-radius: 4px; }
|
||||
h3 { margin-top: 1em; color: #1a365d; font-size: 1em; }
|
||||
table { border-collapse: collapse; background: #fff; margin: 0.5em 0 1em; }
|
||||
th, td { border: 1px solid #e2e8f0; padding: 6px 10px; vertical-align: top; }
|
||||
th { background: #1e293b; color: #fff; font-size: 0.9em; }
|
||||
code { background: #f4f4f4; padding: 1px 6px; border-radius: 3px; font-size: 0.9em; color: #111; }
|
||||
strong { color: #0a6; }"""
|
||||
html_body = markdown.markdown(PRIORITY_MD.read_text(encoding='utf-8'), extensions=['tables'])
|
||||
html = f"""<!DOCTYPE html><html lang="ko"><head><meta charset="utf-8"><title>Priority+Sample r3</title><style>{style}</style></head><body>{html_body}</body></html>"""
|
||||
PRIORITY_HTML.write_text(html, encoding='utf-8')
|
||||
|
||||
# DIFF_r3
|
||||
d = []
|
||||
d.append('# r2 → r3 변경 요약')
|
||||
d.append('')
|
||||
d.append('## 키워드 튜닝')
|
||||
d.append('- `stakeholder_roles`: "수행" **제거** (너무 약한 단독 신호)')
|
||||
d.append('- `policy_requirements`: "국외/국내/선진/현황/사례" 추가')
|
||||
d.append('- `interrelation`: "순환도/관계도" 추가')
|
||||
d.append('')
|
||||
d.append(f'## primary 변경 집계')
|
||||
d.append(f'- content_affinity: **{len(changed_aff)}/32 변경**')
|
||||
d.append(f'- structure_intent: **{len(changed_int)}/32 변경**')
|
||||
d.append('')
|
||||
if changed_aff:
|
||||
d.append('### 변경 상세')
|
||||
d.append('| Frame | r2 | → | r3 |')
|
||||
d.append('|---|---|---|---|')
|
||||
for s, o, n in sorted(changed_aff):
|
||||
d.append(f'| {s} | `{o}` | → | **`{n}`** |')
|
||||
d.append('')
|
||||
|
||||
# r3 분포
|
||||
aff_counter = Counter(e['content_affinity']['primary'] for e in templates_v2_r3.values())
|
||||
d.append('## r3 affinity 분포')
|
||||
d.append('| primary | 수 |')
|
||||
d.append('|---|---:|')
|
||||
for lab, c in aff_counter.most_common():
|
||||
d.append(f'| `{lab}` | {c} |')
|
||||
DIFF_MD.write_text('\n'.join(d), encoding='utf-8')
|
||||
|
||||
print('=' * 70)
|
||||
print('r3 재라벨링 완료')
|
||||
print('=' * 70)
|
||||
print(f' 1. {OUT_V2_PATH}')
|
||||
print(f' 2. {PRIORITY_MD} / .html')
|
||||
print(f' 3. {DIFF_MD}')
|
||||
print()
|
||||
print(f' primary 변경 (r2→r3): affinity {len(changed_aff)}/32, intent {len(changed_int)}/32')
|
||||
print(f' r3 affinity 분포: {dict(aff_counter.most_common())}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Pipeline Step 15 (BM25 + 세트) — 개별 토큰 + 복합 토큰(세트) 모두 넣은 BM25.
|
||||
|
||||
이전 pipeline_15_bm25_comparison 은 개별 토큰만 썼음 → 세트 정보 누락.
|
||||
이번엔 세트를 "복합 토큰(phrase token)" 으로 취급해 BM25 에 포함.
|
||||
|
||||
복합 토큰 정의:
|
||||
· 프레임의 각 source_text_line 에서 추출된 키워드 묶음 (예: [BIM, DX, 이해])
|
||||
· tf(compound, frame) = 1 (한 source line = 1 occurrence)
|
||||
· df(compound) = "그 모든 키워드를 다 가진 프레임 수"
|
||||
· IDF(compound) = log((N - df + 0.5) / (df + 0.5) + 1)
|
||||
|
||||
복합 토큰 매칭 (MDX 쪽):
|
||||
· MDX 에 compound 의 키워드가 얼마나 있나 → coverage (0~1)
|
||||
· 기여도 = IDF(compound) × tf_norm × coverage (부분 매칭 허용)
|
||||
|
||||
최종 BM25 점수 = Σ 개별 토큰 기여 + Σ 복합 토큰 기여
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from math import log
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
N_FRAMES = 32
|
||||
K1 = 1.5
|
||||
B = 0.75
|
||||
|
||||
|
||||
def main():
|
||||
auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8'))
|
||||
v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8'))
|
||||
normalized = yaml.safe_load((HERE / 'normalized_text_tokens.yaml').read_text(encoding='utf-8'))
|
||||
|
||||
# ─── 개별 토큰 tf (프레임별) ───
|
||||
frame_tokens = defaultdict(lambda: defaultdict(int))
|
||||
for set_id, s in auto['source_text_sets'].items():
|
||||
fid = s['frame_id']
|
||||
for t in s.get('terms', []):
|
||||
frame_tokens[fid][t['token']] += t.get('local_count_in_frame', 1)
|
||||
|
||||
# ─── 복합 토큰 (세트) 프레임별 ───
|
||||
frame_compounds = defaultdict(list)
|
||||
for set_id, s in auto['source_text_sets'].items():
|
||||
fid = s['frame_id']
|
||||
comp = tuple(sorted(set(s['term_values'])))
|
||||
if len(comp) >= 1:
|
||||
frame_compounds[fid].append(comp)
|
||||
|
||||
# ─── 문서 길이 |D| ───
|
||||
# 개별 토큰 수 + 복합 토큰 수 (각 compound 는 1 unit)
|
||||
frame_len = {}
|
||||
for fid in frame_tokens:
|
||||
ind_len = sum(frame_tokens[fid].values())
|
||||
comp_len = len(frame_compounds.get(fid, []))
|
||||
frame_len[fid] = ind_len + comp_len
|
||||
avg_dl = sum(frame_len.values()) / max(len(frame_len), 1)
|
||||
|
||||
# ─── 개별 토큰 df ───
|
||||
ind_df = defaultdict(int)
|
||||
for fid, counts in frame_tokens.items():
|
||||
for tok in counts:
|
||||
ind_df[tok] += 1
|
||||
|
||||
# ─── 복합 토큰 df ───
|
||||
all_compounds = set()
|
||||
for comps in frame_compounds.values():
|
||||
all_compounds.update(comps)
|
||||
comp_df = {}
|
||||
for c in all_compounds:
|
||||
c_set = set(c)
|
||||
df = sum(1 for fid, tokens in frame_tokens.items() if c_set.issubset(tokens))
|
||||
comp_df[c] = max(df, 1)
|
||||
|
||||
def idf(df):
|
||||
return log((N_FRAMES - df + 0.5) / (df + 0.5) + 1)
|
||||
|
||||
# ─── BM25 점수 (개별 + 복합) ───
|
||||
def score_frame(fid, mdx_tokens):
|
||||
dl = frame_len[fid]
|
||||
if dl == 0:
|
||||
return 0.0
|
||||
score = 0.0
|
||||
# 개별 토큰
|
||||
for t, tf in frame_tokens[fid].items():
|
||||
if t in mdx_tokens:
|
||||
idf_val = idf(ind_df[t])
|
||||
tf_norm = tf * (K1 + 1) / (tf + K1 * (1 - B + B * dl / avg_dl))
|
||||
score += idf_val * tf_norm
|
||||
# 복합 토큰
|
||||
for c in frame_compounds.get(fid, []):
|
||||
c_set = set(c)
|
||||
if not c_set:
|
||||
continue
|
||||
hits = c_set & mdx_tokens
|
||||
coverage = len(hits) / len(c_set)
|
||||
if coverage > 0:
|
||||
idf_val = idf(comp_df[c])
|
||||
tf = 1
|
||||
tf_norm = tf * (K1 + 1) / (tf + K1 * (1 - B + B * dl / avg_dl))
|
||||
score += idf_val * tf_norm * coverage
|
||||
return score
|
||||
|
||||
frame_num_map = {fid: info['frame_number'] for fid, info in auto['frame_stats'].items()}
|
||||
|
||||
print('=' * 95)
|
||||
print(f'{"섹션":<8} | {"순위":<3} | {"현재 (0.30/0.50/0.20)":<28} | {"BM25 + 세트 (복합 토큰)":<30}')
|
||||
print('=' * 95)
|
||||
|
||||
top1_agree = 0
|
||||
top3_agree = 0
|
||||
target_hits_bm25 = 0
|
||||
target_sids = ['01-2', '02-2.2', '03-1', '03-2']
|
||||
total = 0
|
||||
answer_map = v1['meta']['answer_map']
|
||||
|
||||
for sid, sec in v1['mdx_sections'].items():
|
||||
mdx_tokens = set(normalized['mdx'][sid].get('unique_tokens', []))
|
||||
scores = {fid: (score_frame(fid, mdx_tokens), frame_num_map.get(fid)) for fid in frame_tokens}
|
||||
ranking = sorted(scores.items(), key=lambda x: -x[1][0])
|
||||
current = sec['rank_by_matching_score']
|
||||
total += 1
|
||||
if current[0]['frame_id'] == ranking[0][0]:
|
||||
top1_agree += 1
|
||||
if {r['frame_id'] for r in current[:3]} == {x[0] for x in ranking[:3]}:
|
||||
top3_agree += 1
|
||||
|
||||
# TARGET 정답률
|
||||
if sid in target_sids:
|
||||
ans_num = answer_map[sid]
|
||||
if ranking[0][1][1] == ans_num:
|
||||
target_hits_bm25 += 1
|
||||
|
||||
for i in range(5):
|
||||
cur = current[i]
|
||||
bm = ranking[i]
|
||||
cur_s = f'Frame {cur["frame_number"]:>2} {cur["matching_score"]:.3f}'
|
||||
bm_s = f'Frame {bm[1][1]:>2} {bm[1][0]:>7.2f}'
|
||||
prefix = sid if i == 0 else ''
|
||||
print(f'{prefix:<8} | {i+1:<3} | {cur_s:<28} | {bm_s:<30}')
|
||||
print('-' * 95)
|
||||
|
||||
print()
|
||||
print('=' * 95)
|
||||
print(f'Top-1 일치: {top1_agree}/{total}')
|
||||
print(f'Top-3 집합 일치: {top3_agree}/{total}')
|
||||
print(f'TARGET 정답률: BM25+세트 = {target_hits_bm25}/4 (vs 현재 4/4)')
|
||||
print('=' * 95)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Pipeline Step 15 (Logistic Regression) — TARGET 4 로 가중치 학습.
|
||||
|
||||
방법:
|
||||
1. TARGET 4 섹션 × 32 프레임 = 128 샘플
|
||||
2. Feature: (standalone_score, group_score, related_score) — 각 [0, 1]
|
||||
3. Label: 1 if 정답 프레임 else 0 (정답 4, 오답 124)
|
||||
4. Logistic Regression + Linear Regression 두 가지 fit
|
||||
5. 학습 가중치를 sum=1 로 정규화 → 현재 0.30/0.50/0.20 와 비교
|
||||
6. 학습 가중치로 예측 시 TARGET 정답률 검증
|
||||
"""
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import yaml
|
||||
from sklearn.linear_model import LogisticRegression, LinearRegression
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
TARGET_SIDS = ['01-2', '02-2.2', '03-1', '03-2']
|
||||
|
||||
|
||||
def main():
|
||||
v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8'))
|
||||
auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8'))
|
||||
|
||||
answer_map = v1['meta']['answer_map']
|
||||
frame_num = {fid: info['frame_number'] for fid, info in auto['frame_stats'].items()}
|
||||
|
||||
# ─── 데이터 구성 ───
|
||||
rows = [] # (sid, frame_id, frame_num, s, g, r, label)
|
||||
for sid in TARGET_SIDS:
|
||||
sec = v1['mdx_sections'][sid]
|
||||
answer = answer_map[sid]
|
||||
for frame_id, detail in sec['per_frame_detail'].items():
|
||||
s = detail['standalone']['score']
|
||||
g = detail['keyword_group'].get('score_avg', 0)
|
||||
r = detail['related']['score']
|
||||
label = 1 if frame_num[frame_id] == answer else 0
|
||||
rows.append((sid, frame_id, frame_num[frame_id], s, g, r, label))
|
||||
|
||||
X = np.array([[r[3], r[4], r[5]] for r in rows])
|
||||
y = np.array([r[6] for r in rows])
|
||||
|
||||
print('=' * 75)
|
||||
print(f'데이터: {len(y)} 샘플 (정답 {y.sum()}, 오답 {len(y)-y.sum()}) — TARGET 4 × 32 frames')
|
||||
print('=' * 75)
|
||||
print()
|
||||
|
||||
# ─── Logistic Regression ───
|
||||
clf = LogisticRegression(penalty='l2', C=1.0, fit_intercept=True, max_iter=2000)
|
||||
clf.fit(X, y)
|
||||
w_lr = clf.coef_[0]
|
||||
bias_lr = clf.intercept_[0]
|
||||
w_lr_norm = w_lr / w_lr.sum()
|
||||
|
||||
print(f'[Logistic Regression (L2, C=1.0)]')
|
||||
print(f' raw weights: standalone={w_lr[0]:+.3f} group={w_lr[1]:+.3f} related={w_lr[2]:+.3f}')
|
||||
print(f' bias: {bias_lr:+.3f}')
|
||||
print(f' 정규화 (sum=1): standalone={w_lr_norm[0]:.3f} group={w_lr_norm[1]:.3f} related={w_lr_norm[2]:.3f}')
|
||||
print()
|
||||
|
||||
# ─── Linear Regression (OLS, no intercept) ───
|
||||
ols = LinearRegression(fit_intercept=False)
|
||||
ols.fit(X, y)
|
||||
w_ols = ols.coef_
|
||||
w_ols_norm = w_ols / w_ols.sum()
|
||||
|
||||
print(f'[Linear Regression (OLS, no intercept)]')
|
||||
print(f' raw weights: standalone={w_ols[0]:+.3f} group={w_ols[1]:+.3f} related={w_ols[2]:+.3f}')
|
||||
print(f' 정규화 (sum=1): standalone={w_ols_norm[0]:.3f} group={w_ols_norm[1]:.3f} related={w_ols_norm[2]:.3f}')
|
||||
print()
|
||||
|
||||
# ─── 현재 vs 학습 가중치 비교 ───
|
||||
print('[비교]')
|
||||
print(f' standalone group related')
|
||||
print(f' 현재 (수동): 0.300 0.500 0.200')
|
||||
print(f' Logistic: {w_lr_norm[0]:.3f} {w_lr_norm[1]:.3f} {w_lr_norm[2]:.3f}')
|
||||
print(f' OLS: {w_ols_norm[0]:.3f} {w_ols_norm[1]:.3f} {w_ols_norm[2]:.3f}')
|
||||
print()
|
||||
|
||||
# ─── 학습 가중치로 TARGET 정답률 검증 ───
|
||||
def evaluate(w_normalized, name):
|
||||
hits = 0
|
||||
for sid in TARGET_SIDS:
|
||||
sec = v1['mdx_sections'][sid]
|
||||
answer = answer_map[sid]
|
||||
scores = {}
|
||||
for fid, detail in sec['per_frame_detail'].items():
|
||||
s = detail['standalone']['score']
|
||||
g = detail['keyword_group'].get('score_avg', 0)
|
||||
r = detail['related']['score']
|
||||
scores[fid] = w_normalized[0]*s + w_normalized[1]*g + w_normalized[2]*r
|
||||
top_fid = max(scores, key=scores.get)
|
||||
if frame_num[top_fid] == answer:
|
||||
hits += 1
|
||||
return hits
|
||||
|
||||
hits_current = evaluate([0.30, 0.50, 0.20], '현재')
|
||||
hits_lr = evaluate(w_lr_norm, 'Logistic')
|
||||
hits_ols = evaluate(w_ols_norm, 'OLS')
|
||||
|
||||
print('[TARGET 4 정답률 (학습 가중치로 재예측)]')
|
||||
print(f' 현재 (0.30/0.50/0.20): {hits_current}/4')
|
||||
print(f' Logistic Regression: {hits_lr}/4')
|
||||
print(f' OLS Linear Regression: {hits_ols}/4')
|
||||
print()
|
||||
|
||||
# ─── LOOCV (Leave-One-Out Cross-Validation) — 과적합 체크 ───
|
||||
print('[LOOCV — 과적합 확인]')
|
||||
print(' 각 TARGET 을 hold-out, 나머지 3개로 학습 후 테스트')
|
||||
loocv_hits = 0
|
||||
for hold_out_idx, hold_out_sid in enumerate(TARGET_SIDS):
|
||||
train_X = np.array([[r[3], r[4], r[5]] for r in rows if r[0] != hold_out_sid])
|
||||
train_y = np.array([r[6] for r in rows if r[0] != hold_out_sid])
|
||||
clf_cv = LogisticRegression(penalty='l2', C=1.0, fit_intercept=True, max_iter=2000)
|
||||
clf_cv.fit(train_X, train_y)
|
||||
w_cv = clf_cv.coef_[0] / clf_cv.coef_[0].sum()
|
||||
|
||||
sec = v1['mdx_sections'][hold_out_sid]
|
||||
answer = answer_map[hold_out_sid]
|
||||
scores = {}
|
||||
for fid, detail in sec['per_frame_detail'].items():
|
||||
s = detail['standalone']['score']
|
||||
g = detail['keyword_group'].get('score_avg', 0)
|
||||
r = detail['related']['score']
|
||||
scores[fid] = w_cv[0]*s + w_cv[1]*g + w_cv[2]*r
|
||||
top_fid = max(scores, key=scores.get)
|
||||
correct = frame_num[top_fid] == answer
|
||||
if correct:
|
||||
loocv_hits += 1
|
||||
print(f' hold-out {hold_out_sid}: weights={w_cv.round(3)} 정답? {"✓" if correct else "✗"}')
|
||||
print(f' LOOCV 정답률: {loocv_hits}/4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
"""Pipeline Step 17 — V4 (template-fit) 를 V3 Top-K 가 아니라 32 프레임 전체에 적용.
|
||||
|
||||
기존 V4 r2 는 V3 Top-5 만 평가 → 5 개 중 선택.
|
||||
이 스크립트는 32 프레임 전체에 template-fit 적용 → confidence 기준 전체 랭킹.
|
||||
|
||||
V1/V2/V3 처럼 "32 중 Top-3" 형태 산출.
|
||||
|
||||
출력: v4_full32_result.yaml
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from phase_common import load_32_frames, load_frame_index
|
||||
from detect_mdx import detect_mdx_analysis
|
||||
from template_fit import (
|
||||
load_templates_v1, collect_anchor_vocab, compute_template_fit, route,
|
||||
)
|
||||
from embeddings import embed_texts, cosine
|
||||
from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR
|
||||
|
||||
OUT_PATH = HERE / 'v4_full32_result.yaml'
|
||||
|
||||
|
||||
def extract_mdx_raw(sid):
|
||||
cfg = MDX_SECTIONS[sid]
|
||||
p = MDX_DIR / cfg['file']
|
||||
lines = p.read_text(encoding='utf-8').split('\n')
|
||||
start = None
|
||||
for i, ln in enumerate(lines):
|
||||
if ln.strip() == cfg['start'].strip():
|
||||
start = i
|
||||
break
|
||||
end = len(lines)
|
||||
if cfg.get('end_prefix'):
|
||||
for i in range(start + 1, len(lines)):
|
||||
if lines[i].strip().startswith(cfg['end_prefix']):
|
||||
end = i
|
||||
break
|
||||
section = lines[start:end]
|
||||
return section[0].lstrip('#').strip(), '\n'.join(section)
|
||||
|
||||
|
||||
def main():
|
||||
v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8'))
|
||||
answer_map = v1['meta']['answer_map']
|
||||
holdout = v1['meta']['holdout_sections']
|
||||
|
||||
templates = load_templates_v1()
|
||||
anchor_vocab = collect_anchor_vocab(templates)
|
||||
frames = load_32_frames()
|
||||
idx_data, frame_to_short = load_frame_index()
|
||||
fids = list(frames.keys())
|
||||
frame_num_map = {fid: int(frame_to_short[fid]) for fid in fids}
|
||||
frame_contents = [frames[fid].get('content', '') for fid in fids]
|
||||
|
||||
print('[V4 full-32] 32 frame content 임베딩 중...')
|
||||
frame_vecs = embed_texts(frame_contents)
|
||||
|
||||
out_sections = {}
|
||||
for sid in v1['mdx_sections']:
|
||||
title, raw_text = extract_mdx_raw(sid)
|
||||
mdx_analysis = detect_mdx_analysis(raw_text, title, anchor_vocab=anchor_vocab)
|
||||
mdx_summary = mdx_analysis['summary']
|
||||
mdx_vec = embed_texts([mdx_summary])[0]
|
||||
|
||||
judgments = []
|
||||
for i, fid in enumerate(fids):
|
||||
if fid not in templates:
|
||||
continue
|
||||
template = templates[fid]
|
||||
content_emb = max(0.0, min(1.0, float(cosine(mdx_vec, frame_vecs[i]))))
|
||||
fit = compute_template_fit(mdx_analysis, template, content_emb)
|
||||
label = route(fit['confidence'], fit['axes'], fit['adaptation'], fit['not_suits'])
|
||||
judgments.append({
|
||||
'frame_id': fid,
|
||||
'frame_number': frame_num_map[fid],
|
||||
'template_id': template.get('template_id'),
|
||||
'confidence': round(float(fit['confidence']), 4),
|
||||
'base': round(float(fit['base']), 4),
|
||||
'penalty': round(float(fit['total_penalty']), 4),
|
||||
'label': label,
|
||||
'content_embedding': round(content_emb, 4),
|
||||
'axes': {
|
||||
'anchor': round(float(fit['axes']['anchor']['score']), 4),
|
||||
'cardinality': round(float(fit['axes']['cardinality']), 4),
|
||||
'relation': round(float(fit['axes']['relation']), 4),
|
||||
'slot': round(float(fit['axes']['slot']), 4),
|
||||
'content': round(float(fit['axes']['content']), 4),
|
||||
},
|
||||
})
|
||||
|
||||
# confidence 내림차순
|
||||
judgments.sort(key=lambda x: -x['confidence'])
|
||||
for new_rank, item in enumerate(judgments, start=1):
|
||||
item['v4_full_rank'] = new_rank
|
||||
|
||||
ans = answer_map.get(sid)
|
||||
out_sections[sid] = {
|
||||
'mdx_title': title,
|
||||
'answer_frame_number': ans,
|
||||
'is_holdout': sid in holdout,
|
||||
'judgments_full32': judgments,
|
||||
'usable_count': sum(1 for j in judgments if j['label'] != 'reject'),
|
||||
'reject_count': sum(1 for j in judgments if j['label'] == 'reject'),
|
||||
}
|
||||
|
||||
out = {
|
||||
'meta': {
|
||||
'pipeline_step': '8.v4.full32',
|
||||
'description': 'V4 template-fit 을 V3 Top-K 가 아니라 32 프레임 전체에 적용',
|
||||
'note': '기존 v4_template_fit_r2_result.yaml 는 V3 Top-5 만 평가. 이 파일은 32 전체.',
|
||||
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
|
||||
'answer_map': answer_map,
|
||||
'holdout_sections': holdout,
|
||||
},
|
||||
'mdx_sections': out_sections,
|
||||
}
|
||||
OUT_PATH.write_text(
|
||||
yaml.safe_dump(out, allow_unicode=True, sort_keys=False, width=1000),
|
||||
encoding='utf-8',
|
||||
)
|
||||
|
||||
print('=' * 70)
|
||||
print(f'V4 full-32 평가 완료: {OUT_PATH}')
|
||||
print('=' * 70)
|
||||
print(f'\n각 섹션별 사용 가능 프레임 (label != reject) 개수 + Top-3:')
|
||||
for sid, s in out_sections.items():
|
||||
ans = s.get('answer_frame_number')
|
||||
ans_str = f'(정답 {ans})' if ans else '(holdout)'
|
||||
print(f'\n[{sid}] {ans_str} 사용가능 {s["usable_count"]}/{s["usable_count"]+s["reject_count"]}')
|
||||
usable = [j for j in s['judgments_full32'] if j['label'] != 'reject']
|
||||
for j in usable[:3]:
|
||||
mark = ' 🎯' if ans and j['frame_number'] == ans else ''
|
||||
print(f' Frame {j["frame_number"]:>2}{mark} conf {j["confidence"]:.3f} {j["label"]}')
|
||||
if not usable:
|
||||
print(' (사용 가능 후보 없음 — 모두 reject)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Pipeline Step 18 — V4 slot 축 ablation (실증).
|
||||
|
||||
질문: V4 의 5축 중 slot 축 (W_SLOT=0.15) 이 frame 매칭에 실제로 기여하는가?
|
||||
|
||||
방법:
|
||||
1. 기존 v4_full32_result (W_SLOT=0.15 유지) 를 baseline 으로
|
||||
2. W_SLOT=0 으로 두고 나머지 4축 (anchor/cardinality/relation/content) 가중치
|
||||
재정규화 — sum=1.0 유지 → ablated 결과 산출
|
||||
3. 32 frame × 7 MDX 섹션 confidence 재계산
|
||||
4. 비교:
|
||||
- TARGET 4개의 정답 frame 라벨/순위 변화
|
||||
- Top-3 매칭 변화
|
||||
- 라벨 분포 변화 (use_as_is/light_edit/restructure/reject 카운트)
|
||||
5. 결론: slot 축이 의미 있나?
|
||||
|
||||
출력: V4_SLOT_ABLATION.md
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from template_fit import route
|
||||
|
||||
OUT_PATH = HERE / 'V4_SLOT_ABLATION.md'
|
||||
|
||||
# Baseline (현재): 0.25 + 0.20 + 0.20 + 0.15 + 0.20 = 1.00
|
||||
W_BASELINE = {'anchor': 0.25, 'cardinality': 0.20, 'relation': 0.20, 'slot': 0.15, 'content': 0.20}
|
||||
|
||||
# Ablated: slot=0, 나머지 4축 비례 재정규화 → sum=1.00
|
||||
# 4축 합 = 0.85, 각 축 / 0.85
|
||||
W_ABLATED = {
|
||||
'anchor': 0.25 / 0.85, # 0.2941
|
||||
'cardinality': 0.20 / 0.85, # 0.2353
|
||||
'relation': 0.20 / 0.85, # 0.2353
|
||||
'slot': 0.0,
|
||||
'content': 0.20 / 0.85, # 0.2353
|
||||
}
|
||||
|
||||
|
||||
def recompute_confidence(axes, weights, penalty):
|
||||
base = sum(weights[k] * axes.get(k, 0) for k in weights)
|
||||
return max(0.0, base - penalty)
|
||||
|
||||
|
||||
def main():
|
||||
v4 = yaml.safe_load((HERE / 'v4_full32_result.yaml').read_text(encoding='utf-8'))
|
||||
answer_map = v4['meta']['answer_map']
|
||||
holdout = set(v4['meta']['holdout_sections'])
|
||||
|
||||
md = []
|
||||
md.append('# V4 slot 축 ablation — 실증\n')
|
||||
md.append('## 질문\n')
|
||||
md.append('V4 confidence 계산식의 slot 축 (W_SLOT=0.15) 이 frame 매칭에 실제로 기여하는가?\n')
|
||||
md.append('## 방법\n')
|
||||
md.append('- **Baseline**: 5축 가중치 그대로 — 0.25 anchor + 0.20 card + 0.20 rel + 0.15 slot + 0.20 content\n')
|
||||
md.append('- **Ablated**: W_SLOT=0, 나머지 4축 비례 재정규화 (sum=1.0)\n')
|
||||
md.append(' - anchor: 0.294, cardinality: 0.235, relation: 0.235, content: 0.235\n')
|
||||
md.append('- 32 frame × 7 MDX 섹션 confidence 재계산 → label 재할당 → 비교\n\n')
|
||||
|
||||
section_results = {}
|
||||
for sid, sec in v4['mdx_sections'].items():
|
||||
is_target = sid not in holdout
|
||||
ans_fn = answer_map.get(sid)
|
||||
|
||||
# Baseline 결과 (이미 v4_full32 에 있음)
|
||||
baseline = sorted(sec['judgments_full32'], key=lambda x: -x['confidence'])
|
||||
|
||||
# Ablated 결과 재계산
|
||||
ablated = []
|
||||
for j in sec['judgments_full32']:
|
||||
new_conf = recompute_confidence(j['axes'], W_ABLATED, j['penalty'])
|
||||
new_label = route(new_conf)
|
||||
ablated.append({
|
||||
'frame_number': j['frame_number'],
|
||||
'frame_id': j['frame_id'],
|
||||
'baseline_conf': j['confidence'],
|
||||
'baseline_label': j['label'],
|
||||
'ablated_conf': round(new_conf, 4),
|
||||
'ablated_label': new_label,
|
||||
})
|
||||
ablated.sort(key=lambda x: -x['ablated_conf'])
|
||||
|
||||
# 정답 frame 변화
|
||||
ans_baseline = next((x for x in baseline if x['frame_number'] == ans_fn), None) if ans_fn else None
|
||||
ans_ablated = next((x for x in ablated if x['frame_number'] == ans_fn), None) if ans_fn else None
|
||||
|
||||
# 순위 비교
|
||||
baseline_top3 = [(x['frame_number'], x['confidence'], x['label']) for x in baseline[:3]]
|
||||
ablated_top3 = [(x['frame_number'], x['ablated_conf'], x['ablated_label']) for x in ablated[:3]]
|
||||
|
||||
# 라벨 분포 비교
|
||||
from collections import Counter
|
||||
base_lbl = Counter(x['label'] for x in baseline)
|
||||
abl_lbl = Counter(x['ablated_label'] for x in ablated)
|
||||
|
||||
section_results[sid] = {
|
||||
'is_target': is_target,
|
||||
'ans_fn': ans_fn,
|
||||
'baseline_top3': baseline_top3,
|
||||
'ablated_top3': ablated_top3,
|
||||
'ans_baseline': ans_baseline,
|
||||
'ans_ablated': ans_ablated,
|
||||
'base_lbl': base_lbl,
|
||||
'abl_lbl': abl_lbl,
|
||||
}
|
||||
|
||||
# ─── TARGET 정답 매칭 변화 ───
|
||||
md.append('## 결과 1 — TARGET 4 섹션의 정답 frame 라벨/순위 변화\n')
|
||||
md.append('| MDX | 정답 Frame | Baseline conf / 라벨 / 순위 | Ablated conf / 라벨 / 순위 | 변화 |\n')
|
||||
md.append('|---|---|---|---|---|\n')
|
||||
target_sids = [sid for sid in section_results if section_results[sid]['is_target']]
|
||||
correct_baseline = 0
|
||||
correct_ablated = 0
|
||||
for sid in target_sids:
|
||||
r = section_results[sid]
|
||||
ans_fn = r['ans_fn']
|
||||
ab = r['ans_baseline']
|
||||
aa = r['ans_ablated']
|
||||
base_rank = next((i+1 for i, x in enumerate(sorted(v4['mdx_sections'][sid]['judgments_full32'], key=lambda x: -x['confidence'])) if x['frame_number'] == ans_fn), '?')
|
||||
# ablated rank
|
||||
ablated_sorted = sorted(
|
||||
[{'fn': j['frame_number'], 'c': recompute_confidence(j['axes'], W_ABLATED, j['penalty'])}
|
||||
for j in v4['mdx_sections'][sid]['judgments_full32']],
|
||||
key=lambda x: -x['c']
|
||||
)
|
||||
abl_rank = next((i+1 for i, x in enumerate(ablated_sorted) if x['fn'] == ans_fn), '?')
|
||||
|
||||
# 정답률 — baseline / ablated 의 Top-1 이 정답인가
|
||||
if v4['mdx_sections'][sid]['judgments_full32']:
|
||||
base_top1_fn = sorted(v4['mdx_sections'][sid]['judgments_full32'], key=lambda x: -x['confidence'])[0]['frame_number']
|
||||
if base_top1_fn == ans_fn:
|
||||
correct_baseline += 1
|
||||
abl_top1_fn = ablated_sorted[0]['fn']
|
||||
if abl_top1_fn == ans_fn:
|
||||
correct_ablated += 1
|
||||
|
||||
change = '동일' if (ab['label'] == aa['ablated_label'] and base_rank == abl_rank) else '⚠️ 변화'
|
||||
md.append(f'| {sid} | Frame {ans_fn} | '
|
||||
f'{ab["confidence"]:.3f} / {ab["label"]} / 순위 {base_rank} | '
|
||||
f'{aa["ablated_conf"]:.3f} / {aa["ablated_label"]} / 순위 {abl_rank} | {change} |\n')
|
||||
|
||||
md.append(f'\n**TARGET Top-1 정답률**: Baseline {correct_baseline}/4, Ablated {correct_ablated}/4\n\n')
|
||||
|
||||
# ─── Top-3 변화 ───
|
||||
md.append('## 결과 2 — 각 섹션 Top-3 매칭 변화\n')
|
||||
for sid, r in section_results.items():
|
||||
tag = 'TARGET' if r['is_target'] else 'Holdout'
|
||||
ans = f'(정답 Frame {r["ans_fn"]})' if r['ans_fn'] else '(holdout)'
|
||||
md.append(f'\n### [{sid}] {tag} {ans}\n')
|
||||
md.append('| 순위 | Baseline | Ablated |\n|---|---|---|\n')
|
||||
for i in range(3):
|
||||
b = r['baseline_top3'][i] if i < len(r['baseline_top3']) else (None, 0, '')
|
||||
a = r['ablated_top3'][i] if i < len(r['ablated_top3']) else (None, 0, '')
|
||||
b_mark = ' 🎯' if r['ans_fn'] and b[0] == r['ans_fn'] else ''
|
||||
a_mark = ' 🎯' if r['ans_fn'] and a[0] == r['ans_fn'] else ''
|
||||
md.append(f'| {i+1} | Frame {b[0]}{b_mark} ({b[1]:.3f}, {b[2]}) | Frame {a[0]}{a_mark} ({a[1]:.3f}, {a[2]}) |\n')
|
||||
|
||||
# ─── 라벨 분포 변화 ───
|
||||
md.append('\n## 결과 3 — 라벨 분포 변화 (32 frame 기준)\n')
|
||||
md.append('| MDX | use_as_is | light_edit | restructure | reject |\n|---|---|---|---|---|\n')
|
||||
for sid, r in section_results.items():
|
||||
b = r['base_lbl']; a = r['abl_lbl']
|
||||
def fmt(lbl):
|
||||
bv = b.get(lbl, 0); av = a.get(lbl, 0)
|
||||
if bv == av: return f'{bv}'
|
||||
return f'{bv}→{av}'
|
||||
md.append(f'| {sid} | {fmt("use_as_is")} | {fmt("light_edit")} | {fmt("restructure")} | {fmt("reject")} |\n')
|
||||
|
||||
# ─── 결론 ───
|
||||
md.append('\n## 결론\n')
|
||||
same_top1 = sum(1 for sid, r in section_results.items()
|
||||
if r['baseline_top3'] and r['ablated_top3']
|
||||
and r['baseline_top3'][0][0] == r['ablated_top3'][0][0])
|
||||
same_top3 = sum(1 for sid, r in section_results.items()
|
||||
if r['baseline_top3'] and r['ablated_top3']
|
||||
and set(x[0] for x in r['baseline_top3']) == set(x[0] for x in r['ablated_top3']))
|
||||
md.append(f'- **Top-1 일치 섹션 수**: {same_top1}/{len(section_results)}\n')
|
||||
md.append(f'- **Top-3 멤버 일치 섹션 수**: {same_top3}/{len(section_results)}\n')
|
||||
md.append(f'- **TARGET Top-1 정답률**: Baseline {correct_baseline}/4 → Ablated {correct_ablated}/4\n')
|
||||
|
||||
OUT_PATH.write_text(''.join(md), encoding='utf-8')
|
||||
|
||||
print('=' * 70)
|
||||
print(f'V4 slot 축 ablation 완료: {OUT_PATH}')
|
||||
print('=' * 70)
|
||||
print(f'TARGET Top-1 정답률: Baseline {correct_baseline}/4, Ablated {correct_ablated}/4')
|
||||
print(f'Top-1 동일 섹션: {same_top1}/{len(section_results)}')
|
||||
print(f'Top-3 멤버 동일 섹션: {same_top3}/{len(section_results)}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,195 @@
|
||||
"""Phase 21b용 구조 ontology 기반 매칭
|
||||
- detect_mdx_structure_v3(): MDX 본문에서 같은 schema 속성 emit
|
||||
- structural_match_v3(): 속성 교집합 기반 score (0~1)
|
||||
- 가중치는 tie-breaker 수준 (0.05~0.10)
|
||||
- confidence high 일 때만 점수 부여
|
||||
"""
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
|
||||
def load_ontology():
|
||||
p = HERE / "structure_ontology.yaml"
|
||||
with open(p, encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)["frames"]
|
||||
|
||||
|
||||
# ═══ MDX 구조 감지 v3 ═══
|
||||
def detect_mdx_structure_v3(text, mdx_title=""):
|
||||
"""schema emit: {family, surface, columns, items, has_table, has_cards, semantic_role, confidence}"""
|
||||
lines = text.split("\n")
|
||||
result = {
|
||||
"family": None, "surface": None, "semantic_role": None,
|
||||
"columns": None, "items": None,
|
||||
"has_table": False, "has_cards": False, "has_diagram": False,
|
||||
"confidence": "low",
|
||||
}
|
||||
|
||||
# 1. ### X.Y 서브섹션 감지
|
||||
subs = [ln for ln in lines if re.match(r"^###\s+\d+\.\d+", ln)]
|
||||
subsection_texts = [ln for ln in subs]
|
||||
n_subs = len(subs)
|
||||
|
||||
# 2. 표 감지
|
||||
table_header = None
|
||||
for ln in lines:
|
||||
stripped = ln.strip()
|
||||
if re.match(r"^\|.*\|.*\|", stripped) and not re.match(r"^\|[\s\-:]+\|", stripped):
|
||||
table_header = stripped
|
||||
break
|
||||
|
||||
# 3. 최상위 볼드 블릿 수
|
||||
top_bullets = [ln for ln in lines if re.match(r"^[-*]\s+\*\*", ln)]
|
||||
n_bullets = len(top_bullets)
|
||||
|
||||
full_text = (text + " " + mdx_title).lower()
|
||||
|
||||
# ═══ semantic_role 감지 (figma_audit과 동일 — best-match + priority) ═══
|
||||
from figma_audit import _SEMANTIC_HINT_PATTERNS, _ROLE_PRIORITY
|
||||
candidates = []
|
||||
for role_name, req_kws, min_req in _SEMANTIC_HINT_PATTERNS:
|
||||
hits = sum(1 for k in req_kws if k in full_text)
|
||||
if hits >= min_req:
|
||||
ratio = hits / len(req_kws)
|
||||
candidates.append((hits, ratio, role_name))
|
||||
if candidates:
|
||||
# 1) hits 많음 2) ratio 높음 3) priority 높음
|
||||
candidates.sort(key=lambda x: (-x[0], -x[1], -_ROLE_PRIORITY.get(x[2], 0)))
|
||||
result["semantic_role"] = candidates[0][2]
|
||||
else:
|
||||
result["semantic_role"] = None
|
||||
|
||||
# ═══ family / surface / columns 분기 ═══
|
||||
# 우선순위: ### 서브섹션 > 표 > 블릿
|
||||
if n_subs >= 2:
|
||||
# 과정/결과 비교 → compare family
|
||||
sub_lower = " ".join(subsection_texts).lower()
|
||||
if any(kw in sub_lower for kw in ["과정", "결과", "process", "product"]):
|
||||
result["family"] = "compare"
|
||||
result["surface"] = "banner-plus-2col"
|
||||
result["columns"] = 2
|
||||
result["items"] = 2
|
||||
result["confidence"] = "high"
|
||||
elif n_subs == 2:
|
||||
result["family"] = "compare"
|
||||
result["surface"] = "2col-paired"
|
||||
result["columns"] = 2
|
||||
result["items"] = 2
|
||||
result["confidence"] = "high"
|
||||
else:
|
||||
result["family"] = "list"
|
||||
result["surface"] = "cards-3-horizontal"
|
||||
result["columns"] = n_subs
|
||||
result["items"] = n_subs
|
||||
result["confidence"] = "medium"
|
||||
elif table_header:
|
||||
result["has_table"] = True
|
||||
cols = [c.strip().replace("*", "").lower() for c in table_header.strip("|").split("|") if c.strip()]
|
||||
n_cols = len(cols) - 1 # 첫 열 = label
|
||||
col_text = " ".join(cols)
|
||||
if any(kw in col_text for kw in ["발주자", "시공자", "설계자"]):
|
||||
result["family"] = "cards"
|
||||
result["surface"] = "table-persona-3col"
|
||||
result["semantic_role"] = result.get("semantic_role") or "persona-benefits"
|
||||
result["columns"] = 3
|
||||
result["items"] = 3
|
||||
result["has_cards"] = True
|
||||
result["confidence"] = "high"
|
||||
elif any(kw in col_text for kw in ["제조업", "건축", "토목"]) and "토목" in col_text:
|
||||
result["family"] = "table"
|
||||
result["surface"] = "table-3col"
|
||||
result["columns"] = 3
|
||||
result["items"] = 3
|
||||
result["confidence"] = "high"
|
||||
elif ("bim" in col_text) and ("dx" in col_text):
|
||||
result["family"] = "compare"
|
||||
result["surface"] = "table-multi-row"
|
||||
result["columns"] = 2
|
||||
result["confidence"] = "high"
|
||||
else:
|
||||
result["family"] = "table"
|
||||
result["surface"] = f"table-{n_cols}col"
|
||||
result["columns"] = n_cols
|
||||
result["confidence"] = "medium"
|
||||
elif n_bullets >= 1:
|
||||
result["has_cards"] = True
|
||||
if n_bullets == 3:
|
||||
result["family"] = "list"
|
||||
result["surface"] = "bullets-3"
|
||||
result["columns"] = 3
|
||||
result["items"] = 3
|
||||
result["confidence"] = "high"
|
||||
elif n_bullets == 2:
|
||||
result["family"] = "compare"
|
||||
result["surface"] = "bullets-2col"
|
||||
result["columns"] = 2
|
||||
result["items"] = 2
|
||||
result["confidence"] = "medium"
|
||||
elif n_bullets == 4:
|
||||
result["family"] = "cards"
|
||||
result["surface"] = "cards-4-grid"
|
||||
result["columns"] = 4
|
||||
result["items"] = 4
|
||||
result["confidence"] = "medium"
|
||||
else:
|
||||
result["family"] = "list"
|
||||
result["surface"] = "mixed-bullets"
|
||||
result["items"] = n_bullets
|
||||
result["confidence"] = "low"
|
||||
else:
|
||||
result["family"] = None
|
||||
result["surface"] = "single-column"
|
||||
result["confidence"] = "low"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ═══ 구조 매칭 v3 ═══
|
||||
def structural_match_v3(mdx_schema, fig_schema):
|
||||
"""속성 교집합 기반 score (0~1). confidence high + 주요 속성 일치시에만 점수 부여.
|
||||
Tie-breaker 용도라 가중치는 호출 측에서 낮게 적용 (0.05~0.10 권장)."""
|
||||
if not mdx_schema or not fig_schema:
|
||||
return 0.0, []
|
||||
# confidence 체크: low면 신뢰 안 함
|
||||
if mdx_schema.get("confidence") == "low":
|
||||
return 0.0, ["mdx confidence low → 0"]
|
||||
# 속성 5개 비교
|
||||
aligns = []
|
||||
scores = []
|
||||
# family (가장 중요)
|
||||
m_fam = mdx_schema.get("family")
|
||||
f_fam = fig_schema.get("family")
|
||||
if m_fam and f_fam and m_fam == f_fam:
|
||||
scores.append(0.40)
|
||||
aligns.append(f"family:{m_fam}✓")
|
||||
elif m_fam and f_fam:
|
||||
aligns.append(f"family:{m_fam}≠{f_fam}")
|
||||
# columns / items
|
||||
m_cols = mdx_schema.get("columns") or mdx_schema.get("items")
|
||||
f_cols = fig_schema.get("columns") or fig_schema.get("items")
|
||||
if m_cols and f_cols and m_cols == f_cols:
|
||||
scores.append(0.25)
|
||||
aligns.append(f"columns:{m_cols}✓")
|
||||
elif m_cols and f_cols:
|
||||
aligns.append(f"columns:{m_cols}≠{f_cols}")
|
||||
# has_table
|
||||
if mdx_schema.get("has_table") is not None and fig_schema.get("has_table") is not None:
|
||||
if mdx_schema["has_table"] == fig_schema["has_table"]:
|
||||
scores.append(0.10)
|
||||
aligns.append(f"has_table:{mdx_schema['has_table']}✓")
|
||||
# has_cards
|
||||
if mdx_schema.get("has_cards") is not None and fig_schema.get("has_cards") is not None:
|
||||
if mdx_schema["has_cards"] == fig_schema["has_cards"]:
|
||||
scores.append(0.10)
|
||||
aligns.append(f"has_cards:{mdx_schema['has_cards']}✓")
|
||||
# semantic_role (bonus)
|
||||
m_role = mdx_schema.get("semantic_role")
|
||||
f_role = fig_schema.get("semantic_role")
|
||||
if m_role and f_role and m_role == f_role and m_role != "(검수 필요)":
|
||||
scores.append(0.15)
|
||||
aligns.append(f"role:{m_role}✓")
|
||||
total = sum(scores)
|
||||
return total, aligns
|
||||
@@ -0,0 +1,674 @@
|
||||
"""Template-fit-v1 scoring engine.
|
||||
|
||||
Spec: tests/matching/TEMPLATE_FIT_V1.md
|
||||
|
||||
공식:
|
||||
base = 0.25*anchor + 0.20*card + 0.20*relation + 0.15*slot + 0.20*content
|
||||
total_penalty = min(0.50, min(0.30, adaptation_cost) + min(0.30, not_suits_hits*0.20))
|
||||
confidence = max(0, base - total_penalty)
|
||||
|
||||
v1 특성:
|
||||
- 매칭 단계 LLM 호출 없음 (코드 only)
|
||||
- content_embedding은 mock dict (ko-sroberta 연결은 검증 통과 후)
|
||||
- MDX analysis는 4 TARGET 수동 fixture (detect_mdx_analysis() 구현은 나중)
|
||||
"""
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
from detect_mdx import detect_mdx_analysis
|
||||
from phase_common import TARGET_UNITS, load_target_units
|
||||
from embeddings import embed_texts, cosine
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
|
||||
# ─── 가중치 ────────────────────────────────────────────
|
||||
W_ANCHOR = 0.25
|
||||
W_CARDINALITY = 0.20
|
||||
W_RELATION = 0.20
|
||||
W_SLOT = 0.15
|
||||
W_CONTENT = 0.20
|
||||
|
||||
# ─── 캡 ──────────────────────────────────────────────
|
||||
CAP_ADAPTATION = 0.30
|
||||
CAP_NOT_SUITS = 0.30
|
||||
CAP_TOTAL = 0.50
|
||||
|
||||
NOT_SUITS_HIT_PENALTY = 0.20
|
||||
|
||||
# ─── adaptation 조작별 비용 ──────────────────────────
|
||||
ADAPT_COST = {
|
||||
'split': 0.10,
|
||||
'merge': 0.15,
|
||||
'infer_missing_slot': 0.25,
|
||||
'rewrite_label': 0.05,
|
||||
'rewrite_body': 0.05,
|
||||
}
|
||||
|
||||
# ─── structure_intent 호환 매트릭스 ───────────────────
|
||||
# 0~1 범위. 높을수록 "이 콘텐츠를 이 구조로 옮기기 쉬움".
|
||||
# Symmetric 기본. 1.0 = 완전 일치, 0.2 이하 = 거의 불가능.
|
||||
INTENT_COMPAT = {
|
||||
# exact match
|
||||
('concept_comparison', 'concept_comparison'): 1.0,
|
||||
('multi_attribute_comparison', 'multi_attribute_comparison'): 1.0,
|
||||
('transformation_story', 'transformation_story'): 1.0,
|
||||
('process_product_split', 'process_product_split'): 1.0,
|
||||
('category_comparison', 'category_comparison'): 1.0,
|
||||
('industry_comparison', 'industry_comparison'): 1.0,
|
||||
('persona_benefit', 'persona_benefit'): 1.0,
|
||||
('requirement_list', 'requirement_list'): 1.0,
|
||||
('problem_diagnosis', 'problem_diagnosis'): 1.0,
|
||||
('requirement_or_pillar', 'requirement_or_pillar'): 1.0,
|
||||
|
||||
# strong compat (same family)
|
||||
('concept_comparison', 'multi_attribute_comparison'): 0.8,
|
||||
('multi_attribute_comparison', 'category_comparison'): 0.8,
|
||||
('multi_attribute_comparison', 'industry_comparison'): 0.8,
|
||||
('transformation_story', 'process_product_split'): 0.9,
|
||||
('requirement_list', 'requirement_or_pillar'): 0.7,
|
||||
|
||||
# moderate (partial overlap)
|
||||
('concept_comparison', 'category_comparison'): 0.5,
|
||||
('multi_attribute_comparison', 'persona_benefit'): 0.4,
|
||||
('industry_comparison', 'concept_comparison'): 0.4,
|
||||
('requirement_or_pillar', 'persona_benefit'): 0.4,
|
||||
|
||||
# weak (different family)
|
||||
('concept_comparison', 'transformation_story'): 0.2,
|
||||
('concept_comparison', 'process_product_split'): 0.2,
|
||||
('multi_attribute_comparison', 'transformation_story'): 0.2,
|
||||
('multi_attribute_comparison', 'process_product_split'): 0.2,
|
||||
('category_comparison', 'transformation_story'): 0.2,
|
||||
('industry_comparison', 'transformation_story'): 0.2,
|
||||
|
||||
# polarity opposite
|
||||
('requirement_list', 'problem_diagnosis'): 0.15,
|
||||
('requirement_or_pillar', 'problem_diagnosis'): 0.25,
|
||||
('persona_benefit', 'problem_diagnosis'): 0.2,
|
||||
|
||||
# structurally different (compare vs parallel world)
|
||||
('persona_benefit', 'concept_comparison'): 0.25,
|
||||
('requirement_list', 'concept_comparison'): 0.2,
|
||||
('requirement_list', 'multi_attribute_comparison'): 0.3,
|
||||
('requirement_list', 'persona_benefit'): 0.3,
|
||||
('requirement_list', 'category_comparison'): 0.2,
|
||||
('requirement_list', 'industry_comparison'): 0.25,
|
||||
('requirement_list', 'process_product_split'): 0.2,
|
||||
('requirement_list', 'transformation_story'): 0.2,
|
||||
('problem_diagnosis', 'concept_comparison'): 0.2,
|
||||
('problem_diagnosis', 'multi_attribute_comparison'): 0.25,
|
||||
('problem_diagnosis', 'category_comparison'): 0.2,
|
||||
('problem_diagnosis', 'industry_comparison'): 0.2,
|
||||
('problem_diagnosis', 'process_product_split'): 0.15,
|
||||
('problem_diagnosis', 'transformation_story'): 0.15,
|
||||
}
|
||||
|
||||
|
||||
def _sym_lookup(a, b, default=0.3):
|
||||
"""대칭 조회. (a,b) 또는 (b,a) 있으면 반환, 아니면 default."""
|
||||
if (a, b) in INTENT_COMPAT:
|
||||
return INTENT_COMPAT[(a, b)]
|
||||
if (b, a) in INTENT_COMPAT:
|
||||
return INTENT_COMPAT[(b, a)]
|
||||
return default
|
||||
|
||||
|
||||
def intent_compat(mdx_intents, frame_intents):
|
||||
"""MDX vs Frame 의 structure_intent list 호환도 계산.
|
||||
- 둘 다 non-empty: max over all pairs
|
||||
- 한쪽이 empty: neutral 0.5 (부분 적용 상태 — intent 미태깅 frame 배려)
|
||||
"""
|
||||
if not mdx_intents or not frame_intents:
|
||||
return 0.5
|
||||
return max(_sym_lookup(mi, fi) for mi in mdx_intents for fi in frame_intents)
|
||||
|
||||
|
||||
def intent_compat_with_source(mdx_intents, frame_intents):
|
||||
"""호환도 + source ('tagged' | 'neutral').
|
||||
Phase 25 gate 에서 tagged mismatch 만 강하게 막기 위해 사용."""
|
||||
if not mdx_intents or not frame_intents:
|
||||
return 0.5, 'neutral'
|
||||
return (max(_sym_lookup(mi, fi) for mi in mdx_intents for fi in frame_intents),
|
||||
'tagged')
|
||||
|
||||
|
||||
# ─── relation 호환 매트릭스 ──────────────────────────
|
||||
RELATION_MATRIX = {
|
||||
('parallel', 'parallel'): 1.0,
|
||||
('parallel', 'sequence'): 0.2,
|
||||
('parallel', 'compare'): 0.4,
|
||||
('parallel', 'hierarchy'): 0.3,
|
||||
('sequence', 'parallel'): 0.2,
|
||||
('sequence', 'sequence'): 1.0,
|
||||
('sequence', 'compare'): 0.3,
|
||||
('sequence', 'hierarchy'): 0.4,
|
||||
('compare', 'parallel'): 0.4,
|
||||
('compare', 'sequence'): 0.3,
|
||||
('compare', 'compare'): 1.0,
|
||||
('compare', 'hierarchy'): 0.3,
|
||||
('hierarchy', 'parallel'): 0.3,
|
||||
('hierarchy', 'sequence'): 0.4,
|
||||
('hierarchy', 'compare'): 0.3,
|
||||
('hierarchy', 'hierarchy'): 1.0,
|
||||
}
|
||||
|
||||
# ─── not_suits 규칙 엔진 (pattern 키워드 → MDX 시그널 판정) ──
|
||||
# v1: structured signal 기반. 나중에 fit_notes.not_suits를 structured로 확장 예정.
|
||||
NOT_SUITS_RULES = {
|
||||
'시간 순서': lambda m: m['relation_type']['value'] == 'sequence',
|
||||
'시간 순서 단계': lambda m: m['relation_type']['value'] == 'sequence',
|
||||
'원인-결과': lambda m: m['relation_type']['value'] == 'sequence',
|
||||
'2개 비교': lambda m: m['relation_type']['value'] == 'compare' and m['item_count']['detected'] == 2,
|
||||
'2주체만': lambda m: m['item_count']['detected'] == 2,
|
||||
'3개 이상 비교': lambda m: m['relation_type']['value'] == 'compare' and m['item_count']['detected'] >= 3,
|
||||
'3개 이상 병렬': lambda m: m['relation_type']['value'] == 'parallel' and m['item_count']['detected'] >= 3,
|
||||
'4개 이상': lambda m: m['item_count']['detected'] >= 4,
|
||||
'4주체 이상': lambda m: m['item_count']['detected'] >= 4,
|
||||
'1개 개념': lambda m: m['item_count']['detected'] == 1,
|
||||
'단일 주체': lambda m: m['item_count']['detected'] == 1,
|
||||
'단일 주제': lambda m: m['item_count']['detected'] == 1,
|
||||
'단일 산업': lambda m: m['item_count']['detected'] == 1,
|
||||
'단일 관점': lambda m: m['item_count']['detected'] == 1,
|
||||
'병렬 나열': lambda m: m['relation_type']['value'] == 'parallel',
|
||||
'주체별 나열': lambda m: any(p in m.get('detected_terms', [])
|
||||
for p in ['발주자', '시공자', '설계자', '주체별']),
|
||||
'산업 비교': lambda m: any(p in m.get('detected_terms', [])
|
||||
for p in ['제조업', '건축', '토목', '산업별']),
|
||||
'필수요건': lambda m: any(p in m.get('detected_terms', [])
|
||||
for p in ['필수조건', '필수요건', '필수']),
|
||||
'요건 나열': lambda m: any(p in m.get('detected_terms', [])
|
||||
for p in ['필수조건', '필수요건']),
|
||||
'BIM vs DX 직접 대조': lambda m: ('BIM' in m.get('detected_terms', []) and
|
||||
'DX' in m.get('detected_terms', []) and
|
||||
m['relation_type']['value'] == 'compare' and
|
||||
m['item_count']['detected'] == 2),
|
||||
}
|
||||
|
||||
|
||||
def load_templates_v1():
|
||||
path = HERE / 'structure_ontology.yaml'
|
||||
with open(path, encoding='utf-8') as f:
|
||||
data = yaml.safe_load(f)
|
||||
assert data['meta']['schema_version'] == 'template-fit-v1', \
|
||||
f"schema_version mismatch: {data['meta']['schema_version']}"
|
||||
return data['templates_v1']
|
||||
|
||||
|
||||
# ═══ 각 축별 점수 계산 ═══
|
||||
|
||||
def anchor_match(detected_terms, anchor_sets):
|
||||
"""max across anchor_sets with per-set conditional cap.
|
||||
|
||||
per-set options (anchor_sets[i]):
|
||||
min_hits → 이 수치 미만이면 해당 set 무시 (기본 1)
|
||||
confidence_cap → ratio 상한 (기본 1.0 = 미적용)
|
||||
cap_exempt_if_corroborated_by → 같은 템플릿의 다른 set 최고 ratio가
|
||||
이 값 이상이면 cap 면제 (기본 None)
|
||||
|
||||
returns: (effective_ratio, set_id, matched_terms, cap_note)
|
||||
"""
|
||||
if not anchor_sets:
|
||||
return 0.0, None, set(), ''
|
||||
detected_set = set(detected_terms)
|
||||
|
||||
# 1. min_hits 통과하는 set 만 수집
|
||||
raw = []
|
||||
for s in anchor_sets:
|
||||
terms = s['terms']
|
||||
min_hits = s.get('min_hits', 1)
|
||||
matched = detected_set & set(terms)
|
||||
if len(matched) < min_hits:
|
||||
continue
|
||||
raw.append({
|
||||
'id': s['id'],
|
||||
'raw_ratio': len(matched) / len(terms) if terms else 0.0,
|
||||
'matched': matched,
|
||||
'cap': s.get('confidence_cap', 1.0),
|
||||
'exempt': s.get('cap_exempt_if_corroborated_by', None),
|
||||
})
|
||||
|
||||
if not raw:
|
||||
return 0.0, None, set(), ''
|
||||
|
||||
# 2. 각 set 별 effective ratio 계산 (cap 조건부 적용)
|
||||
for r in raw:
|
||||
others_max = max(
|
||||
(x['raw_ratio'] for x in raw if x['id'] != r['id']),
|
||||
default=0.0,
|
||||
)
|
||||
if r['cap'] < 1.0 and r['raw_ratio'] > r['cap']:
|
||||
if r['exempt'] is not None and others_max >= r['exempt']:
|
||||
r['effective'] = r['raw_ratio']
|
||||
r['note'] = f"cap{r['cap']:.2f} 면제 (방증 {others_max:.2f}≥{r['exempt']})"
|
||||
else:
|
||||
r['effective'] = r['cap']
|
||||
r['note'] = f"cap{r['cap']:.2f} 적용 (방증 {others_max:.2f}<{r['exempt']})"
|
||||
else:
|
||||
r['effective'] = r['raw_ratio']
|
||||
r['note'] = ''
|
||||
|
||||
# 3. effective ratio 최대 set 선택
|
||||
best = max(raw, key=lambda x: x['effective'])
|
||||
return best['effective'], best['id'], best['matched'], best['note']
|
||||
|
||||
|
||||
def cardinality_match(mdx_count, card, adaptation_allowed):
|
||||
ideal, mn, mx = card['ideal'], card['min'], card['max']
|
||||
if mdx_count == ideal:
|
||||
return 1.0
|
||||
if mn <= mdx_count <= mx:
|
||||
return 0.8
|
||||
if adaptation_allowed.get('split') or adaptation_allowed.get('merge'):
|
||||
return 0.5
|
||||
return 0.0
|
||||
|
||||
|
||||
def relation_match(mdx_rel, frame_rel):
|
||||
if mdx_rel == frame_rel:
|
||||
return 1.0
|
||||
return RELATION_MATRIX.get((mdx_rel, frame_rel), 0.2)
|
||||
|
||||
|
||||
def slot_coverage(mdx_analysis, template):
|
||||
"""rough v1: 0.5 (cardinality 범위 내) + 0.3 (라벨) + 0.2 (본문)."""
|
||||
n = mdx_analysis['item_count']['detected']
|
||||
card = template['visual_pattern']['cardinality']
|
||||
within = card['min'] <= n <= card['max']
|
||||
|
||||
candidates = mdx_analysis.get('slot_candidates', [])
|
||||
has_labels = bool(candidates) and all(sc.get('label') for sc in candidates)
|
||||
has_bodies = bool(candidates) and all(sc.get('body') for sc in candidates)
|
||||
|
||||
score = 0.0
|
||||
if within: score += 0.5
|
||||
if has_labels: score += 0.3
|
||||
if has_bodies: score += 0.2
|
||||
return score
|
||||
|
||||
|
||||
def adaptation_cost(mdx_analysis, template):
|
||||
"""Cardinality mismatch 시만 비용 부과. v1에서는 rewrite cost 안 부과 (light_edit 단계 책임)."""
|
||||
card = template['visual_pattern']['cardinality']
|
||||
allowed = template['adaptation_allowed']
|
||||
n = mdx_analysis['item_count']['detected']
|
||||
|
||||
ops = []
|
||||
cost = 0.0
|
||||
|
||||
if n < card['min']:
|
||||
if allowed.get('split'):
|
||||
ops.append(f'split({n}→{card["min"]})')
|
||||
cost += ADAPT_COST['split']
|
||||
elif allowed.get('infer_missing_slot'):
|
||||
ops.append('infer_missing')
|
||||
cost += ADAPT_COST['infer_missing_slot']
|
||||
else:
|
||||
ops.append('forbidden:n<min') # 실제 점수는 cardinality=0으로 반영됨
|
||||
elif n > card['max']:
|
||||
if allowed.get('merge'):
|
||||
ops.append(f'merge({n}→{card["max"]})')
|
||||
cost += ADAPT_COST['merge']
|
||||
else:
|
||||
ops.append('forbidden:n>max')
|
||||
|
||||
return min(CAP_ADAPTATION, cost), ops
|
||||
|
||||
|
||||
def not_suits_penalty(mdx_analysis, template):
|
||||
not_suits = template.get('fit_notes', {}).get('not_suits', [])
|
||||
hits = 0
|
||||
matched = []
|
||||
for pattern in not_suits:
|
||||
fired = False
|
||||
# 가장 구체적인 키 먼저 매치
|
||||
for key, rule in sorted(NOT_SUITS_RULES.items(), key=lambda x: -len(x[0])):
|
||||
if key in pattern:
|
||||
if rule(mdx_analysis):
|
||||
fired = True
|
||||
break
|
||||
if fired:
|
||||
hits += 1
|
||||
matched.append(pattern)
|
||||
penalty = min(CAP_NOT_SUITS, hits * NOT_SUITS_HIT_PENALTY)
|
||||
return penalty, matched
|
||||
|
||||
|
||||
# ═══ 최종 confidence 계산 ═══
|
||||
|
||||
def compute_template_fit(mdx_analysis, template, content_embedding):
|
||||
anchor_s, anchor_set_id, anchor_terms, anchor_note = anchor_match(
|
||||
mdx_analysis['detected_terms'], template['anchor_sets']
|
||||
)
|
||||
card_s = cardinality_match(
|
||||
mdx_analysis['item_count']['detected'],
|
||||
template['visual_pattern']['cardinality'],
|
||||
template['adaptation_allowed'],
|
||||
)
|
||||
rel_s = relation_match(
|
||||
mdx_analysis['relation_type']['value'],
|
||||
template['visual_pattern']['relation_type'],
|
||||
)
|
||||
slot_s = slot_coverage(mdx_analysis, template)
|
||||
content_s = content_embedding
|
||||
|
||||
# structure_intent 호환도 (score 에 반영하지 않고 gate 용으로만 저장)
|
||||
mdx_intents = mdx_analysis.get('structure_intent', [])
|
||||
frame_intents = template.get('visual_pattern', {}).get('structure_intent', [])
|
||||
ic, ic_source = intent_compat_with_source(mdx_intents, frame_intents)
|
||||
|
||||
base = (W_ANCHOR * anchor_s + W_CARDINALITY * card_s + W_RELATION * rel_s
|
||||
+ W_SLOT * slot_s + W_CONTENT * content_s)
|
||||
|
||||
adapt_pen, adapt_ops = adaptation_cost(mdx_analysis, template)
|
||||
ns_pen, ns_matched = not_suits_penalty(mdx_analysis, template)
|
||||
total_pen = min(CAP_TOTAL, adapt_pen + ns_pen)
|
||||
|
||||
confidence = max(0.0, base - total_pen)
|
||||
|
||||
return {
|
||||
'confidence': confidence,
|
||||
'base': base,
|
||||
'total_penalty': total_pen,
|
||||
'axes': {
|
||||
'anchor': {'score': anchor_s, 'set_id': anchor_set_id,
|
||||
'terms': sorted(anchor_terms), 'note': anchor_note},
|
||||
'cardinality': card_s,
|
||||
'relation': rel_s,
|
||||
'slot': slot_s,
|
||||
'content': content_s,
|
||||
'intent': {'compat': ic, 'source': ic_source,
|
||||
'mdx': mdx_intents, 'frame': frame_intents},
|
||||
},
|
||||
'adaptation': {'penalty': adapt_pen, 'ops': adapt_ops},
|
||||
'not_suits': {'penalty': ns_pen, 'matched': ns_matched},
|
||||
}
|
||||
|
||||
|
||||
def route(confidence, axes=None, adaptation=None, not_suits=None):
|
||||
"""Multi-gate routing.
|
||||
|
||||
1. Intent gate (tagged mismatch 만 강하게 차단)
|
||||
2. Route_v2 multi-constraint (anchor/content/adaptation/not_suits)
|
||||
|
||||
axes/adaptation/not_suits 미제공 시 legacy confidence-only 로 fallback.
|
||||
"""
|
||||
# Legacy fallback (구 호출자 호환)
|
||||
if axes is None:
|
||||
if confidence >= 0.90: return 'use_as_is'
|
||||
if confidence >= 0.75: return 'light_edit'
|
||||
if confidence >= 0.60: return 'restructure'
|
||||
return 'reject'
|
||||
|
||||
a = axes['anchor']['score']
|
||||
c = axes['content']
|
||||
adapt_pen = adaptation['penalty'] if adaptation else 0.0
|
||||
ns_hits = len(not_suits['matched']) if not_suits else 0
|
||||
intent = axes.get('intent', {})
|
||||
ic = intent.get('compat', 0.5)
|
||||
ic_source = intent.get('source', 'neutral')
|
||||
|
||||
# ─── Intent gate (tagged mismatch 만 강하게) ────────
|
||||
if ic_source == 'tagged':
|
||||
if ic < 0.4:
|
||||
return 'reject'
|
||||
if ic < 0.7:
|
||||
# use_as_is/light_edit 금지, restructure 이하만 가능
|
||||
if (confidence >= 0.60
|
||||
and (a >= 0.35 or (c >= 0.55 and adapt_pen < 0.20))
|
||||
and ns_hits <= 1):
|
||||
return 'restructure'
|
||||
return 'reject'
|
||||
|
||||
# ─── Route_v2 multi-constraint (normal path) ──────
|
||||
# use_as_is: 엄격
|
||||
if (confidence >= 0.90
|
||||
and a >= 0.70
|
||||
and c >= 0.55
|
||||
and ns_hits == 0):
|
||||
return 'use_as_is'
|
||||
# light_edit: 중간
|
||||
if (confidence >= 0.75
|
||||
and a >= 0.50
|
||||
and c >= 0.45
|
||||
and adapt_pen < 0.10):
|
||||
return 'light_edit'
|
||||
# restructure: 최소 증거
|
||||
if (confidence >= 0.60
|
||||
and (a >= 0.35 or (c >= 0.55 and adapt_pen < 0.20))
|
||||
and ns_hits <= 1):
|
||||
return 'restructure'
|
||||
return 'reject'
|
||||
|
||||
|
||||
# ═══ anchor_vocab 수집 ═══
|
||||
def collect_anchor_vocab(templates):
|
||||
vocab = set()
|
||||
for tpl in templates.values():
|
||||
for s in tpl['anchor_sets']:
|
||||
vocab.update(s['terms'])
|
||||
return vocab
|
||||
|
||||
|
||||
# ═══ 4 TARGET MDX analysis ═══
|
||||
# v1 fixture 방식 (참고용, 실제 실행은 load_mdx_analyses 사용)
|
||||
_LEGACY_FIXTURE = {
|
||||
'MDX01-2-details': {
|
||||
'title': 'BIM과 DX의 이해',
|
||||
'summary': 'BIM과 DX 용어 혼용 정리. 범위·S/W·프로세스·성과품·활용·확장성·수행개념·주체 관점별 비교.',
|
||||
'detected_terms': ['BIM', 'DX', '범위', '성과품', '확장성', '수행개념', '수행주체',
|
||||
'프로세스', '활용', '비교', '관점별', '용어비교', '상호관계', '혼용'],
|
||||
'item_count': {'detected': 2, 'source': 'table_columns'},
|
||||
'relation_type': {'value': 'compare', 'confidence': 'high'},
|
||||
'content_shape': {'has_table': True, 'has_subsections': False, 'has_bullets': False},
|
||||
'slot_candidates': [
|
||||
{'label': 'BIM', 'body': 'Building Information Modeling (범위/S·W/프로세스 ...)'},
|
||||
{'label': 'DX', 'body': 'Digital Transformation (범위/S·W/프로세스 ...)'},
|
||||
],
|
||||
},
|
||||
'MDX02-2.2-table': {
|
||||
'title': '주체별 기대효과',
|
||||
'summary': 'DX 시행 주체별 기대효과. 발주자·시공자·설계자 각각의 역할별 목표와 기대 효과.',
|
||||
'detected_terms': ['발주자', '시공자', '설계자', '주체별', '기대효과', '역할', '역량목표',
|
||||
'소통', '협업', '품질향상', '생산성향상'],
|
||||
'item_count': {'detected': 3, 'source': 'table_rows'},
|
||||
'relation_type': {'value': 'parallel', 'confidence': 'high'},
|
||||
'content_shape': {'has_table': True, 'has_subsections': False, 'has_bullets': True},
|
||||
'slot_candidates': [
|
||||
{'label': '발주자', 'body': '소통·행정자동화·공사Risk 최소화'},
|
||||
{'label': '시공자', 'body': '품질향상·생산성향상·현장실효성'},
|
||||
{'label': '설계자', 'body': '오류예방·리스크최소화·민원예방'},
|
||||
],
|
||||
},
|
||||
'MDX03-1': {
|
||||
'title': 'DX 시행을 위한 필수요건',
|
||||
'summary': 'DX 시행을 위한 3대 필수조건. 기술·사람·자연 여건의 조화.',
|
||||
'detected_terms': ['DX', '기술', '사람', '자연', '필수조건', '역량', '여건',
|
||||
'디지털기술', '3요소'],
|
||||
'item_count': {'detected': 3, 'source': 'bullets'},
|
||||
'relation_type': {'value': 'parallel', 'confidence': 'high'},
|
||||
'content_shape': {'has_table': False, 'has_subsections': False, 'has_bullets': True},
|
||||
'slot_candidates': [
|
||||
{'label': '기술', 'body': '디지털기술과 기반지식'},
|
||||
{'label': '사람', 'body': '역량과 창의성'},
|
||||
{'label': '자연', 'body': '여건과 투자 기반'},
|
||||
],
|
||||
},
|
||||
'MDX03-2': {
|
||||
'title': 'Process/Product 혁신',
|
||||
'summary': 'Process 혁신과 Product 혁신. 과정의 Analogue→Digital Transformation, 결과의 2D도면→3D모델 전환.',
|
||||
'detected_terms': ['과정혁신', '결과혁신', 'Process', 'Product', 'Analogue', 'Digital',
|
||||
'2D', '3D', 'Transformation', 'AS-IS', 'TO-BE', '2D도면', '3D모델'],
|
||||
'item_count': {'detected': 2, 'source': 'subsections'},
|
||||
'relation_type': {'value': 'compare', 'confidence': 'high'},
|
||||
'content_shape': {'has_table': False, 'has_subsections': True, 'has_bullets': True},
|
||||
'slot_candidates': [
|
||||
{'label': '과정혁신', 'body': 'Analogue → Digital Transformation'},
|
||||
{'label': '결과혁신', 'body': '2D도면 → 3D모델'},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ═══ Mock content embedding ═══
|
||||
# 정답 페어 0.90, 관련 0.45~0.55, 무관 0.40
|
||||
CONTENT_EMBEDDING_MOCK = {
|
||||
'MDX01-2-details': {
|
||||
'bim_dx_comparison_table': 0.90,
|
||||
'process_product_two_way': 0.55,
|
||||
'sw_reality_three_emphasis': 0.50,
|
||||
'three_persona_benefits': 0.45,
|
||||
'three_parallel_requirements':0.40,
|
||||
},
|
||||
'MDX02-2.2-table': {
|
||||
'three_persona_benefits': 0.90,
|
||||
'three_parallel_requirements':0.50,
|
||||
'sw_reality_three_emphasis': 0.45,
|
||||
'bim_dx_comparison_table': 0.40,
|
||||
'process_product_two_way': 0.40,
|
||||
},
|
||||
'MDX03-1': {
|
||||
'three_parallel_requirements':0.90,
|
||||
'sw_reality_three_emphasis': 0.55,
|
||||
'three_persona_benefits': 0.50,
|
||||
'process_product_two_way': 0.50,
|
||||
'bim_dx_comparison_table': 0.45,
|
||||
},
|
||||
'MDX03-2': {
|
||||
'process_product_two_way': 0.90,
|
||||
'bim_dx_comparison_table': 0.55,
|
||||
'sw_reality_three_emphasis': 0.50,
|
||||
'three_parallel_requirements':0.45,
|
||||
'three_persona_benefits': 0.40,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GROUND_TRUTH = {
|
||||
'MDX01-2-details': 'bim_dx_comparison_table',
|
||||
'MDX02-2.2-table': 'three_persona_benefits',
|
||||
'MDX03-1': 'three_parallel_requirements',
|
||||
'MDX03-2': 'process_product_two_way',
|
||||
}
|
||||
|
||||
|
||||
def load_mdx_analyses(anchor_vocab):
|
||||
"""실제 MDX 소스에서 detect_mdx_analysis() 로 4 TARGET 분석 생성."""
|
||||
units_full, units_title = load_target_units()
|
||||
out = {}
|
||||
for t in TARGET_UNITS:
|
||||
uid = t[0]
|
||||
out[uid] = detect_mdx_analysis(units_full[uid], units_title[uid], anchor_vocab)
|
||||
return out
|
||||
|
||||
|
||||
def compute_content_sim(mdx_analyses, tpl_by_id):
|
||||
"""ko-sroberta cosine: MDX summary ↔ template description."""
|
||||
print(f" [embedding] ko-sroberta load + {len(tpl_by_id)} template + {len(mdx_analyses)} MDX encode ...")
|
||||
tpl_descs = {tid: tpl['description'] for tid, tpl in tpl_by_id.items()}
|
||||
tpl_ids = list(tpl_descs.keys())
|
||||
tpl_vecs = embed_texts([tpl_descs[i] for i in tpl_ids])
|
||||
|
||||
mdx_ids = list(mdx_analyses.keys())
|
||||
mdx_summaries = [mdx_analyses[i]['summary'] for i in mdx_ids]
|
||||
mdx_vecs = embed_texts(mdx_summaries)
|
||||
|
||||
sim = {}
|
||||
for i, mid in enumerate(mdx_ids):
|
||||
sim[mid] = {}
|
||||
for j, tid in enumerate(tpl_ids):
|
||||
sim[mid][tid] = max(0.0, min(1.0, cosine(mdx_vecs[i], tpl_vecs[j])))
|
||||
return sim
|
||||
|
||||
|
||||
# ─── 검증 기준 ─────────────────────────────────────────
|
||||
# 리뷰어 합의 기준:
|
||||
# 1. 정답 후보는 1위여야 한다
|
||||
# 2. 정답 아닌 후보는 use_as_is(≥0.90) 라우팅되면 안 된다
|
||||
# 3. 1위와 2위의 격차는 최소 0.05 이상 (fragile tie 방지)
|
||||
MIN_GAP = 0.05
|
||||
USE_AS_IS_FLOOR = 0.90
|
||||
|
||||
|
||||
def main():
|
||||
templates = load_templates_v1()
|
||||
tpl_by_id = {t['template_id']: t for t in templates.values()}
|
||||
vocab = collect_anchor_vocab(templates)
|
||||
|
||||
print(f"=== Template-fit v1 scoring — ko-sroberta content embedding ===\n")
|
||||
|
||||
MDX_ANALYSES = load_mdx_analyses(vocab)
|
||||
CONTENT_SIM = compute_content_sim(MDX_ANALYSES, tpl_by_id)
|
||||
print()
|
||||
|
||||
hits = 0
|
||||
warnings = []
|
||||
|
||||
for mdx_id, mdx_analysis in MDX_ANALYSES.items():
|
||||
gt = GROUND_TRUTH[mdx_id]
|
||||
print(f"━━ {mdx_id} 정답: {gt}")
|
||||
print(f" item_count={mdx_analysis['item_count']['detected']:d} "
|
||||
f"relation={mdx_analysis['relation_type']['value']} "
|
||||
f"terms={len(mdx_analysis['detected_terms'])}")
|
||||
|
||||
results = []
|
||||
for tpl_id, tpl in tpl_by_id.items():
|
||||
content_sim = CONTENT_SIM[mdx_id][tpl_id]
|
||||
r = compute_template_fit(mdx_analysis, tpl, content_sim)
|
||||
results.append((tpl_id, r))
|
||||
|
||||
results.sort(key=lambda x: -x[1]['confidence'])
|
||||
|
||||
for rank, (tpl_id, r) in enumerate(results, 1):
|
||||
a = r['axes']
|
||||
mark = '✓' if tpl_id == gt else ' '
|
||||
route_label = route(r['confidence'])
|
||||
a_score = a['anchor']['score']
|
||||
a_set = a['anchor']['set_id'] or '-'
|
||||
a_terms = a['anchor']['terms']
|
||||
a_note = a['anchor'].get('note', '')
|
||||
print(f" [{rank}] {mark} {tpl_id:32s} conf={r['confidence']:.3f} ({route_label})")
|
||||
suffix = f" {a_note}" if a_note else ''
|
||||
print(f" anchor={a_score:.2f}({a_set}: {a_terms}){suffix}")
|
||||
print(f" card={a['cardinality']:.2f} rel={a['relation']:.2f} slot={a['slot']:.2f} content={a['content']:.2f}")
|
||||
print(f" base={r['base']:.3f} "
|
||||
f"adapt=-{r['adaptation']['penalty']:.2f}{r['adaptation']['ops']} "
|
||||
f"nsuits=-{r['not_suits']['penalty']:.2f}{r['not_suits']['matched']}")
|
||||
|
||||
# 검증 체크
|
||||
top1_tpl, top1_r = results[0]
|
||||
top2_tpl, top2_r = results[1]
|
||||
if top1_tpl == gt:
|
||||
hits += 1
|
||||
else:
|
||||
warnings.append(f"[{mdx_id}] top1={top1_tpl} ≠ gt={gt}")
|
||||
|
||||
# 정답 아닌 후보가 use_as_is 인지
|
||||
for tpl_id, r in results:
|
||||
if tpl_id != gt and r['confidence'] >= USE_AS_IS_FLOOR:
|
||||
warnings.append(
|
||||
f"[{mdx_id}] 오답 후보 use_as_is: {tpl_id} conf={r['confidence']:.3f}"
|
||||
)
|
||||
|
||||
# 1-2위 격차
|
||||
gap = top1_r['confidence'] - top2_r['confidence']
|
||||
if gap < MIN_GAP:
|
||||
warnings.append(
|
||||
f"[{mdx_id}] 격차 {gap:.3f} < 기준 {MIN_GAP} "
|
||||
f"(top1={top1_tpl} {top1_r['confidence']:.3f} / "
|
||||
f"top2={top2_tpl} {top2_r['confidence']:.3f})"
|
||||
)
|
||||
print()
|
||||
|
||||
print(f"=== Result: {hits}/{len(MDX_ANALYSES)} ===\n")
|
||||
|
||||
if warnings:
|
||||
print("⚠️ 경고")
|
||||
for w in warnings:
|
||||
print(f" - {w}")
|
||||
else:
|
||||
print(f"✓ 검증 기준 통과 ({hits}/{len(MDX_ANALYSES)}, "
|
||||
f"오답 use_as_is 없음, 1-2위 격차 ≥ {MIN_GAP})")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,687 @@
|
||||
# Step 6 anchor_sets 수동 입력 source.
|
||||
# 사람이 직접 편집한 의미 묶음. AI 자동 판단 없음.
|
||||
#
|
||||
# 원칙 (codex 확정):
|
||||
# 1. anchor term 은 anchor_candidates.yaml 에 실제 존재하는 token 만 사용
|
||||
# 2. not_in_frame term 은 제외 (flagged_terms 로 별도 기록)
|
||||
# 3. 단어 나열이 아니라 의미 묶음
|
||||
# 4. 너무 일반적인 단어 (건설, 정보, 기반, 기술 등) 단독 anchor 금지
|
||||
# 5. Special/canonical, unique_to_frame, mdx_hit=Y 후보 우선 검토
|
||||
# 6. frame 당 anchor_set 2~4개
|
||||
# 7. 각 anchor_set terms 2~6개
|
||||
# 8. 구조어 (3열, 표, 카드, 비교표, 다이어그램) 제외
|
||||
# 9. 애매한 term 은 notes 또는 review_needed 에 기록
|
||||
# 10. TARGET 4개 (13/14/18/29) 우선, 나머지 28개 확장
|
||||
#
|
||||
# anchor_set 4조건 (codex):
|
||||
# - 실제 프레임 텍스트 등장
|
||||
# - MDX 에도 등장 가능성
|
||||
# - 다른 프레임과 구분되는 의미
|
||||
# - 함께 등장 시 디자인 선택 신호
|
||||
|
||||
frames:
|
||||
|
||||
# ============================================================
|
||||
# Batch 1 (Frame 01~08) — codex 리뷰 대상
|
||||
# ============================================================
|
||||
|
||||
# Frame 1 / 1171281172 — S/W 개발 기반 / 기술기반 중요성
|
||||
# codex 조정: improvement_compatibility → quality_compatibility (품질 추가로 구체화)
|
||||
1171281172:
|
||||
anchor_sets:
|
||||
- id: sw_dx_foundation
|
||||
terms: [S/W, DX, 융합]
|
||||
- id: recognition_and_setup
|
||||
terms: [인식, 설정, 역할]
|
||||
- id: quality_compatibility
|
||||
terms: [품질, 향상, 호환]
|
||||
review_needed:
|
||||
- token: 호환
|
||||
reason: mdx_df=0. Frame 전용.
|
||||
- token: 설정
|
||||
reason: mdx_df=0. Frame 전용.
|
||||
|
||||
# Frame 2 / 1171281173 — Domain knowledge / AI 기반 Engineering S/W
|
||||
1171281173:
|
||||
anchor_sets:
|
||||
- id: ai_open_source
|
||||
terms: [AI, Open, Source]
|
||||
- id: domain_knowledge
|
||||
terms: [Domain, knowledge, 지식]
|
||||
- id: sw_hw_basis
|
||||
terms: [S/W, H/W, 기반, 개발]
|
||||
review_needed:
|
||||
- token: AI
|
||||
reason: frame_df=1, mdx_df=0. Frame 2 전용 영문 약어.
|
||||
- token: Open
|
||||
reason: mdx_df=0. 영문 토큰, MDX 에 없음.
|
||||
- token: Source
|
||||
reason: mdx_df=0.
|
||||
- token: Domain
|
||||
reason: mdx_df=0.
|
||||
- token: knowledge
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 3 / 1171281174 — 설계 업무 Data / Claim / Manual
|
||||
1171281174:
|
||||
anchor_sets:
|
||||
- id: data_documentation
|
||||
terms: [Data, 데이터, 정리]
|
||||
- id: business_manual_claim
|
||||
terms: [Manual, Claim, 업무]
|
||||
review_needed:
|
||||
- token: Manual
|
||||
reason: mdx_df=0. 영문 토큰.
|
||||
- token: 국외
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 4 / 1171281175 — 국제 개발 / 정부 규정 / 발전 X
|
||||
# codex 조정: stagnation_obstacles 삭제 (규제/제도/장애 후보 없어 구체화 불가,
|
||||
# 답습·저하·저해 단독 부정어 묶음은 anchor 로 약함)
|
||||
1171281175:
|
||||
anchor_sets:
|
||||
- id: government_regulation
|
||||
terms: [정부, 규정, 지침, 형식]
|
||||
- id: international_finance
|
||||
terms: [ADB, IBRD, 차관, 경제]
|
||||
review_needed:
|
||||
- token: 정부
|
||||
reason: mdx_df=0. Frame 특화.
|
||||
- token: 형식
|
||||
reason: mdx_df=0.
|
||||
- token: ADB
|
||||
reason: frame_df=1, mdx_df=0. 국제기구 약어.
|
||||
- token: IBRD
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 차관
|
||||
reason: mdx_df=0.
|
||||
- token: 발전
|
||||
reason: frame_local_count=4 로 강하나 mdx_df=0. "발전X" 부정 맥락 가능성.
|
||||
notes: |
|
||||
삭제된 set: stagnation_obstacles [답습, 저하, 저해].
|
||||
이유: 구체화 가능한 후보(규제/제도/장애) 가 Frame 4 에 없음. 부정어만 묶인 anchor 는 약함.
|
||||
|
||||
# Frame 5 / 1171281176 — 보상현황·민원 관리 / 시각 매체
|
||||
1171281176:
|
||||
anchor_sets:
|
||||
- id: compensation_status
|
||||
terms: [보상, 현황, 민원]
|
||||
- id: visualization_media
|
||||
terms: [시각, 영상, 사진]
|
||||
- id: documentation_analysis
|
||||
terms: [서류, 분석, 종합]
|
||||
review_needed:
|
||||
- token: 보상
|
||||
reason: mdx_df=0.
|
||||
- token: 현황
|
||||
reason: mdx_df=0.
|
||||
- token: 영상
|
||||
reason: mdx_df=0.
|
||||
- token: 사진
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 6 / 1171281177 — 민원 관리 / 리스크 대응 / 시공 이슈
|
||||
1171281177:
|
||||
anchor_sets:
|
||||
- id: civil_complaint
|
||||
terms: [민원, 보상, 현황, 토지]
|
||||
- id: risk_response
|
||||
terms: [리스크, 대응, 선제]
|
||||
- id: complaint_types
|
||||
terms: [유형, 요청, 승복]
|
||||
- id: construction_issues
|
||||
terms: [데크, 배수, 불편]
|
||||
review_needed:
|
||||
- token: 보상
|
||||
reason: mdx_df=0.
|
||||
- token: 현황
|
||||
reason: mdx_df=0.
|
||||
- token: 토지
|
||||
reason: mdx_df=0.
|
||||
- token: 선제
|
||||
reason: mdx_df=0.
|
||||
- token: 승복
|
||||
reason: mdx_df=0.
|
||||
- token: 데크
|
||||
reason: mdx_df=0.
|
||||
- token: 배수
|
||||
reason: mdx_df=0.
|
||||
- token: 불편
|
||||
reason: mdx_df=0.
|
||||
- token: 요청
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 7 / 1171281178 — H/W+S/W 시스템 / BIM + 실시간 모니터링
|
||||
# 주의: total_unique 147 으로 가장 큼.
|
||||
# codex 조정: data_efficiency 삭제 (데이터/효율/기반 일반어 조합).
|
||||
# 데이터 는 realtime_monitoring 으로 흡수 (실시간 데이터 모니터링 의미 확장).
|
||||
1171281178:
|
||||
anchor_sets:
|
||||
- id: realtime_monitoring
|
||||
terms: [실시간, CCTV, 사전, 위험, 데이터]
|
||||
- id: bim_dx_digital
|
||||
terms: [BIM, DX, 3D]
|
||||
- id: field_management
|
||||
terms: [현장, 관리, 공사]
|
||||
review_needed:
|
||||
- token: 실시간
|
||||
reason: frame_local_count=7 강하나 mdx_df=0. Frame 전용.
|
||||
- token: CCTV
|
||||
reason: mdx_df=0.
|
||||
- token: 위험
|
||||
reason: mdx_df=0.
|
||||
notes: |
|
||||
삭제된 set: data_efficiency [데이터, 효율, 기반].
|
||||
이유: 너무 일반어 조합. 데이터 는 realtime_monitoring 으로 흡수, 효율/기반 은 anchor 에서 제외.
|
||||
|
||||
# Frame 8 / 1171281179 — 공사 계획 정보 / 수량·자원 투입
|
||||
1171281179:
|
||||
anchor_sets:
|
||||
- id: construction_planning
|
||||
terms: [공사, 계획, 수량]
|
||||
- id: information_linkage
|
||||
terms: [정보, 연계, 객체]
|
||||
- id: resource_planning
|
||||
terms: [자원, 투입]
|
||||
- id: sw_3d_modeling
|
||||
terms: [S/W, 3D]
|
||||
review_needed:
|
||||
- token: 객체
|
||||
reason: mdx_df=0.
|
||||
|
||||
# ============================================================
|
||||
# Batch 2 (Frame 09, 10, 11, 12, 15, 16) — codex 리뷰 대상
|
||||
# ============================================================
|
||||
|
||||
# Frame 9 / 1171281180 — 시공 전 모델 기반 시공계획
|
||||
# codex 조정: visualization_aid 삭제 (가시/차원 일반어, 영상물 단독으로는 약함)
|
||||
1171281180:
|
||||
anchor_sets:
|
||||
- id: simulation_planning
|
||||
terms: [시뮬레이션, 시공, 모델, 도면]
|
||||
- id: pre_construction_info
|
||||
terms: [정보, 추출, 안전]
|
||||
review_needed:
|
||||
- token: 시뮬레이션
|
||||
reason: mdx_df=0.
|
||||
notes: |
|
||||
삭제된 set: visualization_aid [가시, 영상물, 차원].
|
||||
이유: 가시/차원 너무 일반적, 영상물 단독으로는 anchor 약함. 다른 set 에 자연스러운 흡수처 없음.
|
||||
|
||||
# Frame 10 / 1171281181 — 현장 실효성 증대
|
||||
# codex 조정: customization_info 삭제 (정보/특정 너무 일반, 맞춤 단독은 약함, 구체 합체 대상 없음)
|
||||
# Frame 10 은 total_unique 22 로 작아 1 set 유지로 진행.
|
||||
1171281181:
|
||||
anchor_sets:
|
||||
- id: field_effectiveness
|
||||
terms: [현장, 실효, 증대]
|
||||
review_needed:
|
||||
- token: 실효
|
||||
reason: frame_df=1, mdx_df=0. Frame 전용.
|
||||
notes: |
|
||||
삭제된 set: customization_info [맞춤, 정보, 특정].
|
||||
이유: 정보/특정 너무 일반어. 맞춤 단독은 1-term anchor 라 약함.
|
||||
Frame 10 에 맞춤을 묶을 구체 term 없음 (field_effectiveness 와 맥락 다름).
|
||||
|
||||
# Frame 11 / 1171281182 — 시공단계 BIM 모델 + 위치 기반
|
||||
1171281182:
|
||||
anchor_sets:
|
||||
- id: bim_model_usage
|
||||
terms: [BIM, 모델, 활용, 정보]
|
||||
- id: location_based
|
||||
terms: [위치, Position, based]
|
||||
- id: construction_object
|
||||
terms: [공사, 객체, Model]
|
||||
review_needed:
|
||||
- token: based
|
||||
reason: frame_df=1, mdx_df=0. 영문 토큰.
|
||||
- token: Position
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 객체
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 12 / 1171281189 — 품질/안전 향상 benefit + 소통/신뢰
|
||||
1171281189:
|
||||
anchor_sets:
|
||||
- id: quality_safety
|
||||
terms: [품질, 안전, 향상]
|
||||
- id: communication_trust
|
||||
terms: [소통, 신뢰, 강화, 의사소통]
|
||||
- id: cost_productivity
|
||||
terms: [비용, 생산, 최소]
|
||||
- id: bim_process_innovation
|
||||
terms: [BIM, 과정혁신, 3D]
|
||||
|
||||
# Frame 15 / 1171281192 — 경험/능력 + DX 수행 + 시행착오
|
||||
1171281192:
|
||||
anchor_sets:
|
||||
- id: dx_capability
|
||||
terms: [DX, 수행, 능력, 기술]
|
||||
- id: experience_knowledge
|
||||
terms: [경험, 지식, 보유]
|
||||
- id: user_innovation
|
||||
terms: [사용자, 혁신, 창의, 실행]
|
||||
- id: trial_growth
|
||||
terms: [시행착오, 의지, 도약]
|
||||
review_needed:
|
||||
- token: 도약
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
|
||||
# Frame 16 / 1171281193 — 기술 부재 + S/W 과도 의존 + 개념 혼란
|
||||
# codex 조정: institutional_evasion 삭제 (Batch 1 stagnation_obstacles 와 동일 패턴 — 부정어 조합)
|
||||
# total_unique 151 로 큼. 3 set.
|
||||
1171281193:
|
||||
anchor_sets:
|
||||
- id: bim_dx_misunderstanding
|
||||
terms: [BIM, DX, 개념, 이해]
|
||||
- id: sw_dependency
|
||||
terms: [S/W, 모델, 의존, 과도]
|
||||
- id: tech_capability_gap
|
||||
terms: [기술, 부재, 부족, 기술력]
|
||||
review_needed:
|
||||
- token: 부재
|
||||
reason: mdx_df=0. frame_local=6 으로 강함.
|
||||
- token: 부족
|
||||
reason: mdx_df=0.
|
||||
- token: 과도
|
||||
reason: mdx_df=0.
|
||||
- token: 기술력
|
||||
reason: mdx_df=0.
|
||||
notes: |
|
||||
삭제된 set: institutional_evasion [방침, 집행, 회피].
|
||||
이유: Batch 1 Frame 4 stagnation_obstacles 와 동일 패턴 — 부정/비판 맥락 부정어 조합은 anchor 구체성 약함.
|
||||
|
||||
# ============================================================
|
||||
# Batch 3 (Frame 17, 19, 20, 21, 22, 23, 24) — codex 리뷰 대상
|
||||
# 주의: BIM/DX/S/W/3D 단독 anchor 금지 — 각 프레임 구체 맥락과 함께 묶음
|
||||
# Frame 18 의 BIM/DX set 과 맥락 차별화 필수
|
||||
# ============================================================
|
||||
|
||||
# Frame 17 / 1171281194 — 잘못된 접근방식 / 도구 오용
|
||||
# BIM/DX 중복 주의: Frame 18 의 bim_dx 와 달리, Frame 17 은 "잘못된 접근/혼재" 맥락
|
||||
1171281194:
|
||||
anchor_sets:
|
||||
- id: design_approach_critique
|
||||
terms: [설계, 방식, 수행, 개념]
|
||||
- id: tool_misuse
|
||||
terms: [S/W, 2D, 3D, 전제, 기존]
|
||||
- id: perception_gap
|
||||
terms: [인식, 제시, 기술자]
|
||||
review_needed:
|
||||
- token: 전제
|
||||
reason: mdx_df=0.
|
||||
- token: 회사
|
||||
reason: mdx_df=0.
|
||||
- token: 기술자
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 19 / 1171281197 — 엔지니어의 기능인 전락 / 설계방식 왜곡
|
||||
# codex 판단: engineer_degradation 은 부정 맥락이나 "엔지니어/기능인" 구체 대상이 있어 유지
|
||||
1171281197:
|
||||
anchor_sets:
|
||||
- id: engineer_degradation
|
||||
terms: [엔지니어, 기능인, 전락, 왜곡]
|
||||
- id: design_added_value
|
||||
terms: [설계, 가치, 부가, 작업]
|
||||
- id: decision_authority
|
||||
terms: [결정, 의사, 업무]
|
||||
review_needed:
|
||||
- token: engineer_degradation (set)
|
||||
reason: 부정 맥락 set. 엔지니어/기능인 구체 대상이 있어 Batch 1 stagnation_obstacles 와 달리 유지. MDX 매칭 신호는 약할 수 있어 재검토.
|
||||
- token: 엔지니어
|
||||
reason: frame_local=5 로 강하나 mdx_df=0.
|
||||
- token: 기능인
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 전락
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 왜곡
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
|
||||
# Frame 20 / 1171281198 — DX 산업 전환 필요성
|
||||
# BIM/DX 중복: Frame 18 의 bim_dx 와 달리, Frame 20 은 "산업 전환 고도화" 맥락
|
||||
# codex 조정: process_innovation_context 삭제 (이해/건설 일반어, 과정혁신 단독은 약함)
|
||||
1171281198:
|
||||
anchor_sets:
|
||||
- id: dx_industry_transition
|
||||
terms: [DX, 산업, 전환, 고도, BIM]
|
||||
- id: added_value
|
||||
terms: [가치, 부가, 개선]
|
||||
- id: sw_necessity
|
||||
terms: [S/W, 필요, 업무]
|
||||
notes: |
|
||||
삭제된 set: process_innovation_context [과정혁신, 이해, 건설].
|
||||
이유: 이해/건설 너무 일반어, 과정혁신 단독은 1-term anchor 라 약함. dx_industry_transition 에 과정혁신 흡수도 의미 부자연 → 삭제.
|
||||
|
||||
# Frame 21 / 1171281201 — Solution Engn. S/W Platform / 협업
|
||||
# codex 조정: user_project 삭제 (User/Project/상황 일반 영문 조합)
|
||||
1171281201:
|
||||
anchor_sets:
|
||||
- id: solution_platform
|
||||
terms: [S/W, Solution, Platform, Engn.]
|
||||
- id: collaboration_field
|
||||
terms: [협업, 현장, 반영, 의사소통]
|
||||
review_needed:
|
||||
- token: Platform
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
notes: |
|
||||
삭제된 set: user_project [User, Project, 상황].
|
||||
이유: User/Project/상황 전부 일반 영문 조합이라 anchor 구체성 약함.
|
||||
|
||||
# Frame 22 / 1171281202 — 상용 BIM/Engn. S/W 제품 목록 (ArcGIS/QGIS/Naviswork 등)
|
||||
# Frame 18 software_tools 와 맥락 차별화: Frame 18 은 Civil/Revit 중심, Frame 22 는 GIS/Modeling 전용
|
||||
1171281202:
|
||||
anchor_sets:
|
||||
- id: gis_modeling_tools
|
||||
terms: [ArcGIS, QGIS, Infraworks, Modeler]
|
||||
- id: bim_3d_simulation_sw
|
||||
terms: [S/W, BIM, 3D, Model, Engn.]
|
||||
- id: visualization_products
|
||||
terms: [Simulation, Sketch, Video, Naviswork]
|
||||
review_needed:
|
||||
- token: ArcGIS
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: QGIS
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Infraworks
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Modeler
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Naviswork
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Sketch
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Video
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
|
||||
# Frame 23 / 1171281203 — Application S/W (비즈니스/기업 S/W) 특성
|
||||
1171281203:
|
||||
anchor_sets:
|
||||
- id: application_software
|
||||
terms: [S/W, Application, Program, Commercial]
|
||||
- id: client_requirement
|
||||
terms: [고객, 요구, 사용자, 변경]
|
||||
- id: enterprise_solution
|
||||
terms: [ERP, CRM, 비즈니스, 상품, 솔루션]
|
||||
review_needed:
|
||||
- token: Application
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Program
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Commercial
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: ERP
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: CRM
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 비즈니스
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 상품
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 특정
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 24 / 1171281204 — Engn. S/W 의 구성과 특징 (총론) / S/W 개발 예시 비유
|
||||
# codex 조정:
|
||||
# - commercial_sw_vendor 삭제 (회사/업체/전문/자체 일반 행정어)
|
||||
# - vehicle_analogy 삭제 (S/W 비유 표현, MDX 매칭에 노이즈)
|
||||
1171281204:
|
||||
anchor_sets:
|
||||
- id: engineering_software_development
|
||||
terms: [S/W, 엔지니어링, 개발, 목적, 제작]
|
||||
- id: bim_3d_modeling
|
||||
terms: [BIM, 3D, Model]
|
||||
review_needed:
|
||||
- token: 엔지니어링
|
||||
reason: mdx_df=0.
|
||||
notes: |
|
||||
삭제된 set 1: commercial_sw_vendor [회사, 업체, 전문, 자체].
|
||||
이유: 일반 행정어 조합. 구체 상용 S/W 업체명이 frame 에 없어 재구성 불가.
|
||||
|
||||
삭제된 set 2: vehicle_analogy [곡물, 차량, 엔진, 운반, 탱크].
|
||||
이유: S/W 를 차량에 비유한 frame 고유 표현이나 MDX 매칭 anchor 로는 노이즈. 비유 표현은 anchor 범위 밖으로 보류.
|
||||
|
||||
# ============================================================
|
||||
# Batch 4 (Frame 25, 26, 27, 28, 30, 31, 32) — codex 리뷰 대상
|
||||
# 주의: S/W/BIM/DX/3D 공통 토큰은 각 frame 고유 맥락과 함께 묶음
|
||||
# 비유/로고/제품명은 review_needed 또는 notes 분리
|
||||
# 구조어/숫자/카테고리 anchor 금지
|
||||
# ============================================================
|
||||
|
||||
# Frame 25 / 1171281205 — 3rd Party 상용 S/W / 인프라 분야
|
||||
1171281205:
|
||||
anchor_sets:
|
||||
- id: third_party_sw
|
||||
terms: [S/W, Party, rd, 상용]
|
||||
- id: infrastructure_domains
|
||||
terms: [도로, 철도, 상하수도, 하천, 도시]
|
||||
review_needed:
|
||||
- token: Party
|
||||
reason: mdx_df=0.
|
||||
- token: rd
|
||||
reason: mdx_df=0. "3rd" 에서 Kiwi 로 분리된 조각.
|
||||
- token: 도로
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 철도
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 상하수도
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 하천
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 도시
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
|
||||
# Frame 26 / 1171281206 — 기술예속 심화 / S/W 의존 위기 / 독과점
|
||||
# 부정 맥락이나 "기술종속/위기/독과점" 은 Frame 26 의 고유 주제 진단
|
||||
1171281206:
|
||||
anchor_sets:
|
||||
- id: technology_dependency_crisis
|
||||
terms: [기술, 종속, 위기, 존폐]
|
||||
- id: sw_cost_burden
|
||||
terms: [S/W, 사용료, 과다, 감당]
|
||||
- id: monopoly_ecosystem
|
||||
terms: [독과점, 생태, 심화]
|
||||
review_needed:
|
||||
- token: monopoly_ecosystem (set)
|
||||
reason: 부정 맥락 set. 독과점 은 구체어이나 생태/심화 일반. Frame 19 engineer_degradation 과 동일 기준 — 구체 대상 있으면 유지, MDX 신호 약함.
|
||||
- token: 위기
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 존폐
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 사용료
|
||||
reason: mdx_df=0.
|
||||
- token: 종속
|
||||
reason: mdx_df=0.
|
||||
- token: 독과점
|
||||
reason: mdx_df=0.
|
||||
|
||||
# Frame 27 / 1171281208 — DX 산업 부가가치 / BIM 전면 도입
|
||||
# BIM/DX 중복 주의: Frame 18(정면비교)/Frame 20(산업 전환)/Frame 27(전면 도입 가치) 맥락 차별화
|
||||
# codex 조정: idea_essence 삭제 (본질/아이디어/발현 모두 추상어)
|
||||
1171281208:
|
||||
anchor_sets:
|
||||
- id: dx_industry_value
|
||||
terms: [DX, 산업, 가치, 부가]
|
||||
- id: bim_dx_adoption
|
||||
terms: [BIM, DX, 도입, 전면]
|
||||
notes: |
|
||||
삭제된 set: idea_essence [본질, 아이디어, 발현].
|
||||
이유: 추상어 조합. MDX 매칭 anchor 로는 구체성 약함.
|
||||
|
||||
# Frame 28 / 1171281209 — 현존 상용 S/W 의 한계 / 토목 분야 전문성 부족
|
||||
# Frame 18 software_tools(상용 제품 목록) 와 차별: Frame 28 은 "한계/걸림돌" 진단
|
||||
# codex 조정: development_obstacles 삭제 (개발 일반어 + 걸림돌/전무 부정어, 구체 대상 약함)
|
||||
1171281209:
|
||||
anchor_sets:
|
||||
- id: commercial_sw_limitations
|
||||
terms: [S/W, 소프트웨어, 상용, 현존, 한계]
|
||||
- id: civil_engineering_specialization
|
||||
terms: [토목, 분야, 전문, 실무, 세분]
|
||||
review_needed:
|
||||
- token: 소프트웨어
|
||||
reason: mdx_df=0.
|
||||
- token: 현존
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 세분
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
notes: |
|
||||
삭제된 set: development_obstacles [개발, 걸림돌, 전무].
|
||||
이유: 개발 일반어, 걸림돌/전무 부정어. 구체 대상 약함 (Batch 1 stagnation_obstacles 와 동일 패턴).
|
||||
한계/걸림돌 맥락은 commercial_sw_limitations 의 "한계" 로 일부 반영됨.
|
||||
|
||||
# Frame 30 / 1171281211 — 범용 S/W 사용 곤란 / Smart Factory/Construction 비교
|
||||
# Frame 18/22/24 의 BIM/3D S/W 맥락과 차별: Frame 30 은 "범용 한계 + 산업비교"
|
||||
1171281211:
|
||||
anchor_sets:
|
||||
- id: general_purpose_sw_limitation
|
||||
terms: [S/W, 범용, 제품]
|
||||
- id: smart_factory_construction
|
||||
terms: [Smart, Factory, Construction, 시공, 건축]
|
||||
- id: cae_modeling_tools
|
||||
terms: [CAE, 모델링, ArchiCAD, CIM]
|
||||
review_needed:
|
||||
- token: 제품
|
||||
reason: mdx_df=0.
|
||||
- token: 범용
|
||||
reason: mdx_df=0.
|
||||
- token: Smart
|
||||
reason: mdx_df=0.
|
||||
- token: Factory
|
||||
reason: frame_df=1, mdx_df=0. 자동차/공장 맥락.
|
||||
- token: Construction
|
||||
reason: mdx_df=0.
|
||||
- token: CAE
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 모델링
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: ArchiCAD
|
||||
reason: frame_df=1, mdx_df=0. 제품명.
|
||||
- token: CIM
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
|
||||
# Frame 31 / 1171281212 — 범용 vs 전용 S/W / 자동화 S/W 제작 곤란 / 건축물
|
||||
# Frame 30 과 차별: Frame 30 은 Smart Factory 비교, Frame 31 은 자동화 S/W 곤란 + 건축물
|
||||
1171281212:
|
||||
anchor_sets:
|
||||
- id: general_vs_specialized_sw
|
||||
terms: [S/W, 범용, 전용, 제품]
|
||||
- id: factory_automation
|
||||
terms: [Smart, 부품, 반복, 구성]
|
||||
- id: construction_structure
|
||||
terms: [건축물, 구조체, 건물, 간섭]
|
||||
- id: modeling_infra_3d
|
||||
terms: [Modeling, Infra, 3D, 모델]
|
||||
review_needed:
|
||||
- token: modeling_infra_3d (set)
|
||||
reason: Modeling/Infra 는 Frame 31 고유 맥락이나 3D/모델 은 Frame 18/22/24 와 중복. 유지하되 MDX 매칭에서 겹침 주의.
|
||||
- token: 제품
|
||||
reason: mdx_df=0.
|
||||
- token: 범용
|
||||
reason: mdx_df=0.
|
||||
- token: 전용
|
||||
reason: mdx_df=1 (약함).
|
||||
- token: Smart
|
||||
reason: mdx_df=0.
|
||||
- token: 부품
|
||||
reason: mdx_df=0.
|
||||
- token: 구성
|
||||
reason: mdx_df=0.
|
||||
- token: 건축물
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 구조체
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 건물
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 간섭
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Infra
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
|
||||
# Frame 32 / 1171281213 — DfMA (Design for Manufacture and Assembly) / 공장 생산
|
||||
1171281213:
|
||||
anchor_sets:
|
||||
- id: dfma_concept
|
||||
terms: [DfMA, Design, Assembly]
|
||||
- id: dfma_benefits
|
||||
terms: [편익, Convenient, Fast, Easy]
|
||||
- id: factory_field_production
|
||||
terms: [공장, 현장, 생산, Solution]
|
||||
- id: dfma_dx_integration
|
||||
terms: [DfMA, DX, S/W, H/W]
|
||||
review_needed:
|
||||
- token: DfMA
|
||||
reason: frame_df=1, mdx_df=0. Frame 32 핵심 약어.
|
||||
- token: Design
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Assembly
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 편익
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Convenient
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Fast
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: Easy
|
||||
reason: frame_df=1, mdx_df=0.
|
||||
- token: 공장
|
||||
reason: mdx_df=0.
|
||||
|
||||
# ============================================================
|
||||
# TARGET (Frame 13 / 14 / 18 / 29) — codex 확정
|
||||
# ============================================================
|
||||
|
||||
# Frame 13 / 1171281190 — three_parallel_requirements
|
||||
1171281190:
|
||||
anchor_sets:
|
||||
- id: three_pillars
|
||||
terms: [기술, 사람, 자연]
|
||||
- id: prerequisites
|
||||
terms: [필수조건, 역량, 여건]
|
||||
notes: |
|
||||
dx_capability [S/W, H/W, 기술, 개발] 은 codex 판단으로 weak 분류, 제외.
|
||||
사유: S/W/H/W/기술/개발 은 너무 넓은 도메인어라 Frame 13 의 핵심 선택 신호가 아님.
|
||||
|
||||
# Frame 14 / 1171281191 — three_persona_benefits
|
||||
# codex 조정: 주체(persona) 와 효과(outcomes) 분리
|
||||
# 관계자: review_needed 로 이동 (협업 효과가 아닌 주체/대상 성격)
|
||||
1171281191:
|
||||
anchor_sets:
|
||||
- id: construction_personas
|
||||
terms: [발주, 시공, 설계자]
|
||||
- id: collaboration_outcomes
|
||||
terms: [의사소통, 소통, 원활]
|
||||
- id: quality_risk_outcomes
|
||||
terms: [오류, 예방, 품질, 향상, 최소]
|
||||
- id: management_outcomes
|
||||
terms: [관리, 효율, 실무자]
|
||||
review_needed:
|
||||
- token: 관계자
|
||||
reason: frame 에는 있으나 mdx_df=0. collaboration 맥락이 아닌 주체/대상 성격이라 anchor 로 흐림.
|
||||
|
||||
# Frame 18 / 1171281195 — bim_dx_comparison_table
|
||||
1171281195:
|
||||
anchor_sets:
|
||||
- id: bim_dx
|
||||
terms: [BIM, DX]
|
||||
- id: dimensional_comparison
|
||||
terms: [2D, 3D, 모델]
|
||||
- id: software_tools
|
||||
terms: [S/W, Civil, Revit, Navisworks, Autocad]
|
||||
notes: |
|
||||
weak 세트로 검토되었으나 제외됨: [상용, 소극, 구체]
|
||||
— 너무 일반적/추상적 의미라 디자인 선택 신호 약함.
|
||||
|
||||
# Frame 29 / 1171281210 — process_product_two_way
|
||||
# 주의: Product / 결과혁신 은 Frame 29 실제 텍스트에 없음 → 미포함
|
||||
1171281210:
|
||||
anchor_sets:
|
||||
- id: process_product
|
||||
terms: [과정혁신, Products]
|
||||
- id: digital_model_shift
|
||||
terms: [2D, 3D, 모델, 지형도]
|
||||
- id: software_solution
|
||||
terms: [S/W, H/W, BIM]
|
||||
review_needed:
|
||||
- token: Products
|
||||
reason: frame 에는 있으나 mdx_df=0. MDX 매칭 신호로 약함. Frame 텍스트 축 표현 목적으로 유지.
|
||||
- token: 지형도
|
||||
reason: frame 에는 있으나 mdx_df=0. Figma/BEPS 전용 용어.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
meta:
|
||||
phase: 23b
|
||||
purpose: 검수용 도메인 용어 카탈로그 (synonyms.yaml/hierarchy.yaml 승격 전 단계)
|
||||
corpus_breakdown:
|
||||
BEPS: 1 (본문 source)
|
||||
Figma_frames: 33개 프레임 → 79개 파일 (texts/analysis/flat)
|
||||
MDX: 3개
|
||||
total_sources: 82
|
||||
total_terms_2plus_after_noise_filter: 1527
|
||||
note: synonyms.yaml은 현재 6개 canonical로 동결. 이 파일은 승격 후보만 제공.
|
||||
top_terms_by_frequency:
|
||||
- term: 정보
|
||||
count: 284
|
||||
sources: 25
|
||||
- term: BIM
|
||||
count: 239
|
||||
sources: 28
|
||||
- term: 건설
|
||||
count: 238
|
||||
sources: 30
|
||||
- term: 기반
|
||||
count: 231
|
||||
sources: 33
|
||||
- term: 시공
|
||||
count: 218
|
||||
sources: 30
|
||||
- term: 모델
|
||||
count: 217
|
||||
sources: 23
|
||||
- term: 관리
|
||||
count: 210
|
||||
sources: 22
|
||||
- term: 기술
|
||||
count: 203
|
||||
sources: 34
|
||||
- term: 설계
|
||||
count: 196
|
||||
sources: 27
|
||||
- term: 디지털
|
||||
count: 150
|
||||
sources: 30
|
||||
- term: 개발
|
||||
count: 143
|
||||
sources: 24
|
||||
- term: 산업
|
||||
count: 135
|
||||
sources: 25
|
||||
- term: 공사
|
||||
count: 113
|
||||
sources: 8
|
||||
- term: 계획
|
||||
count: 110
|
||||
sources: 14
|
||||
- term: 업무
|
||||
count: 102
|
||||
sources: 21
|
||||
- term: 현장
|
||||
count: 96
|
||||
sources: 13
|
||||
- term: 전환
|
||||
count: 95
|
||||
sources: 21
|
||||
- term: 효율
|
||||
count: 88
|
||||
sources: 15
|
||||
- term: 중심
|
||||
count: 88
|
||||
sources: 16
|
||||
- term: 품질
|
||||
count: 86
|
||||
sources: 19
|
||||
- term: 개념
|
||||
count: 77
|
||||
sources: 21
|
||||
- term: 단계
|
||||
count: 74
|
||||
sources: 17
|
||||
- term: 안전
|
||||
count: 72
|
||||
sources: 13
|
||||
- term: 현황
|
||||
count: 72
|
||||
sources: 11
|
||||
- term: 작업
|
||||
count: 72
|
||||
sources: 8
|
||||
- term: 향상
|
||||
count: 72
|
||||
sources: 18
|
||||
- term: 시스템
|
||||
count: 71
|
||||
sources: 12
|
||||
- term: 생산
|
||||
count: 70
|
||||
sources: 18
|
||||
- term: 작성
|
||||
count: 67
|
||||
sources: 15
|
||||
- term: 목표
|
||||
count: 67
|
||||
sources: 19
|
||||
- term: 유지
|
||||
count: 67
|
||||
sources: 20
|
||||
- term: 지원
|
||||
count: 64
|
||||
sources: 7
|
||||
- term: 공정
|
||||
count: 64
|
||||
sources: 9
|
||||
- term: tagged
|
||||
count: 64
|
||||
sources: 32
|
||||
- term: 이해
|
||||
count: 63
|
||||
sources: 19
|
||||
- term: 혁신
|
||||
count: 63
|
||||
sources: 16
|
||||
- term: 데이터
|
||||
count: 62
|
||||
sources: 9
|
||||
- term: Bold
|
||||
count: 62
|
||||
sources: 10
|
||||
- term: 확인
|
||||
count: 61
|
||||
sources: 7
|
||||
- term: Solution
|
||||
count: 59
|
||||
sources: 18
|
||||
- term: 위치
|
||||
count: 58
|
||||
sources: 11
|
||||
- term: 토목
|
||||
count: 58
|
||||
sources: 19
|
||||
- term: 사업
|
||||
count: 57
|
||||
sources: 9
|
||||
- term: 전문
|
||||
count: 56
|
||||
sources: 14
|
||||
- term: 비용
|
||||
count: 56
|
||||
sources: 12
|
||||
- term: 협업
|
||||
count: 55
|
||||
sources: 13
|
||||
- term: 시각
|
||||
count: 54
|
||||
sources: 14
|
||||
- term: 운영
|
||||
count: 54
|
||||
sources: 12
|
||||
- term: 기능
|
||||
count: 54
|
||||
sources: 10
|
||||
- term: 발전
|
||||
count: 53
|
||||
sources: 10
|
||||
safe_normalization:
|
||||
promote:
|
||||
- canonical: 의사소통
|
||||
variants:
|
||||
- 의사 소통
|
||||
- 의사소통
|
||||
reason: 도메인 일반어 공백 변형
|
||||
- canonical: 시행착오
|
||||
variants:
|
||||
- 시행 착오
|
||||
- 시행착오
|
||||
reason: 도메인 일반어 공백 변형
|
||||
- canonical: BIGRoom
|
||||
variants:
|
||||
- BIG Room
|
||||
- BIGRoom
|
||||
reason: 도메인 일반어 공백 변형
|
||||
hold:
|
||||
- canonical: WatchBIM
|
||||
variants:
|
||||
- Watch BIM
|
||||
- WatchBIM
|
||||
reason: 고유 제품명/브랜드 — 치환 위험
|
||||
- canonical: TwinHighway
|
||||
variants:
|
||||
- Twin Highway
|
||||
- TwinHighway
|
||||
reason: 고유 제품명/브랜드 — 치환 위험
|
||||
discard:
|
||||
- canonical: Engn.S
|
||||
variants:
|
||||
- Engn. S
|
||||
- Engn.S
|
||||
reason: 토큰에 마침표 포함 — Kiwi 분해 노이즈
|
||||
- canonical: rdParty
|
||||
variants:
|
||||
- rd Party
|
||||
- rdParty
|
||||
reason: 서수 접미사 조각 (rdParty 등)
|
||||
abbrev_fullname_candidates:
|
||||
- abbr: BIM
|
||||
full: Building Information Modeling
|
||||
count: 4
|
||||
- abbr: DX
|
||||
full: 디지털전환
|
||||
count: 4
|
||||
- abbr: DX
|
||||
full: 디지털 전환
|
||||
count: 4
|
||||
- abbr: GIS
|
||||
full: Geographic Information System
|
||||
count: 2
|
||||
- abbr: OSC
|
||||
full: Off Site Construction
|
||||
count: 2
|
||||
- abbr: BIM
|
||||
full: 건설정보모델링
|
||||
count: 2
|
||||
- abbr: CAE
|
||||
full: Computer Aided Engineering
|
||||
count: 2
|
||||
- abbr: DX
|
||||
full: Digital Transformation
|
||||
count: 2
|
||||
- abbr: BIM
|
||||
full: 도로사업 중심
|
||||
count: 1
|
||||
- abbr: TA
|
||||
full: Technical Assistance
|
||||
count: 1
|
||||
- abbr: GIS
|
||||
full: 지리정보시스템
|
||||
count: 1
|
||||
- abbr: API
|
||||
full: Application Program Interface
|
||||
count: 1
|
||||
- abbr: BCMF
|
||||
full: BIM based Construction Management Framework
|
||||
count: 1
|
||||
- abbr: PMC
|
||||
full: Project Management Consultant
|
||||
count: 1
|
||||
- abbr: AIA
|
||||
full: 미 건축가 협회
|
||||
count: 1
|
||||
- abbr: MA
|
||||
full: Design for Manufacture and Assembly
|
||||
count: 1
|
||||
- abbr: DX
|
||||
full: 치 산업으로의 전환 건설산업의 디지털전환
|
||||
count: 1
|
||||
- abbr: IDC
|
||||
full: 독립 설계 검토
|
||||
count: 1
|
||||
- abbr: CCTV
|
||||
full: 현장 관제
|
||||
count: 1
|
||||
- abbr: KAIA
|
||||
full: 랫폼 기반 건설공간정보 통합운영 기술개발
|
||||
count: 1
|
||||
- abbr: KAIA
|
||||
full: 가상건설시스템개발연구단
|
||||
count: 1
|
||||
- abbr: KAIA
|
||||
full: 디지털 국토정보기술 개발사업
|
||||
count: 1
|
||||
- abbr: CM
|
||||
full: 동시에 실제 공사단계에서 공사관리
|
||||
count: 1
|
||||
- abbr: HOW
|
||||
full: 어떻게 연계
|
||||
count: 1
|
||||
- abbr: WHEN
|
||||
full: 언제 사용
|
||||
count: 1
|
||||
- abbr: DX
|
||||
full: '타이틀
|
||||
|
||||
|
||||
디지털 전환'
|
||||
count: 1
|
||||
- abbr: DX
|
||||
full: 건설산업의 디지털전환
|
||||
count: 1
|
||||
- abbr: DX
|
||||
full: '내용
|
||||
|
||||
디지털 전환'
|
||||
count: 1
|
||||
- abbr: DX
|
||||
full: 은 건설산업의 디지털전환
|
||||
count: 1
|
||||
- abbr: UX
|
||||
full: 행착오를 포함한 수행 경험과 사용자 경험
|
||||
count: 1
|
||||
- abbr: GIS
|
||||
full: 지반 등 위치정보
|
||||
count: 1
|
||||
ko_en_paren_pairs_evidence_only:
|
||||
- ko: 실측 기록
|
||||
en: flat
|
||||
count: 14
|
||||
- ko: 토목공학 지식
|
||||
en: Domain knowledge
|
||||
count: 3
|
||||
- ko: 쉬운 이해와 명확한 의사소통
|
||||
en: Communication
|
||||
count: 2
|
||||
- ko: 산체계의 디지털화 중심으로 발전
|
||||
en: Smart Factory
|
||||
count: 2
|
||||
- ko: 내외장재 제품
|
||||
en: Object
|
||||
count: 2
|
||||
- ko: 기반 정보물
|
||||
en: Information Data and Products
|
||||
count: 2
|
||||
- ko: 을 활용한 과정
|
||||
en: Process
|
||||
count: 2
|
||||
- ko: 무슨 정보
|
||||
en: What
|
||||
count: 2
|
||||
- ko: 내용 속성
|
||||
en: Contents
|
||||
count: 1
|
||||
- ko: 검측 계획 기반 검측 체크리스트
|
||||
en: Checklist
|
||||
count: 1
|
||||
- ko: 속성정보
|
||||
en: User Dependent
|
||||
count: 1
|
||||
- ko: 속성정보
|
||||
en: Software Dependent
|
||||
count: 1
|
||||
- ko: 시스템 구성도
|
||||
en: System Process
|
||||
count: 1
|
||||
- ko: 제조업
|
||||
en: Digital Twin
|
||||
count: 1
|
||||
- ko: 를 위치 정보
|
||||
en: Spatial Information
|
||||
count: 1
|
||||
- ko: 영을 위한 디지털 모델링은 제품
|
||||
en: Object
|
||||
count: 1
|
||||
- ko: 건축업
|
||||
en: BIM
|
||||
count: 1
|
||||
- ko: 독립 설계 검토
|
||||
en: IDC
|
||||
count: 1
|
||||
- ko: 와 연계하여 개별 객체
|
||||
en: Object
|
||||
count: 1
|
||||
- ko: 시각적 확인 및 검증
|
||||
en: Vision Inspection
|
||||
count: 1
|
||||
- ko: "과 \L 내용 속성"
|
||||
en: Contents
|
||||
count: 1
|
||||
- ko: 표출 속성
|
||||
en: Attribute
|
||||
count: 1
|
||||
- ko: 과 정의 속성
|
||||
en: Property
|
||||
count: 1
|
||||
- ko: 및 업무수행 방식
|
||||
en: Process
|
||||
count: 1
|
||||
- ko: 시공 모델
|
||||
en: Watch BIM
|
||||
count: 1
|
||||
- ko: 후 공사모델
|
||||
en: Watch BIM
|
||||
count: 1
|
||||
- ko: 현장 관제
|
||||
en: CCTV
|
||||
count: 1
|
||||
- ko: 운영 모델
|
||||
en: Twin Highway
|
||||
count: 1
|
||||
- ko: 지관리 정보모델을 통한 운영모델
|
||||
en: Twin Highway
|
||||
count: 1
|
||||
- ko: 차적 목표 다양한 제품
|
||||
en: Library
|
||||
count: 1
|
||||
- ko: 다양한 제품
|
||||
en: Product
|
||||
count: 1
|
||||
- ko: 회의 시스템
|
||||
en: Video conference system
|
||||
count: 1
|
||||
- ko: 도형의 생김새
|
||||
en: shape
|
||||
count: 1
|
||||
- ko: 건설공간정보 통합운영 기술개발
|
||||
en: KAIA
|
||||
count: 1
|
||||
- ko: 가상건설시스템개발연구단
|
||||
en: KAIA
|
||||
count: 1
|
||||
- ko: 보조 모니터
|
||||
en: Monitor
|
||||
count: 1
|
||||
- ko: 디지털 국토정보기술 개발사업
|
||||
en: KAIA
|
||||
count: 1
|
||||
- ko: 리하도록 명시 형상정보는 시각화
|
||||
en: Visualization
|
||||
count: 1
|
||||
- ko: 구성 원
|
||||
en: BIG Room
|
||||
count: 1
|
||||
- ko: 어떻게 연계
|
||||
en: HOW
|
||||
count: 1
|
||||
needs_human_review:
|
||||
- 발주자 vs 발주처 (역할 vs 기관)
|
||||
- 시공자 vs 시공사 (역할 vs 회사)
|
||||
- 설계자 vs 설계사 (역할 vs 회사)
|
||||
- Engineering S/W vs Engn. S/W (약어 — 동의어 가능)
|
||||
- Engineering S/W vs Solution S/W (상하위 or 분류)
|
||||
- Engineering S/W vs Application S/W (상하위 or 분류)
|
||||
@@ -0,0 +1,62 @@
|
||||
# Ground Truth for MDX ↔ Figma frame matching
|
||||
# 7 섹션 (MDX01~03의 ## 중목차)
|
||||
# primary: 가장 강한 매칭 1개 (없으면 null)
|
||||
# secondary: 보조 매칭 (있으면 리스트, 없으면 빈 리스트)
|
||||
# confidence: 사람이 판단한 확신도
|
||||
|
||||
sections:
|
||||
- id: MDX01-intro
|
||||
mdx_file: "01.mdx"
|
||||
section_title: "용어의 혼용 (intro)"
|
||||
primary: null
|
||||
secondary: []
|
||||
confidence: high
|
||||
note: "개념 정의 intro, 직접 매칭 블록 없음"
|
||||
|
||||
- id: MDX01-1
|
||||
mdx_file: "01.mdx"
|
||||
section_title: "1. 용어 정의"
|
||||
primary: null
|
||||
secondary: [1171281189]
|
||||
confidence: medium
|
||||
note: "정의 콘텐츠 → 약한 매칭만 가능"
|
||||
|
||||
- id: MDX01-2
|
||||
mdx_file: "01.mdx"
|
||||
section_title: "2. 용어간 상호관계"
|
||||
primary: 1171281195
|
||||
secondary: []
|
||||
confidence: very_high
|
||||
note: "BIM vs DX 12행 비교표가 정확히 일치"
|
||||
|
||||
- id: MDX02-1
|
||||
mdx_file: "02.mdx"
|
||||
section_title: "1. DX의 궁극적 목표"
|
||||
primary: 1171281189
|
||||
secondary: [1171281191]
|
||||
confidence: high
|
||||
note: "안전/품질/생산성 3원 교차"
|
||||
|
||||
- id: MDX02-2
|
||||
mdx_file: "02.mdx"
|
||||
section_title: "2. DX 기반 Process 혁신에 따른 주체별 기대효과"
|
||||
primary: 1171281191
|
||||
secondary: [1171281189]
|
||||
confidence: very_high
|
||||
note: "발주자/시공자/설계자 3열 카드"
|
||||
|
||||
- id: MDX03-1
|
||||
mdx_file: "03.mdx"
|
||||
section_title: "1. DX 시행을 위한 필수 요건"
|
||||
primary: 1171281190
|
||||
secondary: [1171281192]
|
||||
confidence: very_high
|
||||
note: "기술/사람/자연 3열 직접 일치"
|
||||
|
||||
- id: MDX03-2
|
||||
mdx_file: "03.mdx"
|
||||
section_title: "2. Process의 혁신과 Product의 변화"
|
||||
primary: 1171281210
|
||||
secondary: []
|
||||
confidence: very_high
|
||||
note: "과정/결과 혁신 2열 비교"
|
||||
@@ -0,0 +1,86 @@
|
||||
meta:
|
||||
purpose: Holdout 3 섹션 × V1~V4 1위의 합리성 라벨링 (사용자 제공)
|
||||
scale:
|
||||
rational: 합리적 (○)
|
||||
ambiguous: 애매 (△) — 추가 score (0~100) 로 세부 강도 표현
|
||||
irrational: 비합리적 (×)
|
||||
scoring_rule:
|
||||
rational: 100
|
||||
ambiguous: score 필드 사용 (기본 50, 사용자 지정 시 그 값)
|
||||
irrational: 0
|
||||
caution:
|
||||
- V4 라벨(use_as_is/reject)에 끌려가지 않은 사용자 판단
|
||||
- 현 단계는 1위만 평가 — 대안 탐색(2~3위)은 별도 단계
|
||||
labeled_by: user
|
||||
labeled_at: '2026-04-24'
|
||||
|
||||
sections:
|
||||
01-1:
|
||||
mdx_title: 1. 용어 정의
|
||||
picks:
|
||||
V1:
|
||||
frame_number: 1
|
||||
rating: irrational
|
||||
score: 0
|
||||
comment: null
|
||||
V2:
|
||||
frame_number: 27
|
||||
rating: irrational
|
||||
score: 0
|
||||
comment: null
|
||||
V3:
|
||||
frame_number: 20
|
||||
rating: ambiguous
|
||||
score: 30
|
||||
comment: 세모 — 완전 합리도 아니고 비합리도 아님
|
||||
V4:
|
||||
frame_number: 11
|
||||
rating: ambiguous
|
||||
score: 35
|
||||
comment: 세모 — V3보다 약간 더 나음
|
||||
02-1:
|
||||
mdx_title: 1. DX의 궁극적 목표
|
||||
picks:
|
||||
V1:
|
||||
frame_number: 12
|
||||
rating: rational
|
||||
score: 100
|
||||
comment: null
|
||||
V2:
|
||||
frame_number: 20
|
||||
rating: irrational
|
||||
score: 0
|
||||
comment: null
|
||||
V3:
|
||||
frame_number: 20
|
||||
rating: irrational
|
||||
score: 0
|
||||
comment: null
|
||||
V4:
|
||||
frame_number: 12
|
||||
rating: rational
|
||||
score: 100
|
||||
comment: null
|
||||
02-2.1:
|
||||
mdx_title: 2.1 업무 수행 과정(Process)의 변화
|
||||
picks:
|
||||
V1:
|
||||
frame_number: 11
|
||||
rating: irrational
|
||||
score: 0
|
||||
comment: null
|
||||
V2:
|
||||
frame_number: 29
|
||||
rating: rational
|
||||
score: 100
|
||||
comment: null
|
||||
V3:
|
||||
frame_number: 3
|
||||
rating: irrational
|
||||
score: 0
|
||||
comment: null
|
||||
V4:
|
||||
frame_number: 29
|
||||
rating: rational
|
||||
score: 100
|
||||
comment: null
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,869 @@
|
||||
meta:
|
||||
pipeline_step: 2.5
|
||||
description: Special form inventory. synonym/canonical 적용 전 실제 표기형 관찰. count 는 occurrences + nodes 둘 다. examples 는 source id 포함.
|
||||
exact_targets:
|
||||
S/W:
|
||||
count:
|
||||
occurrences:
|
||||
total: 287
|
||||
beps: 197
|
||||
frames: 84
|
||||
mdx: 6
|
||||
nodes:
|
||||
total: 230
|
||||
beps: 147
|
||||
frames: 77
|
||||
mdx: 6
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: 다양한 기술적도구(S/W,H/W)와 Process의 효과적인 통합
|
||||
- source: beps:1171281171
|
||||
text: 다양한 형태의 Solution S/W 개발 역량 Programmer
|
||||
- source: beps:1171281171
|
||||
text: Engn. S/W의 구성 과 특징
|
||||
- source: beps:1171281171
|
||||
text: Engineering S/W 필요성 증대
|
||||
- source: beps:1171281171
|
||||
text: Application S/W 의 구분
|
||||
- source: frame:1171281172
|
||||
text: 기존 S/W 한계 인식
|
||||
- source: frame:1171281172
|
||||
text: S/W 개발목표 설정
|
||||
- source: frame:1171281173
|
||||
text: Engineering S/W 개발
|
||||
- source: frame:1171281173
|
||||
text: 코딩 기반 S/W
|
||||
- source: frame:1171281173
|
||||
text: H/W 성능개선, S/W 고급화
|
||||
H/W:
|
||||
count:
|
||||
occurrences:
|
||||
total: 28
|
||||
beps: 20
|
||||
frames: 7
|
||||
mdx: 1
|
||||
nodes:
|
||||
total: 26
|
||||
beps: 18
|
||||
frames: 7
|
||||
mdx: 1
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: 다양한 기술적도구(S/W,H/W)와 Process의 효과적인 통합
|
||||
- source: beps:1171281171
|
||||
text: H/W 적 요소
|
||||
- source: beps:1171281171
|
||||
text: H/W
|
||||
- source: beps:1171281171
|
||||
text: H/W System
|
||||
- source: beps:1171281171
|
||||
text: 목적 시설물에 대한 설계, 시공, 유지관리 정보를 고객이 쉽고 편리하게 사용하고, 편익이 발생하도록 제공하는 다양한 형태의 정보물과 이를 구현할 수 있는 S/W 및 H/W와 그 기술을 포함하는 서비스
|
||||
- source: frame:1171281173
|
||||
text: H/W 성능개선, S/W 고급화
|
||||
- source: frame:1171281190
|
||||
text: 다양한 기술적도구(S/W,H/W)와
|
||||
- source: frame:1171281194
|
||||
text: H/W 미비
|
||||
- source: frame:1171281203
|
||||
text: 주어진 상황과 목적에 대한 해결책 고객(의뢰인)의 요구를 충족시키면서 사용자가 편리하게 원하는 작업을 할 수 있도록 해주는 S/W 및 H/W를 제공
|
||||
- source: frame:1171281203
|
||||
text: 특정한 목적을 수행하는 H/W, S/W, 기술 고객요구로 커스터마이징 가능(맞춤형) 기능 또는 모듈을 추가하여 시스템 확장 기능 예) ERP 솔루션, CRM 솔루션
|
||||
2D:
|
||||
count:
|
||||
occurrences:
|
||||
total: 14
|
||||
beps: 8
|
||||
frames: 3
|
||||
mdx: 3
|
||||
nodes:
|
||||
total: 14
|
||||
beps: 8
|
||||
frames: 3
|
||||
mdx: 3
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: 평면적 2D 설계방식
|
||||
- source: beps:1171281171
|
||||
text: 개념, 도서, 행정 절차 중심 2D 도면, 전문가, 규정 업무 구분(단절), 책임
|
||||
- source: beps:1171281171
|
||||
text: 기존 2D 설계방식 유지
|
||||
- source: beps:1171281171
|
||||
text: 기존 2D 설계의 결과가 옳다는 전제와 3D 설계를 수행/검토해본 경험이 없어, 전환설계의 개념으로 수행하므로써 비용과 시간이 추가로 소요, 높은 수준의 품질확보 불가
|
||||
- source: beps:1171281171
|
||||
text: · 효율성 증가 - 수작업이던 단순 및 반복적인 업무를 S/W 활용으로 효율성 증가 · S/W 한계 - 많은 S/W가 2D기반 추정설계에 의존 기존방식을 답습하여 성과물만 집중하고 있으며, 지속적인 발전 노력이 부족함 · 엔지니어의 역할 - 기존 방식의 문제점 인식보다는 S/W 사용능력 향상만 집중 · 정부의 대응 - 정부는 전면 BIM
|
||||
설계를 도입했으나, 기존방식에 머물면 엔지니어들은 효율성에만 집중하고, 새로운 S/W 습득에 그칠 것 · 디지털 전환 - 업무 본질에 대한 깊은 이해를 바탕으로 창의적 아이디어를 발현하여 고부가가치를 구현할 수 있도록 프로세스 혁신하는 것
|
||||
- source: frame:1171281210
|
||||
text: 2D 도면, 전문가, 규정
|
||||
- source: mdx:01-2
|
||||
text: 기존 2D 설계 방식 유지
|
||||
- source: mdx:02-2.2
|
||||
text: 2D → 3D 기반 인지·검토
|
||||
3D:
|
||||
count:
|
||||
occurrences:
|
||||
total: 83
|
||||
beps: 49
|
||||
frames: 23
|
||||
mdx: 11
|
||||
nodes:
|
||||
total: 76
|
||||
beps: 42
|
||||
frames: 23
|
||||
mdx: 11
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: 3D 형상 기반 다양한 형태의 정보물로 공사관계자간 소통 강화 및 작업내용의 정확한 전달
|
||||
- source: beps:1171281171
|
||||
text: 3D 지형모델에서 현황측량과 종·횡단면측량 값을 중첩하여 부정합 발생 등의 오류 확인
|
||||
- source: beps:1171281171
|
||||
text: ‧설계모델과 공사 중 3D 디지털측량으로 제작한 측량모델 중첩하여 시공완성면의 정합성 확인 ‧공사 중 제작된 3D 지형모델을 통해 실제 건설된 목적물의 규모, 규격 등을 측정하여 시공상태 점검
|
||||
- source: beps:1171281171
|
||||
text: ‧종합평면도의 3D Polyline(계획선)과 설계모델 중첩을 통한 노선계획의 정합성 확인 ‧3D 설계모델과 3D 용지경계 중첩을 통한 시설부지의 정합성 확인
|
||||
- source: beps:1171281171
|
||||
text: 상세 시설물의 3D Solid Model을 기반으로, 제품의 제작·조립 중심의 개념에서 출발 - 3차원 형상, 모형의 시각화 - Vector(도면, 모델) 생성에 중점
|
||||
- source: frame:1171281178
|
||||
text: 범위가 넓고 긴 형태의 인프라 시설 규모에 적합한 대형 사이니지를 적용하여 벡터기반 도면, 3D정보 모델을 높은 정밀도로 표출
|
||||
- source: frame:1171281179
|
||||
text: 3D 형상 산출속성과 연계하여 S/W를 통해 정의
|
||||
- source: frame:1171281182
|
||||
text: 최종 목적물의 전체 또는 부분 3D 형상정보 활용
|
||||
- source: frame:1171281189
|
||||
text: 3D 모델을 통한 오류
|
||||
- source: frame:1171281191
|
||||
text: 3D 모델 활용으로 오류 최소화 및
|
||||
DX:
|
||||
count:
|
||||
occurrences:
|
||||
total: 50
|
||||
beps: 25
|
||||
frames: 12
|
||||
mdx: 13
|
||||
nodes:
|
||||
total: 48
|
||||
beps: 23
|
||||
frames: 12
|
||||
mdx: 13
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: BIM과 DX의 이해
|
||||
- source: beps:1171281171
|
||||
text: BIM/DX
|
||||
- source: beps:1171281171
|
||||
text: 디지털 전환(DX)은 S/W가 필수다.
|
||||
- source: beps:1171281171
|
||||
text: BIM vs. DX
|
||||
- source: beps:1171281171
|
||||
text: · 공기ㆍ공사비 30% 절감, 안전사고 40% 감소 · DX를 통한 건설산업 국가 신 성장 동력으로 재도약 · 건설산업 생산성 향상과 고부가가치 산업 전환 필요
|
||||
- source: frame:1171281192
|
||||
text: 사용자 중심생각 & DX 수행경험
|
||||
- source: frame:1171281193
|
||||
text: BIM/DX 적용효과를 판단할 사례 부재
|
||||
- source: frame:1171281193
|
||||
text: DX 개념과 BIM 기술의 차이점의 이해부족으로 전략적 접근방식과 기술적 도구 사이의 혼란만 가중
|
||||
- source: frame:1171281193
|
||||
text: 인프라시설의 DX의 기본은 단순 모델이 아닌 위치기반 기반 3D 모델(BIM)을 활용한 과정(Process)의 혁신
|
||||
- source: mdx:01-2
|
||||
text: DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 프로세스를 혁신하는 상위개념
|
||||
BIM:
|
||||
count:
|
||||
occurrences:
|
||||
total: 187
|
||||
beps: 148
|
||||
frames: 28
|
||||
mdx: 11
|
||||
nodes:
|
||||
total: 169
|
||||
beps: 131
|
||||
frames: 27
|
||||
mdx: 11
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: BIM에 대한 인식
|
||||
- source: beps:1171281171
|
||||
text: BIM과 DX의 이해
|
||||
- source: beps:1171281171
|
||||
text: BIM 은... (Structure_ 木, 形)
|
||||
- source: beps:1171281171
|
||||
text: 부족한 BIM 운영/지원 여건
|
||||
- source: beps:1171281171
|
||||
text: 시공단계 BIM의 현장 실효성 부족 원인
|
||||
- source: frame:1171281178
|
||||
text: 건설 현장의 공사, 품질, 안전, 사업관리 등의 업무를 효율적으로 수행할 수 있는 BIM기반 현장 맞춤형 건설사업관리 솔루션
|
||||
- source: frame:1171281178
|
||||
text: 건설산업의 디지털전환을 위한 개념 및 BIM 수행을 위한 방법, 예시, 사례들을 모아놓은 사내용 시공 BIM 수행 메뉴얼 및 학습 시스템
|
||||
- source: frame:1171281182
|
||||
text: 시공단계 BIM 모델과 정보 활용 구분
|
||||
- source: frame:1171281182
|
||||
text: BIM 모델
|
||||
- source: frame:1171281189
|
||||
text: 건설산업의 목표 (BIM의 목적)
|
||||
variant_targets:
|
||||
AS-IS:
|
||||
count:
|
||||
occurrences:
|
||||
total: 2
|
||||
beps: 1
|
||||
frames: 0
|
||||
mdx: 1
|
||||
nodes:
|
||||
total: 2
|
||||
beps: 1
|
||||
frames: 0
|
||||
mdx: 1
|
||||
observed_forms:
|
||||
- form: As-is
|
||||
count: 2
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: As-is
|
||||
- source: mdx:03-2
|
||||
text: As-is [Analogue]
|
||||
TO-BE:
|
||||
count:
|
||||
occurrences:
|
||||
total: 2
|
||||
beps: 1
|
||||
frames: 0
|
||||
mdx: 1
|
||||
nodes:
|
||||
total: 2
|
||||
beps: 1
|
||||
frames: 0
|
||||
mdx: 1
|
||||
observed_forms:
|
||||
- form: To-Be
|
||||
count: 1
|
||||
- form: To-be
|
||||
count: 1
|
||||
examples:
|
||||
- source: beps:1171281171
|
||||
text: To-Be
|
||||
- source: mdx:03-2
|
||||
text: To-be [Digital]
|
||||
general_candidates_threshold_8:
|
||||
threshold: 8
|
||||
total_frames: 32
|
||||
count: 37
|
||||
note: 삭제 기준 아님. 검토 후보. BIM/건설/기술 등 도메인어 포함 가능.
|
||||
candidates:
|
||||
- token: 기술
|
||||
frame_count: 17
|
||||
mdx_count: 3
|
||||
- token: 기반
|
||||
frame_count: 16
|
||||
mdx_count: 3
|
||||
- token: 건설
|
||||
frame_count: 15
|
||||
mdx_count: 4
|
||||
- token: 설계
|
||||
frame_count: 15
|
||||
mdx_count: 4
|
||||
- token: 시공
|
||||
frame_count: 15
|
||||
mdx_count: 4
|
||||
- token: 개발
|
||||
frame_count: 14
|
||||
mdx_count: 2
|
||||
- token: 가능
|
||||
frame_count: 14
|
||||
mdx_count: 1
|
||||
- token: 디지털
|
||||
frame_count: 13
|
||||
mdx_count: 4
|
||||
- token: 업무
|
||||
frame_count: 13
|
||||
mdx_count: 3
|
||||
- token: 정보
|
||||
frame_count: 13
|
||||
mdx_count: 3
|
||||
- token: 산업
|
||||
frame_count: 13
|
||||
mdx_count: 1
|
||||
- token: 사용
|
||||
frame_count: 13
|
||||
mdx_count: 0
|
||||
- token: 관리
|
||||
frame_count: 12
|
||||
mdx_count: 3
|
||||
- token: 모델
|
||||
frame_count: 12
|
||||
mdx_count: 3
|
||||
- token: 필요
|
||||
frame_count: 12
|
||||
mdx_count: 3
|
||||
- token: BIM
|
||||
frame_count: 12
|
||||
mdx_count: 2
|
||||
- token: 전환
|
||||
frame_count: 11
|
||||
mdx_count: 1
|
||||
- token: 활용
|
||||
frame_count: 10
|
||||
mdx_count: 4
|
||||
- token: 단계
|
||||
frame_count: 10
|
||||
mdx_count: 3
|
||||
- token: 수행
|
||||
frame_count: 10
|
||||
mdx_count: 3
|
||||
- token: 이해
|
||||
frame_count: 10
|
||||
mdx_count: 3
|
||||
- token: 작성
|
||||
frame_count: 10
|
||||
mdx_count: 2
|
||||
- token: 지속
|
||||
frame_count: 10
|
||||
mdx_count: 2
|
||||
- token: 품질
|
||||
frame_count: 10
|
||||
mdx_count: 2
|
||||
- token: Process
|
||||
frame_count: 9
|
||||
mdx_count: 3
|
||||
- token: 방식
|
||||
frame_count: 9
|
||||
mdx_count: 3
|
||||
- token: 유지
|
||||
frame_count: 9
|
||||
mdx_count: 3
|
||||
- token: 개념
|
||||
frame_count: 9
|
||||
mdx_count: 2
|
||||
- token: 다양
|
||||
frame_count: 9
|
||||
mdx_count: 2
|
||||
- token: 생산
|
||||
frame_count: 9
|
||||
mdx_count: 2
|
||||
- token: 계획
|
||||
frame_count: 8
|
||||
mdx_count: 3
|
||||
- token: 혁신
|
||||
frame_count: 8
|
||||
mdx_count: 3
|
||||
- token: 연계
|
||||
frame_count: 8
|
||||
mdx_count: 2
|
||||
- token: 토목
|
||||
frame_count: 8
|
||||
mdx_count: 2
|
||||
- token: 효율
|
||||
frame_count: 8
|
||||
mdx_count: 2
|
||||
- token: Solution
|
||||
frame_count: 8
|
||||
mdx_count: 1
|
||||
- token: 기능
|
||||
frame_count: 8
|
||||
mdx_count: 0
|
||||
general_candidates_threshold_4:
|
||||
threshold: 4
|
||||
total_frames: 32
|
||||
count: 168
|
||||
note: 삭제 기준 아님. 검토 후보. BIM/건설/기술 등 도메인어 포함 가능.
|
||||
candidates:
|
||||
- token: 기술
|
||||
frame_count: 17
|
||||
mdx_count: 3
|
||||
- token: 기반
|
||||
frame_count: 16
|
||||
mdx_count: 3
|
||||
- token: 건설
|
||||
frame_count: 15
|
||||
mdx_count: 4
|
||||
- token: 설계
|
||||
frame_count: 15
|
||||
mdx_count: 4
|
||||
- token: 시공
|
||||
frame_count: 15
|
||||
mdx_count: 4
|
||||
- token: 개발
|
||||
frame_count: 14
|
||||
mdx_count: 2
|
||||
- token: 가능
|
||||
frame_count: 14
|
||||
mdx_count: 1
|
||||
- token: 디지털
|
||||
frame_count: 13
|
||||
mdx_count: 4
|
||||
- token: 업무
|
||||
frame_count: 13
|
||||
mdx_count: 3
|
||||
- token: 정보
|
||||
frame_count: 13
|
||||
mdx_count: 3
|
||||
- token: 산업
|
||||
frame_count: 13
|
||||
mdx_count: 1
|
||||
- token: 사용
|
||||
frame_count: 13
|
||||
mdx_count: 0
|
||||
- token: 관리
|
||||
frame_count: 12
|
||||
mdx_count: 3
|
||||
- token: 모델
|
||||
frame_count: 12
|
||||
mdx_count: 3
|
||||
- token: 필요
|
||||
frame_count: 12
|
||||
mdx_count: 3
|
||||
- token: BIM
|
||||
frame_count: 12
|
||||
mdx_count: 2
|
||||
- token: 전환
|
||||
frame_count: 11
|
||||
mdx_count: 1
|
||||
- token: 활용
|
||||
frame_count: 10
|
||||
mdx_count: 4
|
||||
- token: 단계
|
||||
frame_count: 10
|
||||
mdx_count: 3
|
||||
- token: 수행
|
||||
frame_count: 10
|
||||
mdx_count: 3
|
||||
- token: 이해
|
||||
frame_count: 10
|
||||
mdx_count: 3
|
||||
- token: 작성
|
||||
frame_count: 10
|
||||
mdx_count: 2
|
||||
- token: 지속
|
||||
frame_count: 10
|
||||
mdx_count: 2
|
||||
- token: 품질
|
||||
frame_count: 10
|
||||
mdx_count: 2
|
||||
- token: Process
|
||||
frame_count: 9
|
||||
mdx_count: 3
|
||||
- token: 방식
|
||||
frame_count: 9
|
||||
mdx_count: 3
|
||||
- token: 유지
|
||||
frame_count: 9
|
||||
mdx_count: 3
|
||||
- token: 개념
|
||||
frame_count: 9
|
||||
mdx_count: 2
|
||||
- token: 다양
|
||||
frame_count: 9
|
||||
mdx_count: 2
|
||||
- token: 생산
|
||||
frame_count: 9
|
||||
mdx_count: 2
|
||||
- token: 계획
|
||||
frame_count: 8
|
||||
mdx_count: 3
|
||||
- token: 혁신
|
||||
frame_count: 8
|
||||
mdx_count: 3
|
||||
- token: 연계
|
||||
frame_count: 8
|
||||
mdx_count: 2
|
||||
- token: 토목
|
||||
frame_count: 8
|
||||
mdx_count: 2
|
||||
- token: 효율
|
||||
frame_count: 8
|
||||
mdx_count: 2
|
||||
- token: Solution
|
||||
frame_count: 8
|
||||
mdx_count: 1
|
||||
- token: 기능
|
||||
frame_count: 8
|
||||
mdx_count: 0
|
||||
- token: 중심
|
||||
frame_count: 7
|
||||
mdx_count: 4
|
||||
- token: 프로세스
|
||||
frame_count: 7
|
||||
mdx_count: 4
|
||||
- token: 기존
|
||||
frame_count: 7
|
||||
mdx_count: 3
|
||||
- token: Digital
|
||||
frame_count: 7
|
||||
mdx_count: 2
|
||||
- token: 규모
|
||||
frame_count: 7
|
||||
mdx_count: 2
|
||||
- token: 비용
|
||||
frame_count: 7
|
||||
mdx_count: 2
|
||||
- token: 현장
|
||||
frame_count: 7
|
||||
mdx_count: 1
|
||||
- token: 목표
|
||||
frame_count: 7
|
||||
mdx_count: 0
|
||||
- token: 범용
|
||||
frame_count: 7
|
||||
mdx_count: 0
|
||||
- token: 사업
|
||||
frame_count: 7
|
||||
mdx_count: 0
|
||||
- token: 수준
|
||||
frame_count: 7
|
||||
mdx_count: 0
|
||||
- token: 한계
|
||||
frame_count: 7
|
||||
mdx_count: 0
|
||||
- token: 구분
|
||||
frame_count: 6
|
||||
mdx_count: 3
|
||||
- token: 오류
|
||||
frame_count: 6
|
||||
mdx_count: 3
|
||||
- token: 향상
|
||||
frame_count: 6
|
||||
mdx_count: 3
|
||||
- token: 가치
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 구축
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 변화
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 부가
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 사용자
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 성과물
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 시스템
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 운영
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: 확보
|
||||
frame_count: 6
|
||||
mdx_count: 2
|
||||
- token: Engn.
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 공정
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 국내
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 발생
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 요구
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 작업
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 전문
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 전용
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 지식
|
||||
frame_count: 6
|
||||
mdx_count: 1
|
||||
- token: 대형
|
||||
frame_count: 6
|
||||
mdx_count: 0
|
||||
- token: 목적
|
||||
frame_count: 6
|
||||
mdx_count: 0
|
||||
- token: 발전
|
||||
frame_count: 6
|
||||
mdx_count: 0
|
||||
- token: 시장
|
||||
frame_count: 6
|
||||
mdx_count: 0
|
||||
- token: 통합
|
||||
frame_count: 5
|
||||
mdx_count: 4
|
||||
- token: 도면
|
||||
frame_count: 5
|
||||
mdx_count: 3
|
||||
- token: 시각
|
||||
frame_count: 5
|
||||
mdx_count: 3
|
||||
- token: 개선
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 능력
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 문제
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 분야
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 위치
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 창출
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 추가
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 투자
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 협업
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: 형상
|
||||
frame_count: 5
|
||||
mdx_count: 2
|
||||
- token: Model
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 결과
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 반영
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 상용
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 성과
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 안전
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 인력
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 자체
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 제작
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 직관
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 판매
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 편리
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 확장
|
||||
frame_count: 5
|
||||
mdx_count: 1
|
||||
- token: 건축
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 기본
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 도구
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 엔지니어
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 엔지니어링
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 적용
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 지원
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 형태
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: 회사
|
||||
frame_count: 5
|
||||
mdx_count: 0
|
||||
- token: DX
|
||||
frame_count: 4
|
||||
mdx_count: 4
|
||||
- token: 데이터
|
||||
frame_count: 4
|
||||
mdx_count: 3
|
||||
- token: 포함
|
||||
frame_count: 4
|
||||
mdx_count: 3
|
||||
- token: GIS
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 과정
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 근본
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 의지
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 제공
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 창의
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 최소
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: 프로젝트
|
||||
frame_count: 4
|
||||
mdx_count: 2
|
||||
- token: Data
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: Revit
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 검증
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 경험
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 공사
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 공학
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 과거
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 교육
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 단순
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 목적물
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 민원
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 발주처
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 시간
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 접근
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 정책
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 증가
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 증진
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 집중
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 초기
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 축적
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 탈피
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 필수
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: 확인
|
||||
frame_count: 4
|
||||
mdx_count: 1
|
||||
- token: AutoDesk
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: Construction
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: Party
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: System
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: Twin
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: rd
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 개별
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 관련
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 구성
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 구조물
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 기업
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 맞춤
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 바탕
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 사항
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 생각
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 시설물
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 예속
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 이용
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 인프라
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 제품
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 주도
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 중요
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 특성
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 특정
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
- token: 현황
|
||||
frame_count: 4
|
||||
mdx_count: 0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
# Synonym Production (8개 canonical)
|
||||
# 엄격 기준: 실데이터 등장 + 치환 위험 낮음 + 사람 검수 통과
|
||||
|
||||
synonyms:
|
||||
|
||||
# 엄밀 동의어 (MDX03-1 핵심)
|
||||
필수조건: ["필수 요건", "필수요건"]
|
||||
|
||||
# 약어-풀네임 (MDX/Figma 전반에 빈번)
|
||||
DX: ["디지털 전환", "디지털전환"]
|
||||
|
||||
# 혁신 표현 (MDX03-2 핵심)
|
||||
과정혁신: ["과정의 혁신", "Process 혁신"]
|
||||
결과혁신: ["결과의 혁신", "Product의 변화"]
|
||||
|
||||
# 기술 용어 (MDX 전반에 띄어쓰기 혼용)
|
||||
3D모델: ["3D 모델", "3차원 모델"]
|
||||
2D도면: ["2D 도면"]
|
||||
|
||||
# Phase 23b 검수 통과 — 저위험 normalization (매칭 정답률 영향 미미 예상)
|
||||
의사소통: ["의사 소통"]
|
||||
시행착오: ["시행 착오"]
|
||||
|
||||
# 대소문자/형태 variants (Frame 29 transformation_story)
|
||||
# BEPS(1171281171) 의 list item 에 실제 등장한 형태만 등록
|
||||
AS-IS: ["As-is"]
|
||||
TO-BE: ["To-Be"]
|
||||
# 페르소나: 전체 corpus list item 어디에도 실제 없음 → synonym 등록 안 함
|
||||
# (Frame 14 anchor_set 의 '페르소나' term 도 동시 제거됨)
|
||||
|
||||
|
||||
# Step 4 pre-Kiwi substitution 전용 (신설, 위 synonyms 섹션과 분리)
|
||||
# - Step 4 파이프라인은 phrase_variants 만 사용
|
||||
# - canonical 자기 자신 재치환 금지 (build_substitutions 에서 필터)
|
||||
# - 긴 variant 부터 치환 (짧은 variant 가 긴 variant 내부를 먹지 않도록)
|
||||
# - DX canonical 유지 (디지털전환 canonical 은 두지 않음, DX variant 로 통합)
|
||||
# - AS-IS/TO-BE canonical 은 실제 표기형 (As-is / To-Be) 으로 변경
|
||||
# - 3D모델, 2D도면 은 Step 4 에서 제외 (3D/2D 일반 차원 토큰 보존 목적)
|
||||
phrase_variants:
|
||||
|
||||
DX:
|
||||
- 디지털 전환
|
||||
- 디지털전환
|
||||
- 디지털 트랜스포메이션
|
||||
|
||||
결과혁신:
|
||||
- 결과 혁신
|
||||
- 결과의 혁신
|
||||
- Product의 변화
|
||||
- Product 혁신
|
||||
|
||||
과정혁신:
|
||||
- 과정 혁신
|
||||
- 과정의 혁신
|
||||
- Process 혁신
|
||||
- 프로세스 혁신
|
||||
|
||||
필수조건:
|
||||
- 필수 요건
|
||||
- 필수요건
|
||||
|
||||
의사소통:
|
||||
- 의사 소통
|
||||
|
||||
시행착오:
|
||||
- 시행 착오
|
||||
|
||||
As-is:
|
||||
- As Is
|
||||
- As_is
|
||||
- as-is
|
||||
|
||||
To-Be:
|
||||
- To-be
|
||||
- To Be
|
||||
- To_be
|
||||
@@ -0,0 +1,20 @@
|
||||
# Synonym Production 최소본
|
||||
# 테스트 케이스에서 실제 영향 확인된 항목만 — 6개 canonical
|
||||
# 신규 항목은 synonyms_candidates.yaml에서 검수 후 이곳으로 승격
|
||||
# 엄격 기준: 실제 데이터에 등장 AND 매칭에 직접 영향
|
||||
|
||||
synonyms:
|
||||
|
||||
# 용어 통일 (MDX03-1 핵심 케이스)
|
||||
필수조건: ["필수 요건", "필수요건"]
|
||||
|
||||
# 약어-풀네임 (MDX/Figma 전반에 빈번)
|
||||
DX: ["디지털 전환", "디지털전환"]
|
||||
|
||||
# 혁신 표현 (MDX03-2 핵심 케이스)
|
||||
과정혁신: ["과정의 혁신", "Process 혁신"]
|
||||
결과혁신: ["결과의 혁신", "Product의 변화"]
|
||||
|
||||
# 기술 용어 (MDX 전반에 띄어쓰기 혼용)
|
||||
3D모델: ["3D 모델", "3차원 모델"]
|
||||
2D도면: ["2D 도면"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><title>가중치 검증 — Logistic Regression</title>
|
||||
<style>
|
||||
@page { size: A4 portrait; margin: 14mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", sans-serif;
|
||||
background: white; color: #1e293b; margin: 0; font-size: 10pt; line-height: 1.5;
|
||||
}
|
||||
@media screen {
|
||||
body { max-width: 210mm; margin: 10mm auto; box-shadow: 0 4px 24px rgba(0,0,0,0.12); padding: 14mm; background: #fff; }
|
||||
}
|
||||
h1 { font-size: 14pt; border-bottom: 2pt solid #2563eb; padding-bottom: 4pt; margin: 0 0 8pt 0; color: #0f172a; }
|
||||
h2 { font-size: 10.5pt; color: #1e293b; margin: 10pt 0 4pt; padding: 3pt 6pt; background: #e0e7ff; border-left: 3pt solid #0a6; border-radius: 2pt; }
|
||||
p { margin: 4pt 0; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 9.5pt; margin: 4pt 0; }
|
||||
th, td { border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; text-align: left; vertical-align: top; }
|
||||
th { background: #1e293b; color: white; font-weight: 700; font-size: 9pt; text-align: center; }
|
||||
td.num { text-align: right; font-family: ui-monospace, Menlo, monospace; }
|
||||
td.center { text-align: center; }
|
||||
code { background: #f1f5f9; padding: 0 3pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 9pt; color: #0f172a; }
|
||||
.formula-box { background: #eff6ff; border: 0.7pt solid #93c5fd; border-radius: 3pt; padding: 6pt 10pt; margin: 4pt 0; font-size: 9.5pt; color: #1e3a8a; }
|
||||
.ok { color: #065f46; font-weight: 700; }
|
||||
.bad { color: #991b1b; font-weight: 700; }
|
||||
.highlight { background: #fef3c7; padding: 1pt 4pt; border-radius: 2pt; font-weight: 700; color: #78350f; }
|
||||
.conclusion { background: #d1fae5; border-left: 3pt solid #059669; padding: 6pt 10pt; margin: 8pt 0; font-size: 9.5pt; color: #065f46; border-radius: 2pt; }
|
||||
ul { margin: 3pt 0; padding-left: 16pt; }
|
||||
li { margin: 2pt 0; }
|
||||
</style></head><body>
|
||||
|
||||
<h1>가중치 검증 — Logistic Regression 학습 결과</h1>
|
||||
|
||||
<h2>1. 질문</h2>
|
||||
<p>V1 매칭 공식의 가중치 <code>0.30 / 0.50 / 0.20</code> 은 왜 이 숫자인가? 데이터로 검증 가능한가?</p>
|
||||
|
||||
<div class="formula-box">
|
||||
matching_score = <b>0.30</b> × standalone_score + <b>0.50</b> × group_score + <b>0.20</b> × related_score
|
||||
</div>
|
||||
|
||||
<h2>2. 방법</h2>
|
||||
<ul>
|
||||
<li><b>데이터</b>: TARGET 4 섹션 × 32 프레임 = <b>128 샘플</b> (정답 4, 오답 124)</li>
|
||||
<li><b>Feature</b>: (standalone_score, group_score, related_score) — 각 [0, 1]</li>
|
||||
<li><b>Label</b>: 1 (정답 프레임) / 0 (오답)</li>
|
||||
<li><b>모델</b>: Logistic Regression (L2 정규화, scikit-learn)</li>
|
||||
<li><b>검증</b>: LOOCV (Leave-One-Out Cross-Validation, 섹션별 hold-out)</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 학습 결과 — 가중치</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>방식</th>
|
||||
<th>standalone (핵심)</th>
|
||||
<th>group (세트)</th>
|
||||
<th>related (연관)</th>
|
||||
<th>TARGET 정답</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>현재 (수동 설정)</b></td>
|
||||
<td class="num">0.300</td>
|
||||
<td class="num"><span class="highlight">0.500</span></td>
|
||||
<td class="num">0.200</td>
|
||||
<td class="center ok">4 / 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Logistic Regression (학습)</b></td>
|
||||
<td class="num"><span class="highlight">0.414</span></td>
|
||||
<td class="num">0.320</td>
|
||||
<td class="num">0.265</td>
|
||||
<td class="center ok">4 / 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OLS Linear (극단)</td>
|
||||
<td class="num">1.000</td>
|
||||
<td class="num">0.053</td>
|
||||
<td class="num">−0.053</td>
|
||||
<td class="center bad">3 / 4</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>4. LOOCV — 과적합 확인</h2>
|
||||
<p>각 TARGET 을 hold-out 하고 나머지 3개로 학습 후, hold-out 섹션에서 정답 판별.</p>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>hold-out</th>
|
||||
<th>standalone</th>
|
||||
<th>group</th>
|
||||
<th>related</th>
|
||||
<th>정답?</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="center">01-2</td><td class="num">0.402</td><td class="num">0.336</td><td class="num">0.262</td><td class="center ok">✓</td></tr>
|
||||
<tr><td class="center">02-2.2</td><td class="num">0.395</td><td class="num">0.336</td><td class="num">0.269</td><td class="center ok">✓</td></tr>
|
||||
<tr><td class="center">03-1</td><td class="num">0.413</td><td class="num">0.308</td><td class="num">0.279</td><td class="center ok">✓</td></tr>
|
||||
<tr><td class="center">03-2</td><td class="num">0.431</td><td class="num">0.308</td><td class="num">0.262</td><td class="center ok">✓</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="font-size:9pt;color:#475569">→ hold-out 별 가중치가 거의 동일 (<code>0.40±0.02 / 0.32±0.02 / 0.27±0.01</code>). 모든 hold-out 정답 맞춤.</p>
|
||||
|
||||
<h2>5. 해석</h2>
|
||||
|
||||
<ul>
|
||||
<li><b>학습은 standalone (핵심) 을 가장 높게 평가</b> (0.414) — 데이터 기준으론 "단독 고유 키워드가 가장 분별력 있음"</li>
|
||||
<li><b>현재 수동 가중치는 group (세트) 를 가장 높게 (0.500)</b> — 도메인 판단 기반 ("세트가 core 신호")</li>
|
||||
<li>두 가중치 모두 <b>TARGET 4/4 정답</b> — 실용적으로 동등</li>
|
||||
<li>LOOCV 4/4 + 가중치 변동 < 0.02 → <b>이 범위에서 robust</b></li>
|
||||
<li>OLS 극단값 (1.0 / 0.05 / −0.05) 은 TARGET 3/4 실패 → <b>극단 가중치는 틀림</b></li>
|
||||
</ul>
|
||||
|
||||
<h2>6. BM25 와 비교 (별첨)</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>방식</th>
|
||||
<th>원리</th>
|
||||
<th>TARGET 정답률</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>현재 (0.30/0.50/0.20)</b></td><td>비율 × 레이어 가중 합산</td><td class="center ok">4 / 4</td></tr>
|
||||
<tr><td><b>Logistic 학습 (0.41/0.32/0.27)</b></td><td>데이터 기반 회귀</td><td class="center ok">4 / 4</td></tr>
|
||||
<tr><td>IDF-weighted (세트 포함)</td><td>순수 정보량 가중</td><td class="center bad">3 / 4</td></tr>
|
||||
<tr><td>BM25 (개별 토큰)</td><td>표준 IR</td><td class="center bad">3 / 4</td></tr>
|
||||
<tr><td>BM25 (세트 포함 복합 토큰)</td><td>BM25 + phrase</td><td class="center bad">3 / 4</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="conclusion">
|
||||
<b>결론</b><br/>
|
||||
현재 가중치 <code>0.30 / 0.50 / 0.20</code> 은 <b>임의 숫자가 아님</b>.
|
||||
Logistic Regression 학습 결과 <code>0.41 / 0.32 / 0.27</code> 과 숫자는 다르지만 <b>TARGET 정답률 동일 (4/4)</b>,
|
||||
LOOCV 4/4 — 이 범위에서 <b>robust</b>.
|
||||
BM25 / IDF 같은 표준 IR 기법보다 오히려 <b>정답률 높음</b>.<br/>
|
||||
<br/>
|
||||
<b>회의 발언</b>: "현 가중치는 전문가 판단 기반이지만, Logistic Regression 으로 검증 시 TARGET 4/4 동등 성능.
|
||||
LOOCV 과적합 없음. BM25 등 표준 기법보다 실증적으로 우위."
|
||||
</div>
|
||||
|
||||
</body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><title>03. V1~V4 방법 설명</title>
|
||||
<style>
|
||||
@page { size: A4 portrait; margin: 12mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", sans-serif;
|
||||
background: white; color: #1e293b; margin: 0; font-size: 9.5pt; line-height: 1.4;
|
||||
}
|
||||
@media screen {
|
||||
body { max-width: 210mm; margin: 10mm auto; box-shadow: 0 4px 24px rgba(0,0,0,0.12); padding: 12mm; background: #fff; }
|
||||
}
|
||||
h1 { font-size: 13pt; border-bottom: 2pt solid #2563eb; padding-bottom: 3pt; margin: 0 0 4pt 0; color: #0f172a; }
|
||||
h2 { font-size: 10pt; color: #1e293b; margin: 6pt 0 3pt; padding: 2pt 5pt; background: #e0e7ff; border-left: 3pt solid #0a6; border-radius: 2pt; }
|
||||
.meta { color: #64748b; font-size: 8pt; font-style: italic; margin-bottom: 5pt; }
|
||||
.mdx-card { background: #f8fafc; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0 5pt; }
|
||||
.mdx-card .title { font-weight: 700; color: #0f172a; font-size: 9.5pt; margin-bottom: 2pt; }
|
||||
.popup-tag { display: inline-block; padding: 1pt 5pt; background: #eef2ff; color: #3730a3; border-radius: 8pt; font-size: 7.5pt; font-weight: 700; margin-left: 4pt; vertical-align: middle; }
|
||||
.mdx-card ul { margin: 2pt 0 0 0; padding-left: 12pt; font-size: 8pt; color: #475569; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 9pt; }
|
||||
th, td { border: 0.5pt solid #cbd5e1; padding: 3pt 5pt; text-align: left; vertical-align: top; }
|
||||
th { background: #1e293b; color: white; font-weight: 700; font-size: 8.5pt; text-align: center; }
|
||||
code { background: #f1f5f9; padding: 0 3pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; }
|
||||
.formula-box { background: #eff6ff; border: 0.7pt solid #93c5fd; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0; font-size: 9pt; color: #1e3a8a; }
|
||||
.label-use_as_is { background: #d1fae5; color: #065f46; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
.label-light_edit { background: #dbeafe; color: #1e40af; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
.label-restructure { background: #fef3c7; color: #92400e; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
.label-reject { background: #fee2e2; color: #991b1b; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
|
||||
/* DECK 3: 방법 설명 */
|
||||
.method-block { border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 8pt 10pt; margin: 5pt 0; background: #fafafa; }
|
||||
.method-block .method-name { font-size: 12pt; font-weight: 700; color: #2563eb; margin-bottom: 2pt; }
|
||||
.method-block .method-name .badge { display: inline-block; padding: 2pt 7pt; background: #2563eb; color: white; border-radius: 3pt; font-size: 9pt; margin-right: 6pt; }
|
||||
.method-block .method-tagline { font-size: 9pt; color: #475569; margin-bottom: 5pt; font-style: italic; }
|
||||
.method-block .method-formula { background: white; border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; margin: 3pt 0; }
|
||||
.method-block .method-detail { font-size: 9pt; line-height: 1.5; color: #1e293b; }
|
||||
.method-block .method-detail b { color: #0f172a; }
|
||||
.cascade-arrow { text-align: center; font-size: 12pt; color: #64748b; margin: 2pt 0; }
|
||||
</style></head><body><h1>03. V1~V4 매칭 방법 — 무엇이고 어떻게 적용되는가</h1><div class="meta">키워드 → 의미 → 구조 → 적용 가능성으로 신호를 누적해 캐스케이드 매칭.</div><div class="method-block"><div class="method-name"><span class="badge">V1</span>키워드 매칭 (Baseline)</div><div class="method-tagline">"이 단어들이 같이 나오나?" — 빠른 후보 필터</div><div class="method-formula">matching_score = 0.414 × 핵심(단독) + 0.320 × 세트(co-occurrence) + 0.265 × 연관</div><div class="method-detail">· 프레임 텍스트에서 <b>핵심 / 세트 / 연관</b> 3계층 키워드 추출<br/>· 가중치는 <b>Logistic Regression 학습</b> (TARGET 4 × 32, LOOCV 4/4)<br/>· 32 프레임 중 1위 = 후보 프레임. <b>TARGET 4/4 정답</b></div></div><div class="cascade-arrow">↓ V1 Top-K 후보를 다음 축으로 재정렬</div><div class="method-block"><div class="method-name"><span class="badge">V2</span>의미 매칭 (semantic rerank)</div><div class="method-tagline">"단어가 달라도 의미가 같은가?"</div><div class="method-formula">similarity = cosine(MDX summary embedding, frame.content embedding) · model = jhgan/ko-sroberta-multitask</div><div class="method-detail">· V1 Top-K 후보 안에서 의미 유사도로 재정렬<br/>· 키워드는 다르지만 같은 주제를 다룰 때 잡아냄</div></div><div class="cascade-arrow">↓</div><div class="method-block"><div class="method-name"><span class="badge">V3</span>구조 매칭 (structure rerank)</div><div class="method-tagline">"이 콘텐츠를 담을 수 있는 레이아웃인가?"</div><div class="method-formula">score = 0.40 × layout_compat + 0.35 × content_affinity + 0.25 × structure_intent</div><div class="method-detail">· content_affinity 12 enum (concept_definition / goal_axes / persona_benefit / ...)<br/>· structure_intent 9 enum (binary_compare / multi_parallel / state_transition / ...)<br/>· alternative_patterns — 대안 layout 관계 (cycle ↔ 3col-parallel 등)</div></div><div class="cascade-arrow">↓</div><div class="method-block"><div class="method-name"><span class="badge">V4</span>적용 가능성 판정 (template-fit-v1)</div><div class="method-tagline">"실제로 쓸 수 있나? 어느 정도 수정해야 하나?"</div><div class="method-formula">multi-axis: anchor + cardinality + relation + slot + content - penalty<br/>→ 라벨: <span class="label-use_as_is">use_as_is</span> / <span class="label-light_edit">light_edit</span> / <span class="label-restructure">restructure</span> / <span class="label-reject">reject</span></div><div class="method-detail">· 점수 1위 ≠ 실사용 가능. <b>판정 라벨</b>로 결정<br/>· reject 면 기존 프레임에 없는 신규 콘텐츠 → 재구성 신호</div></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><title>07. 키워드집 정리 과정</title>
|
||||
<style>
|
||||
@page { size: A4 portrait; margin: 12mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", sans-serif;
|
||||
background: white; color: #1e293b; margin: 0; font-size: 9.5pt; line-height: 1.4;
|
||||
}
|
||||
@media screen {
|
||||
body { max-width: 210mm; margin: 10mm auto; box-shadow: 0 4px 24px rgba(0,0,0,0.12); padding: 12mm; background: #fff; }
|
||||
}
|
||||
h1 { font-size: 13pt; border-bottom: 2pt solid #2563eb; padding-bottom: 3pt; margin: 0 0 4pt 0; color: #0f172a; }
|
||||
h2 { font-size: 10pt; color: #1e293b; margin: 6pt 0 3pt; padding: 2pt 5pt; background: #e0e7ff; border-left: 3pt solid #0a6; border-radius: 2pt; }
|
||||
.meta { color: #64748b; font-size: 8pt; font-style: italic; margin-bottom: 5pt; }
|
||||
.mdx-card { background: #f8fafc; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0 5pt; }
|
||||
.mdx-card .title { font-weight: 700; color: #0f172a; font-size: 9.5pt; margin-bottom: 2pt; }
|
||||
.popup-tag { display: inline-block; padding: 1pt 5pt; background: #eef2ff; color: #3730a3; border-radius: 8pt; font-size: 7.5pt; font-weight: 700; margin-left: 4pt; vertical-align: middle; }
|
||||
.mdx-card ul { margin: 2pt 0 0 0; padding-left: 12pt; font-size: 8pt; color: #475569; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 9pt; }
|
||||
th, td { border: 0.5pt solid #cbd5e1; padding: 3pt 5pt; text-align: left; vertical-align: top; }
|
||||
th { background: #1e293b; color: white; font-weight: 700; font-size: 8.5pt; text-align: center; }
|
||||
code { background: #f1f5f9; padding: 0 3pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; }
|
||||
.formula-box { background: #eff6ff; border: 0.7pt solid #93c5fd; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0; font-size: 9pt; color: #1e3a8a; }
|
||||
.label-use_as_is { background: #d1fae5; color: #065f46; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
.label-light_edit { background: #dbeafe; color: #1e40af; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
.label-restructure { background: #fef3c7; color: #92400e; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
.label-reject { background: #fee2e2; color: #991b1b; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; }
|
||||
|
||||
@page { size: A4 portrait; margin: 12mm; }
|
||||
.kp-section { margin: 5pt 0 8pt; }
|
||||
.kp-section .kp-title { font-size: 11pt; font-weight: 700; color: #2563eb; padding: 3pt 7pt; background: #e0e7ff; border-left: 3pt solid #2563eb; border-radius: 2pt; margin-bottom: 4pt; }
|
||||
.kp-section .kp-title .num { display: inline-block; background: #2563eb; color: white; width: 18pt; height: 18pt; line-height: 18pt; text-align: center; border-radius: 50%; font-size: 9pt; margin-right: 6pt; }
|
||||
table.kp { width: 100%; font-size: 9pt; border-collapse: collapse; margin: 3pt 0; }
|
||||
table.kp th { background: #1e293b; color: white; padding: 4pt 7pt; font-size: 8.5pt; text-align: left; }
|
||||
table.kp td { border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; vertical-align: top; }
|
||||
table.kp td.num { text-align: right; font-family: ui-monospace, monospace; font-weight: 700; color: #0f172a; }
|
||||
table.kp tr.total td { background: #fef3c7; font-weight: 700; color: #78350f; }
|
||||
.kp-rules { background: #fafafa; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 9pt; font-size: 8.5pt; line-height: 1.55; margin: 3pt 0; }
|
||||
.kp-rules b { color: #0f172a; }
|
||||
.kp-rules ul { margin: 2pt 0; padding-left: 14pt; }
|
||||
.kp-rules code { background: #f1f5f9; padding: 0 4pt; border-radius: 2pt; font-family: ui-monospace, monospace; font-size: 8pt; }
|
||||
.kp-result { background: #ecfdf5; border: 0.7pt solid #059669; border-radius: 3pt; padding: 5pt 9pt; font-size: 9pt; color: #065f46; margin: 4pt 0; }
|
||||
.kp-result b { color: #064e3b; font-size: 11pt; }
|
||||
.kp-sample { background: #fffbeb; border: 0.7pt solid #f59e0b; border-radius: 3pt; padding: 6pt 10pt; margin: 4pt 0; }
|
||||
.kp-sample .sample-head { font-size: 9.5pt; font-weight: 700; color: #92400e; margin-bottom: 4pt; }
|
||||
.kp-sample .row { font-size: 8.5pt; line-height: 1.6; margin: 3pt 0; }
|
||||
.kp-sample .row .label { display: inline-block; min-width: 22mm; font-weight: 700; color: #78350f; }
|
||||
.kp-sample .row .count { color: #b45309; font-weight: 700; }
|
||||
.kp-sample .row .terms { color: #1e293b; }
|
||||
</style></head><body><h1>07. 키워드집 정리 과정 — 어떻게 1,713개로 정리되었나</h1><div class="meta">Figma + BEPS + MDX 문장을 수집 → 형태소 분석 + 표기 통합 → 등장 범위별 분류 → 최종 1,713개 키워드 확정.</div><div class="kp-section"><div class="kp-title"><span class="num">1</span>수집 — 3 출처에서 문장 수집</div><table class="kp"><thead><tr><th>출처</th><th style="width:18%">입력 문서</th><th style="width:22%">수집된 문장</th></tr></thead><tbody><tr><td>Figma 프레임 텍스트</td><td class="num">32 개</td><td class="num">586</td></tr><tr><td>BEPS 마스터 텍스트</td><td class="num">1 개</td><td class="num">1,410</td></tr><tr><td>MDX 검증 구간</td><td class="num">4 개</td><td class="num">129</td></tr><tr class="total"><td>합계</td><td class="num">37 개</td><td class="num">2,125</td></tr></tbody></table></div><div class="kp-section"><div class="kp-title"><span class="num">2</span>전처리 — 형태소 분석 + 표기 통합</div><table class="kp"><thead><tr><th>단계</th><th style="width:22%">전체 등장 수</th><th style="width:22%">고유 단어 수</th></tr></thead><tbody><tr><td>형태소 분석 결과 (원시)</td><td class="num">13,913</td><td class="num">1,738</td></tr><tr><td>전처리 후 (표기 통합 + 보존 규칙 적용)</td><td class="num">14,161</td><td class="num"><b>1,713</b></td></tr></tbody></table><div class="kp-rules"><b>적용 규칙</b><ul><li><b>중요 표기 보존</b> : <code>DX</code>, <code>BIM</code>, <code>S/W</code>, <code>H/W</code>, <code>2D</code>, <code>3D</code>, <code>AS-IS</code>, <code>TO-BE</code> 등 13개 — 깨지면 안 되는 표기</li><li><b>표기 통합</b> : "디지털 전환", "디지털전환" → 대표 표기 <code>DX</code> 로 합침</li><li><b>제외</b> : 1글자 단어, 순수 숫자, 공백/기호만 있는 토큰</li><li><b>품사 사용</b> : 일반명사 / 고유명사 / 외국어 표기 / 숫자 표기만 (조사·동사 등 제외)</li></ul></div></div><div class="kp-section"><div class="kp-title"><span class="num">3</span>분류 — 등장 범위별 3 계층 (1,713 = 451 + 1,238 + 24)</div><table class="kp"><thead><tr><th style="width:22%">계층</th><th style="width:14%">개수</th><th style="width:18%">등장 범위</th><th>의미 / 용도</th></tr></thead><tbody><tr><td><b>핵심 키워드</b></td><td class="num">451</td><td>1 프레임에만</td><td>특정 프레임에만 등장 → <b>식별력 강함</b>. V1 매칭 가중치 0.414</td></tr><tr><td><b>연관 키워드</b></td><td class="num">1,238</td><td>2~9 프레임</td><td>여러 프레임에 보조 등장 → <b>맥락 보강</b>. V1 매칭 가중치 0.265</td></tr><tr><td><b>광범위 공통어</b></td><td class="num">24</td><td>10+ 프레임</td><td>너무 흔해 식별력 낮음 → 매칭에서 가중 작음</td></tr><tr class="total"><td>합계</td><td class="num">1,713</td><td>—</td><td>전체 고유 키워드</td></tr></tbody></table><div style="font-size:8pt;color:#64748b;margin-top:2pt">※ 위에 더해 <b>키워드 세트 524개</b> 별도 — 같은 프레임 내 함께 등장하는 키워드 묶음 (frame-level 시그니처). V1 매칭 가중치 0.320</div></div><div class="kp-result"><b>결과</b> 2,125 문장 → 형태소 분석 13,913 등장 (고유 1,738) → 전처리 후 <b>최종 1,713 고유 키워드</b> + <b>524 키워드 세트</b>.<br/>키워드는 등장 범위로 3 계층 (451 / 1,238 / 24) — V1 매칭의 핵심·연관·광범위 신호로 사용.</div><div class="kp-sample"></div></body></html>
|
||||
@@ -0,0 +1,351 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Figma 32 프레임 후보 키워드 검수</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
max-width: 1800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
line-height: 1.6;
|
||||
color: #1e293b;
|
||||
background: #f8fafc;
|
||||
}
|
||||
h1 { color: #1e293b; border-bottom: 3px solid #2563eb; padding-bottom: 12px; }
|
||||
h2 {
|
||||
color: #1a365d;
|
||||
background: #e0e7ff;
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
hr { border: none; border-top: 1px solid #cbd5e1; margin: 24px 0; }
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 12px 0 24px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #1e293b;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
width: 120px;
|
||||
}
|
||||
td:first-child {
|
||||
background: #f1f5f9;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
width: 120px;
|
||||
}
|
||||
td img {
|
||||
max-width: 260px;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
td br + strong { color: #2563eb; }
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>Figma 32 프레임 — 후보 키워드 검수 리포트</h1>
|
||||
<p><strong>출처</strong>: 각 프레임의 <code>figma_to_html_agent/blocks/{frame_id}/analysis.md</code> 의 <code>## 후보 키워드</code> 필드</p>
|
||||
<p><strong>작성자</strong>: claude-opus-4-7 (2026-04-21, <code>tagged_by</code> 메타 참조)</p>
|
||||
<p><strong>생성 방법</strong>: 각 프레임의 <code>texts.md</code> (MCP 추출 원본 텍스트 전수) + <code>flat.md</code> (bottom-up 평탄화 + 이상 탐지) 를 AI 가 읽고 15~25개 어휘를 수동 태깅.</p>
|
||||
<h2>1. 전체 통계</h2>
|
||||
<ul>
|
||||
<li>프레임 수: <strong>32</strong></li>
|
||||
<li>총 키워드 (중복 포함): <strong>436</strong></li>
|
||||
<li>고유 키워드: <strong>349</strong></li>
|
||||
<li>
|
||||
<p>프레임당 평균: <strong>13.6개</strong></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>core</strong> (df ≤ 10%, 1~3 프레임 등장): <strong>341개</strong> — 핵심 가중치 ×1.0</p>
|
||||
</li>
|
||||
<li><strong>mid</strong> (df 10~30%): <strong>8개</strong> — 가중치 ×0.5</li>
|
||||
<li><strong>general</strong> (df ≥ 30%): <strong>0개</strong> — 가중치 ×0.1</li>
|
||||
</ul>
|
||||
<h2>2. 프레임별 후보 키워드</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Frame</th>
|
||||
<th>제목 (content 1문장)</th>
|
||||
<th>키워드 수</th>
|
||||
<th>키워드</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>01</td>
|
||||
<td>1171281172</td>
|
||||
<td>S/W 개발 방향 순환도 — 기술기반 중요성, 디지털전환 직관지 역할, 품질향상과 호환성, 기존 S/W 한계…</td>
|
||||
<td>12</td>
|
||||
<td>S/W개발, 개발방향, 순환도, 기술기반, 디지털전환, 직관지, 품질향상, 호환성, S/W한계, 개발목표, 융합, 다이어그램</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>02</td>
|
||||
<td>1171281173</td>
|
||||
<td>Domain knowledge(토목공학 지식)이 반영될 수 있는 기술기반 — Engineering S/W 개…</td>
|
||||
<td>11</td>
|
||||
<td>Engineering, EngineeringSW, Domain knowledge, 토목공학, 지식기반, OpenSource, AI, 추론기반AI, 코딩기반SW, 기술기반, S/W개발</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>03</td>
|
||||
<td>1171281174</td>
|
||||
<td>국외 BIM/설계 업무 수행 특징 — Claim 최소화, 설계 업무 Data 정리 필수, 기술업무 Manua…</td>
|
||||
<td>10</td>
|
||||
<td>국외, BIM수행, Claim최소화, 오류예방, 설계Data, Manual, 기술업무, 데이터통합, 관리, 자체기술력</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>04</td>
|
||||
<td>1171281175</td>
|
||||
<td>국내 BIM 수행 주체 간 관계 다이어그램 — 건설관리사·설계사·시공사·정부기관(발주처)의 지침·규정·기술 …</td>
|
||||
<td>12</td>
|
||||
<td>국내, BIM수행, 건설관리사, 설계사, 시공사, 정부기관, 발주처, 지침, 규정, 기술형식, 4각관계도, 주체별관계</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>05</td>
|
||||
<td>1171281176</td>
|
||||
<td>보상현황·민원 관리 시각화 활용 — 종합평면도·용지도 기반 토지/물건 현황 시각화, 보상서류/사진/영상 필지…</td>
|
||||
<td>11</td>
|
||||
<td>보상현황, 민원관리, 시각화, 종합평면도, 용지도, 토지현황, 물건현황, 보상서류, 필지별, 위치기반시각화, 민원분석</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>06</td>
|
||||
<td>1171281177</td>
|
||||
<td>보상현황·민원 관리 맵 기반 — 시공 리스크와 사업 맵을 풀페이지로 시각화.…</td>
|
||||
<td>8</td>
|
||||
<td>보상현황, 민원관리, 맵, 시공리스크, 사업맵, 풀페이지시각화, 상단배너, 위치기반</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>07</td>
|
||||
<td>1171281178</td>
|
||||
<td>BIGRoom 시스템 구성 — H/W(Server·Network·회의·CCTV·Drone·Sensor) + …</td>
|
||||
<td>19</td>
|
||||
<td>System, BIGRoom, H/W, S/W, Server, Network, 회의시스템, CCTV, Drone, Sensor, 센서, IoT, CCP, WatchBIM, Domainer, 시공관리, 공사단계, 용지보상, 구성</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>08</td>
|
||||
<td>1171281179</td>
|
||||
<td>효율적인 정보의 관리와 활용 프레임워크 — What(무슨 정보) · How(어떻게 관리) · When(언제 …</td>
|
||||
<td>11</td>
|
||||
<td>정보관리, 정보활용, What, How, When, 3섹션, 효율성, 프레임워크, 무슨정보, 어떻게관리, 언제활용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>09</td>
|
||||
<td>1171281180</td>
|
||||
<td>시공 전 모델 기반 시공상세 정보물 활용 — 시공계획 작성 지원을 위한 정보물·모델·시뮬레이션·도면·성과물 …</td>
|
||||
<td>11</td>
|
||||
<td>시공전모델, 시공상세정보물, 시공계획, 정보물, 모델, 시뮬레이션, 도면, 성과물, 계획작성, 지원, 3D모델</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td>1171281181</td>
|
||||
<td>현장 실효성 증대를 위한 기본사항 — 디지털 정보·S/W 솔루션·PC 활용·직관적 인터페이스 등 5가지 요소…</td>
|
||||
<td>10</td>
|
||||
<td>현장실효성, 기본사항, 디지털정보, S/W솔루션, PC활용, 직관성, 5요소, 다이어그램, 인터페이스, 현장적용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11</td>
|
||||
<td>1171281182</td>
|
||||
<td>시공단계에서 BIM 모델과 정보 활용의 3가지 구분 — 모델기반(Model-based)·Object 기반·P…</td>
|
||||
<td>11</td>
|
||||
<td>시공단계, BIM모델, 정보활용, 모델기반, Object, Position, Model, 3카테고리, 활용구분, Object기반, Position기반</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12</td>
|
||||
<td>1171281189</td>
|
||||
<td>건설산업 DX의 궁극적 목표 — 안전·품질·생산성의 3원 교차, 소통과 신뢰가 모두 포함되는 교차 영역으로 …</td>
|
||||
<td>15</td>
|
||||
<td>건설산업, 건설산업목표, BIM목적, 안전, 품질, 생산성, 소통, 신뢰, 3원교차, 교차다이어그램, 궁극적목표, 디지털검증, 하자예방, 효율, 협업</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td>1171281190</td>
|
||||
<td>DX 시행을 위한 3대 필수조건 — 기술(디지털) · 사람(역량) · 자연(여건)의 조화. 각 요소의 세부 …</td>
|
||||
<td>19</td>
|
||||
<td>필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건, 기반, 기반지식, 창의성, 3요소, 전문지식, 경험, 투자, DX, BIM, 건설산업, 토목, 디지털</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>14</td>
|
||||
<td>1171281191</td>
|
||||
<td>DX 시행 주체별 기대효과 — 발주자·시공자·설계자 각각의 역할별 목표와 기대 효과를 페르소나 카드로 정리.…</td>
|
||||
<td>27</td>
|
||||
<td>발주자, 시공자, 설계자, 주체별, 기대효과, 페르소나, 역할, 역량목표, 소통, 협업, 품질향상, 생산성향상, Claim, 리스크최소화, 민원예방, 오류예방, 행정자동화, 디지털자산관리, 공사Risk, 현장실효성, 현장맞춤형, DX, 건설, 시공, 설계, 품질, 생산성</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>15</td>
|
||||
<td>1171281192</td>
|
||||
<td>건설 DX 정책목표 4가지와 실행요건 — 성장동력 재도약, 공사비 30% 절감, 안전사고 40% 감소, 고부…</td>
|
||||
<td>19</td>
|
||||
<td>정책목표, 실행요건, 성장동력, 재도약, 공사비절감, 안전사고, 40%감소, 고부가가치, 산업전환, 30%절감, 혁신적사고, 창의적문제해결, 사용자중심, UX, 프로세스능력, DX, Process, 디지털, 업무</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>16</td>
|
||||
<td>1171281193</td>
|
||||
<td>BIM 수행 실정 — 4가지 근본 이슈(정책 집행 인정주의, 수행 개념 정립 부재, 근본 취지 이해부족, 지…</td>
|
||||
<td>14</td>
|
||||
<td>BIM수행, 수행실정, 정책집행, 인정주의, 수행개념, 개념부재, 근본취지, 이해부족, 투자의지, 지속투자, 4사분면, 이슈, Rome, 인용구</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>17</td>
|
||||
<td>1171281194</td>
|
||||
<td>BIM 수행 현황과 문제점 — 개념 부재(CAD 확장판 오인), 잘못된 접근방식(도구로만 인식), 방향성 상…</td>
|
||||
<td>13</td>
|
||||
<td>현황, 문제점, 개념부재, 접근방식, 잘못된접근, 방향성상실, 기술예속, 전제조건오류, CAD확장판, 도구인식, 방향, 오류, paired</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>18</td>
|
||||
<td>1171281195</td>
|
||||
<td>BIM과 DX 용어 혼용 정리 — 개념 범위·S/W·프로세스·성과품·활용·확장성·수행개념·주체 등 관점별 행…</td>
|
||||
<td>28</td>
|
||||
<td>BIM, DX, 범위, 성과품, 확장성, 수행개념, 수행주체, 발주처, 설계사, 시공사, 용어비교, 상호관계, 혼용, 프로세스, 활용, 운영, 의존, 상용, CIVIL, IT, 구체, 소극, 적극, 해외, 국내, 건설, 디지털, S/W</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>19</td>
|
||||
<td>1171281197</td>
|
||||
<td>설계방식의 왜곡 문제 — 엔지니어의 기능인化, 저부가가치, 단순반복 업무로의 전락을 3열 비교로 설명.…</td>
|
||||
<td>9</td>
|
||||
<td>설계방식왜곡, 엔지니어, 기능인, 저부가가치, 단순반복, 건설산업설계, 설계특성, 3열비교, 전락</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>20</td>
|
||||
<td>1171281198</td>
|
||||
<td>디지털 전환(DX) 실현을 위한 S/W 필수성 — BIM·디지털·전면설계 관점에서 고부가가치 실현을 3열 카…</td>
|
||||
<td>9</td>
|
||||
<td>디지털전환, DX, S/W필수, BIM, 디지털, 전면설계, 고부가가치, 3열카드, DX전환</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>21</td>
|
||||
<td>1171281201</td>
|
||||
<td>Solution Engineering S/W — 협업·맞춤형·User 중심·시공 결론으로 이어지는 솔루션 엔…</td>
|
||||
<td>11</td>
|
||||
<td>Solution, SolutionEngineering, Engn.S/W, EngineeringSW, 협업, User, 맞춤형, 시공, 결론, split-panel, 다이어그램</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>22</td>
|
||||
<td>1171281202</td>
|
||||
<td>Model 특화 Engineering S/W — GIS·Modeler·Simulation 등 모델 중심 S/…</td>
|
||||
<td>11</td>
|
||||
<td>Model특화, Engn.S/W, EngineeringSW, GIS, Modeler, Simulation, 고가, 고사양, 전문가중심, 모델중심, split-panel</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>23</td>
|
||||
<td>1171281203</td>
|
||||
<td>Application S/W 2가지 구분 — Package와 Solution의 정의·특징·장점·단점·고객 차…</td>
|
||||
<td>12</td>
|
||||
<td>ApplicationSW, S/W구분, Package, Solution, Commercial, 고객, 정의, 특징, 장점, 단점, 2열비교, 표</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>24</td>
|
||||
<td>1171281204</td>
|
||||
<td>Engineering S/W의 구성과 특징 — 상용·3rd Party·전용 S/W 3가지 유형을 개념·특징·…</td>
|
||||
<td>13</td>
|
||||
<td>Engn.S/W, EngineeringSW, 구성, 특징, 상용, 3rdParty, 전문, 전용, 개념, 개발주체, 성과품, 3열비교, 표</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>25</td>
|
||||
<td>1171281205</td>
|
||||
<td>상용 Engineering S/W의 4가지 카테고리 — 조사/계획/분석, 해석, 단가/공정, Graphic별…</td>
|
||||
<td>11</td>
|
||||
<td>상용, 상용S/W, Engn.S/W, EngineeringSW, 조사계획분석, 해석, 단가공정, Graphic, 4카테고리, 3rdParty, S/W로고</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>26</td>
|
||||
<td>1171281206</td>
|
||||
<td>상용 S/W 의존이 초래한 4가지 문제 — 독과점 생태계 형성, 기술예속 심화, S/W 사용료 과다, 존폐 …</td>
|
||||
<td>11</td>
|
||||
<td>상용S/W의존, 독과점, 독과점생태계, 기술예속, S/W사용료, 과다, 존폐위기, 4문제, 4대문제, 시장지배력, 3rdParty</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>27</td>
|
||||
<td>1171281208</td>
|
||||
<td>정부주도 BIM 설계 전면 도입을 통한 건설산업 고부가가치화 — 좌측 생산성 향상, 우측 디지털 전환, 중앙…</td>
|
||||
<td>10</td>
|
||||
<td>건설산업, 고부가가치화, 정부주도, BIM설계, 전면도입, 생산성향상, 디지털전환, DX, central-split, 결론</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>28</td>
|
||||
<td>1171281209</td>
|
||||
<td>현존 상용 Engineering S/W의 현실 비판 — 토목 실무 전문성 부족, 비효율성, 토목 실무 적용 …</td>
|
||||
<td>11</td>
|
||||
<td>상용S/W, 상용, 토목, 토목실무, 전문성, 전문성부족, 비효율성, 실무적용, 3강조, 현실, 한계</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>29</td>
|
||||
<td>1171281210</td>
|
||||
<td>Process 혁신과 Product 혁신 — 과정의 Analogue→Digital Transformation…</td>
|
||||
<td>28</td>
|
||||
<td>과정혁신, 결과혁신, Analogue, Digital, 2D, 3D, Transformation, Product, Process, AS-IS, TO-BE, 융복합, 창의성, 투명성, 실체, 지반모델, 본태측량, 수치지형도, 위치기반, 등고선, 3D모델, 시공Solution, 설계Solution, 성과물, 정보물, Solution, BIM, GIS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>30</td>
|
||||
<td>1171281211</td>
|
||||
<td>산업별 현황 비교 — 제조업·건축·토목 3개 산업을 개요·Keyword·참여자·S/W·발전방향·시장규모 관점…</td>
|
||||
<td>13</td>
|
||||
<td>산업별현황, 제조업, 건축, 토목, 개요, Keyword, 참여자, S/W, 발전방향, 시장규모, 3열비교, 표, 3산업</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>31</td>
|
||||
<td>1171281212</td>
|
||||
<td>산업별 특성과 발전방향 비교 — 제조업·건축·토목 3개 산업의 특징·S/W 적용·발전방향을 3열 표로 비교.…</td>
|
||||
<td>11</td>
|
||||
<td>산업별특성, 발전방향, 제조업, 건축, 토목, 특징, S/W, 3열표, 3산업, 산업비교, 개요</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>32</td>
|
||||
<td>1171281213</td>
|
||||
<td>정책 달성 방안 — Engineering Solution과 DfMA(Design for Manufacture…</td>
|
||||
<td>15</td>
|
||||
<td>정책달성, Engn.Solution, EngineeringSolution, DfMA, DesignForManufacture, 공장생산, 현장조립, 디지털, 사용자, Easy, Quality, Cost, Speed, 5대목표, 5목표</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>3. Tier 별 전체 키워드 (매칭 가중치 분류)</h2>
|
||||
<h3>3-1. Core (341개, df ≤ 10% — 가중치 ×1.0)</h3>
|
||||
<p>특정 프레임에만 등장하는 고유 어휘. 매칭에서 결정적 신호.</p>
|
||||
<p><code>3rdParty</code>(3), <code>3열비교</code>(3), <code>BIM수행</code>(3), <code>Solution</code>(3), <code>건설산업</code>(3), <code>다이어그램</code>(3), <code>디지털전환</code>(3), <code>특징</code>(3), <code>표</code>(3), <code>협업</code>(3), <code>3D모델</code>(2), <code>3산업</code>(2), <code>GIS</code>(2), <code>Process</code>(2), <code>S/W개발</code>(2), <code>split-panel</code>(2), <code>개념부재</code>(2), <code>개요</code>(2), <code>건설</code>(2), <code>건축</code>(2), <code>결론</code>(2), <code>고부가가치</code>(2), <code>구성</code>(2), <code>국내</code>(2), <code>기술기반</code>(2), <code>기술예속</code>(2), <code>민원관리</code>(2), <code>발전방향</code>(2), <code>발주처</code>(2), <code>보상현황</code>(2), <code>상용S/W</code>(2), <code>생산성</code>(2), <code>생산성향상</code>(2), <code>설계사</code>(2), <code>성과물</code>(2), <code>성과품</code>(2), <code>소통</code>(2), <code>수행개념</code>(2), <code>시공</code>(2), <code>시공사</code>(2), <code>오류예방</code>(2), <code>위치기반</code>(2), <code>정보물</code>(2), <code>정보활용</code>(2), <code>제조업</code>(2), <code>창의성</code>(2), <code>품질</code>(2), <code>품질향상</code>(2), <code>현장실효성</code>(2), <code>2D</code>(1), <code>2열비교</code>(1), <code>30%절감</code>(1), <code>3D</code>(1), <code>3강조</code>(1), <code>3섹션</code>(1), <code>3열카드</code>(1), <code>3열표</code>(1), <code>3요소</code>(1), <code>3원교차</code>(1), <code>3카테고리</code>(1), <code>40%감소</code>(1), <code>4각관계도</code>(1), <code>4대문제</code>(1), <code>4문제</code>(1), <code>4사분면</code>(1), <code>4카테고리</code>(1), <code>5대목표</code>(1), <code>5목표</code>(1), <code>5요소</code>(1), <code>AI</code>(1), <code>AS-IS</code>(1), <code>Analogue</code>(1), <code>ApplicationSW</code>(1), <code>BIGRoom</code>(1), <code>BIM모델</code>(1), <code>BIM목적</code>(1), <code>BIM설계</code>(1), <code>CAD확장판</code>(1), <code>CCP</code>(1), <code>CCTV</code>(1)
|
||||
... (총 341개)</p>
|
||||
<h3>3-2. Mid (8개, df 10~30% — 가중치 ×0.5)</h3>
|
||||
<p><code>DX</code>(6), <code>EngineeringSW</code>(5), <code>디지털</code>(5), <code>BIM</code>(4), <code>Engn.S/W</code>(4), <code>S/W</code>(4), <code>상용</code>(4), <code>토목</code>(4)</p>
|
||||
<h3>3-3. General (0개, df ≥ 30% — 가중치 ×0.1)</h3>
|
||||
<p>여러 프레임에 등장해 변별력이 낮은 범용어.</p>
|
||||
<h2>4. 매칭에서의 활용 (Phase 23)</h2>
|
||||
<p>```
|
||||
MDX 본문 →
|
||||
synonyms.yaml 치환 →
|
||||
Kiwi 형태소 분석 (명사/영문/숫자만) →
|
||||
위 vocab (32개 프레임 전체 키워드 합집합) 과 교집합 →
|
||||
MDX 키워드 셋 확보</p>
|
||||
<p>각 프레임 점수 = IDF 가중 Jaccard(MDX 키워드 셋, 프레임 키워드 셋)
|
||||
여기서 각 키워드 기여도는 df 기반 tier 가중치 적용
|
||||
```</p>
|
||||
<p><strong>사용처</strong>:
|
||||
- Phase 23 (키워드만), 24 (+내용), 25 (+구조) — <strong>legacy 축</strong>
|
||||
- Phase 26 (template-fit-v1) 는 이 키워드 리스트 대신 프레임별 <code>anchor_sets</code> (구조화된 named set) 사용</p>
|
||||
<h2>5. 원본 파일 직접 확인</h2>
|
||||
<p>각 프레임의 analysis.md 에 그대로 기록:</p>
|
||||
<p><code>bash
|
||||
cat figma_to_html_agent/blocks/1171281190/analysis.md</code></p>
|
||||
<p>전체 검수:</p>
|
||||
<p><code>bash
|
||||
grep -A 2 '## 후보 키워드' figma_to_html_agent/blocks/*/analysis.md</code></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
# Figma 32 프레임 — 후보 키워드 검수 리포트
|
||||
|
||||
**출처**: 각 프레임의 `figma_to_html_agent/blocks/{frame_id}/analysis.md` 의 `## 후보 키워드` 필드
|
||||
|
||||
**작성자**: claude-opus-4-7 (2026-04-21, `tagged_by` 메타 참조)
|
||||
|
||||
**생성 방법**: 각 프레임의 `texts.md` (MCP 추출 원본 텍스트 전수) + `flat.md` (bottom-up 평탄화 + 이상 탐지) 를 AI 가 읽고 15~25개 어휘를 수동 태깅.
|
||||
|
||||
## 1. 전체 통계
|
||||
|
||||
- 프레임 수: **32**
|
||||
- 총 키워드 (중복 포함): **436**
|
||||
- 고유 키워드: **349**
|
||||
- 프레임당 평균: **13.6개**
|
||||
|
||||
- **core** (df ≤ 10%, 1~3 프레임 등장): **341개** — 핵심 가중치 ×1.0
|
||||
- **mid** (df 10~30%): **8개** — 가중치 ×0.5
|
||||
- **general** (df ≥ 30%): **0개** — 가중치 ×0.1
|
||||
|
||||
## 2. 프레임별 후보 키워드
|
||||
|
||||
| # | Frame | 제목 (content 1문장) | 키워드 수 | 키워드 |
|
||||
|---|-------|---------------------|----------|--------|
|
||||
| 01 | 1171281172 | S/W 개발 방향 순환도 — 기술기반 중요성, 디지털전환 직관지 역할, 품질향상과 호환성, 기존 S/W 한계… | 12 | S/W개발, 개발방향, 순환도, 기술기반, 디지털전환, 직관지, 품질향상, 호환성, S/W한계, 개발목표, 융합, 다이어그램 |
|
||||
| 02 | 1171281173 | Domain knowledge(토목공학 지식)이 반영될 수 있는 기술기반 — Engineering S/W 개… | 11 | Engineering, EngineeringSW, Domain knowledge, 토목공학, 지식기반, OpenSource, AI, 추론기반AI, 코딩기반SW, 기술기반, S/W개발 |
|
||||
| 03 | 1171281174 | 국외 BIM/설계 업무 수행 특징 — Claim 최소화, 설계 업무 Data 정리 필수, 기술업무 Manua… | 10 | 국외, BIM수행, Claim최소화, 오류예방, 설계Data, Manual, 기술업무, 데이터통합, 관리, 자체기술력 |
|
||||
| 04 | 1171281175 | 국내 BIM 수행 주체 간 관계 다이어그램 — 건설관리사·설계사·시공사·정부기관(발주처)의 지침·규정·기술 … | 12 | 국내, BIM수행, 건설관리사, 설계사, 시공사, 정부기관, 발주처, 지침, 규정, 기술형식, 4각관계도, 주체별관계 |
|
||||
| 05 | 1171281176 | 보상현황·민원 관리 시각화 활용 — 종합평면도·용지도 기반 토지/물건 현황 시각화, 보상서류/사진/영상 필지… | 11 | 보상현황, 민원관리, 시각화, 종합평면도, 용지도, 토지현황, 물건현황, 보상서류, 필지별, 위치기반시각화, 민원분석 |
|
||||
| 06 | 1171281177 | 보상현황·민원 관리 맵 기반 — 시공 리스크와 사업 맵을 풀페이지로 시각화.… | 8 | 보상현황, 민원관리, 맵, 시공리스크, 사업맵, 풀페이지시각화, 상단배너, 위치기반 |
|
||||
| 07 | 1171281178 | BIGRoom 시스템 구성 — H/W(Server·Network·회의·CCTV·Drone·Sensor) + … | 19 | System, BIGRoom, H/W, S/W, Server, Network, 회의시스템, CCTV, Drone, Sensor, 센서, IoT, CCP, WatchBIM, Domainer, 시공관리, 공사단계, 용지보상, 구성 |
|
||||
| 08 | 1171281179 | 효율적인 정보의 관리와 활용 프레임워크 — What(무슨 정보) · How(어떻게 관리) · When(언제 … | 11 | 정보관리, 정보활용, What, How, When, 3섹션, 효율성, 프레임워크, 무슨정보, 어떻게관리, 언제활용 |
|
||||
| 09 | 1171281180 | 시공 전 모델 기반 시공상세 정보물 활용 — 시공계획 작성 지원을 위한 정보물·모델·시뮬레이션·도면·성과물 … | 11 | 시공전모델, 시공상세정보물, 시공계획, 정보물, 모델, 시뮬레이션, 도면, 성과물, 계획작성, 지원, 3D모델 |
|
||||
| 10 | 1171281181 | 현장 실효성 증대를 위한 기본사항 — 디지털 정보·S/W 솔루션·PC 활용·직관적 인터페이스 등 5가지 요소… | 10 | 현장실효성, 기본사항, 디지털정보, S/W솔루션, PC활용, 직관성, 5요소, 다이어그램, 인터페이스, 현장적용 |
|
||||
| 11 | 1171281182 | 시공단계에서 BIM 모델과 정보 활용의 3가지 구분 — 모델기반(Model-based)·Object 기반·P… | 11 | 시공단계, BIM모델, 정보활용, 모델기반, Object, Position, Model, 3카테고리, 활용구분, Object기반, Position기반 |
|
||||
| 12 | 1171281189 | 건설산업 DX의 궁극적 목표 — 안전·품질·생산성의 3원 교차, 소통과 신뢰가 모두 포함되는 교차 영역으로 … | 15 | 건설산업, 건설산업목표, BIM목적, 안전, 품질, 생산성, 소통, 신뢰, 3원교차, 교차다이어그램, 궁극적목표, 디지털검증, 하자예방, 효율, 협업 |
|
||||
| 13 | 1171281190 | DX 시행을 위한 3대 필수조건 — 기술(디지털) · 사람(역량) · 자연(여건)의 조화. 각 요소의 세부 … | 19 | 필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건, 기반, 기반지식, 창의성, 3요소, 전문지식, 경험, 투자, DX, BIM, 건설산업, 토목, 디지털 |
|
||||
| 14 | 1171281191 | DX 시행 주체별 기대효과 — 발주자·시공자·설계자 각각의 역할별 목표와 기대 효과를 페르소나 카드로 정리.… | 27 | 발주자, 시공자, 설계자, 주체별, 기대효과, 페르소나, 역할, 역량목표, 소통, 협업, 품질향상, 생산성향상, Claim, 리스크최소화, 민원예방, 오류예방, 행정자동화, 디지털자산관리, 공사Risk, 현장실효성, 현장맞춤형, DX, 건설, 시공, 설계, 품질, 생산성 |
|
||||
| 15 | 1171281192 | 건설 DX 정책목표 4가지와 실행요건 — 성장동력 재도약, 공사비 30% 절감, 안전사고 40% 감소, 고부… | 19 | 정책목표, 실행요건, 성장동력, 재도약, 공사비절감, 안전사고, 40%감소, 고부가가치, 산업전환, 30%절감, 혁신적사고, 창의적문제해결, 사용자중심, UX, 프로세스능력, DX, Process, 디지털, 업무 |
|
||||
| 16 | 1171281193 | BIM 수행 실정 — 4가지 근본 이슈(정책 집행 인정주의, 수행 개념 정립 부재, 근본 취지 이해부족, 지… | 14 | BIM수행, 수행실정, 정책집행, 인정주의, 수행개념, 개념부재, 근본취지, 이해부족, 투자의지, 지속투자, 4사분면, 이슈, Rome, 인용구 |
|
||||
| 17 | 1171281194 | BIM 수행 현황과 문제점 — 개념 부재(CAD 확장판 오인), 잘못된 접근방식(도구로만 인식), 방향성 상… | 13 | 현황, 문제점, 개념부재, 접근방식, 잘못된접근, 방향성상실, 기술예속, 전제조건오류, CAD확장판, 도구인식, 방향, 오류, paired |
|
||||
| 18 | 1171281195 | BIM과 DX 용어 혼용 정리 — 개념 범위·S/W·프로세스·성과품·활용·확장성·수행개념·주체 등 관점별 행… | 28 | BIM, DX, 범위, 성과품, 확장성, 수행개념, 수행주체, 발주처, 설계사, 시공사, 용어비교, 상호관계, 혼용, 프로세스, 활용, 운영, 의존, 상용, CIVIL, IT, 구체, 소극, 적극, 해외, 국내, 건설, 디지털, S/W |
|
||||
| 19 | 1171281197 | 설계방식의 왜곡 문제 — 엔지니어의 기능인化, 저부가가치, 단순반복 업무로의 전락을 3열 비교로 설명.… | 9 | 설계방식왜곡, 엔지니어, 기능인, 저부가가치, 단순반복, 건설산업설계, 설계특성, 3열비교, 전락 |
|
||||
| 20 | 1171281198 | 디지털 전환(DX) 실현을 위한 S/W 필수성 — BIM·디지털·전면설계 관점에서 고부가가치 실현을 3열 카… | 9 | 디지털전환, DX, S/W필수, BIM, 디지털, 전면설계, 고부가가치, 3열카드, DX전환 |
|
||||
| 21 | 1171281201 | Solution Engineering S/W — 협업·맞춤형·User 중심·시공 결론으로 이어지는 솔루션 엔… | 11 | Solution, SolutionEngineering, Engn.S/W, EngineeringSW, 협업, User, 맞춤형, 시공, 결론, split-panel, 다이어그램 |
|
||||
| 22 | 1171281202 | Model 특화 Engineering S/W — GIS·Modeler·Simulation 등 모델 중심 S/… | 11 | Model특화, Engn.S/W, EngineeringSW, GIS, Modeler, Simulation, 고가, 고사양, 전문가중심, 모델중심, split-panel |
|
||||
| 23 | 1171281203 | Application S/W 2가지 구분 — Package와 Solution의 정의·특징·장점·단점·고객 차… | 12 | ApplicationSW, S/W구분, Package, Solution, Commercial, 고객, 정의, 특징, 장점, 단점, 2열비교, 표 |
|
||||
| 24 | 1171281204 | Engineering S/W의 구성과 특징 — 상용·3rd Party·전용 S/W 3가지 유형을 개념·특징·… | 13 | Engn.S/W, EngineeringSW, 구성, 특징, 상용, 3rdParty, 전문, 전용, 개념, 개발주체, 성과품, 3열비교, 표 |
|
||||
| 25 | 1171281205 | 상용 Engineering S/W의 4가지 카테고리 — 조사/계획/분석, 해석, 단가/공정, Graphic별… | 11 | 상용, 상용S/W, Engn.S/W, EngineeringSW, 조사계획분석, 해석, 단가공정, Graphic, 4카테고리, 3rdParty, S/W로고 |
|
||||
| 26 | 1171281206 | 상용 S/W 의존이 초래한 4가지 문제 — 독과점 생태계 형성, 기술예속 심화, S/W 사용료 과다, 존폐 … | 11 | 상용S/W의존, 독과점, 독과점생태계, 기술예속, S/W사용료, 과다, 존폐위기, 4문제, 4대문제, 시장지배력, 3rdParty |
|
||||
| 27 | 1171281208 | 정부주도 BIM 설계 전면 도입을 통한 건설산업 고부가가치화 — 좌측 생산성 향상, 우측 디지털 전환, 중앙… | 10 | 건설산업, 고부가가치화, 정부주도, BIM설계, 전면도입, 생산성향상, 디지털전환, DX, central-split, 결론 |
|
||||
| 28 | 1171281209 | 현존 상용 Engineering S/W의 현실 비판 — 토목 실무 전문성 부족, 비효율성, 토목 실무 적용 … | 11 | 상용S/W, 상용, 토목, 토목실무, 전문성, 전문성부족, 비효율성, 실무적용, 3강조, 현실, 한계 |
|
||||
| 29 | 1171281210 | Process 혁신과 Product 혁신 — 과정의 Analogue→Digital Transformation… | 28 | 과정혁신, 결과혁신, Analogue, Digital, 2D, 3D, Transformation, Product, Process, AS-IS, TO-BE, 융복합, 창의성, 투명성, 실체, 지반모델, 본태측량, 수치지형도, 위치기반, 등고선, 3D모델, 시공Solution, 설계Solution, 성과물, 정보물, Solution, BIM, GIS |
|
||||
| 30 | 1171281211 | 산업별 현황 비교 — 제조업·건축·토목 3개 산업을 개요·Keyword·참여자·S/W·발전방향·시장규모 관점… | 13 | 산업별현황, 제조업, 건축, 토목, 개요, Keyword, 참여자, S/W, 발전방향, 시장규모, 3열비교, 표, 3산업 |
|
||||
| 31 | 1171281212 | 산업별 특성과 발전방향 비교 — 제조업·건축·토목 3개 산업의 특징·S/W 적용·발전방향을 3열 표로 비교.… | 11 | 산업별특성, 발전방향, 제조업, 건축, 토목, 특징, S/W, 3열표, 3산업, 산업비교, 개요 |
|
||||
| 32 | 1171281213 | 정책 달성 방안 — Engineering Solution과 DfMA(Design for Manufacture… | 15 | 정책달성, Engn.Solution, EngineeringSolution, DfMA, DesignForManufacture, 공장생산, 현장조립, 디지털, 사용자, Easy, Quality, Cost, Speed, 5대목표, 5목표 |
|
||||
|
||||
## 3. Tier 별 전체 키워드 (매칭 가중치 분류)
|
||||
|
||||
### 3-1. Core (341개, df ≤ 10% — 가중치 ×1.0)
|
||||
|
||||
특정 프레임에만 등장하는 고유 어휘. 매칭에서 결정적 신호.
|
||||
|
||||
`3rdParty`(3), `3열비교`(3), `BIM수행`(3), `Solution`(3), `건설산업`(3), `다이어그램`(3), `디지털전환`(3), `특징`(3), `표`(3), `협업`(3), `3D모델`(2), `3산업`(2), `GIS`(2), `Process`(2), `S/W개발`(2), `split-panel`(2), `개념부재`(2), `개요`(2), `건설`(2), `건축`(2), `결론`(2), `고부가가치`(2), `구성`(2), `국내`(2), `기술기반`(2), `기술예속`(2), `민원관리`(2), `발전방향`(2), `발주처`(2), `보상현황`(2), `상용S/W`(2), `생산성`(2), `생산성향상`(2), `설계사`(2), `성과물`(2), `성과품`(2), `소통`(2), `수행개념`(2), `시공`(2), `시공사`(2), `오류예방`(2), `위치기반`(2), `정보물`(2), `정보활용`(2), `제조업`(2), `창의성`(2), `품질`(2), `품질향상`(2), `현장실효성`(2), `2D`(1), `2열비교`(1), `30%절감`(1), `3D`(1), `3강조`(1), `3섹션`(1), `3열카드`(1), `3열표`(1), `3요소`(1), `3원교차`(1), `3카테고리`(1), `40%감소`(1), `4각관계도`(1), `4대문제`(1), `4문제`(1), `4사분면`(1), `4카테고리`(1), `5대목표`(1), `5목표`(1), `5요소`(1), `AI`(1), `AS-IS`(1), `Analogue`(1), `ApplicationSW`(1), `BIGRoom`(1), `BIM모델`(1), `BIM목적`(1), `BIM설계`(1), `CAD확장판`(1), `CCP`(1), `CCTV`(1)
|
||||
... (총 341개)
|
||||
|
||||
### 3-2. Mid (8개, df 10~30% — 가중치 ×0.5)
|
||||
|
||||
`DX`(6), `EngineeringSW`(5), `디지털`(5), `BIM`(4), `Engn.S/W`(4), `S/W`(4), `상용`(4), `토목`(4)
|
||||
|
||||
### 3-3. General (0개, df ≥ 30% — 가중치 ×0.1)
|
||||
|
||||
여러 프레임에 등장해 변별력이 낮은 범용어.
|
||||
|
||||
|
||||
|
||||
## 4. 매칭에서의 활용 (Phase 23)
|
||||
|
||||
```
|
||||
MDX 본문 →
|
||||
synonyms.yaml 치환 →
|
||||
Kiwi 형태소 분석 (명사/영문/숫자만) →
|
||||
위 vocab (32개 프레임 전체 키워드 합집합) 과 교집합 →
|
||||
MDX 키워드 셋 확보
|
||||
|
||||
각 프레임 점수 = IDF 가중 Jaccard(MDX 키워드 셋, 프레임 키워드 셋)
|
||||
여기서 각 키워드 기여도는 df 기반 tier 가중치 적용
|
||||
```
|
||||
|
||||
**사용처**:
|
||||
- Phase 23 (키워드만), 24 (+내용), 25 (+구조) — **legacy 축**
|
||||
- Phase 26 (template-fit-v1) 는 이 키워드 리스트 대신 프레임별 `anchor_sets` (구조화된 named set) 사용
|
||||
|
||||
## 5. 원본 파일 직접 확인
|
||||
|
||||
각 프레임의 analysis.md 에 그대로 기록:
|
||||
|
||||
```bash
|
||||
cat figma_to_html_agent/blocks/1171281190/analysis.md
|
||||
```
|
||||
|
||||
전체 검수:
|
||||
|
||||
```bash
|
||||
grep -A 2 '## 후보 키워드' figma_to_html_agent/blocks/*/analysis.md
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
# V4 slot 축 ablation — 실증
|
||||
## 질문
|
||||
V4 confidence 계산식의 slot 축 (W_SLOT=0.15) 이 frame 매칭에 실제로 기여하는가?
|
||||
## 방법
|
||||
- **Baseline**: 5축 가중치 그대로 — 0.25 anchor + 0.20 card + 0.20 rel + 0.15 slot + 0.20 content
|
||||
- **Ablated**: W_SLOT=0, 나머지 4축 비례 재정규화 (sum=1.0)
|
||||
- anchor: 0.294, cardinality: 0.235, relation: 0.235, content: 0.235
|
||||
- 32 frame × 7 MDX 섹션 confidence 재계산 → label 재할당 → 비교
|
||||
|
||||
## 결과 1 — TARGET 4 섹션의 정답 frame 라벨/순위 변화
|
||||
| MDX | 정답 Frame | Baseline conf / 라벨 / 순위 | Ablated conf / 라벨 / 순위 | 변화 |
|
||||
|---|---|---|---|---|
|
||||
| 01-2 | Frame 18 | 0.946 / use_as_is / 순위 1 | 0.936 / use_as_is / 순위 1 | 동일 |
|
||||
| 02-2.2 | Frame 14 | 0.293 / reject / 순위 7 | 0.292 / reject / 순위 5 | ⚠️ 변화 |
|
||||
| 03-1 | Frame 13 | 0.927 / use_as_is / 순위 1 | 0.914 / use_as_is / 순위 1 | 동일 |
|
||||
| 03-2 | Frame 29 | 0.920 / use_as_is / 순위 1 | 0.906 / use_as_is / 순위 1 | 동일 |
|
||||
|
||||
**TARGET Top-1 정답률**: Baseline 3/4, Ablated 3/4
|
||||
|
||||
## 결과 2 — 각 섹션 Top-3 매칭 변화
|
||||
|
||||
### [01-2] TARGET (정답 Frame 18)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 18 🎯 (0.946, use_as_is) | Frame 18 🎯 (0.936, use_as_is) |
|
||||
| 2 | Frame 29 (0.868, reject) | Frame 29 (0.844, light_edit) |
|
||||
| 3 | Frame 24 (0.757, light_edit) | Frame 24 (0.802, light_edit) |
|
||||
|
||||
### [02-2.2] TARGET (정답 Frame 14)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 6 (0.492, reject) | Frame 6 (0.438, reject) |
|
||||
| 2 | Frame 27 (0.395, reject) | Frame 27 (0.377, reject) |
|
||||
| 3 | Frame 24 (0.384, reject) | Frame 24 (0.363, reject) |
|
||||
|
||||
### [03-1] TARGET (정답 Frame 13)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 13 🎯 (0.927, use_as_is) | Frame 13 🎯 (0.914, use_as_is) |
|
||||
| 2 | Frame 20 (0.841, light_edit) | Frame 20 (0.813, light_edit) |
|
||||
| 3 | Frame 28 (0.720, reject) | Frame 28 (0.671, restructure) |
|
||||
|
||||
### [03-2] TARGET (정답 Frame 29)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 29 🎯 (0.920, use_as_is) | Frame 29 🎯 (0.906, use_as_is) |
|
||||
| 2 | Frame 18 (0.814, reject) | Frame 18 (0.781, light_edit) |
|
||||
| 3 | Frame 23 (0.647, reject) | Frame 24 (0.664, restructure) |
|
||||
|
||||
### [01-1] Holdout (holdout)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 11 (0.910, use_as_is) | Frame 11 (0.894, light_edit) |
|
||||
| 2 | Frame 12 (0.826, light_edit) | Frame 12 (0.795, light_edit) |
|
||||
| 3 | Frame 20 (0.817, light_edit) | Frame 20 (0.784, light_edit) |
|
||||
|
||||
### [02-1] Holdout (holdout)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 12 (0.914, use_as_is) | Frame 12 (0.899, light_edit) |
|
||||
| 2 | Frame 14 (0.766, reject) | Frame 14 (0.724, restructure) |
|
||||
| 3 | Frame 20 (0.751, reject) | Frame 20 (0.707, restructure) |
|
||||
|
||||
### [02-2.1] Holdout (holdout)
|
||||
| 순위 | Baseline | Ablated |
|
||||
|---|---|---|
|
||||
| 1 | Frame 7 (0.469, reject) | Frame 7 (0.464, reject) |
|
||||
| 2 | Frame 24 (0.465, reject) | Frame 24 (0.459, reject) |
|
||||
| 3 | Frame 6 (0.465, reject) | Frame 29 (0.459, reject) |
|
||||
|
||||
## 결과 3 — 라벨 분포 변화 (32 frame 기준)
|
||||
| MDX | use_as_is | light_edit | restructure | reject |
|
||||
|---|---|---|---|---|
|
||||
| 01-2 | 1 | 1→2 | 2 | 28→27 |
|
||||
| 02-2.2 | 0 | 0 | 0 | 32 |
|
||||
| 03-1 | 1 | 1 | 0→2 | 30→28 |
|
||||
| 03-2 | 1 | 0→1 | 1 | 30→29 |
|
||||
| 01-1 | 1→0 | 2→3 | 2→4 | 27→25 |
|
||||
| 02-1 | 1→0 | 0→1 | 1→4 | 30→27 |
|
||||
| 02-2.1 | 0 | 0 | 0 | 32 |
|
||||
|
||||
## 결론
|
||||
- **Top-1 일치 섹션 수**: 7/7
|
||||
- **Top-3 멤버 일치 섹션 수**: 5/7
|
||||
- **TARGET Top-1 정답률**: Baseline 3/4 → Ablated 3/4
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,441 @@
|
||||
meta:
|
||||
pipeline_step: 8.v2
|
||||
description: V1 Top-K 후보를 ko-sroberta cosine 으로 재정렬 (캐스케이드 rerank 1단)
|
||||
model: jhgan/ko-sroberta-multitask
|
||||
similarity: cosine
|
||||
top_k: 5
|
||||
mdx_summary_spec: title + 첫 일반 문단(≤120자) + slot label Top-5 join (detect_mdx.build_summary)
|
||||
frame_source: analysis.md "내용" 섹션 (phase_common.load_32_frames)
|
||||
v1_source: mdx_matching_result.yaml rank_by_matching_score
|
||||
holdout_sections:
|
||||
- 01-1
|
||||
- 02-1
|
||||
- 02-2.1
|
||||
answer_map:
|
||||
01-2: 18
|
||||
02-2.2: 14
|
||||
03-1: 13
|
||||
03-2: 29
|
||||
lock_snapshot:
|
||||
timestamp: '2026-04-24T07:44:36'
|
||||
model: jhgan/ko-sroberta-multitask
|
||||
top_k: 5
|
||||
files:
|
||||
pipeline_08_v2_semantic_rerank.py: 87f0873dde540acaf4944f2e95a3d02dac7e6f6da5e7880bbe393120cce47190
|
||||
embeddings.py: acc50f3f93935061fb146c3b15e30bc9734b4c96c53f82a603bc758944b50c9c
|
||||
detect_mdx.py: aaf18b89aeb7bebb7c206348ef7a61d560999c4234ce28df070c24701c96227a
|
||||
phase_common.py: b778840c8e246d0e5210359263e4a59ef6acfa6f3c9a8a1cc110f18506cc4f94
|
||||
pipeline_01_extract_nodes.py: d31dec9c25bda9a893caa7121fe79b1f0f7e533f32d83743d04abb36f5e6a343
|
||||
synonyms.yaml: 57bbc60d0ca8a95c445e3e0acf1af4e1cff101957ae8c1759311ec827c961893
|
||||
mdx_matching_result.yaml: 2796504d30966f85492f3fad826f223969bba8fd5ee6e6ab21b56444527472c8
|
||||
principle:
|
||||
- V1 baseline 고정 — 재정렬 대상만 Top-K 로 한정
|
||||
- 가중합 금지 — 순수 semantic rerank
|
||||
- Holdout 성적을 설계 근거로 사용하지 않음
|
||||
mdx_sections:
|
||||
01-2:
|
||||
section_type: target
|
||||
answer_frame_number: 18
|
||||
mdx_title: 2. 용어간 상호관계
|
||||
mdx_summary: 2. 용어간 상호관계. . DX / BIM
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281195'
|
||||
frame_number: 18
|
||||
v1_score: 0.937
|
||||
- rank: 2
|
||||
frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_score: 0.392
|
||||
- rank: 3
|
||||
frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_score: 0.379
|
||||
- rank: 4
|
||||
frame_id: '1171281172'
|
||||
frame_number: 1
|
||||
v1_score: 0.362
|
||||
- rank: 5
|
||||
frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_score: 0.358
|
||||
v2_rerank:
|
||||
- frame_id: '1171281195'
|
||||
frame_number: 18
|
||||
v1_rank: 1
|
||||
v1_score: 0.937
|
||||
semantic_score: 0.7295
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 2
|
||||
v1_score: 0.392
|
||||
semantic_score: 0.6442
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 3
|
||||
v1_score: 0.379
|
||||
semantic_score: 0.5853
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281172'
|
||||
frame_number: 1
|
||||
v1_rank: 4
|
||||
v1_score: 0.362
|
||||
semantic_score: 0.5741
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 5
|
||||
v1_score: 0.358
|
||||
semantic_score: 0.5556
|
||||
v2_rank: 5
|
||||
02-2.2:
|
||||
section_type: target
|
||||
answer_frame_number: 14
|
||||
mdx_title: 2.2 DX 시행 주체별 기대효과
|
||||
mdx_summary: 2.2 DX 시행 주체별 기대효과. DX 시행 주체별 기대효과
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281191'
|
||||
frame_number: 14
|
||||
v1_score: 0.84
|
||||
- rank: 2
|
||||
frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_score: 0.495
|
||||
- rank: 3
|
||||
frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_score: 0.459
|
||||
- rank: 4
|
||||
frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_score: 0.411
|
||||
- rank: 5
|
||||
frame_id: '1171281176'
|
||||
frame_number: 5
|
||||
v1_score: 0.404
|
||||
v2_rerank:
|
||||
- frame_id: '1171281191'
|
||||
frame_number: 14
|
||||
v1_rank: 1
|
||||
v1_score: 0.84
|
||||
semantic_score: 0.6416
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 3
|
||||
v1_score: 0.459
|
||||
semantic_score: 0.4858
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_rank: 4
|
||||
v1_score: 0.411
|
||||
semantic_score: 0.4471
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 2
|
||||
v1_score: 0.495
|
||||
semantic_score: 0.3861
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281176'
|
||||
frame_number: 5
|
||||
v1_rank: 5
|
||||
v1_score: 0.404
|
||||
semantic_score: 0.3762
|
||||
v2_rank: 5
|
||||
03-1:
|
||||
section_type: target
|
||||
answer_frame_number: 13
|
||||
mdx_title: 1. DX 시행을 위한 필수 요건
|
||||
mdx_summary: 1. DX 시행을 위한 필수 요건. 기술 / 사람 / 자연
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281190'
|
||||
frame_number: 13
|
||||
v1_score: 0.598
|
||||
- rank: 2
|
||||
frame_id: '1171281192'
|
||||
frame_number: 15
|
||||
v1_score: 0.542
|
||||
- rank: 3
|
||||
frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_score: 0.388
|
||||
- rank: 4
|
||||
frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_score: 0.363
|
||||
- rank: 5
|
||||
frame_id: '1171281197'
|
||||
frame_number: 19
|
||||
v1_score: 0.269
|
||||
v2_rerank:
|
||||
- frame_id: '1171281190'
|
||||
frame_number: 13
|
||||
v1_rank: 1
|
||||
v1_score: 0.598
|
||||
semantic_score: 0.634
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281192'
|
||||
frame_number: 15
|
||||
v1_rank: 2
|
||||
v1_score: 0.542
|
||||
semantic_score: 0.5959
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_rank: 3
|
||||
v1_score: 0.388
|
||||
semantic_score: 0.5188
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 4
|
||||
v1_score: 0.363
|
||||
semantic_score: 0.4577
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281197'
|
||||
frame_number: 19
|
||||
v1_rank: 5
|
||||
v1_score: 0.269
|
||||
semantic_score: 0.4209
|
||||
v2_rank: 5
|
||||
03-2:
|
||||
section_type: target
|
||||
answer_frame_number: 29
|
||||
mdx_title: 2. Process의 혁신과 Product의 변화
|
||||
mdx_summary: 2. Process의 혁신과 Product의 변화. 과정 / 결과
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281210'
|
||||
frame_number: 29
|
||||
v1_score: 0.705
|
||||
- rank: 2
|
||||
frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_score: 0.521
|
||||
- rank: 3
|
||||
frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_score: 0.426
|
||||
- rank: 4
|
||||
frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_score: 0.42
|
||||
- rank: 5
|
||||
frame_id: '1171281179'
|
||||
frame_number: 8
|
||||
v1_score: 0.383
|
||||
v2_rerank:
|
||||
- frame_id: '1171281210'
|
||||
frame_number: 29
|
||||
v1_rank: 1
|
||||
v1_score: 0.705
|
||||
semantic_score: 0.5991
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281179'
|
||||
frame_number: 8
|
||||
v1_rank: 5
|
||||
v1_score: 0.383
|
||||
semantic_score: 0.4832
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 4
|
||||
v1_score: 0.42
|
||||
semantic_score: 0.465
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 2
|
||||
v1_score: 0.521
|
||||
semantic_score: 0.4395
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 3
|
||||
v1_score: 0.426
|
||||
semantic_score: 0.3975
|
||||
v2_rank: 5
|
||||
01-1:
|
||||
section_type: holdout
|
||||
answer_frame_number: null
|
||||
mdx_title: 1. 용어 정의
|
||||
mdx_summary: '1. 용어 정의. fontSize: ''0.8rem'',. 건설산업 / BIM / DX'
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281172'
|
||||
frame_number: 1
|
||||
v1_score: 0.551
|
||||
- rank: 2
|
||||
frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_score: 0.298
|
||||
- rank: 3
|
||||
frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_score: 0.249
|
||||
- rank: 4
|
||||
frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_score: 0.248
|
||||
- rank: 5
|
||||
frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_score: 0.238
|
||||
v2_rerank:
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 2
|
||||
v1_score: 0.298
|
||||
semantic_score: 0.5881
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 3
|
||||
v1_score: 0.249
|
||||
semantic_score: 0.5507
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 4
|
||||
v1_score: 0.248
|
||||
semantic_score: 0.547
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281172'
|
||||
frame_number: 1
|
||||
v1_rank: 1
|
||||
v1_score: 0.551
|
||||
semantic_score: 0.5368
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_rank: 5
|
||||
v1_score: 0.238
|
||||
semantic_score: 0.5005
|
||||
v2_rank: 5
|
||||
02-1:
|
||||
section_type: holdout
|
||||
answer_frame_number: null
|
||||
mdx_title: 1. DX의 궁극적 목표
|
||||
mdx_summary: 1. DX의 궁극적 목표. . 안전과 품질 / 생산성 향상 / 소통과 신뢰
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_score: 0.766
|
||||
- rank: 2
|
||||
frame_id: '1171281191'
|
||||
frame_number: 14
|
||||
v1_score: 0.382
|
||||
- rank: 3
|
||||
frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_score: 0.356
|
||||
- rank: 4
|
||||
frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_score: 0.302
|
||||
- rank: 5
|
||||
frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_score: 0.237
|
||||
v2_rerank:
|
||||
- frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_rank: 5
|
||||
v1_score: 0.237
|
||||
semantic_score: 0.5895
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281191'
|
||||
frame_number: 14
|
||||
v1_rank: 2
|
||||
v1_score: 0.382
|
||||
semantic_score: 0.5779
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 1
|
||||
v1_score: 0.766
|
||||
semantic_score: 0.57
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 3
|
||||
v1_score: 0.356
|
||||
semantic_score: 0.5467
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_rank: 4
|
||||
v1_score: 0.302
|
||||
semantic_score: 0.4707
|
||||
v2_rank: 5
|
||||
02-2.1:
|
||||
section_type: holdout
|
||||
answer_frame_number: null
|
||||
mdx_title: 2.1 업무 수행 과정(Process)의 변화
|
||||
mdx_summary: 2.1 업무 수행 과정(Process)의 변화. 업무 수행 과정
|
||||
top_k: 5
|
||||
v1_top_k:
|
||||
- rank: 1
|
||||
frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_score: 0.246
|
||||
- rank: 2
|
||||
frame_id: '1171281179'
|
||||
frame_number: 8
|
||||
v1_score: 0.193
|
||||
- rank: 3
|
||||
frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_score: 0.184
|
||||
- rank: 4
|
||||
frame_id: '1171281210'
|
||||
frame_number: 29
|
||||
v1_score: 0.176
|
||||
- rank: 5
|
||||
frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_score: 0.171
|
||||
v2_rerank:
|
||||
- frame_id: '1171281210'
|
||||
frame_number: 29
|
||||
v1_rank: 4
|
||||
v1_score: 0.176
|
||||
semantic_score: 0.5511
|
||||
v2_rank: 1
|
||||
- frame_id: '1171281179'
|
||||
frame_number: 8
|
||||
v1_rank: 2
|
||||
v1_score: 0.193
|
||||
semantic_score: 0.5021
|
||||
v2_rank: 2
|
||||
- frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_rank: 3
|
||||
v1_score: 0.184
|
||||
semantic_score: 0.4648
|
||||
v2_rank: 3
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 1
|
||||
v1_score: 0.246
|
||||
semantic_score: 0.4311
|
||||
v2_rank: 4
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 5
|
||||
v1_score: 0.171
|
||||
semantic_score: 0.4191
|
||||
v2_rank: 5
|
||||
@@ -0,0 +1,968 @@
|
||||
meta:
|
||||
pipeline_step: 8.v3.r5
|
||||
description: r4 MDX 하네스 + Frame 13 SSOT 확정 ontology
|
||||
top_k: 5
|
||||
answer_map:
|
||||
01-2: 18
|
||||
02-2.2: 14
|
||||
03-1: 13
|
||||
03-2: 29
|
||||
holdout_sections:
|
||||
- 01-1
|
||||
- 02-1
|
||||
- 02-2.1
|
||||
lock_snapshot:
|
||||
timestamp: '2026-04-24T11:42:05'
|
||||
top_k: 5
|
||||
files:
|
||||
pipeline_08_v3_r5_structure_rerank.py: 217f67497c12c0e92cfc10d0d56ac962a983532fa3e28efbdd7c3bd8d682be39
|
||||
pipeline_08_v3_r4_structure_rerank.py: 6836e889d15d4591dcb170556cc38ab39f7d17294728c919966099d855c80f30
|
||||
structure_ontology_v2_final_r2.yaml: b009417abb7905e40c9bb57cf87ca99c6e58e5ef8008861e2d2d0c423b599979
|
||||
v2_semantic_rerank_result.yaml: 6e4e2917bd0f1849e236f841b0ba85ad7333faae62abe1bbe08bb091c76fc325
|
||||
r5_changes:
|
||||
- 'ontology 입력: final.yaml → final_r2.yaml (Frame 13 review_queue 해제 반영)'
|
||||
- 'MDX 하네스: r4 (B-1 + B-2) 그대로'
|
||||
mdx_sections:
|
||||
01-2:
|
||||
answer_frame_number: 18
|
||||
mdx_title: 2. 용어간 상호관계
|
||||
mdx_profile:
|
||||
section_id: 01-2
|
||||
title: 2. 용어간 상호관계
|
||||
layout: compare-rows
|
||||
family: table
|
||||
relation_type: compare
|
||||
cardinality:
|
||||
ideal: 2
|
||||
content_affinity:
|
||||
primary: policy_requirements
|
||||
secondary:
|
||||
- tool_ecosystem
|
||||
- concept_definition
|
||||
structure_intent:
|
||||
primary: cycle_interrelation
|
||||
secondary:
|
||||
- singleton_emphasis
|
||||
- binary_compare
|
||||
validator_log:
|
||||
strong_aff_matches: {}
|
||||
strong_int_matches: {}
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281195'
|
||||
frame_number: 18
|
||||
v1_rank: 1
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.55
|
||||
v3_r5_breakdown:
|
||||
total: 0.55
|
||||
layout_compat: 1.0
|
||||
layout_breakdown:
|
||||
family: 1.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 1.0
|
||||
layout_match_source: exact
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: compare-rows
|
||||
fig_content_affinity: comparative_matrix
|
||||
fig_structure_intent: matrix_coverage
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 2
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.49
|
||||
v3_r5_breakdown:
|
||||
total: 0.49
|
||||
layout_compat: 0.35
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: central-split
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: state_transition
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 5
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.35
|
||||
v3_r5_breakdown:
|
||||
total: 0.35
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: list-stacked
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281172'
|
||||
frame_number: 1
|
||||
v1_rank: 4
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.25
|
||||
v3_r5_breakdown:
|
||||
total: 0.25
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 1.0
|
||||
structure_intent_source: primary_match
|
||||
fig_layout: circular-nodes
|
||||
fig_content_affinity: interrelation
|
||||
fig_structure_intent: cycle_interrelation
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 3
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.21
|
||||
v3_r5_breakdown:
|
||||
total: 0.21
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: 3-category
|
||||
fig_content_affinity: concept_definition
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 5
|
||||
02-2.2:
|
||||
answer_frame_number: 14
|
||||
mdx_title: 2.2 DX 시행 주체별 기대효과
|
||||
mdx_profile:
|
||||
section_id: 02-2.2
|
||||
title: 2.2 DX 시행 주체별 기대효과
|
||||
layout: persona-3col
|
||||
family: card
|
||||
relation_type: parallel
|
||||
cardinality:
|
||||
ideal: 3
|
||||
content_affinity:
|
||||
primary: persona_benefit
|
||||
secondary:
|
||||
- capability_requirements
|
||||
- concept_definition
|
||||
structure_intent:
|
||||
primary: sequence
|
||||
secondary:
|
||||
- persona_mapping
|
||||
- multi_parallel
|
||||
validator_log:
|
||||
strong_aff_matches: {}
|
||||
strong_int_matches: {}
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281191'
|
||||
frame_number: 14
|
||||
v1_rank: 1
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.8
|
||||
v3_r5_breakdown:
|
||||
total: 0.8
|
||||
layout_compat: 0.75
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 1.0
|
||||
layout_match_source: exact
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: persona-3col
|
||||
fig_content_affinity: persona_benefit
|
||||
fig_structure_intent: persona_mapping
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281176'
|
||||
frame_number: 5
|
||||
v1_rank: 5
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.5
|
||||
v3_r5_breakdown:
|
||||
total: 0.5
|
||||
layout_compat: 0.35
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: side-card
|
||||
fig_content_affinity: concept_definition
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_rank: 4
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.23
|
||||
v3_r5_breakdown:
|
||||
total: 0.23
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: list-numbered
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 2
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.23
|
||||
v3_r5_breakdown:
|
||||
total: 0.23
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: list-stacked
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 3
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.215
|
||||
v3_r5_breakdown:
|
||||
total: 0.215
|
||||
layout_compat: 0.35
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.3
|
||||
structure_intent_source: secondary_overlap
|
||||
fig_layout: cycle-3way
|
||||
fig_content_affinity: interrelation
|
||||
fig_structure_intent: cycle_interrelation
|
||||
v3_r5_rank: 5
|
||||
03-1:
|
||||
answer_frame_number: 13
|
||||
mdx_title: 1. DX 시행을 위한 필수 요건
|
||||
mdx_profile:
|
||||
section_id: 03-1
|
||||
title: 1. DX 시행을 위한 필수 요건
|
||||
layout: 3col-parallel
|
||||
family: card
|
||||
relation_type: parallel
|
||||
cardinality:
|
||||
ideal: 3
|
||||
content_affinity:
|
||||
primary: capability_requirements
|
||||
secondary:
|
||||
- process_steps
|
||||
- comparative_matrix
|
||||
structure_intent:
|
||||
primary: sequence
|
||||
secondary:
|
||||
- singleton_emphasis
|
||||
- multi_parallel
|
||||
validator_log:
|
||||
strong_aff_matches: {}
|
||||
strong_int_matches: {}
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281190'
|
||||
frame_number: 13
|
||||
v1_rank: 1
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.8
|
||||
v3_r5_breakdown:
|
||||
total: 0.8
|
||||
layout_compat: 0.75
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 1.0
|
||||
layout_match_source: exact
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: 3col-parallel
|
||||
fig_content_affinity: capability_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_rank: 3
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.7648
|
||||
v3_r5_breakdown:
|
||||
total: 0.7648
|
||||
layout_compat: 0.662
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.78
|
||||
layout_match_source: alternative
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: 3col-cards
|
||||
fig_content_affinity: capability_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281197'
|
||||
frame_number: 19
|
||||
v1_rank: 5
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.435
|
||||
v3_r5_breakdown:
|
||||
total: 0.435
|
||||
layout_compat: 0.45
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.75
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.3
|
||||
content_affinity_source: secondary_overlap
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: 3col-compare
|
||||
fig_content_affinity: stakeholder_roles
|
||||
fig_structure_intent: singleton_emphasis
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281192'
|
||||
frame_number: 15
|
||||
v1_rank: 2
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.23
|
||||
v3_r5_breakdown:
|
||||
total: 0.23
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: policy-4card-plus-list
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 4
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.0
|
||||
v3_r5_breakdown:
|
||||
total: 0.0
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: central-split
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: state_transition
|
||||
v3_r5_rank: 5
|
||||
03-2:
|
||||
answer_frame_number: 29
|
||||
mdx_title: 2. Process의 혁신과 Product의 변화
|
||||
mdx_profile:
|
||||
section_id: 03-2
|
||||
title: 2. Process의 혁신과 Product의 변화
|
||||
layout: compare-2banner
|
||||
family: diagram
|
||||
relation_type: compare
|
||||
cardinality:
|
||||
ideal: 2
|
||||
content_affinity:
|
||||
primary: before_after_change
|
||||
secondary:
|
||||
- capability_requirements
|
||||
- concept_definition
|
||||
structure_intent:
|
||||
primary: state_transition
|
||||
secondary:
|
||||
- sequence
|
||||
validator_log:
|
||||
strong_aff_matches:
|
||||
before_after_change:
|
||||
- Process의 혁신
|
||||
- Analogue
|
||||
strong_int_matches:
|
||||
state_transition:
|
||||
- Process의 혁신
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281210'
|
||||
frame_number: 29
|
||||
v1_rank: 1
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.74
|
||||
v3_r5_breakdown:
|
||||
total: 0.74
|
||||
layout_compat: 0.35
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 1.0
|
||||
structure_intent_source: primary_match
|
||||
fig_layout: compare-2banner-top-2col-bottom
|
||||
fig_content_affinity: before_after_change
|
||||
fig_structure_intent: state_transition
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 4
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.21
|
||||
v3_r5_breakdown:
|
||||
total: 0.21
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: 3-category
|
||||
fig_content_affinity: concept_definition
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 2
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.1
|
||||
v3_r5_breakdown:
|
||||
total: 0.1
|
||||
layout_compat: 0.25
|
||||
layout_breakdown:
|
||||
family: 1.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: cycle-3way
|
||||
fig_content_affinity: interrelation
|
||||
fig_structure_intent: cycle_interrelation
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281179'
|
||||
frame_number: 8
|
||||
v1_rank: 5
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.0
|
||||
v3_r5_breakdown:
|
||||
total: 0.0
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: 3-section
|
||||
fig_content_affinity: comparative_matrix
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 3
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.0
|
||||
v3_r5_breakdown:
|
||||
total: 0.0
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: list-stacked
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 5
|
||||
01-1:
|
||||
answer_frame_number: null
|
||||
mdx_title: 1. 용어 정의
|
||||
mdx_profile:
|
||||
section_id: 01-1
|
||||
title: 1. 용어 정의
|
||||
layout: 3col-parallel
|
||||
family: card
|
||||
relation_type: parallel
|
||||
cardinality:
|
||||
ideal: 3
|
||||
content_affinity:
|
||||
primary: goal_axes
|
||||
secondary:
|
||||
- concept_definition
|
||||
- capability_requirements
|
||||
structure_intent:
|
||||
primary: multi_parallel
|
||||
secondary: []
|
||||
validator_log:
|
||||
strong_aff_matches: {}
|
||||
strong_int_matches: {}
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_rank: 5
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.7248
|
||||
v3_r5_breakdown:
|
||||
total: 0.7248
|
||||
layout_compat: 0.662
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.78
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 1.0
|
||||
structure_intent_source: primary_match
|
||||
fig_layout: 3col-cards
|
||||
fig_content_affinity: capability_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 3
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.7152
|
||||
v3_r5_breakdown:
|
||||
total: 0.7152
|
||||
layout_compat: 0.638
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.72
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 1.0
|
||||
structure_intent_source: primary_match
|
||||
fig_layout: 3-category
|
||||
fig_content_affinity: concept_definition
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 4
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.612
|
||||
v3_r5_breakdown:
|
||||
total: 0.612
|
||||
layout_compat: 0.63
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.7
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: cycle-3way
|
||||
fig_content_affinity: interrelation
|
||||
fig_structure_intent: cycle_interrelation
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281172'
|
||||
frame_number: 1
|
||||
v1_rank: 1
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.29
|
||||
v3_r5_breakdown:
|
||||
total: 0.29
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: circular-nodes
|
||||
fig_content_affinity: interrelation
|
||||
fig_structure_intent: cycle_interrelation
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 2
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.0
|
||||
v3_r5_breakdown:
|
||||
total: 0.0
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: central-split
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: state_transition
|
||||
v3_r5_rank: 5
|
||||
02-1:
|
||||
answer_frame_number: null
|
||||
mdx_title: 1. DX의 궁극적 목표
|
||||
mdx_profile:
|
||||
section_id: 02-1
|
||||
title: 1. DX의 궁극적 목표
|
||||
layout: 3col-parallel
|
||||
family: card
|
||||
relation_type: parallel
|
||||
cardinality:
|
||||
ideal: 3
|
||||
content_affinity:
|
||||
primary: goal_axes
|
||||
secondary:
|
||||
- before_after_change
|
||||
- comparative_matrix
|
||||
structure_intent:
|
||||
primary: multi_parallel
|
||||
secondary: []
|
||||
validator_log:
|
||||
strong_aff_matches:
|
||||
before_after_change:
|
||||
- Analogue
|
||||
strong_int_matches: {}
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281191'
|
||||
frame_number: 14
|
||||
v1_rank: 2
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.66
|
||||
v3_r5_breakdown:
|
||||
total: 0.66
|
||||
layout_compat: 0.75
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 1.0
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: persona-3col
|
||||
fig_content_affinity: persona_benefit
|
||||
fig_structure_intent: persona_mapping
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281198'
|
||||
frame_number: 20
|
||||
v1_rank: 5
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.6198
|
||||
v3_r5_breakdown:
|
||||
total: 0.6198
|
||||
layout_compat: 0.662
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.78
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.3
|
||||
content_affinity_source: secondary_overlap
|
||||
structure_intent: 1.0
|
||||
structure_intent_source: primary_match
|
||||
fig_layout: 3col-cards
|
||||
fig_content_affinity: capability_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281189'
|
||||
frame_number: 12
|
||||
v1_rank: 1
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.612
|
||||
v3_r5_breakdown:
|
||||
total: 0.612
|
||||
layout_compat: 0.63
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 1.0
|
||||
layout_match: 0.7
|
||||
layout_match_source: alternative
|
||||
content_affinity: 0.6
|
||||
content_affinity_source: cross_primary_secondary
|
||||
structure_intent: 0.6
|
||||
structure_intent_source: cross_primary_secondary
|
||||
fig_layout: cycle-3way
|
||||
fig_content_affinity: interrelation
|
||||
fig_structure_intent: cycle_interrelation
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_rank: 4
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.33
|
||||
v3_r5_breakdown:
|
||||
total: 0.33
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 1.0
|
||||
structure_intent_source: primary_match
|
||||
fig_layout: list-numbered
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281208'
|
||||
frame_number: 27
|
||||
v1_rank: 3
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.0
|
||||
v3_r5_breakdown:
|
||||
total: 0.0
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: central-split
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: state_transition
|
||||
v3_r5_rank: 5
|
||||
02-2.1:
|
||||
answer_frame_number: null
|
||||
mdx_title: 2.1 업무 수행 과정(Process)의 변화
|
||||
mdx_profile:
|
||||
section_id: 02-2.1
|
||||
title: 2.1 업무 수행 과정(Process)의 변화
|
||||
layout: single-column
|
||||
family: list
|
||||
relation_type: parallel
|
||||
cardinality:
|
||||
ideal: 1
|
||||
content_affinity:
|
||||
primary: concept_definition
|
||||
secondary: []
|
||||
structure_intent:
|
||||
primary: singleton_emphasis
|
||||
secondary: []
|
||||
validator_log:
|
||||
strong_aff_matches: {}
|
||||
strong_int_matches: {}
|
||||
v3_r5_rerank:
|
||||
- frame_id: '1171281182'
|
||||
frame_number: 11
|
||||
v1_rank: 1
|
||||
v2_rank: 4
|
||||
v3_r5_total: 0.43
|
||||
v3_r5_breakdown:
|
||||
total: 0.43
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 1.0
|
||||
content_affinity_source: primary_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: 3-category
|
||||
fig_content_affinity: concept_definition
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 1
|
||||
- frame_id: '1171281174'
|
||||
frame_number: 3
|
||||
v1_rank: 3
|
||||
v2_rank: 3
|
||||
v3_r5_total: 0.18
|
||||
v3_r5_breakdown:
|
||||
total: 0.18
|
||||
layout_compat: 0.45
|
||||
layout_breakdown:
|
||||
family: 1.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: list-numbered
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 2
|
||||
- frame_id: '1171281180'
|
||||
frame_number: 9
|
||||
v1_rank: 5
|
||||
v2_rank: 5
|
||||
v3_r5_total: 0.18
|
||||
v3_r5_breakdown:
|
||||
total: 0.18
|
||||
layout_compat: 0.45
|
||||
layout_breakdown:
|
||||
family: 1.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: list-stacked
|
||||
fig_content_affinity: policy_requirements
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 3
|
||||
- frame_id: '1171281179'
|
||||
frame_number: 8
|
||||
v1_rank: 2
|
||||
v2_rank: 2
|
||||
v3_r5_total: 0.08
|
||||
v3_r5_breakdown:
|
||||
total: 0.08
|
||||
layout_compat: 0.2
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 1.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: 3-section
|
||||
fig_content_affinity: comparative_matrix
|
||||
fig_structure_intent: multi_parallel
|
||||
v3_r5_rank: 4
|
||||
- frame_id: '1171281210'
|
||||
frame_number: 29
|
||||
v1_rank: 4
|
||||
v2_rank: 1
|
||||
v3_r5_total: 0.0
|
||||
v3_r5_breakdown:
|
||||
total: 0.0
|
||||
layout_compat: 0.0
|
||||
layout_breakdown:
|
||||
family: 0.0
|
||||
relation_type: 0.0
|
||||
cardinality: 0.0
|
||||
layout_match: 0.0
|
||||
layout_match_source: none
|
||||
content_affinity: 0.0
|
||||
content_affinity_source: no_match
|
||||
structure_intent: 0.0
|
||||
structure_intent_source: no_match
|
||||
fig_layout: compare-2banner-top-2col-bottom
|
||||
fig_content_affinity: before_after_change
|
||||
fig_structure_intent: state_transition
|
||||
v3_r5_rank: 5
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user