fix(creatomate): 슬롯명이 명명 규칙을 어겨도 썸네일·태그 배정이 폴백되도록 수정
This commit is contained in:
parent
b04e239344
commit
4c4297c37a
@ -574,8 +574,15 @@ class CreatomateService:
|
|||||||
다운로드 실패를 배제로 오판해 풀 전체가 날아가는 것을 막기 위함.
|
다운로드 실패를 배제로 오판해 풀 전체가 날아가는 것을 막기 위함.
|
||||||
일반 씬 슬롯은 태그 점수를 그대로 반환한다.
|
일반 씬 슬롯은 태그 점수를 그대로 반환한다.
|
||||||
"""
|
"""
|
||||||
scores = self.calculate_image_slot_score_multi(pool_subset, slot)
|
is_thumbnail = slot.endswith(THUMBNAIL_SLOT_MARKER)
|
||||||
if not thumbnail_fitness_map or not slot.endswith(THUMBNAIL_SLOT_MARKER):
|
if is_thumbnail and self.parse_slot_name_to_tag(slot) is None:
|
||||||
|
# 슬롯명이 명명 규칙을 어겨 태그 매칭이 불가능한 썸네일 슬롯.
|
||||||
|
# 태그 점수를 0으로 두면 pool 첫 컷이 뽑혀 사실상 무작위가 되므로
|
||||||
|
# 전 이미지 중립(1.0)으로 두고 아래 픽셀 적합도만으로 순위를 가른다.
|
||||||
|
scores = [1.0] * len(pool_subset)
|
||||||
|
else:
|
||||||
|
scores = self.calculate_image_slot_score_multi(pool_subset, slot)
|
||||||
|
if not thumbnail_fitness_map or not is_thumbnail:
|
||||||
return scores
|
return scores
|
||||||
|
|
||||||
adjusted = []
|
adjusted = []
|
||||||
@ -589,6 +596,31 @@ class CreatomateService:
|
|||||||
adjusted.append(score * fitness["score"])
|
adjusted.append(score * fitness["score"])
|
||||||
return adjusted
|
return adjusted
|
||||||
|
|
||||||
|
def _collect_thumbnail_slots(self, template_component_data: dict) -> list[str]:
|
||||||
|
"""배정 대상 썸네일 슬롯(-9999)을 수집합니다.
|
||||||
|
|
||||||
|
일반 씬 슬롯과 달리 슬롯명 파싱에 실패해도 제외하지 않는다. 썸네일은
|
||||||
|
노출 면적이 가장 큰 표면이라, 미배정 시 modify_element가 템플릿 원본
|
||||||
|
(샘플 이미지)을 그대로 남겨 완성 영상에 그대로 나가기 때문이다. 태그
|
||||||
|
매칭이 불가능한 슬롯은 _slot_scores_with_fitness가 픽셀 적합도만으로
|
||||||
|
고른다. 단 '-fixed' 고정 자산은 여기서도 제외한다.
|
||||||
|
|
||||||
|
파싱 실패는 템플릿 슬롯명 오타이므로 ERROR로 남겨 드러나게 한다
|
||||||
|
(조용히 넘어가면 샘플 이미지가 나가도 아무도 알아채지 못한다).
|
||||||
|
"""
|
||||||
|
slots = [
|
||||||
|
name for name, t in template_component_data.items()
|
||||||
|
if t == "image" and name.endswith(THUMBNAIL_SLOT_MARKER)
|
||||||
|
and not is_fixed_slot_name(name)
|
||||||
|
]
|
||||||
|
for name in slots:
|
||||||
|
if self.parse_slot_name_to_tag(name) is None:
|
||||||
|
logger.error(
|
||||||
|
f"[_collect_thumbnail_slots] 썸네일 슬롯명이 명명 규칙 위반 — "
|
||||||
|
f"'{name}' — 템플릿 슬롯명 수정 필요. 픽셀 적합도 기준으로 폴백 배정합니다."
|
||||||
|
)
|
||||||
|
return slots
|
||||||
|
|
||||||
def rank_thumbnail_candidates(
|
def rank_thumbnail_candidates(
|
||||||
self,
|
self,
|
||||||
template: dict,
|
template: dict,
|
||||||
@ -607,11 +639,7 @@ class CreatomateService:
|
|||||||
후보가 없는 슬롯은 키 자체를 포함하지 않는다.
|
후보가 없는 슬롯은 키 자체를 포함하지 않는다.
|
||||||
"""
|
"""
|
||||||
component = self.parse_template_component_name(template["source"]["elements"])
|
component = self.parse_template_component_name(template["source"]["elements"])
|
||||||
thumbnail_slots = [
|
thumbnail_slots = self._collect_thumbnail_slots(component)
|
||||||
name for name, t in component.items()
|
|
||||||
if t == "image" and name.endswith(THUMBNAIL_SLOT_MARKER)
|
|
||||||
and not is_fixed_slot_name(name) and self.parse_slot_name_to_tag(name) is not None
|
|
||||||
]
|
|
||||||
result: dict[str, list[dict]] = {}
|
result: dict[str, list[dict]] = {}
|
||||||
for slot in thumbnail_slots:
|
for slot in thumbnail_slots:
|
||||||
scores = self._slot_scores_with_fitness(taged_image_list, slot, thumbnail_fitness_map)
|
scores = self._slot_scores_with_fitness(taged_image_list, slot, thumbnail_fitness_map)
|
||||||
@ -669,9 +697,13 @@ class CreatomateService:
|
|||||||
# 않는 image 요소는 콘텐츠 슬롯이 아니므로 배정 대상에서 제외 — 그렇지
|
# 않는 image 요소는 콘텐츠 슬롯이 아니므로 배정 대상에서 제외 — 그렇지
|
||||||
# 않으면 파싱 실패로 0점 처리되어 "가장 까다로운 슬롯"으로 취급되고
|
# 않으면 파싱 실패로 0점 처리되어 "가장 까다로운 슬롯"으로 취급되고
|
||||||
# 무작위 이미지로 덮어써진다.
|
# 무작위 이미지로 덮어써진다.
|
||||||
|
# 썸네일 슬롯(-9999)은 파싱 실패해도 배정해야 하므로 여기서 제외하고
|
||||||
|
# _collect_thumbnail_slots가 별도로 수집한다.
|
||||||
image_slots = [
|
image_slots = [
|
||||||
name for name, t in template_component_data.items()
|
name for name, t in template_component_data.items()
|
||||||
if t == "image" and not is_fixed_slot_name(name) and self.parse_slot_name_to_tag(name) is not None
|
if t == "image" and not is_fixed_slot_name(name)
|
||||||
|
and not name.endswith(THUMBNAIL_SLOT_MARKER)
|
||||||
|
and self.parse_slot_name_to_tag(name) is not None
|
||||||
]
|
]
|
||||||
text_slots = [(name, t) for name, t in template_component_data.items() if t == "text"]
|
text_slots = [(name, t) for name, t in template_component_data.items() if t == "text"]
|
||||||
|
|
||||||
@ -686,8 +718,7 @@ class CreatomateService:
|
|||||||
# thumbnail_choice(비전 LLM 최종 선택)가 해당 슬롯에 있으면 그 이미지를,
|
# thumbnail_choice(비전 LLM 최종 선택)가 해당 슬롯에 있으면 그 이미지를,
|
||||||
# 없으면(선택 실패/미제공) 결정론적 최고점 컷을 사용한다 — 폴백 안전.
|
# 없으면(선택 실패/미제공) 결정론적 최고점 컷을 사용한다 — 폴백 안전.
|
||||||
thumbnail_choice = thumbnail_choice or {}
|
thumbnail_choice = thumbnail_choice or {}
|
||||||
thumbnail_slots = [s for s in image_slots if s.endswith(THUMBNAIL_SLOT_MARKER)]
|
thumbnail_slots = self._collect_thumbnail_slots(template_component_data)
|
||||||
image_slots = [s for s in image_slots if not s.endswith(THUMBNAIL_SLOT_MARKER)]
|
|
||||||
for slot in thumbnail_slots:
|
for slot in thumbnail_slots:
|
||||||
if not pool:
|
if not pool:
|
||||||
logger.warning(f"[template_matching_taged_image] 이미지 풀 없음 — 썸네일 슬롯 배정 불가: {slot}")
|
logger.warning(f"[template_matching_taged_image] 이미지 풀 없음 — 썸네일 슬롯 배정 불가: {slot}")
|
||||||
@ -885,7 +916,13 @@ class CreatomateService:
|
|||||||
"""슬롯 이름을 파싱하여 태그 딕셔너리를 반환합니다.
|
"""슬롯 이름을 파싱하여 태그 딕셔너리를 반환합니다.
|
||||||
|
|
||||||
슬롯 이름 형식: {space_type}-{subject}-{camera}-{motion}-{narrative}
|
슬롯 이름 형식: {space_type}-{subject}-{camera}-{motion}-{narrative}
|
||||||
파싱 실패 시 None을 반환합니다 (호출자가 해당 슬롯을 skip+log 처리).
|
|
||||||
|
위치 기반 파싱에 실패하면 토큰 위치를 무시한 대조로 한 번 더 시도한다
|
||||||
|
(_parse_slot_name_loosely). 슬롯명 오타로 슬롯이 배정에서 빠지면
|
||||||
|
modify_element가 템플릿 원본(샘플 이미지)을 그대로 남겨 완성 영상에
|
||||||
|
그대로 나가기 때문이다.
|
||||||
|
|
||||||
|
둘 다 실패하면 None을 반환합니다 (호출자가 해당 슬롯을 skip+log 처리).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tag_list = slot_name.split("-")
|
tag_list = slot_name.split("-")
|
||||||
@ -907,9 +944,64 @@ class CreatomateService:
|
|||||||
}
|
}
|
||||||
return tag_dict
|
return tag_dict
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
|
loose = self._parse_slot_name_loosely(tag_list)
|
||||||
|
if loose is not None:
|
||||||
|
logger.warning(
|
||||||
|
f"[parse_slot_name_to_tag] 슬롯명이 명명 규칙 위반: '{slot_name}' — {e} — "
|
||||||
|
f"위치 무시 대조로 복구: { {k: v.value for k, v in loose.items()} } — 템플릿 슬롯명 수정 권장"
|
||||||
|
)
|
||||||
|
return loose
|
||||||
logger.warning(f"[parse_slot_name_to_tag] 슬롯명 파싱 실패: '{slot_name}' — {e} — 슬롯 skip")
|
logger.warning(f"[parse_slot_name_to_tag] 슬롯명 파싱 실패: '{slot_name}' — {e} — 슬롯 skip")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _parse_slot_name_loosely(self, tag_list: list[str]) -> dict[str, StrEnum] | None:
|
||||||
|
"""토큰 위치를 무시하고 각 enum에 대조해 태그를 복구합니다.
|
||||||
|
|
||||||
|
위치 기반 파싱이 실패했을 때만 호출한다. 토큰 하나는 한 카테고리에만
|
||||||
|
쓰이며, 앞선 토큰부터 순서대로 소비한다(중복 후보가 있으면 앞선 것 채택
|
||||||
|
— 위치 기반과 같은 값을 고르게 된다).
|
||||||
|
|
||||||
|
space_type/subject/narrative는 필수다. 이 셋을 못 채우면 슬롯명이 아닌
|
||||||
|
것으로 보고 None을 반환한다(고정 자산·비규칙 요소가 배정 대상에 섞여
|
||||||
|
무작위 이미지로 덮어써지는 것을 막기 위함). camera/motion은 선택이며
|
||||||
|
못 찾으면 키 자체를 넣지 않는다 — 점수 계산은 태그 딕셔너리를 순회하므로
|
||||||
|
없는 키는 자연히 가중치에서 빠진다.
|
||||||
|
"""
|
||||||
|
used: set[int] = set()
|
||||||
|
|
||||||
|
def take(converter) -> StrEnum | None:
|
||||||
|
for idx, token in enumerate(tag_list):
|
||||||
|
if idx in used:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
value = converter(token)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if value is not None:
|
||||||
|
used.add(idx)
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
space_type = take(SpaceType)
|
||||||
|
subject = take(Subject)
|
||||||
|
narrative = take(NarrativePhase)
|
||||||
|
if space_type is None or subject is None or narrative is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
camera = take(Camera)
|
||||||
|
motion = take(lambda t: MOTION_TOKEN_NORMALIZATION.get(t) or MotionRecommended(t))
|
||||||
|
|
||||||
|
tag_dict: dict[str, StrEnum] = {
|
||||||
|
"space_type": space_type,
|
||||||
|
"subject": subject,
|
||||||
|
"narrative_preference": narrative,
|
||||||
|
}
|
||||||
|
if camera is not None:
|
||||||
|
tag_dict["camera"] = camera
|
||||||
|
if motion is not None:
|
||||||
|
tag_dict["motion_recommended"] = motion
|
||||||
|
return tag_dict
|
||||||
|
|
||||||
def elements_connect_resource_blackbox(
|
def elements_connect_resource_blackbox(
|
||||||
self,
|
self,
|
||||||
elements: list,
|
elements: list,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user