Files
C.E.L_Slide_test2/src/kei_client.py
T
kmlee1andClaude Opus 4.6 33bd3a56c6 5단계 파이프라인 전면 재작성 + Figma 추출 계획 업데이트
DA-12: 1단계 Kei 실장 — 꼭지 2~5개 추출 + 레이어/강조/배치/이미지/표/자세히보기 판단
DA-13: 2단계 디자인 팀장 — catalog 연동 + 블록 매핑 + 공간 배분 + 글자 수 가이드
DA-13b: 3단계 텍스트 편집자 — 글자 수 가이드 참고, 의미 우선 편집 + 자세히보기(요약+상세)
DA-14: 4단계 실무자(AI+코드) + 5단계 팀장 재검토 (균형 점검 → 2차 조정)

문서:
- CLAUDE.md: 5단계 프로세스 + 이미지/표/자세히보기 처리 원칙
- PLAN.md: DA-12~14 태스크 전면 재작성
- PROGRESS.md: 동기화
- FIGMA-COMPONENT-EXTRACTION-PLAN.md: 모드 독립 블록, 변환 규칙, image-block/details-block, MCP, 토큰 매핑

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:44:10 +09:00

150 lines
4.9 KiB
Python

"""DA-12: 1단계 — Kei 실장 (꼭지 추출 + 분석).
본문에서 핵심 꼭지를 추출하고, 각 꼭지의 레이어/강조/배치 방향을 분석한다.
이미지/표/상세 콘텐츠도 판단한다.
"""
from __future__ import annotations
import json
import logging
import re
from typing import Any
import anthropic
from src.config import settings
logger = logging.getLogger(__name__)
CLASSIFICATION_PROMPT = """당신은 콘텐츠를 분석하여 슬라이드 구조를 설계하는 실장이다.
## 역할
본문에서 핵심 꼭지를 추출하고, 각 꼭지의 성격을 분석하여 슬라이드 구조를 설계한다.
## 꼭지 추출 규칙
- 본문에서 2~5개의 핵심 꼭지(파트)를 추출한다
- 1페이지 적정 꼭지 수: 5개
- 꼭지가 5개를 넘고 중요도가 동등하면 → 2페이지로 분리 (의미 기반 분할)
- 5개인데 내용이 많으면 → 세부 내용은 "자세히보기" 대상으로 표시
## 각 꼭지 분석 항목
1. **레이어 수준**: 도입(문제 제기, 배경) / 핵심(핵심 내용, 정의) / 보조(사례, 근거) / 결론(요약, 핵심 메시지)
2. **강조**: 눈에 띄게 해야 하는 꼭지 표시 (true/false)
3. **배치 방향**: 세로로 긴 내용(vertical) / 가로로 나열(horizontal) / 유연(flexible)
4. **콘텐츠 유형**: text(텍스트) / image(이미지) / table(표) / mixed(혼합)
5. **이미지 정보** (이미지가 있는 경우):
- 핵심인지 보조인지 (core/supplementary)
- 텍스트 포함 여부 (도표/차트는 true)
6. **표 정보** (표가 있는 경우):
- 대략적 행/열 수
- 전체 표시 가능한지 판단
7. **자세히보기 대상**: 너무 구체적/세부적인 내용은 detail_target: true
## 출력 형식 (반드시 JSON만. 설명 없이.)
```json
{
"title": "슬라이드 제목",
"total_pages": 1,
"topics": [
{
"id": 1,
"title": "꼭지 제목",
"summary": "꼭지 내용 요약 (1~2줄)",
"layer": "intro|core|supporting|conclusion",
"emphasis": true,
"direction": "vertical|horizontal|flexible",
"content_type": "text|image|table|mixed",
"image_info": {"role": "core|supplementary", "has_text": true},
"table_info": {"rows": 5, "cols": 3, "fits_page": true},
"detail_target": false,
"page": 1
}
]
}
```"""
async def classify_content(content: str) -> dict[str, Any] | None:
"""1단계: 본문에서 꼭지를 추출하고 분석한다.
Args:
content: 원본 텍스트 콘텐츠
Returns:
분류 결과 JSON. 실패 시 None.
"""
if not settings.anthropic_api_key:
logger.warning("ANTHROPIC_API_KEY 미설정. 수동 분류 모드.")
return None
try:
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
system=CLASSIFICATION_PROMPT,
messages=[
{
"role": "user",
"content": f"다음 콘텐츠를 분석하여 꼭지를 추출하고 구조를 설계해줘:\n\n{content}",
}
],
)
result_text = response.content[0].text
analysis = _parse_json(result_text)
if analysis and "topics" in analysis:
logger.info(
f"꼭지 추출 완료: {analysis.get('title', 'untitled')}, "
f"{len(analysis['topics'])}개 꼭지, "
f"{analysis.get('total_pages', 1)}페이지"
)
return analysis
else:
logger.warning(f"분류 JSON 파싱 실패. 응답: {result_text[:200]}")
return None
except Exception as e:
logger.warning(f"실장 분류 호출 실패: {e}")
return None
def _parse_json(text: str) -> dict[str, Any] | None:
"""텍스트에서 JSON을 추출한다."""
patterns = [
r"```json\s*(.*?)```",
r"```\s*(.*?)```",
r"(\{.*\})",
]
for pattern in patterns:
match = re.search(pattern, text, re.DOTALL)
if match:
try:
return json.loads(match.group(1).strip())
except json.JSONDecodeError:
continue
return None
def manual_classify(content: str) -> dict[str, Any]:
"""실장 분류 실패 시 기본 구조를 반환하는 fallback."""
return {
"title": "슬라이드",
"total_pages": 1,
"topics": [
{
"id": 1,
"title": "핵심 내용",
"summary": content[:100],
"layer": "core",
"emphasis": False,
"direction": "flexible",
"content_type": "text",
"detail_target": False,
"page": 1,
},
],
}