220 lines
9.7 KiB
Python
220 lines
9.7 KiB
Python
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()
|
|
# 관광지(DVAT0001) 템플릿 어휘
|
|
entrance_gate = auto()
|
|
exhibition_hall = auto()
|
|
landmark_main = auto()
|
|
landmark_detail = auto()
|
|
night_view = auto()
|
|
panorama_view = auto()
|
|
photo_spot = auto()
|
|
seasonal_scene = auto()
|
|
walking_path = auto()
|
|
# 미용실(DVSL0001) 템플릿 어휘
|
|
mirror_detail = auto()
|
|
styling_zone = auto()
|
|
waiting_lounge = auto()
|
|
# 병원(DVCL0001) 템플릿 어휘
|
|
consultation_room = auto()
|
|
detail_amenity = auto()
|
|
equipment_zone = auto()
|
|
recovery_room = auto()
|
|
treatment_room = auto()
|
|
waiting_area = auto()
|
|
# 피트니스(DVFT0001) 템플릿 어휘
|
|
apparatus_zone = auto()
|
|
brand_sign = auto()
|
|
detail_equipment = auto()
|
|
locker_room = auto()
|
|
powder_room = auto()
|
|
pt_zone = auto()
|
|
reformer_zone = auto()
|
|
# 학원(DVAC0001) 템플릿 어휘
|
|
classroom = auto()
|
|
counseling_room = auto()
|
|
detail_facility = auto()
|
|
detail_interior_prop = auto()
|
|
detail_materials = auto()
|
|
library_corner = auto()
|
|
study_room = 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()
|
|
# 관광지(DVAT0001) 템플릿 어휘
|
|
scenery = auto()
|
|
structure = auto()
|
|
# 병원(DVCL0001) 템플릿 어휘
|
|
interior_clean = auto()
|
|
medical_equipment = auto()
|
|
# 피트니스(DVFT0001) 템플릿 어휘
|
|
interior_scale = auto()
|
|
pilates_apparatus = auto()
|
|
training_action = auto()
|
|
# 학원(DVAC0001) 템플릿 어휘
|
|
facility_equipment = auto()
|
|
learning_material = auto()
|
|
study_scene = 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="이미지의 내러티브 상 점수")
|
|
|