"""① LLM step — interview answers → VideoSpec (JSON). Uses the Anthropic Python SDK (claude-opus-4-7) with structured outputs so the result is guaranteed to match the Remotion data contract. The guardrail system prompt is cached (stable prefix) to cut cost/latency across requests. """ from __future__ import annotations import json import anthropic from app.schemas import GenerateRequest, VideoSpec, ScriptResult MODEL = "claude-opus-4-7" # Stable, cacheable guardrail + role prompt. SYSTEM_PROMPT = """\ 당신은 한국 로컬 비즈니스(숙소·매장·제품)의 8초 세로형 숏폼 광고 카피 디렉터입니다. 사장/마케터가 직접 답한 정보만으로, 영상 사양(VideoSpec)을 설계합니다. 복잡한 분석은 하지 않습니다. [절대 가드레일] 1. 식민지 유산·역사정치 민감 용어 금지: "적산가옥", "일제", "식민지", "근대사" 등 → 미학·경험 어휘로 대체. 2. 거짓·과장 금지: 사용자가 주지 않은 거리(예: "서울 40분")·효능·수치를 지어내지 않는다. 주어진 정보만 사용. 3. 자막은 흰색 모노크롬 전제 — 카피에 색 지시어를 넣지 않는다. 4. AI 디스클로저는 end_card.disclosure에 고정 문구로 포함. [hook 작성 — 레퍼런스 바이럴 공식 "[반전/호기심] + [한방]"] - eyebrow: 짧은 도발/맥락 한 줄 (예: "아무도 모르는 군산"). - title: 굵은 한방 헤드라인. 사용자의 셀링포인트를 가장 강하게 압축. [selling_point.items] 정확히 3개. 사용자 한방 셀링포인트 + 유형 기반의 구체 명사구(짧게). 이모지 금지. [brand_lines] 감성 2줄 (각 줄 짧게). 한 덩어리로 읽히게. [end_card] brand=업체/상품명, location=주소나 URL(없으면 빈 문자열 ""), disclosure 고정. [caption] 업로드용. 정보(업체/주소/가격) + 짧은 감성 + 해시태그 5~8개. 유형에 맞는 해시태그. [profile 선택] - place + 정적/프라이빗/힐링 → "Still Cinema" - place + 뷰/활동성/포토스팟, 또는 product 일반 → "Rhythm Reveal" - 파티/워터파크/대형 액티비티 → "Maximum Viral" [higgsfield_prompt] 영문. marketing_studio_video tv_spot용. 빠르고 역동적인 카메라(quick punchy moves, crash zoom, kinetic glide). 유형 반영: place=공간 투어, product=제품 쇼케이스. 반드시 "keep structure realistic, do not distort" 포함. """ # Structured-output JSON schema (additionalProperties:false everywhere). _SPEC_SCHEMA = { "type": "object", "additionalProperties": False, "properties": { "profile": {"type": "string", "enum": ["Still Cinema", "Rhythm Reveal", "Maximum Viral"]}, "higgsfield_prompt": {"type": "string"}, "hook": { "type": "object", "additionalProperties": False, "properties": {"eyebrow": {"type": "string"}, "title": {"type": "string"}}, "required": ["eyebrow", "title"], }, "selling_point": { "type": "object", "additionalProperties": False, "properties": {"items": {"type": "array", "items": {"type": "string"}}}, "required": ["items"], }, "brand_lines": {"type": "array", "items": {"type": "string"}}, "end_card": { "type": "object", "additionalProperties": False, "properties": { "brand": {"type": "string"}, "location": {"type": "string"}, "disclosure": {"type": "string"}, }, "required": ["brand", "location", "disclosure"], }, "caption": {"type": "string"}, }, "required": [ "profile", "higgsfield_prompt", "hook", "selling_point", "brand_lines", "end_card", "caption", ], } def build_spec(req: GenerateRequest, client: anthropic.Anthropic | None = None) -> VideoSpec: client = client or anthropic.Anthropic() user_msg = ( f"유형: {'장소' if req.kind == 'place' else '메시지(축하·안부·홍보)' if req.kind == 'message' else '물건(제품·음식 등 정적 아이템)'}\n" f"업체/상품명: {req.biz_name}\n" f"주소/판매URL: {req.addr or '(없음)'}\n" f"가격: {req.price or '(없음)'}\n" f"강력한 한방 셀링포인트: {req.selling}\n\n" "위 정보로 VideoSpec을 설계해줘." ) resp = client.messages.create( model=MODEL, max_tokens=2048, thinking={"type": "adaptive"}, output_config={ "effort": "medium", "format": {"type": "json_schema", "name": "video_spec", "schema": _SPEC_SCHEMA}, }, system=[{ "type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}, }], messages=[{"role": "user", "content": user_msg}], ) text = "".join(b.text for b in resp.content if b.type == "text") return VideoSpec.model_validate(json.loads(text)) # ---------- 자막 스크립트 4블록 ---------- SCRIPT_SYSTEM = """\ 당신은 한국 로컬 비즈니스 숏폼 광고의 자막 스크립트 작가입니다. 사장/마케터가 답한 정보로, 영상에 얹을 4개 블록을 만듭니다. [4블록] - intro: 첫 1~2초 후킹. 호기심·반전을 거는 짧은 한 줄. - selling: 가장 강력한 셀링포인트를 한 문장으로 압축. - story: 감성 스토리. 고객이 그 공간/제품에서 느낄 감정을 1~2문장으로. - cta: 행동 유도. 예약·구매·방문으로 자연스럽게. [가드레일] - 거짓·과장 금지(주어지지 않은 거리·효능·수치 X). 주어진 정보만. - 식민지 유산 어휘(적산가옥/일제/식민지 등) 금지 → 미학·경험 어휘로. - 자막은 흰색 모노크롬 전제 — 색 지시어 넣지 않음. 이모지 남발 금지. - 각 블록은 짧고 입에 붙게. 한국어. """ _SCRIPT_SCHEMA = { "type": "object", "additionalProperties": False, "properties": { "intro": {"type": "string"}, "selling": {"type": "string"}, "story": {"type": "string"}, "cta": {"type": "string"}, }, "required": ["intro", "selling", "story", "cta"], } def build_script(req: GenerateRequest, client: anthropic.Anthropic | None = None) -> ScriptResult: client = client or anthropic.Anthropic() user_msg = ( f"유형: {'장소' if req.kind == 'place' else '메시지(축하·안부·홍보)' if req.kind == 'message' else '물건(제품·음식 등 정적 아이템)'}\n" f"업체/상품명: {req.biz_name}\n" f"주소/판매URL: {req.addr or '(없음)'}\n" f"가격: {req.price or '(없음)'}\n" f"강력한 한방 셀링포인트: {req.selling}\n\n" "위 정보로 4블록 자막 스크립트를 만들어줘." ) resp = client.messages.create( model=MODEL, max_tokens=1024, thinking={"type": "adaptive"}, output_config={ "effort": "low", "format": {"type": "json_schema", "name": "script", "schema": _SCRIPT_SCHEMA}, }, system=[{"type": "text", "text": SCRIPT_SYSTEM, "cache_control": {"type": "ephemeral"}}], messages=[{"role": "user", "content": user_msg}], ) text = "".join(b.text for b in resp.content if b.type == "text") return ScriptResult.model_validate(json.loads(text))