"""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, }, ], }