"""Claude로 10페이지 카드 카피를 한 번에 생성.""" import json import logging import re from typing import Any, Dict, Optional from anthropic import Anthropic from .config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL_SONNET from . import db logger = logging.getLogger(__name__) DEFAULT_ACCENT_BY_CATEGORY = { "economy": "#0F62FE", "psychology": "#A66CFF", "celebrity": "#FF5C8A", } DEFAULT_PROMPT = """너는 인스타그램 카드 뉴스 카피라이터다. 카테고리: {category} 키워드: {keyword} 참고 기사: {articles} 10페이지 인스타 카드용 카피를 다음 JSON 한 객체로만 출력해라 (코드펜스 금지): {{ "cover_copy": {{"headline": "<훅 한 줄>", "body": "<서브카피 1~2줄>", "accent_color": "#hex"}}, "body_copies": [ {{"headline": "<포인트 헤드라인>", "body": "<2~4문장 본문>"}}, ... (총 8개) ], "cta_copy": {{"headline": "<요약 한 줄>", "body": "<마무리 1~2줄>", "cta": "팔로우/저장 등"}}, "suggested_caption": "<인스타 캡션 본문>", "hashtags": ["#태그1", "#태그2", ...] }} """ def _client() -> Anthropic: return Anthropic(api_key=ANTHROPIC_API_KEY) def _strip_codefence(s: str) -> str: s = s.strip() if s.startswith("```"): s = re.sub(r"^```(?:json)?\s*|\s*```$", "", s).strip() return s def _load_prompt() -> str: pt = db.get_prompt_template("slate_writer") if pt and pt.get("template"): return pt["template"] return DEFAULT_PROMPT def write_slate(keyword: str, category: str, articles: Optional[list] = None) -> int: """Claude로 10페이지 카피 생성 후 card_slates에 저장. slate_id 반환.""" if articles is None: articles = db.list_news_articles(category=category, days=2) article_text = "\n".join( f"- {a['title']}: {a.get('summary', '')[:120]}" for a in articles[:8] ) or "(참고 기사 없음)" prompt = _load_prompt().format(category=category, keyword=keyword, articles=article_text) msg = _client().messages.create( model=ANTHROPIC_MODEL_SONNET, max_tokens=4000, messages=[{"role": "user", "content": prompt}], ) raw = msg.content[0].text cleaned = _strip_codefence(raw) try: data: Dict[str, Any] = json.loads(cleaned) except json.JSONDecodeError as e: logger.warning("slate JSON parse failed: %s", e) raise ValueError(f"Invalid JSON from LLM: {e}") from e body_copies = data.get("body_copies") or [] if len(body_copies) != 8: raise ValueError(f"body_copies must have 8 items, got {len(body_copies)}") cover = data.get("cover_copy") or {} if not cover.get("accent_color"): cover["accent_color"] = DEFAULT_ACCENT_BY_CATEGORY.get(category, "#222831") sid = db.add_card_slate({ "keyword": keyword, "category": category, "status": "draft", "cover_copy": cover, "body_copies": body_copies, "cta_copy": data.get("cta_copy") or {}, "suggested_caption": data.get("suggested_caption") or "", "hashtags": data.get("hashtags") or [], }) return sid