Initial commit: Kei Design Agent

콘텐츠를 시각적으로 구조화된 슬라이드 HTML로 변환하는 독립 에이전트.

아키텍처 (4단계 파이프라인):
  1. Kei 실장 (Opus) — 콘텐츠 유형 분류 + 블록 배치
  2. 디자인 팀장 (Sonnet) — 레이아웃 컨셉 (블록 배치 + 페이지 수)
  3. 텍스트 편집자 (Sonnet) — 슬롯 텍스트 정리 (핵심 유지)
  4. CSS Grid 렌더러 — HTML 조립

블록 템플릿 7종:
  comparison, card-grid, relationship, process,
  quote-block, conclusion-bar, comparison-table

기술 스택:
  FastAPI + Anthropic API + Jinja2 + CSS Grid
  Pretendard Variable 한국어 폰트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 17:25:47 +09:00
commit c42e65fc7e
28 changed files with 3302 additions and 0 deletions

71
src/pipeline.py Normal file
View File

@@ -0,0 +1,71 @@
"""DA-14: 전체 파이프라인 (3단계).
콘텐츠 입력 → Opus 분류 → 디자인 팀장 컨셉 → 텍스트 편집자 정리 → 렌더러 조립 → HTML 출력.
"""
from __future__ import annotations
import logging
from typing import Any, AsyncIterator
from src.kei_client import classify_content, manual_classify
from src.design_director import create_layout_concept, _fallback_single_page
from src.content_editor import fill_content
from src.renderer import render_slide
logger = logging.getLogger(__name__)
async def generate_slide(
content: str,
manual_layout: dict[str, Any] | None = None,
) -> AsyncIterator[dict[str, str]]:
"""콘텐츠를 슬라이드 HTML로 변환하는 전체 파이프라인.
Args:
content: 원본 텍스트 콘텐츠
manual_layout: 수동 레이아웃 명세 (Opus 대신 사용)
Yields:
SSE 이벤트:
{"event": "progress", "data": "단계 설명"}
{"event": "result", "data": "완성 HTML"}
{"event": "error", "data": "에러 메시지"}
"""
try:
# 1단계: Kei 실장 (Opus) — 콘텐츠 분류
yield {"event": "progress", "data": "1/4 Kei 실장이 콘텐츠를 분석 중..."}
if manual_layout:
classification = manual_layout
else:
classification = await classify_content(content)
if classification is None:
classification = manual_classify(content)
logger.info(f"분류 완료: {len(classification.get('blocks', []))}개 블록")
# 2단계: 디자인 팀장 — 레이아웃 컨셉
yield {"event": "progress", "data": "2/4 디자인 팀장이 레이아웃을 설계 중..."}
layout_concept = await create_layout_concept(content, classification)
total_pages = len(layout_concept.get("pages", []))
total_blocks = sum(len(p.get("blocks", [])) for p in layout_concept.get("pages", []))
logger.info(f"레이아웃 컨셉: {total_pages}페이지, {total_blocks}개 블록")
# 3단계: 텍스트 편집자 (Kei 역할) — 슬롯 텍스트 정리
yield {"event": "progress", "data": "3/4 텍스트 편집자가 핵심을 정리 중..."}
layout_concept = await fill_content(content, layout_concept)
# 4단계: 실무자 — HTML 렌더링
yield {"event": "progress", "data": "4/4 슬라이드를 조립 중..."}
html = render_slide(layout_concept)
yield {"event": "result", "data": html}
logger.info(f"슬라이드 생성 완료: {total_pages}페이지")
except Exception as e:
logger.exception(f"파이프라인 오류: {e}")
yield {"event": "error", "data": str(e)}