썸네일 적용
parent
7ec9e9749d
commit
a253c5dd3a
|
|
@ -229,10 +229,15 @@ DVST0001 = "fe11aeab-ff29-4bc8-9f75-c695c7e243e6"
|
|||
DVRT0001 = "3c4b74c4-82d7-4742-9b05-4b999fef4cbd"
|
||||
DVCF0001 = "ebb532ec-e49a-4a7e-b0e2-a8132f1bf1f7"
|
||||
DVAT0001 = "14d4fa22-49b8-49ca-a233-ac0556436f60"
|
||||
DVSL0001 = "20c526a7-b184-46f3-9138-9dfaff2fa342"
|
||||
DVCL0001 = "b57dca80-167d-430a-8905-30f4674ff72b"
|
||||
DVFT0001 = "03b6d521-4864-4e69-8e6c-1deab9f0ce6e"
|
||||
DVAC0001 = "4db8f5a3-4fd1-42cf-86b3-d997f0713d99"
|
||||
|
||||
DHST0001 = "660be601-080a-43ea-bf0f-adcf4596fa98"
|
||||
|
||||
HST_LIST = [DHST0001]
|
||||
VST_LIST = [DVST0001,DVRT0001,DVCF0001,DVAT0001]
|
||||
VST_LIST = [DVST0001,DVRT0001,DVCF0001,DVAT0001,DVSL0001,DVCL0001,DVFT0001,DVAC0001]
|
||||
|
||||
# industry → 세로형 템플릿 매핑 (신규 세로형 템플릿 추가 시 여기에만 등록)
|
||||
VERTICAL_INDUSTRY_TEMPLATE_MAP: dict[str, str] = {
|
||||
|
|
@ -240,9 +245,13 @@ VERTICAL_INDUSTRY_TEMPLATE_MAP: dict[str, str] = {
|
|||
"restaurant": DVRT0001,
|
||||
"cafe": DVCF0001,
|
||||
"attraction": DVAT0001,
|
||||
"salon": DVSL0001,
|
||||
"clinic": DVCL0001,
|
||||
"fitness": DVFT0001,
|
||||
"academy": DVAC0001,
|
||||
}
|
||||
|
||||
# 매핑에 없는 업종(salon, clinic 등)의 폴백 템플릿
|
||||
# 매핑에 없는 업종(general)의 폴백 템플릿
|
||||
DEFAULT_VERTICAL_TEMPLATE = DVST0001
|
||||
|
||||
# 템플릿 슬롯명의 모션 동의어 → MotionRecommended 표준값 정규화 맵.
|
||||
|
|
@ -264,13 +273,21 @@ AUDIO_TRACK = 2
|
|||
SUBTITLE_TRACK = 3
|
||||
KEYWORD_TRACK = 4
|
||||
|
||||
def select_template(orientation: OrientationType, industry: str | None = None) -> str:
|
||||
def select_template(
|
||||
orientation: OrientationType,
|
||||
industry: str | None = None,
|
||||
project_id: int | None = None,
|
||||
) -> str:
|
||||
"""orientation과 industry에 따라 Creatomate 템플릿 ID를 선택합니다.
|
||||
|
||||
Args:
|
||||
orientation: 영상 방향 ("horizontal" 또는 "vertical")
|
||||
industry: 업종 분류 (stay|restaurant|cafe|attraction 등).
|
||||
세로형에서만 사용되며, 매핑에 없으면 기본 템플릿으로 폴백합니다.
|
||||
세로형에서 매핑에 있으면 전용 템플릿을 반환합니다.
|
||||
project_id: 미매핑 업종(general 등)의 세로형 분배용 키.
|
||||
project_id % len(VST_LIST)로 결정적으로 선택하므로, 같은
|
||||
프로젝트는 몇 번 호출해도 동일 템플릿을 받아 사전/렌더 단계가
|
||||
일치한다. 없으면 DEFAULT_VERTICAL_TEMPLATE로 폴백한다.
|
||||
|
||||
Returns:
|
||||
선택된 템플릿 ID
|
||||
|
|
@ -278,10 +295,17 @@ def select_template(orientation: OrientationType, industry: str | None = None) -
|
|||
if orientation == "horizontal":
|
||||
template_id = DHST0001
|
||||
elif orientation == "vertical":
|
||||
template_id = VERTICAL_INDUSTRY_TEMPLATE_MAP.get(industry, DEFAULT_VERTICAL_TEMPLATE)
|
||||
mapped = VERTICAL_INDUSTRY_TEMPLATE_MAP.get(industry)
|
||||
if mapped is not None:
|
||||
template_id = mapped
|
||||
elif project_id is not None:
|
||||
# 미매핑 업종: project_id 나머지로 VST_LIST 결정적 분배 (공유 상태 없음)
|
||||
template_id = VST_LIST[project_id % len(VST_LIST)]
|
||||
else:
|
||||
template_id = DEFAULT_VERTICAL_TEMPLATE
|
||||
else:
|
||||
raise
|
||||
logger.info(f"[select_template] orientation={orientation}, industry={industry}, template_id={template_id}")
|
||||
logger.info(f"[select_template] orientation={orientation}, industry={industry}, project_id={project_id}, template_id={template_id}")
|
||||
return template_id
|
||||
|
||||
async def get_shared_client() -> httpx.AsyncClient:
|
||||
|
|
@ -332,6 +356,7 @@ class CreatomateService:
|
|||
api_key: str | None = None,
|
||||
orientation: OrientationType = "vertical",
|
||||
industry: str | None = None,
|
||||
project_id: int | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
|
|
@ -339,13 +364,15 @@ class CreatomateService:
|
|||
None일 경우 config에서 자동으로 가져옴
|
||||
orientation: 영상 방향 ("horizontal" 또는 "vertical", 기본값: "vertical")
|
||||
industry: 업종 분류 (stay|restaurant|cafe|attraction 등). 세로형 템플릿 선택에 사용되며,
|
||||
매핑에 없거나 None이면 기본 세로형 템플릿으로 폴백
|
||||
매핑에 있으면 전용 템플릿을 사용
|
||||
project_id: 미매핑 업종(general 등)의 세로형 분배용 키. 매핑에 없을 때
|
||||
project_id % len(VST_LIST)로 결정적 선택
|
||||
"""
|
||||
self.api_key = api_key or apikey_settings.CREATOMATE_API_KEY
|
||||
self.orientation = orientation
|
||||
|
||||
# orientation·industry에 따른 템플릿 설정 가져오기
|
||||
self.template_id = select_template(orientation, industry=industry)
|
||||
self.template_id = select_template(orientation, industry=industry, project_id=project_id)
|
||||
self.headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
|
|
@ -624,14 +651,20 @@ class CreatomateService:
|
|||
_NARR_BONUS = 1.5 # narrative 독립 보너스: base=0이어도 narrative 신호가 점수에 남음
|
||||
_NARR_MIN = 0.0 # narrative_preference 값 clamp 하한 (무경계 스키마 방어)
|
||||
_NARR_MAX = 100.0 # narrative_preference 값 clamp 상한. NarrativePreference 스키마(0.0~100.0)와 스케일 일치
|
||||
_SPACE_TYPE_MISMATCH_PENALTY = 0.1 # 슬롯이 요구하는 space_type과 다른 이미지에 적용하는 페널티 배수 (하드 필터에 가깝게)
|
||||
|
||||
def calculate_image_slot_score_multi(self, taged_image_list : list[dict], slot_name : str):
|
||||
"""이미지 슬롯에 대한 각 이미지의 매칭 점수를 계산합니다.
|
||||
|
||||
점수 = base * narr_mod + NARR_BONUS * narr
|
||||
점수 = (base * narr_mod + NARR_BONUS * narr) * space_type_penalty
|
||||
- base: 태그 카테고리 매칭 합산 (최대 5.5)
|
||||
- narr_mod: [NARR_FLOOR, 1.0] 구간으로 완화된 narrative 계수
|
||||
- NARR_BONUS * narr: narrative 독립 보너스 (base=0이어도 신호 유지)
|
||||
- space_type_penalty: 슬롯이 요구하는 space_type과 이미지가 불일치하면
|
||||
SPACE_TYPE_MISMATCH_PENALTY(0.1)를 곱해 사실상 탈락시킴. narrative_preference
|
||||
점수가 아무리 높아도(예: bathroom 이미지가 intro narr=100) 슬롯이 지정한
|
||||
space_type(예: exterior_front)과 다르면 우선 배정되지 않도록 하기 위함 —
|
||||
같은 space_type 이미지가 pool에 하나도 없을 때만 상대적으로 선택됨(fallback 유지).
|
||||
이 구조로 "base=0, narrative가 높음"과 "완전히 틀린 이미지(0,0)"를 구별할 수 있음.
|
||||
"""
|
||||
image_tag_list = [taged_image["image_tag"] for taged_image in taged_image_list]
|
||||
|
|
@ -642,6 +675,11 @@ class CreatomateService:
|
|||
|
||||
base_score_list = [0.0] * len(image_tag_list)
|
||||
# slot_tag_narrative = NarrativePhase.accent # 기본값 (narrative 토큰 없는 슬롯 대비)
|
||||
slot_space_type = slot_tag_dict.get("space_type")
|
||||
space_type_match_list = [
|
||||
slot_space_type is not None and slot_space_type.value in image_tag.get("space_type", [])
|
||||
for image_tag in image_tag_list
|
||||
]
|
||||
|
||||
for slot_tag_cate, slot_tag_item in slot_tag_dict.items():
|
||||
if slot_tag_cate == "narrative_preference":
|
||||
|
|
@ -674,6 +712,8 @@ class CreatomateService:
|
|||
narr = clamped_narr / self._NARR_MAX # 0.0~1.0로 정규화
|
||||
narr_mod = self._NARR_FLOOR + (1.0 - self._NARR_FLOOR) * narr
|
||||
score = base_score_list[idx] * narr_mod + self._NARR_BONUS * narr
|
||||
if not space_type_match_list[idx]:
|
||||
score *= self._SPACE_TYPE_MISMATCH_PENALTY
|
||||
image_score_list.append(score)
|
||||
|
||||
return image_score_list
|
||||
|
|
@ -1110,7 +1150,7 @@ class CreatomateService:
|
|||
return pitching_list
|
||||
|
||||
|
||||
def make_thumbnail_modification(self, brand_name : str, region : str, brand_concept : str, category_definition : str, target_keywords : list[str]):
|
||||
def make_thumbnail_modification(self, brand_name : str, region : str, category_definition : str, target_keywords : list[str]):
|
||||
|
||||
len_keywords = len(target_keywords) if len(target_keywords) < 3 else 3
|
||||
|
||||
|
|
@ -1118,9 +1158,8 @@ class CreatomateService:
|
|||
|
||||
mod_dict = {
|
||||
"thumb-hashtag-primary" : ' '.join(hashtaged_target_keywords),
|
||||
"thumb-brand-wordmark" : brand_name,
|
||||
"thumb-subheadline-selling_point" : f"{brand_name} · {normalize_location(region)}",
|
||||
"thumb-headline-hook_claim-aspirational" : brand_concept,
|
||||
"thumb-headline-brand_name-factual" : brand_name,
|
||||
"thumb-subheadline-local_info-factual" : normalize_location(region),
|
||||
"thumb-badge-category" : category_definition,
|
||||
}
|
||||
return mod_dict
|
||||
|
|
|
|||
|
|
@ -70,6 +70,33 @@ class SpaceType(StrEnum):
|
|||
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):
|
||||
"""이미지 내 주요 피사체 유형. 화면에 무엇이 담겨 있는지를 분류하는 태그."""
|
||||
|
|
@ -94,6 +121,17 @@ class Subject(StrEnum):
|
|||
# 관광지(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):
|
||||
"""이미지의 촬영 기법·구도·조명 특성. 영상 편집 시 컷의 시각적 스타일을 구분하는 태그."""
|
||||
|
|
|
|||
|
|
@ -221,11 +221,6 @@ async def generate_video(
|
|||
category_definition = marketing_intelligence.intel_result["market_positioning"]["category_definition"]
|
||||
target_keywords = marketing_intelligence.intel_result["target_keywords"]
|
||||
|
||||
brand_concept = ""
|
||||
for sp in marketing_intelligence.intel_result["selling_points"]:
|
||||
if "concept" in sp["english_category"].lower():
|
||||
brand_concept = sp["description"]
|
||||
|
||||
# Lyric 조회
|
||||
lyric_result = await session.execute(
|
||||
select(Lyric)
|
||||
|
|
@ -391,7 +386,6 @@ async def generate_video(
|
|||
thumbnail_modifications = creatomate_service.make_thumbnail_modification(
|
||||
brand_name =brand_name,
|
||||
region = region,
|
||||
brand_concept = brand_concept,
|
||||
category_definition= category_definition,
|
||||
target_keywords=target_keywords)
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ async def generate_creative_assets_background(
|
|||
)
|
||||
marketing_intelligence = marketing_result.scalar_one_or_none()
|
||||
|
||||
creatomate_service = CreatomateService(orientation=orientation, industry=project.industry)
|
||||
creatomate_service = CreatomateService(orientation=orientation, industry=project.industry, project_id=project.id)
|
||||
template = await creatomate_service.get_one_template_data(creatomate_service.template_id)
|
||||
|
||||
store_address = project.detail_region_info
|
||||
|
|
|
|||
Loading…
Reference in New Issue