"""Pipeline Step 13-r2 — 회의 자료 최종 세트 (7개 파일, A4 규격). 사용자 확정 구조 (5-step narrative): 1. 개요 + 기존 라이브러리/모델 시도 (tf-idf, sobert, BM25, cross-encoder) 2. AI 디자인 분석 시도 3. 키워드 전처리 + 가중치 매칭 → 첨부 1, 2 4. 키워드 + 의미/구조/적용 가능성 → 첨부 3, 4, 5 5. Figma 프레임별 추출 및 정리 방안 → 첨부 6 산출: MEETING_BRIEF.html (1장 요약 — 업데이트) ATTACH_01_KEYWORD_ORGANIZATION.html ATTACH_02_KEYWORD_MATCHING.html ATTACH_03_CASCADE_RESULT.html ATTACH_04_STRUCTURE_VOCAB.html (신규) ATTACH_05_SAMPLES.html (신규) ATTACH_06_DB_ORGANIZATION.html """ import base64 import re import sys from collections import Counter from pathlib import Path import yaml HERE = Path(__file__).parent PNG_DIR = (HERE / '..' / '..' / 'data' / 'figma_previews').resolve() sys.path.insert(0, str(HERE)) MEETING_BRIEF = HERE / 'MEETING_BRIEF.html' ATTACH_01 = HERE / 'ATTACH_01_KEYWORD_ORGANIZATION.html' ATTACH_02 = HERE / 'ATTACH_02_KEYWORD_MATCHING.html' ATTACH_03 = HERE / 'ATTACH_03_CASCADE_RESULT.html' ATTACH_04 = HERE / 'ATTACH_04_STRUCTURE_VOCAB.html' ATTACH_05 = HERE / 'ATTACH_05_SAMPLES.html' ATTACH_06 = HERE / 'ATTACH_06_DB_ORGANIZATION.html' TARGET_SIDS = ['01-2', '02-2.2', '03-1', '03-2'] _IMG_CACHE = {} def img_uri(fn): if fn in _IMG_CACHE: return _IMG_CACHE[fn] p = PNG_DIR / f'{fn:02d}.png' if not p.exists(): return '' uri = 'data:image/png;base64,' + base64.b64encode(p.read_bytes()).decode('ascii') _IMG_CACHE[fn] = uri return uri def extract_mdx_raw(sid): from pipeline_01_extract_nodes import MDX_SECTIONS, MDX_DIR 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[1:]) def mdx_excerpt(raw, max_chars=220): """MDX 본문에서 **실제 문장** 2-3줄 추출. JSX style block / 이미지 마크다운 / HTML 태그 / 표 구분자는 모두 제외. List item (`*`, `-`) 은 유지 — 실제 콘텐츠가 여기 있는 경우 많음. """ lines = raw.split('\n') buf = [] in_jsx_block = False for ln in lines: s = ln.strip() # JSX 블록 진입/종료 (
| # | 파일 | 내용 |
|---|---|---|
| 1 | ATTACH_01_KEYWORD_ORGANIZATION.html | Figma 프레임별 3-layer 키워드 정리 (핵심/세트/연관) |
| 2 | ATTACH_02_KEYWORD_MATCHING.html | V1 키워드 매칭 결과 + 한계 사례 (BEPs 밖 콘텐츠) |
| 3 | ATTACH_03_CASCADE_RESULT.html | V1 키워드 → + 의미 → + 구조 → + 적용 가능성 단계별 결과 |
| 4 | ATTACH_04_STRUCTURE_VOCAB.html | 구조 매칭 키워드 정의 (content_affinity 12, structure_intent 9, alt_patterns) |
| 5 | ATTACH_05_SAMPLES.html | fit-v2 적합 샘플 3건 (V1 강함/약함/reject 각 사례) |
| 6 | ATTACH_06_DB_ORGANIZATION.html | Figma DB 형태 (Frame 예시 + 32 프레임 요약) |
{score:.3f})| 신호 | 의미 | 가중치 |
|---|---|---|
| 단독 대표 키워드 | 특정 프레임을 혼자서도 강하게 가리키는 키워드 매칭 | 0.30 |
| 세트 키워드 묶음 | 프레임 원문 한 줄에서 함께 등장하는 키워드 조합 매칭 | 0.50 |
| 연관 키워드 | 참고 키워드 겹침 정도 | 0.20 |
{top_score:.3f} '
f'[{strength}] · 1-2위 격차 {gap:.3f}{hd_top["matching_score"]:.3f}) → '
f'사용자 검수 결과 비합리. '
f'BEPs 프레임 라이브러리에 원래 이 유형의 콘텐츠가 없어서, 키워드 우연 일치가 '
f'1위로 올라와 버림. V4 에서도 reject 판정. '
f'→ 기존 프레임에 없는 콘텐츠 유형 = 재구성 필요 신호.| {v} {desc} | ')
body.append('
|---|
| {inner} | ' fn1 = v1_top['frame_number'] body.append(cell(fn1, f"score {v1_top['matching_score']:.3f}", fn1 == answer)) fn2 = v2_top['frame_number'] body.append(cell(fn2, f"sem {v2_top['semantic_score']:.3f}", fn2 == answer)) fn3 = v3_top['frame_number'] body.append(cell(fn3, f"struct {v3_top['v3_r5_total']:.3f}", fn3 == answer)) fn4 = v4_top['frame_number'] label = v4_top['label'] extra4 = f'{label}' body.append(cell(fn4, f"conf {v4_top['confidence']:.3f}", fn4 == answer, extra4)) body.append('
| 값 | 의미 | 예시 (MDX 또는 Frame) |
|---|---|---|
{v} | {m} | {e} |
| 값 | 의미 | 대표 layout |
|---|---|---|
{v} | {m} | {e} |
각 프레임이 "이 내용을 못 담을 때 어떤 다른 layout 이 대체 가능한가" 를 '
'명시. 기존 방식은 수작업 호환도 테이블(_COMPAT, 커버리지 26%)이었고, '
'새 스키마는 프레임별 alternative_patterns 필드로 파생.
| 예시 프레임 | 기본 layout | 대안 패턴 |
|---|---|---|
| Frame 12 · 건설산업 목표 | cycle-3way | '
'3col-parallel (0.7, 수동 시드 — 순환 대신 평면 병렬), '
'circular-nodes (0.6, 수동 시드 — radial 대안) |
| Frame 18 · BIM vs DX | compare-rows | '
'table-2col, compare-2col, 2col-paired '
'(파생 — 2-way 비교 계열) |
| Frame 29 · Process 혁신 | compare-2banner-top-2col-bottom | '
'compare-2col, table-2col, 2col-paired (파생) |
| 이 축이 없으면 발생하는 문제 | 이 축이 해결하는 것 |
|---|---|
| "3열 병렬" 프레임끼리 구분 불가 — Frame 12 (목표 3축) 와 Frame 20 (S/W 필수 3관점) 둘 다 3열인데 의미 다름 | ' 'content_affinity 가 goal_axes vs capability_requirements 로 구분 |
| "compare-rows" 와 "table-2col" 이 다른 layout 처럼 보이지만 실제론 같은 2-way 비교 | ' 'alternative_patterns 에 명시 — 자동 대체 가능 |
"혁신", "전환" 같은 단어가 과매칭 — 거의 모든 MDX 에 state_transition 붙음 | '
'STRONG 패턴(AS-IS/TO-BE/이중 변환) 없이는 인정 안 함 — validator 강화 |
| 축 | 값 |
|---|---|
| V1 점수 (키워드) | {v1_top["matching_score"]:.3f} → Frame {v1_top["frame_number"]} |
| V4 1위 | Frame {v4_top["frame_number"]} conf {v4_top["confidence"]:.3f} '
f'{v4_top["label"]} |
| V4 axes 분해 | '
f'anchor {v4_top["axes"]["anchor"]} · '
f'cardinality {v4_top["axes"]["cardinality"]} · '
f'relation {v4_top["axes"]["relation"]} · '
f'slot {v4_top["axes"]["slot"]} · '
f'content {v4_top["axes"]["content"]} · '
f'penalty {v4_top["penalty"]}'
f' |
0.937 으로 매우 강함. '
'V4 axes 도 모두 1.0 에 가깝고 label use_as_is. '
'fit-v2 의 multi-axis 판정이 "이 프레임은 그대로 써도 된다" 를 명확히 확증. '
'V1 점수만 있어도 맞지만, V4 의 reject 여부 확인이 있어야 실사용 보장.'),
}))
# Case 2: V1 약함, V4 use_as_is — V4 가 V1 교정
body.append(render_case('03-1', '사례 2 — V1 약함 + V4 use_as_is (V4 가 교정)', {
'note_type': 'ok-note',
'note': ('해석: V1 점수 0.598 로 경계 이하(< 0.7). 키워드만 보면 '
'애매함. V4 의 content_affinity (capability_requirements 매칭) + '
'cardinality + relation + slot 종합 판정으로 Frame 13 이 use_as_is 로 확정. '
'fit-v2 가 V1 의 약한 신호를 보강해 정답에 도달.'),
}))
# Case 3: V1 강함, V4 reject — 쓸 수 없다는 신호
body.append(render_case('02-2.2', '사례 3 — V1 강함 + V4 reject (재구성 필요 신호)', {
'note_type': 'fail-note',
'note': ('해석: V1 점수 0.840 강하고 1위 Frame 14 는 정답 (사용자 검수). '
'그러나 V4 confidence 는 0.293 → reject. '
'axes 분해에서 anchor 와 content 는 맞지만 cardinality/relation/slot 의 점수가 낮음. '
'즉 "정답으로 지목됐지만 그대로 쓰기엔 구조가 부족". '
'fit-v2 가 점수 1위 ≠ 실사용 가능을 드러내는 핵심 사례. '
'단순 구조 매칭 (V3) 으로는 이 신호를 잡을 수 없음.'),
}))
body.append('layout | {vp["layout"]} |
family | {vp.get("family")} |
relation_type | {vp.get("relation_type")} |
cardinality | ideal {card.get("ideal")} / min {card.get("min")} / max {card.get("max")} |
content_affinity.primary | {aff["primary"]} |
content_affinity.secondary | ' +
', '.join(f'{s}' for s in aff.get('secondary', []) or ['(없음)']) + ' |
structure_intent.primary | {intent["primary"]} |
structure_intent.secondary | ' +
', '.join(f'{s}' for s in intent.get('secondary', []) or ['(없음)']) + ' |
| pattern | conf | source | reason |
|---|---|---|---|
{a["pattern"]} | {a["confidence"]} | ' f'{a.get("source", "derived")} | {a["reason"]} |
| id | type | required |
|---|---|---|
{s["id"]} | {s.get("type")} | ' f'{s.get("required", False)} |
| primary | 수 |
|---|---|
{k} | {c} |
| primary | 수 |
|---|---|
{k} | {c} |