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