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

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

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

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

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

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

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

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

417 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""MDX04 F16 override slide-fit preview — slide_fit_preview, NOT a Phase Z final.
배경:
- V4 top1 = F26. 사용자 semantic review 로 F16 채택
- 사유: MDX04 04-2.* 는 4-issue diagnostic 구조 → F16 quadrant pattern 적합
- F26 figma 1:1 변환 부재 (별도 작업 보류)
- anchor 보정 / detect_mdx 수정 / v4_full32_result.yaml 변경 모두 없음
매핑 (사용자 결정 — B 수정판, 그대로 유지):
04-2.1 (4) + 04-2.2 (4) = 8 항목을 4 원인군으로 그룹핑. 04-2.2 보존.
레이아웃 전환 (composition_preview → slide_fit_preview):
이전: 1280×1230 비표준 (composition preview)
현재: 1280×720 표준 슬라이드 (slide_fit_preview)
├ title bar (1280×56)
├ body (1200×590)
│ ├ zone-left (340×590) = 04-1 compact 5-card stack
│ └ zone-right (840×590) = F16 quadrant zone-fit (4분면 + center quote)
└ footer pill (1280×48)
F16 native dim (1280×1015) 폐기. zone (840×590) 에 맞게 좌표 재계산. 폰트 축소.
"""
import json
import re
import sys
from datetime import datetime
from html import escape
from pathlib import Path
import yaml
from jinja2 import Environment, FileSystemLoader, select_autoescape
ROOT = Path(__file__).resolve().parents[2]
MDX_PATH = ROOT / "samples" / "mdx_batch" / "04.mdx"
V4_RESULT = ROOT / "tests" / "matching" / "v4_full32_result.yaml"
RUN_DIR = ROOT / "data" / "runs" / "mdx04_f16_override"
TEMPLATES_DIR = RUN_DIR / "templates"
# ─── 그룹핑 정의 (사용자 예시 그대로) ──────────────────────────
GROUPING_RULE = {
'description': '04-2.1 (4 정책 항목) + 04-2.2 (4 조직 항목) = 8 항목을 4 원인군으로 그룹핑. F16 4 분면 ribbon = 그룹명.',
'reason': 'user wants 04-2.2 보존 + F16 4 분면 디자인 활용. 1:1 짝짓기 강제 회피.',
'groups': [
{
'quadrant': 'q1',
'name': '정책 집행 / 제도 운용 문제',
'items': [
{'source': '04-2.1', 'index': 0}, # 실질적 기술 경쟁을 저해하는 정책 집행
{'source': '04-2.1', 'index': 1}, # 적용 효과가 있는 사례도 없이 방침부터 도입
],
},
{
'quadrant': 'q2',
'name': '개념 이해 부족',
'items': [
{'source': '04-2.1', 'index': 2}, # 엔지니어링 S/W에 대한 개념 부재
{'source': '04-2.2', 'index': 0}, # 공학적 개념 정립 부재
{'source': '04-2.2', 'index': 2}, # DX/BIM의 근본 취지와 목표의 이해 부족
],
},
{
'quadrant': 'q3',
'name': '기술 투자 / 본업 기술력 부족',
'items': [
{'source': '04-2.1', 'index': 3}, # 기술투자(R&D) 없는 성과 창출 기대
{'source': '04-2.2', 'index': 1}, # '본업 기술력 확보' 우선의 개념 부재
],
},
{
'quadrant': 'q4',
'name': '조직 / 수행 역량 문제',
'items': [
{'source': '04-2.2', 'index': 3}, # 과거의 타성에 머무르고 있는 기술자 집단
],
},
],
}
# ─── MDX 04 파싱 ────────────────────────────────────────────────
RE_SUBSECTION_HEAD = re.compile(r'^###\s+(\d+\.\d+)\s+(.+)$', re.MULTILINE)
RE_TOP_BULLET = re.compile(r'^-\s+\*\*([^*]+)\*\*\s*$')
def extract_subsection_items(text, num_label):
lines = text.split('\n')
start = None
for i, ln in enumerate(lines):
m = RE_SUBSECTION_HEAD.match(ln.strip())
if m and m.group(1) == num_label:
start = i
break
if start is None:
return None, []
end = len(lines)
for j in range(start + 1, len(lines)):
s = lines[j].strip()
if RE_SUBSECTION_HEAD.match(s) or s == '---':
end = j
break
section_title = lines[start].lstrip('# ').strip()
body_lines = lines[start + 1:end]
items = []
cur = None
for ln in body_lines:
stripped = ln.strip()
m = RE_TOP_BULLET.match(stripped)
if m:
if cur is not None:
items.append(cur)
cur = {'headline': m.group(1).strip(), 'subs': []}
continue
m2 = re.match(r'^-\s+(.+)$', stripped)
if m2 and cur is not None and not stripped.startswith('- **'):
cur['subs'].append(m2.group(1).strip())
if cur is not None:
items.append(cur)
return section_title, items
def extract_section_04_1_cards(text):
m = re.search(r'## 1\. DX에 대한 인식(.*?)(?=^## 2\.)', text, re.DOTALL | re.MULTILINE)
if not m:
return None, []
body = m.group(1)
cards = []
h3_iter = list(re.finditer(r'<h3[^>]*>([^<]+)</h3>', body))
for idx, h3m in enumerate(h3_iter):
label = h3m.group(1).strip()
section_end = h3_iter[idx + 1].start() if idx + 1 < len(h3_iter) else len(body)
section_text = body[h3m.end():section_end]
# 인용 (첫 <p> 의 따옴표 텍스트)
quote_m = re.search(r'<p[^>]*>(?:["“])(.+?)(?:["”])</p>', section_text, re.DOTALL)
if not quote_m:
quote_m = re.search(r'<p[^>]*>([^<]+)</p>', section_text, re.DOTALL)
quote = quote_m.group(1).strip() if quote_m else ''
bullets = [b.strip() for b in re.findall(r'<li[^>]*>([^<]+)</li>', section_text)]
cards.append({'label': label, 'quote': quote, 'bullets': bullets})
return '1. DX에 대한 인식', cards
# ─── F16 grouped mapper ────────────────────────────────────────
def map_to_f16_grouped(items_2_1, items_2_2, slide_title):
"""8 items (2.1 4 + 2.2 4) → 4 quadrant groups (사용자 그룹핑 룰 적용)."""
source_map = {'04-2.1': items_2_1, '04-2.2': items_2_2}
payload = {'center_quote': slide_title}
for group in GROUPING_RULE['groups']:
q = group['quadrant']
items_for_q = []
for ref in group['items']:
src_items = source_map[ref['source']]
idx = ref['index']
if idx < len(src_items):
src_item = src_items[idx]
items_for_q.append({
'source': '[' + ref['source'].replace('04-', '') + ']',
'headline': src_item['headline'],
'subs': src_item['subs'],
})
payload[f'{q}_label'] = group['name']
payload[f'{q}_items'] = items_for_q
return payload
def map_to_5card_compact_slots(cards, section_title):
return {'section_title': section_title, 'cards': cards}
# ─── V4 metadata lookup ───────────────────────────────────────
def get_top1(v4, sid):
sec = v4.get('mdx_sections', {}).get(sid)
if not sec:
return None
j = sec.get('judgments_full32', [])
return j[0] if j else None
def get_frame_judgment(v4, sid, frame_number):
sec = v4.get('mdx_sections', {}).get(sid)
if not sec:
return None
for e in sec.get('judgments_full32', []):
if e['frame_number'] == frame_number:
return e
return None
# ─── 메인 ──────────────────────────────────────────────────────
def main():
if not MDX_PATH.exists():
print(f"ERROR: MDX 04 not found at {MDX_PATH}", file=sys.stderr)
sys.exit(1)
if not V4_RESULT.exists():
print(f"ERROR: V4 result not found at {V4_RESULT}", file=sys.stderr)
sys.exit(1)
mdx_text = MDX_PATH.read_text(encoding='utf-8')
v4 = yaml.safe_load(V4_RESULT.read_text(encoding='utf-8'))
title_2_1, items_2_1 = extract_subsection_items(mdx_text, '2.1')
title_2_2, items_2_2 = extract_subsection_items(mdx_text, '2.2')
title_1, cards_1 = extract_section_04_1_cards(mdx_text)
env = Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=select_autoescape(['html', 'xml']),
)
f16_zonefit_tpl = env.get_template("bim_issues_quadrant_four_zonefit.html.j2")
cards5_left_tpl = env.get_template("cards_5_left_zone.html.j2")
slide_fit_tpl = env.get_template("slide_fit_base.html.j2")
# 04-2 통합 (그룹핑) → F16 zone-fit
payload_f16 = map_to_f16_grouped(items_2_1, items_2_2, slide_title='DX 지연<br>요인')
html_f16_zonefit = f16_zonefit_tpl.render(slot_payload=payload_f16)
# 04-1 → 5-card left zone
payload_cards = map_to_5card_compact_slots(cards_1, section_title=title_1)
html_cards_left = cards5_left_tpl.render(slot_payload=payload_cards)
# slide_fit base 조립 (1280×720)
slide_fit_html = slide_fit_tpl.render(
slide_title='4. DX 지연 요인',
slide_meta='F16 user_semantic_override · slide_fit_preview',
zone_left=html_cards_left,
zone_right=html_f16_zonefit,
slide_footer='검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족이 DX 지연을 반복시킨다',
)
# 통합 1 슬라이드 페이지 (banner + slide_fit + metadata)
timestamp = datetime.now().isoformat(timespec='seconds')
page_html = f'''<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>MDX04 1280×720 slide_fit · F16 user_semantic_override</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: 'Noto Sans KR', 'Pretendard', sans-serif; background: #e8ecf0; padding: 24px; }}
.preview-banner {{ max-width: 1280px; margin: 0 auto 16px; background: #fff7ed; border: 2px solid #f59e0b; border-radius: 8px; padding: 12px 16px; font-size: 12px; color: #92400e; line-height: 1.6; }}
.preview-banner strong {{ color: #78350f; }}
.preview-banner ul {{ margin-top: 6px; padding-left: 18px; font-family: monospace; font-size: 11px; }}
.slide-wrap {{ display: flex; justify-content: center; }}
</style>
</head>
<body>
<div class="preview-banner">
<strong>MDX04 slide_fit_preview · 1280×720 표준 (NOT a Phase Z final)</strong><br>
composition_preview (1280×1230) → <strong>slide_fit_preview (1280×720)</strong> 전환.
같은 grouping rule 유지 (04-2.* 8 항목 → 4 원인군). 04-2.2 보존. F16 native dim 폐기, zone-fit 적용.
<ul>
<li>layout = title (56) + body (1200×590, left 340 + right 840) + footer pill (48)</li>
<li>zone-left = 04-1 compact 5-card stack</li>
<li>zone-right = F16 quadrant zone-fit (4분면 + center quote)</li>
<li>q1 = 정책 집행 / 제도 운용 (2.1×2) · q2 = 개념 이해 부족 (2.1×1 + 2.2×2)</li>
<li>q3 = 기술 투자 / 본업 기술력 부족 (2.1×1 + 2.2×1) · q4 = 조직 / 수행 역량 (2.2×1)</li>
</ul>
</div>
<div class="slide-wrap">
{slide_fit_html}
</div>
</body>
</html>
'''
(RUN_DIR / "index.html").write_text(page_html, encoding='utf-8')
# 단독 slide_fit (banner 없이 슬라이드 자체만)
standalone_slide = f'''<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>MDX04 1280×720 slide_fit (standalone)</title>
<style>* {{margin:0;padding:0;box-sizing:border-box}} body {{font-family:'Noto Sans KR',sans-serif;background:#e8ecf0;padding:20px;display:flex;justify-content:center}}</style></head><body>
{slide_fit_html}
</body></html>
'''
(RUN_DIR / "slide_1280x720.html").write_text(standalone_slide, encoding='utf-8')
# debug.json
top1_2_1 = get_top1(v4, '04-2.1')
top1_2_2 = get_top1(v4, '04-2.2')
top1_1 = get_top1(v4, '04-1')
f16_2_1 = get_frame_judgment(v4, '04-2.1', 16)
f16_2_2 = get_frame_judgment(v4, '04-2.2', 16)
# grouping coverage 검증 (모든 8 항목 사용됐는지)
used = set()
for g in GROUPING_RULE['groups']:
for ref in g['items']:
used.add((ref['source'], ref['index']))
expected = set([('04-2.1', i) for i in range(len(items_2_1))]
+ [('04-2.2', i) for i in range(len(items_2_2))])
missing = sorted(expected - used)
extra = sorted(used - expected)
debug = {
'kind': 'mdx04_f16_override_slide_fit',
'preview_stage': 'slide_fit_preview',
'transition_from': 'composition_preview (1280×1230 비표준)',
'transition_to': 'slide_fit_preview (1280×720 표준)',
'transition_note': '같은 grouping rule 유지. F16 native height (1015px) 폐기. zone-fit 좌표 재계산. 폰트 축소.',
'is_phase_z_final': False,
'is_diagnostic': True,
'is_preview_or_result_candidate': True,
'generated_at': timestamp,
'v4_source': str(V4_RESULT.relative_to(ROOT)),
'mdx_source': str(MDX_PATH.relative_to(ROOT)),
'integrated_slide': True,
'layout': {
'slide_dimensions': '1280×720',
'title_bar_height': 56,
'body': {'width': 1200, 'height': 590, 'left_zone': 340, 'right_zone': 840, 'gap': 20},
'footer_pill_height': 48,
'zone_left': '04-1 compact 5-card stack (frame library gap)',
'zone_right': '04-2 통합 F16 quadrant zone-fit (grouped)',
'mdx_one_slide_principle': True,
'standard_16_9': True,
},
'override_decision': {
'selected_frame_source': 'user_semantic_override',
'selected_frame': 'F16',
'selected_template_id': 'bim_issues_quadrant_four',
'reason': 'F16 quadrant pattern semantically/visually appropriate for MDX04 04-2.* '
'(four-issue diagnostic structure). V4 top1 F26 figma 변환 부재 + semantic '
'review 에서 F16 가 더 적합 판단.',
},
'grouping_rule': GROUPING_RULE,
'grouping_coverage': {
'total_items': len(items_2_1) + len(items_2_2),
'mapped_items': len(used),
'missing': [{'source': s, 'index': i} for s, i in missing],
'extra': [{'source': s, 'index': i} for s, i in extra],
'all_items_preserved': not missing,
},
'sections': {
'04-2.1': {
'mdx_title': title_2_1,
'item_count': len(items_2_1),
'v4_top1': {
'frame_number': top1_2_1['frame_number'],
'template_id': top1_2_1['template_id'],
'label': top1_2_1['label'],
'confidence': top1_2_1['confidence'],
},
'selected_frame': 16,
'original_label': f16_2_1['label'] if f16_2_1 else None,
'original_confidence': f16_2_1['confidence'] if f16_2_1 else None,
},
'04-2.2': {
'mdx_title': title_2_2,
'item_count': len(items_2_2),
'v4_top1': {
'frame_number': top1_2_2['frame_number'],
'template_id': top1_2_2['template_id'],
'label': top1_2_2['label'],
'confidence': top1_2_2['confidence'],
},
'selected_frame': 16,
'original_label': f16_2_2['label'] if f16_2_2 else None,
'original_confidence': f16_2_2['confidence'] if f16_2_2 else None,
'preserved_in_grouping': True,
},
'04-1': {
'mdx_title': title_1,
'card_count': len(cards_1),
'v4_top1': {
'frame_number': top1_1['frame_number'],
'template_id': top1_1['template_id'],
'label': top1_1['label'],
'confidence': top1_1['confidence'],
} if top1_1 else None,
'selected_frame': None,
'override_note': '5-card library gap (32 frame DB 에 cardinality.ideal=5 frame 부재). '
'compact 5-column grid 로 통합 슬라이드 상단에 배치.',
},
},
'caveats': [
'정식 Phase Z final 아님 — V4 lookup 우회',
'preview_stage = slide_fit_preview (1280×720 표준). 이전 composition_preview (1280×1230) 에서 전환',
'F16 partial template = preview 전용 (data/runs/mdx04_f16_override/templates/) — design_agent/templates/phase_z2 미수정',
'anchor 보정 / detect_mdx 수정 / v4_full32_result.yaml 변경 없음',
'04-2.1 의 F16 original_label = reject (anchor=0). override 로 진행',
'04-2.2 의 F16 original_label = restructure (사용 가능 라벨)',
'04-2.2 보존 — 그룹핑으로 8 항목 모두 분면에 매핑',
'04-1 = 5-card library gap. zone-left 에 compact stack 으로 배치',
'그룹핑 룰은 사용자 semantic 결정 (yaml/dict 로 명시). 자동 생성 아님',
'F16 native dim (1280×1015) 폐기 — zone (840×590) 에 맞춰 좌표 재계산. 폰트 14px(ribbon)/11.5px(headline)/9.5px(sub)',
'slide-fit 으로 폰트 작아짐 → 가독성 trade-off. composition_preview 와 비교 필요',
],
}
(RUN_DIR / "debug.json").write_text(
json.dumps(debug, ensure_ascii=False, indent=2), encoding='utf-8',
)
# 이전 composition_preview 산출물 정리 — slide_fit_preview 로 대체
for old in ["slide_04-2.1.html", "slide_04-2.2.html", "slide_04-1.html",
"slide_04-2_grouped.html", "slide_04-1_compact.html"]:
p = RUN_DIR / old
if p.exists():
p.unlink()
print(f"[mdx04_f16_override_slide_fit] generated:")
print(f" index : {RUN_DIR / 'index.html'}")
print(f" slide 1280×720 : {RUN_DIR / 'slide_1280x720.html'}")
print(f" debug : {RUN_DIR / 'debug.json'}")
print()
print(f"Coverage: {len(used)}/{len(expected)} items mapped, missing={list(missing)}")
print(f"Stage: composition_preview → slide_fit_preview")
if __name__ == "__main__":
main()