from pydantic import BaseModel, Field from typing import List, Optional from enum import StrEnum, auto class SpaceType(StrEnum): """이미지가 촬영된 공간·장소 유형. LLM이 이미지를 보고 해당하는 공간 태그를 선택하는 데 사용.""" exterior_front = auto() exterior_night = auto() exterior_aerial = auto() exterior_sign = auto() garden = auto() entrance = auto() lobby = auto() reception = auto() hallway = auto() bedroom = auto() livingroom = auto() kitchen = auto() dining = auto() room = auto() bathroom = auto() amenity = auto() view_window = auto() view_ocean = auto() view_city = auto() view_mountain = auto() balcony = auto() cafe = auto() lounge = auto() rooftop = auto() pool = auto() breakfast_hall = auto() spa = auto() fitness = auto() bbq = auto() terrace = auto() glamping = auto() neighborhood = auto() landmark = auto() detail_welcome = auto() detail_beverage = auto() detail_lighting = auto() detail_decor = auto() detail_tableware = auto() # 레스토랑(DVRT0001) 템플릿 어휘 dining_hall = auto() table_setting = auto() private_room = auto() detail_cooking = auto() detail_plating = auto() detail_menu = auto() detail_dish_main = auto() detail_dish_side = auto() brand_summary = auto() # 카페(DVCF0001) 템플릿 어휘 hall = auto() counter_bar = auto() seating_window = auto() seating_lounge = auto() signage = auto() detail_dessert = auto() detail_signature_drink = auto() class Subject(StrEnum): """이미지 내 주요 피사체 유형. 화면에 무엇이 담겨 있는지를 분류하는 태그.""" empty_space = auto() exterior_building = auto() architecture_detail = auto() decoration = auto() furniture = auto() food_dish = auto() nature = auto() signage = auto() amenity_item = auto() person = auto() # 레스토랑(DVRT0001) 템플릿 어휘 cooking_action = auto() menu_board = auto() triptych = auto() # 카페(DVCF0001) 템플릿 어휘 beverage = auto() brewing_action = auto() dessert = auto() class Camera(StrEnum): """이미지의 촬영 기법·구도·조명 특성. 영상 편집 시 컷의 시각적 스타일을 구분하는 태그.""" wide_angle = auto() tight_crop = auto() panoramic = auto() symmetrical = auto() leading_line = auto() golden_hour = auto() night_shot = auto() high_contrast = auto() low_light = auto() drone_shot = auto() has_face = auto() class MotionRecommended(StrEnum): """해당 이미지에 적용 가능한 카메라 모션 유형. 영상 제작 시 각 컷에 어울리는 움직임을 추천하는 데 사용.""" static = auto() slow_pan = auto() slow_zoom_in = auto() slow_zoom_out = auto() walkthrough = auto() dolly = auto() class NarrativePhase(StrEnum): """광고 영상 내러티브 단계. 이미지가 스토리 흐름에서 어느 위치에 배치되어야 하는지를 나타냄.""" intro = auto() welcome = auto() core = auto() highlight = auto() support = auto() accent = auto() cta = auto() NARRATIVE_PHASE_LABEL: dict[NarrativePhase, str] = { NarrativePhase.intro: "첫인상(intro)", NarrativePhase.welcome: "진입(welcome)", NarrativePhase.core: "핵심(core)", NarrativePhase.highlight: "차별화(highlight)", NarrativePhase.support: "보조(support)", NarrativePhase.accent: "감성(accent)", NarrativePhase.cta: "행동유도(cta)", } class NarrativePreference(BaseModel): """이미지가 각 내러티브 단계에 얼마나 적합한지를 나타내는 점수 모델 (0.0 ~ 100.0). LLM이 이미지를 분석한 뒤 각 단계별 적합도를 수치로 반환하며, 영상 편집기가 이 점수를 기반으로 컷 배치 순서를 자동 결정하는 데 사용.""" intro: float = Field(..., description="첫인상 — 여기가 어디인가 | 장소의 정체성과 위치를 전달하는 이미지. 영상 첫 1~2초에 어떤 곳인지 즉시 인지시키는 역할. 건물 외관, 간판, 정원 등 **장소 자체를 보여주는** 컷") welcome: float = Field(..., description="진입/환영 — 어떻게 들어가나 | 도착 후 내부로 들어가는 경험을 전달하는 이미지. 공간의 첫 분위기와 동선을 보여줘 들어가고 싶다는 기대감을 만드는 역할. **문을 열고 들어갔을 때 보이는** 컷.") core: float = Field(..., description="핵심 가치 — 무엇을 경험하나 | **고객이 이 장소를 찾는 본질적 이유.** 이 이미지가 없으면 영상 자체가 성립하지 않음. 질문: 이 비즈니스에서 돈을 지불하는 대상이 뭔가? → 그 답이 core.") highlight: float = Field(..., description="차별화 — 뭐가 특별한가 | **같은 카테고리의 경쟁사 대비 이곳을 선택하게 만드는 이유.** core가 왜 왔는가라면, highlight는 왜 **여기**인가에 대한 답.") support: float = Field(..., description="보조/부대 — 그 외에 뭐가 있나 | 핵심은 아니지만 전체 경험을 풍성하게 하는 부가 요소. 없어도 영상은 성립하지만, 있으면 설득력이 올라감. **이것도 있어요** 라고 말하는 컷.") accent: float = Field(..., description="감성/마무리 — 어떤 느낌인가 | 공간의 분위기와 톤을 전달하는 감성 디테일 컷. 직접적 정보 전달보다 **느낌과 무드**를 제공. 영상 사이사이에 삽입되어 완성도를 높이는 역할.") cta: float = Field(..., description="행동 유도 — 지금 예약/방문하고 싶게 만드는 컷 | 가격·혜택·한정 오퍼 등 직접적 행동을 촉구하거나, 마지막 장면에서 강한 인상을 남기는 이미지.") # Input 정의 class ImageTagPromptInput(BaseModel): """이미지 태깅 프롬프트에 전달되는 입력 스키마. 분석할 이미지 URL과 업종 정보, LLM이 선택 가능한 태그 후보 리스트를 포함.""" img_url : str = Field(..., description="이미지 URL") industry : str = Field(default="", description = "업종 분류 (stay|restaurant|cafe|salon|clinic|fitness|academy|attraction|general). 통합 프롬프트가 이 값으로 내부 분기") space_type: list[str] = Field(list(SpaceType), description="공간적 정보를 가지는 태그 리스트") subject: list[str] = Field(list(Subject), description="피사체 정보를 가지는 태그 리스트") camera: list[str] = Field(list(Camera), description="카메라 정보를 가지는 태그 리스트") motion_recommended: list[str] = Field(list(MotionRecommended), description="가능한 카메라 모션 리스트") class MarketingFilterOutput(BaseModel): """크롤링 단계에서 이미지의 마케팅(광고 영상) 사용 가능 여부만 판정하는 프롬프트의 출력 스키마. ImageTagPromptOutput과 달리 태깅(공간/피사체/카메라/모션/내러티브)은 포함하지 않는다.""" marketing_acceptable: bool = Field(..., description="광고 영상 사용 가능 여부") reject_reason: str = Field(default="", description="거부 사유 (marketing_acceptable=false 시). 예: 저화질, 인물 신원 노출, 업종 부적합, 민감/의료 장면, 미성년자 식별 노출") # Output 정의 class ImageTagPromptOutput(BaseModel): """이미지 태깅 프롬프트의 LLM 출력 스키마. 공간·피사체·카메라·모션 태그와 내러티브 적합도 점수를 담아 반환. 마케팅 사용 가능 여부는 크롤링 단계(MarketingFilterOutput)에서 이미 판정되므로 포함하지 않는다.""" space_type: list[SpaceType] = Field(..., description="공간적 정보를 가지는 태그 리스트") subject: list[Subject] = Field(..., description="피사체 정보를 가지는 태그 리스트") camera: list[Camera] = Field(..., description="카메라 정보를 가지는 태그 리스트") motion_recommended: list[MotionRecommended] = Field(..., description="가능한 카메라 모션 리스트") narrative_preference: NarrativePreference = Field(..., description="이미지의 내러티브 상 점수")