Phase X-B-1,2 완료: Kei 유형 A/B 선택 + 검증기 완화

X-B-1: KEI_PROMPT에 유형 B 옵션 추가
- 유형 A: 기존 배경/본심/첨부/결론 (참조자료 있는 콘텐츠)
- 유형 B: 본심1(상단)+본심2(하단2분할)+결론 (본문만으로 구성)
- Kei가 콘텐츠 보고 A/B 선택, layout_template 필드로 반환
- 검증: 01번→A, 02번→B 정확히 선택

X-B-2: 검증기 완화
- 유형 A: 본심 필수 유지
- 유형 B: 결론(footer)만 필수, 자유 역할명 허용
- 섹션 수 차이 허용 확대 (유형 B: 4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 10:10:22 +09:00
parent c9677a69f8
commit bc7829b08b
3 changed files with 208 additions and 28 deletions

View File

@@ -30,15 +30,30 @@ KEI_PROMPT = (
"## 3단계: 슬라이드 스토리라인 설계\n"
"핵심 메시지를 전달하기 위한 **흐름**을 설계해줘.\n"
"각 꼭지에 purpose를 부여하고, topics 배열에 기록.\n\n"
"## 4단계: 페이지 구조 판단 (비중 시스템)\n"
"콘텐츠를 분석하여 이 페이지의 **구조와 비중**을 판단하라:\n\n"
"- **본심**: 이 페이지가 말하려는 핵심. 가장 큰 공간을 차지해야 함.\n"
" 비교라면 비교표, 관계라면 관계도, 프로세스라면 흐름도로 구조화.\n"
" 비교 구조일 때 비교 목적(왜 비교하는가)을 summary에 명시.\n"
"- **배경**: 본심을 이해하기 위한 도입/배경. 간결하게. 2-3줄이면 충분.\n"
"- **첨부**: 본심을 보조하는 참조 정보 (용어 정의 등). sidebar 배치.\n"
" role: 'reference'로 표시. 본문 흐름을 방해하지 않도록.\n"
"- **결론**: 절대 잊으면 안 되는 핵심 한 줄. footer.\n\n"
"## 4단계: 레이아웃 유형 선택 + 페이지 구조 판단\n"
"먼저 콘텐츠에 맞는 **레이아웃 유형**을 선택하라:\n\n"
"### 유형 A: 배경 + 본심 + 첨부(sidebar) + 결론\n"
"- 참조자료(용어 정의, 부록 등)가 **별도로 존재**하는 콘텐츠\n"
"- 좌측 body(배경+본심) + 우측 sidebar(첨부) + 하단 결론\n"
"- page_structure 키: 배경, 본심, 첨부, 결론\n\n"
"### 유형 B: 본심1(상단) + 본심2(하단 2분할) + 결론\n"
"- 참조자료 없이 **본문 흐름만**으로 구성되는 콘텐츠\n"
"- 배경/첨부가 없거나 억지로 만들어야 하면 이 유형 선택\n"
"- 상단: 핵심 내용 전체폭 (이미지가 있으면 좌텍스트+우이미지 나란히)\n"
"- 하단: 세부 내용 2분할 (좌/우)\n"
"- page_structure 키: 자유 (예: 핵심목표, 프로세스변화, 기대효과, 결론)\n"
"- 결론 키는 반드시 '결론'\n\n"
"선택한 유형을 **layout_template** 필드에 'A' 또는 'B'로 기록하라.\n\n"
"### 역할별 규칙 (유형 A)\n"
"- **본심**: 이 페이지가 말하려는 핵심. 가장 큰 공간.\n"
"- **배경**: 본심을 이해하기 위한 도입. 간결하게.\n"
"- **첨부**: 본심을 보조하는 참조 정보. sidebar 배치. role: 'reference'.\n"
"- **결론**: 핵심 한 줄. footer.\n\n"
"### 역할별 규칙 (유형 B)\n"
"- 상단 역할: 핵심 내용. 전체폭. zone: 'top'\n"
"- 하단 좌측: zone: 'bottom_left'\n"
"- 하단 우측: zone: 'bottom_right'\n"
"- 결론: zone: 'footer'\n\n"
"각 역할에 해당하는 topic_ids와 **공간 비중(weight, 합계 1.0)**을 결정하라.\n"
"**콘텐츠에 따라 비중은 매번 달라진다. 고정값이 아니다.**\n"
"page_structure 필드에 기록.\n\n"
@@ -56,11 +71,13 @@ KEI_PROMPT = (
"- 1페이지 적정 꼭지: 5개. 분량 적으면 1페이지로.\n"
"- **슬라이드 제목(title)과 첫 번째 꼭지 제목은 달라야 한다.** 슬라이드 제목은 전체 주제, 꼭지 제목은 해당 위치의 구체적 내용.\n\n"
"## 출력 형식 (JSON만)\n"
"layout_template에 따라 page_structure가 달라진다.\n\n"
"유형 A 예시:\n"
"```json\n"
'{"title": "제목", '
'"core_message": "이 슬라이드의 핵심 메시지 한 줄", '
'"core_message": "핵심 메시지", '
'"total_pages": 1, '
'"info_structure": "정보 구조 설명", '
'"layout_template": "A", '
'"page_structure": {'
'"본심": {"topic_ids": [2, 3], "weight": 0.60}, '
'"배경": {"topic_ids": [1], "weight": 0.15}, '
@@ -79,6 +96,20 @@ KEI_PROMPT = (
'"images": [{"topic_id": 1, "role": "key|supporting", "has_text": false, "description": "이미지 설명"}], '
'"tables": [{"topic_id": 2, "rows": 5, "cols": 3, "fits_single_page": true, "description": "표 설명"}]}\n'
"```\n\n"
"유형 B 예시:\n"
"```json\n"
'{"title": "제목", '
'"core_message": "핵심 메시지", '
'"total_pages": 1, '
'"layout_template": "B", '
'"page_structure": {'
'"핵심목표": {"zone": "top", "topic_ids": [1], "weight": 0.35}, '
'"프로세스변화": {"zone": "bottom_left", "topic_ids": [2], "weight": 0.25}, '
'"기대효과": {"zone": "bottom_right", "topic_ids": [3], "weight": 0.25}, '
'"결론": {"zone": "footer", "topic_ids": [4], "weight": 0.15}}, '
'"topics": [...],'
'"images": [...]}\n'
"```\n\n"
"## 콘텐츠:\n"
)

View File

@@ -171,22 +171,38 @@ def validate_stage_1a(
"instruction": f"weight 합이 1.0에 가깝도록 조정하라. 현재 합: {total_weight:.2f}",
})
# 본심 존재 + 본심 weight ≥ 0.3
core_info = page_struct.get("본심", {})
if not core_info or not isinstance(core_info, dict):
errors.append({
"severity": "RETRYABLE",
"field": "page_structure.본심",
"localization": "본심 역할이 page_structure에 없음",
"instruction": "page_structure에 본심 역할을 추가하라. 본심은 슬라이드의 핵심 콘텐츠이다.",
})
elif core_info.get("weight", 0) < 0.3:
errors.append({
"severity": "RETRYABLE",
"field": "page_structure.본심.weight",
"localization": f"본심 weight {core_info['weight']:.2f} < 0.3",
"instruction": "본심은 슬라이드의 핵심. weight 0.3 이상 필요.",
})
# 유형에 따른 구조 검증
layout_template = analysis.get("layout_template", "A")
if layout_template == "A":
# 유형 A: 본심 필수
core_info = page_struct.get("본심", {})
if not core_info or not isinstance(core_info, dict):
errors.append({
"severity": "RETRYABLE",
"field": "page_structure.본심",
"localization": "본심 역할이 page_structure에 없음",
"instruction": "page_structure에 본심 역할을 추가하라. 본심은 슬라이드의 핵심 콘텐츠이다.",
})
elif core_info.get("weight", 0) < 0.3:
errors.append({
"severity": "RETRYABLE",
"field": "page_structure.본심.weight",
"localization": f"본심 weight {core_info['weight']:.2f} < 0.3",
"instruction": "본심은 슬라이드의 핵심. weight 0.3 이상 필요.",
})
elif layout_template == "B":
# 유형 B: 결론(footer) 필수, 나머지 자유
has_footer = any(
isinstance(info, dict) and info.get("zone") == "footer"
for info in page_struct.values()
)
if not has_footer and "결론" not in page_struct:
errors.append({
"severity": "RETRYABLE",
"field": "page_structure.footer",
"localization": "결론(footer) 역할이 없음",
"instruction": "유형 B에서도 결론 역할(zone: footer)은 필수이다.",
})
# 필수 필드 검증
for t in topics:
@@ -226,7 +242,9 @@ def validate_stage_1a(
if clean_text:
# 원본 ## 섹션 수 vs topic 수 비교
original_sections = re.findall(r"^## .+$", clean_text, re.MULTILINE)
if len(original_sections) > 0 and abs(len(topics) - len(original_sections)) > 2:
# 유형 B에서는 하나의 섹션을 여러 꼭지로 나눌 수 있으므로 허용 폭 확대
max_diff = 4 if layout_template == "B" else 2
if len(original_sections) > 0 and abs(len(topics) - len(original_sections)) > max_diff:
errors.append({
"severity": "RETRYABLE",
"field": "topics",