Add Type B slide pipeline and recipe rendering updates

This commit is contained in:
2026-04-15 16:39:50 +09:00
parent 51548fdc41
commit 66c00924ed
22 changed files with 6260 additions and 1322 deletions

View File

@@ -30,33 +30,21 @@ KEI_PROMPT = (
"## 3단계: 슬라이드 스토리라인 설계\n"
"핵심 메시지를 전달하기 위한 **흐름**을 설계해줘.\n"
"각 꼭지에 purpose를 부여하고, topics 배열에 기록.\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"
"## 4단계: 꼭지별 성격 판단\n"
"각 꼭지에 대해 다음을 판단하라:\n\n"
"### sidebar 판단\n"
"- 이 꼭지의 내용이 **본문과 독립된 참조 정보**(용어 정의, 개념 비교, 참조 테이블)인가?\n"
"- 독립 참조 → role: 'reference' (sidebar 후보)\n"
"- 본문 흐름의 일부 → role: 'flow'\n\n"
"### 팝업 판단\n"
"- <details> 안에 있는 콘텐츠 → 팝업 처리 대상\n"
"- 너무 세부적인 내용 → 팝업으로 분리 가능\n\n"
"### 핵심요약\n"
"- :::note[핵심 요약] 등의 결론 텍스트가 있으면 **conclusion_text** 필드에 원본 그대로 기록\n"
"- conclusion_text는 슬라이드 하단 footer에 자동 배치됨\n\n"
"**주의: page_structure, zone, 영역 배치는 판단하지 마라.**\n"
"**영역과 zone은 코드가 블록 매칭을 통해 결정한다.**\n"
"**너는 꼭지 추출 + 각 꼭지의 성격(reference/flow, 팝업 여부)만 판단하라.**\n\n"
"## 원본 텍스트 보존 원칙 (절대 규칙)\n"
"- **제목(##, ###)은 원본 그대로 사용하라. 절대 바꾸지 마라.**\n"
" 원본이 '## 1. DX의 궁극적 목표'이면 꼭지 제목도 'DX의 궁극적 목표'.\n"
@@ -70,61 +58,76 @@ KEI_PROMPT = (
"## 배치 규칙\n"
"- 참조 정보(용어 정의 등)는 role: 'reference'로 표시 → 사이드바 배치\n"
"- 본문 흐름은 role: 'flow' → 메인 영역 배치\n"
"- 결론은 layer: 'conclusion' → 하단 배치\n"
"- 결론/핵심요약은 conclusion_text 필드에 기록. page_structure에 넣지 마라.\n"
"- detail_target: true는 정말로 별도로 봐야 하는 상세 데이터에만 사용\n"
"- 이미지/표가 있으면 images[], tables[]에 기록\n"
"- 1페이지 적정 꼭지: 5개. 분량 적으면 1페이지로.\n"
"- **슬라이드 제목(title)과 첫 번째 꼭지 제목은 달라야 한다.** 슬라이드 제목은 전체 주제, 꼭지 제목은 해당 위치의 구체적 내용.\n\n"
"## 출력 형식 (JSON만)\n"
"layout_template에 따라 page_structure가 달라진다.\n\n"
"유형 A 예시:\n"
"**page_structure는 출력하지 마라. 영역/zone 배치는 코드가 결정한다.**\n\n"
"```json\n"
'{"title": "제목", '
'{"title": "슬라이드 제목 (MDX title 또는 전체 주제)", '
'"core_message": "핵심 메시지", '
'"conclusion_text": "핵심 요약 원본 텍스트 (:::note 등에서 추출. 없으면 빈 문자열)", '
'"total_pages": 1, '
'"layout_template": "A", '
'"page_structure": {'
'"본심": {"topic_ids": [2, 3], "weight": 0.60}, '
'"배경": {"topic_ids": [1], "weight": 0.15}, '
'"첨부": {"topic_ids": [4], "weight": 0.15}, '
'"결론": {"topic_ids": [5], "weight": 0.10}}, '
'"topics": ['
'{"id": 1, "title": "꼭지 제목", "summary": "요약", '
'{"id": 1, "title": "꼭지 제목 (원본 그대로)", "summary": "요약", '
'"purpose": "문제제기|근거사례|핵심전달|용어정의|결론강조|구조시각화", '
'"source_hint": "원본에서 이 위치에 가져올 텍스트 범위 설명", '
'"source_hint": "원본에서 이 꼭지에 해당하는 텍스트 범위 설명", '
'"layer": "intro|core|supporting|conclusion", '
'"role": "flow|reference", '
'"section_title": "sidebar에 표시할 섹션 제목 (reference일 때만. 예: 용어 정의, 참고 자료)", '
'"emphasis": true, "direction": "vertical|horizontal|flexible", '
'"content_type": "text|image|table|mixed", '
'"detail_target": false, "page": 1}], '
'"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"
)
def _detect_structure_hints(content: str) -> str:
"""MDX 구조에서 유형 판단 힌트를 자동 감지."""
hints = []
content_lower = content.lower()
# 용어 정의 섹션 감지
if re.search(r'##\s*\d*\.?\s*용어\s*정의', content):
hints.append("[구조 힌트] '용어 정의' 섹션 감지 → 유형 A 후보")
if re.search(r'##\s*\d*\.?\s*개념\s*비교', content):
hints.append("[구조 힌트] '개념 비교' 섹션 감지 → 유형 A 후보")
# sidebar 마크다운 감지
if 'sidebar:' in content[:200]:
pass # frontmatter의 sidebar는 Starlight 설정이므로 무시
# <details> 감지
if '<details>' in content:
hints.append("[구조 힌트] <details> 참고 사례 감지 → 팝업 처리 대상 (유형 선택과 무관)")
# 표 감지
if '|' in content and '---' in content:
hints.append("[구조 힌트] 표(테이블) 감지 → 비교 구조")
# 이미지 감지
if re.search(r'!\[.*?\]\(.*?\)', content):
hints.append("[구조 힌트] 이미지 감지 → 이미지 배치 필요")
# A 후보 힌트가 없으면 B 유력
if not any("유형 A 후보" in h for h in hints):
hints.append("[구조 힌트] 독립 참조 섹션 없음 → 유형 B 유력")
return "\n".join(hints) + "\n\n"
async def classify_content(content: str) -> dict[str, Any] | None:
"""1단계: Kei API를 통해 꼭지를 추출하고 분석한다.
Kei API만 사용. fallback 없음. 실패 시 None → pipeline에서 에러.
"""
result = await _call_kei_api(content)
# MDX 구조 힌트를 content 앞에 추가
hints = _detect_structure_hints(content)
result = await _call_kei_api(hints + content)
if result:
logger.info(
f"[Kei API] 꼭지 추출 완료: {result.get('title', '')}, "