"""Pipeline Step 13 — 회의용 샘플 HTML 4개 생성 (A4 규격). 산출물: SAMPLE_01_V1_KEYWORD.html — V1 키워드 매칭 (MDX 섹션별) SAMPLE_02_V1_V4_CASCADE.html — V1~V4 캐스케이드 결과 SAMPLE_03_SCHEMA_STACK.html — 스키마 누적 설계 (쌓아간 이유) SAMPLE_04_FIGMA_DB.html — Figma DB 최종 형태 (Frame 12 예시) A4 포맷 (@page size + print CSS). 이미지는 base64 임베딩 (파일 독립). """ import base64 import re from collections import Counter from pathlib import Path import yaml HERE = Path(__file__).parent PNG_DIR = (HERE / '..' / '..' / 'data' / 'figma_previews').resolve() MEETING_BRIEF = HERE / 'MEETING_BRIEF.html' SAMPLE_01 = HERE / 'SAMPLE_01_V1_KEYWORD.html' SAMPLE_02 = HERE / 'SAMPLE_02_V1_V4_CASCADE.html' SAMPLE_03 = HERE / 'SAMPLE_03_SCHEMA_STACK.html' SAMPLE_04 = HERE / 'SAMPLE_04_FIGMA_DB.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): import sys sys.path.insert(0, str(HERE)) 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] title = section[0].lstrip('#').strip() return title, '\n'.join(section[1:]) def mdx_excerpt(raw, max_chars=180): """첫 일반 문단 추출.""" lines = raw.split('\n') buf = [] for ln in lines: s = ln.strip() if not s: if buf: break continue if s.startswith(('#', '|', '<', '*', '-', ':', '```', '---', '>')): continue if re.match(r'^\d+\.\s', s): continue buf.append(s) if sum(len(x) for x in buf) > max_chars: break text = ' '.join(buf) if len(text) > max_chars: text = text[:max_chars - 1] + '…' return text or '(본문 요약 추출 실패)' def build_frame_descriptions(auto): out = {} for fid, info in auto['frame_stats'].items(): fnum = info['frame_number'] for set_id, sinfo in auto['source_text_sets'].items(): if sinfo['frame_id'] == fid and sinfo['source_text_index'] == 1: out[fnum] = sinfo['source_text_raw'] break if fnum not in out: out[fnum] = f'Frame {fnum}' return out A4_CSS = """ @page { size: A4 portrait; margin: 14mm 14mm 16mm 14mm; } * { box-sizing: border-box; } body { font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", "Apple SD Gothic Neo", sans-serif; background: white; color: #1e293b; margin: 0; font-size: 10pt; line-height: 1.45; } @media print { body { margin: 0; } .avoid-break { page-break-inside: avoid; } .page-break { page-break-before: always; } } @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: 15pt; border-bottom: 2pt solid #2563eb; padding-bottom: 4pt; margin: 0 0 6pt 0; color: #0f172a; } h2 { font-size: 11pt; color: #1e293b; margin: 10pt 0 5pt; padding: 3pt 6pt; background: #e0e7ff; border-left: 3pt solid #0a6; border-radius: 2pt; } h3 { font-size: 10pt; margin: 6pt 0 3pt; color: #1a365d; font-weight: 700; } p { margin: 4pt 0; } .meta-bar { color: #64748b; font-size: 8.5pt; margin: -3pt 0 8pt; font-style: italic; } .note { background: #fef3c7; border-left: 3pt solid #f59e0b; padding: 4pt 8pt; margin: 6pt 0; font-size: 9pt; color: #78350f; border-radius: 2pt; } .mdx-card { background: #f8fafc; border: 0.5pt solid #cbd5e1; border-radius: 4pt; padding: 6pt 8pt; margin: 4pt 0; } .mdx-title { font-weight: 700; color: #0f172a; font-size: 10.5pt; margin-bottom: 2pt; } .mdx-excerpt { color: #475569; font-size: 9pt; margin: 2pt 0 4pt; line-height: 1.4; } .kw-chip { display: inline-block; padding: 1pt 6pt; background: #dbeafe; color: #1e40af; border-radius: 8pt; font-size: 8pt; margin: 1pt 2pt 1pt 0; font-family: ui-monospace, monospace; } table { border-collapse: collapse; width: 100%; font-size: 9pt; margin: 4pt 0; } 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; } code { background: #f1f5f9; padding: 0 3pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; } .frame-thumb { max-width: 100%; height: auto; border: 0.5pt solid #94a3b8; border-radius: 3pt; display: block; } .frame-cell { text-align: center; vertical-align: top; padding: 3pt; } .frame-label { font-size: 8.5pt; color: #0f172a; font-weight: 600; margin-top: 2pt; } .frame-score { font-family: ui-monospace, monospace; font-size: 8pt; color: #059669; } .frame-desc { font-size: 7.5pt; color: #64748b; line-height: 1.25; margin-top: 1pt; } .label-badge { display: inline-block; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; } .label-use_as_is { background: #d1fae5; color: #065f46; } .label-light_edit { background: #dbeafe; color: #1e40af; } .label-restructure { background: #fef3c7; color: #92400e; } .label-reject { background: #fee2e2; color: #991b1b; } .answer-box { background: #fff3cd; border: 1.5pt solid #dc2626; border-radius: 3pt; padding: 2pt; } .flex-grid { display: table; width: 100%; border-spacing: 0; } .flex-row { display: table-row; } .flex-cell { display: table-cell; vertical-align: top; padding: 2pt; } """ def html_wrap(title, body): return f""" {title} {body}""" # ============================================================ # MEETING BRIEF: 1장짜리 요약 (맨 앞) # ============================================================ def build_meeting_brief(): body = [] body.append('

MDX ↔ Figma 자동 매칭 — 현황 + Figma DB 제안

') body.append('
첨부 샘플 4장과 함께 검토. 이 한 장이 전체 요약.
') # Section 1: 지금까지 body.append('

1. 지금까지 한 것

') body.append('''

MDX 문서의 각 섹션에 가장 맞는 Figma 프레임을 자동 매칭하는 파이프라인. 텍스트 신호부터 시작해 의미·구조·적용 가능성을 단계적으로 쌓아가며 검증.

단계추가 축확인 포인트결과
V1 키워드3-layer 키워드 점수 (단독/묶음/연관)기본 후보 필터 — 얼마나 맞는가, 어디서 한계가 드러나는가TARGET 4/4 1위 정답. 단, 점수 0.7 미만 케이스(03-1 = 0.598)는 애매
V2 + 의미MDX summary ↔ Frame content 의 ko-sroberta cosine같은 의미인데 키워드가 다른 경우 잡히나Holdout 02-2.1: V1 비합리 → V2 합리 (교정 사례 확인)
V3 + 구조layout / cardinality / relation / alternative patterns구조적으로 담을 수 있는 프레임인가기존 _COMPAT 대체. 3-way 순환(Frame 12) 같은 고아 패턴도 대안 연결
V4 + 적용 가능성template-fit 멀티게이트 + penalty (adaptation / not_suits)정답 맞추기가 아니라 실제 쓸 수 있는가Holdout 합리성 평가 V4 78% (V1/V2 33%, V3 10%) — 가장 안정
''') # Section 2: 핵심 발견 body.append('

2. 핵심 발견 — 운영 로직

') body.append('''

V1 점수로 가지를 나누면 실제로 깔끔한 라우팅이 나옴.

V1 점수의사결정사례
≥ 0.70 강함V1 top1 그대로 사용 · V4 는 실사용 가능성 검증 역할 (use_as_is/reject)01-2 (0.937), 02-2.2 (0.840), 03-2 (0.705)
0.40 ~ 0.70 약함V4 가 재정렬/판정 — label 이 use_as_is / light_edit 이면 사용03-1 (0.598) → V4 로 Frame 13 확정 ✓
< 0.40 매우 약함V4 가 reject 가능성 높음 — 기존 프레임 없음, 재구성 단계로Holdout 02-2.1 (0.246) → V4 reject ← 프레임 라이브러리에 없는 유형
''') # Section 3: Figma DB 제안 body.append('

3. 제안 — Figma DB 는 이렇게 정리

') body.append('''

V4 가 쓸 수 있으려면 프레임마다 아래 필드 + 근거(evidence)가 필요.

필드무엇
visual_patternlayout · family · axis · relation_type · cardinality구조 매칭 기본 축
content_affinity12종 enum (concept_definition / goal_axes / persona_benefit / before_after_change / capability_requirements / …)같은 레이아웃이라도 의미가 다른 프레임을 구분
structure_intent9종 enum (binary_compare / multi_parallel / cycle_interrelation / state_transition / …)시각적으로 전달하려는 의도 (내용과 직교)
alternative_patterns이 레이아웃이 담지 못할 때의 대체 layout 목록 + confidence"대안 찾기" — 고아 layout (cycle-3way 등) 도 연결
slots · suits · not_suits편집 영역 + 적합/부적합 기준template-fit 판정 근거
evidence · resolved_from_review_queue라벨 근거 + 수동 검수 이력SSOT 추적성 — 왜 이 라벨이 붙었는지 언제나 확인 가능

현재 상태: 32 프레임 일괄 AI 라벨링 → 키워드 튜닝 3회 반복 → 수동 오버라이드 3건 (SSOT 확정) + review_queue 3건 잔여. 1 건 해제 (Frame 13).

''') # Footer: 샘플 목록 body.append('

첨부 샘플 4장

') body.append('''
#파일내용
1SAMPLE_01_V1_KEYWORD.htmlTARGET 4 섹션 × MDX 원문 요약 + 추출 키워드 + V1 Top-3
2SAMPLE_02_V1_V4_CASCADE.htmlTARGET 4 섹션 × V1/V2/V3/V4 단계별 Top-1 비교 + 라벨
3SAMPLE_03_SCHEMA_STACK.html왜 V1→V2→V3→V4 로 쌓았는가 (층별 해결 문제 + 대표 사례)
4SAMPLE_04_FIGMA_DB.htmlFigma DB 최종 형태 (Frame 12 전체 필드 예시 + 32 프레임 요약)
''') return html_wrap('MDX ↔ Figma 매칭 — 현황 + Figma DB 제안', ''.join(body)) # ============================================================ # SAMPLE 1: V1 키워드 매칭 # ============================================================ def build_sample1(v1, normalized, descriptions): body = [] body.append('

샘플 1 — V1 키워드 매칭

') body.append('
MDX 섹션에서 추출한 키워드로 32 Figma 프레임과 매칭. ' '각 MDX 의 Top-3 유사 프레임 + 점수.
') for sid in TARGET_SIDS: title, raw = extract_mdx_raw(sid) excerpt = mdx_excerpt(raw) tokens = normalized['mdx'][sid].get('unique_tokens', [])[:14] v1_sec = v1['mdx_sections'][sid] answer = v1_sec.get('answer_frame_number') top3 = v1_sec['rank_by_matching_score'][:3] body.append('
') body.append(f'

{sid} · {title}

') body.append('
') body.append(f'
{excerpt}
') body.append('
') body.append('추출 키워드: ') for t in tokens: body.append(f'{t}') body.append('
') body.append('
') body.append('
') for idx, r in enumerate(top3): fn = r['frame_number'] is_ans = fn == answer cell = '
' body_html = ( f'{fn:02d}' f'
Frame {fn}{"" if not is_ans else " 🎯"}
' f'
{r["matching_score"]:.3f}
' f'
{descriptions.get(fn, "")[:26]}
' ) if is_ans: body_html = f'
{body_html}
' cell += body_html + '
' body.append(cell) body.append('
') body.append('
') body.append('
정답 Frame 은 🎯 노란 박스. V1 은 "키워드 기반 후보 필터" 역할. ' '점수 0.7 이상일 때 신뢰 가능, 그 아래는 V4 까지 가야 교정됨.
') return html_wrap('샘플 1 — V1 키워드 매칭', ''.join(body)) # ============================================================ # SAMPLE 2: V1~V4 캐스케이드 # ============================================================ def build_sample2(v1, v2, v3_r5, v4_r2, descriptions): body = [] body.append('

샘플 2 — V1~V4 캐스케이드 결과

') body.append('
같은 MDX 섹션에 대해 키워드(V1) → 의미(V2) → 구조(V3) → ' '적용 가능성(V4) 순서대로 후보를 재정렬/판정. TARGET 4 섹션 모두 V4 1위 = 정답.
') for sid in TARGET_SIDS: title, raw = extract_mdx_raw(sid) excerpt = mdx_excerpt(raw, 140) v1_sec = v1['mdx_sections'][sid] answer = v1_sec.get('answer_frame_number') v1_top = v1_sec['rank_by_matching_score'][0] v2_top = sorted(v2['mdx_sections'][sid]['v2_rerank'], key=lambda x: x['v2_rank'])[0] v3_top = sorted(v3_r5['mdx_sections'][sid]['v3_r5_rerank'], key=lambda x: x['v3_r5_rank'])[0] v4_top = sorted(v4_r2['mdx_sections'][sid]['v4_r2_judgments'], key=lambda x: x['v4_r2_rank'])[0] body.append('
') body.append(f'

{sid} · {title} (정답 Frame {answer})

') body.append(f'
{excerpt}
') body.append('') for v, desc in [('V1', '키워드'), ('V2', '+ 의미'), ('V3', '+ 구조'), ('V4', '+ 판정')]: body.append(f'') body.append('') def cell(frame, score_label, is_ans=False, extra=''): fn = frame inner = ( f'{fn:02d}' f'
Frame {fn}{"" if not is_ans else " 🎯"}
' f'
{score_label}
' f'
{descriptions.get(fn, "")[:22]}
' ) if extra: inner += f'
{extra}
' if is_ans: inner = f'
{inner}
' return f'' 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('
{v}
{desc}
{inner}
') body.append('
') body.append('
핵심 관찰: V4 가 최종 의사결정. V4 label 이 ' 'use_as_is / ' 'light_edit 면 실사용 가능, ' 'reject 면 기존 프레임 부적합 → 재구성 필요 신호.
') return html_wrap('샘플 2 — V1~V4 캐스케이드', ''.join(body)) # ============================================================ # SAMPLE 3: 스키마 누적 설계 # ============================================================ def build_sample3(): body = [] body.append('

샘플 3 — V1에서 V4로의 스키마 누적 (쌓아간 이유)

') body.append('
단일 축(키워드)로는 놓치는 신호를 각 층에서 보강. ' '각 층의 필드 / 해결하는 문제 / 입력 예시.
') # 흐름도 (텍스트 기반) body.append('

누적 흐름

') body.append('''
V1
키워드
V1 + V2
+ 의미
V1 + V2 + V3
+ 구조
V1 + V2 + V3 + V4
+ 적용 가능성
''') # 층별 표 body.append('

층별 상세

') body.append('' '' '' '' '' '') body.append(''' ''') body.append('
축 / 신호해결하는 문제대표 사례 (TARGET)
V1
키워드 baseline
3-layer: 단독 대표 키워드 / 키워드 묶음 / 연관 키워드
0.30·단독 + 0.50·묶음 + 0.20·연관
가장 빠른 후보 필터. 같은 용어를 다루는 프레임을 찾음. 점수가 강하면 그대로 사용 가능. 01-2 "용어간 상호관계" → Frame 18 (0.937) ✓ · 약한 사례 03-1 (0.598) → V4 까지 가야 교정
V2
+ 의미
MDX summary ↔ Frame content 의 ko-sroberta cosine
V1 Top-K 내 재정렬
키워드가 달라도 내용/주제가 비슷한 프레임 식별. V1 이 놓치는 의미 유사성 보강. Holdout 02-2.1: V1 비합리 → V2 합리 (의미 축이 교정)
V3
+ 구조
MDX 구조(코드 추출) × Figma 구조(AI 산출)
content_affinity + structure_intent + alternative_patterns
같은 주제여도 구조적으로 담을 수 있는 프레임인지 판단. 비교형/병렬형/순환형 등 구분. 기존 _COMPAT 테이블 대체. 3-way 순환 (Frame 12) 은 3col-parallel 대안으로 명시.
V4
+ 적용 가능성
template-fit-v1 multi-gate
anchor + cardinality + relation + slot + content − penalties
이 프레임을 실제로 쓸 수 있는지 최종 판정. use_as_is / light_edit / restructure / reject 4단계. 02-2.2: V1 점수 0.840 (강함) + 정답 frame 1위 → 그러나 V4 reject ← "쓸 수 있는 프레임 아님" 신호
') # 운영 로직 body.append('

운영 로직 (관찰 기반)

') body.append('''
V1 점수의사결정
≥ 0.70 강함V1 top1 사용 · V4 는 "실사용 가능성" 검증 (라벨만 참고)
0.40 ~ 0.70 약함V4 재정렬에 맡김 · label 이 use_as_is/light_edit 이면 사용
< 0.40 매우 약함V4 가 reject 할 가능성 높음 → 기존 프레임 없음, 재구성 단계
''') body.append('
V1 만으로는 약한 케이스에서 오답. V4 만 바로 쓰면 ' '후보 풀이 너무 커짐. 그래서 V1(필터) + V2/V3(보강 신호) + V4(최종 판정) 캐스케이드가 실용적.
') return html_wrap('샘플 3 — 스키마 누적 설계', ''.join(body)) # ============================================================ # SAMPLE 4: Figma DB 최종 형태 (Frame 12) # ============================================================ def build_sample4(ontology_final_r2, descriptions): templates = ontology_final_r2['templates_v2'] # Frame 12 찾기 fid_12 = None for fid, e in templates.items(): if e.get('short_id') == '12': fid_12 = fid break if not fid_12: raise SystemExit("Frame 12 not found in ontology_final_r2") e = templates[fid_12] vp = e['visual_pattern'] aff = e['content_affinity'] intent = e['structure_intent_v2'] alts = e.get('alternative_patterns', []) slots = e.get('slots', []) suits = e.get('suits', []) not_suits = e.get('not_suits', []) v2_meta = e.get('v2_meta', {}) body = [] body.append('

샘플 4 — Figma DB 최종 형태 (Frame 12 예시)

') body.append('
template-fit-v2 스키마. 한 프레임이 담고 있는 전체 필드 — ' '그림 / layout / 의미 축 (content_affinity) / 시각 축 (structure_intent) / ' '대안 관계 / 슬롯 / 적합·부적합 기준 / 라벨 이력.
') # 프레임 헤더 body.append('
') body.append('
') body.append(f'12') body.append(f'
Frame 12 · {e["source"]["title"]}
') body.append(f'
{descriptions.get(12, "")}
') body.append('
') body.append('
') body.append('

visual_pattern (시각 패턴)

') body.append('') body.append(f'') body.append(f'') body.append(f'') card = vp.get("cardinality", {}) body.append(f'') body.append('
layout{vp["layout"]} (원본 {e["source"].get("original_layout")})
family{vp.get("family")}
relation_type{vp.get("relation_type")}
cardinalityideal {card.get("ideal")} / min {card.get("min")} / max {card.get("max")}
') body.append('
') # content_affinity body.append('

content_affinity (의미 축) — "어떤 콘텐츠 성격인가"

') body.append('') body.append(f'') body.append(f'') # evidence evp = aff.get('evidence', {}).get('primary', {}) body.append(f'") body.append('
primary{aff["primary"]}
secondary' + ', '.join(f'{s}' for s in aff.get('secondary', []) or ['(없음)']) + '
evidence.primarysource: {evp.get("source")}' + (f" · keywords: {evp.get('keywords')}" if evp.get('keywords') else '') + (f" · rule: {evp.get('rule')}" if evp.get('rule') else '') + f" · confidence: {evp.get('confidence')}
') # structure_intent body.append('

structure_intent (시각 의도) — "어떻게 전달하는가"

') body.append('') body.append(f'') body.append(f'') body.append('
primary{intent["primary"]}
secondary' + ', '.join(f'{s}' for s in intent.get('secondary', []) or ['(없음)']) + '
') # alternative_patterns body.append('

alternative_patterns (대안 레이아웃) — "담지 못할 때 어떤 대체가 가능한가"

') body.append('') for a in alts: body.append(f'' f'') body.append('
patternconfidencesourcereason
{a["pattern"]}{a["confidence"]}{a.get("source", "derived")}{a["reason"]}
') # slots body.append('

slots — 편집 가능한 영역

') body.append('') for s in slots: constraints = [] if 'max_chars' in s: constraints.append(f'max_chars={s["max_chars"]}') if 'min_items' in s: constraints.append(f'min_items={s["min_items"]}') if 'max_items' in s: constraints.append(f'max_items={s["max_items"]}') body.append(f'' f'') body.append('
idtyperequired제약
{s["id"]}{s.get("type")}{s.get("required", False)}{", ".join(constraints) or "—"}
') # suits / not_suits body.append('
') body.append('
') body.append('

suits (적합 — 긍정 신호)

') body.append('
    ') for s in suits: body.append(f'
  • {s}
  • ') body.append('
') body.append('
') body.append('

not_suits (부적합 — 반대 신호)

') body.append('
    ') for s in not_suits: body.append(f'
  • {s}
  • ') body.append('
') body.append('
') # resolved_from_review_queue (only for Frame 13... Frame 12 has manual_seed for alternatives) if v2_meta.get('resolved_from_review_queue'): prov = v2_meta['resolved_from_review_queue'] body.append('

라벨 이력 (provenance)

') body.append(f'
review_queue 에서 확정됨: ' f'{prov.get("decision_rationale", "")} · 확정 시점 {prov.get("confirmed_at")}
') # 전체 32 프레임 요약 body.append('

32 프레임 요약 (참고)

') aff_counter = Counter(t['content_affinity']['primary'] for t in templates.values()) intent_counter = Counter(t['structure_intent_v2']['primary'] for t in templates.values()) body.append('
') body.append('
') body.append('

content_affinity primary 분포

') body.append('') for k, c in aff_counter.most_common(): body.append(f'') body.append('
primary프레임 수
{k}{c}
') body.append('
') body.append('

structure_intent primary 분포

') body.append('') for k, c in intent_counter.most_common(): body.append(f'') body.append('
primary프레임 수
{k}{c}
') body.append('
') meta = ontology_final_r2['meta'] total = len(templates) mo = len(meta.get('manual_overrides', [])) rv = len(meta.get('review_queue', [])) res = len(meta.get('resolved_review_items', [])) body.append(f'
{total} 프레임. ' f'수동 오버라이드 {mo} 건 (SSOT 확정) · review_queue 잔여 {rv} 건 · ' f'resolved {res} 건. AI 초안 → 키워드 튜닝 → 사용자 검토 → SSOT 확정 루프 적용.
') return html_wrap('샘플 4 — Figma DB 형태', ''.join(body)) # ============================================================ # 메인 # ============================================================ def main(): v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8')) v2 = yaml.safe_load((HERE / 'v2_semantic_rerank_result.yaml').read_text(encoding='utf-8')) v3_r5 = yaml.safe_load((HERE / 'v3_structure_rerank_r5_result.yaml').read_text(encoding='utf-8')) v4_r2 = yaml.safe_load((HERE / 'v4_template_fit_r2_result.yaml').read_text(encoding='utf-8')) normalized = yaml.safe_load((HERE / 'normalized_text_tokens.yaml').read_text(encoding='utf-8')) auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8')) ontology_r2 = yaml.safe_load((HERE / 'structure_ontology_v2_final_r2.yaml').read_text(encoding='utf-8')) descriptions = build_frame_descriptions(auto) MEETING_BRIEF.write_text(build_meeting_brief(), encoding='utf-8') SAMPLE_01.write_text(build_sample1(v1, normalized, descriptions), encoding='utf-8') SAMPLE_02.write_text(build_sample2(v1, v2, v3_r5, v4_r2, descriptions), encoding='utf-8') SAMPLE_03.write_text(build_sample3(), encoding='utf-8') SAMPLE_04.write_text(build_sample4(ontology_r2, descriptions), encoding='utf-8') print('=' * 70) print('회의 자료 5장 생성 완료 (A4 규격)') print('=' * 70) for p in [MEETING_BRIEF, SAMPLE_01, SAMPLE_02, SAMPLE_03, SAMPLE_04]: size_kb = p.stat().st_size / 1024 print(f' {p.name:35} {size_kb:>8.0f} KB') if __name__ == '__main__': main()