diff --git a/app/database/session.py b/app/database/session.py index 6b914b3..7f60630 100644 --- a/app/database/session.py +++ b/app/database/session.py @@ -139,11 +139,18 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]: raise except Exception as e: await session.rollback() - logger.error(traceback.format_exc()) - logger.error( - f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, " - f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms" - ) + # status_code < 500인 도메인 예외(계정 미연동 등)는 정상적인 비즈니스 흐름이므로 ERROR로 남기지 않음 + if getattr(e, "status_code", 500) < 500: + logger.warning( + f"[get_session] ROLLBACK - client error: {type(e).__name__}: {e}, " + f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms" + ) + else: + logger.error(traceback.format_exc()) + logger.error( + f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, " + f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms" + ) raise finally: total_time = time.perf_counter() - start_time diff --git a/app/lyric/schemas/lyric.py b/app/lyric/schemas/lyric.py index ff476d6..7da6024 100644 --- a/app/lyric/schemas/lyric.py +++ b/app/lyric/schemas/lyric.py @@ -70,11 +70,11 @@ class GenerateLyricRequest(BaseModel): language: str = Field( default="Korean", description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)", - ), + ) orientation: Literal["horizontal", "vertical"] = Field( default="vertical", description="영상 방향 (horizontal: 가로형, vertical: 세로형)", - ), + ) m_id : Optional[int] = Field(None, description="마케팅 인텔리전스 ID 값") industry: str = Field(default="", description="업종 분류 (stay|restaurant|cafe|salon|clinic|fitness|academy|attraction|general)") instrumental: bool = Field(default=False, description="BGM 전용 모드 (가사 생성 안 함)") @@ -83,11 +83,6 @@ class GenerateLyricRequest(BaseModel): description="사용자가 수동으로 고른 음악 장르 (kpop|pop|ballad|hip-hop|rnb|edm|jazz|rock). " "'자동 선택'인 경우 None으로 전달하면 GPT가 가사 무드에 맞춰 추천", ) - genre: Optional[str] = Field( - None, - description="사용자가 수동으로 고른 음악 장르 (kpop|pop|ballad|hip-hop|rnb|edm|jazz|rock). " - "'자동 선택'인 경우 None으로 전달하면 GPT가 가사 무드에 맞춰 추천", - ) class GenerateLyricResponse(BaseModel): diff --git a/app/utils/creatomate.py b/app/utils/creatomate.py index c0662f0..f78e3e8 100644 --- a/app/utils/creatomate.py +++ b/app/utils/creatomate.py @@ -806,7 +806,7 @@ class CreatomateService: """이미지 슬롯에 대한 각 이미지의 매칭 점수를 계산합니다. 점수 = (base * narr_mod + NARR_BONUS * narr) * space_type_penalty - - base: 태그 카테고리 매칭 합산 (최대 5.5) + - base: 태그 카테고리 매칭 합산 (최대 6.5 = space_type 2.0 + subject 3.0 + camera 1.0 + motion 0.5) - narr_mod: [NARR_FLOOR, 1.0] 구간으로 완화된 narrative 계수 - NARR_BONUS * narr: narrative 독립 보너스 (base=0이어도 신호 유지) - space_type_penalty: 슬롯이 요구하는 space_type과 이미지가 불일치하면 @@ -830,6 +830,20 @@ class CreatomateService: for image_tag in image_tag_list ] + # 썸네일 슬롯 한정: subject가 슬롯 요구와 일치하면 space_type 불일치 + # 페널티를 면제한다. 배경: 일부 업종(예: 레스토랑 dining_hall-food_dish)은 + # 태깅 관행상 슬롯이 요구하는 subject(음식 클로즈업)가 실제로는 다른 + # space_type(table_setting/detail_plating 등)으로 붙는다 — space_type과 + # subject가 한 사진에 공존하기 어려운 조합. 이런 슬롯은 space_type 하드 + # 필터가 정작 원하는 subject 이미지를 전부 걸러내는 역효과를 낸다. + # 템플릿 슬롯명을 바꾸지 않고 여기서 흡수(썸네일 한정이라 일반 씬 슬롯의 + # space_type 하드 필터는 그대로 유지). + slot_subject = slot_tag_dict.get("subject") + if slot_name.endswith(THUMBNAIL_SLOT_MARKER) and slot_subject is not None: + for idx, image_tag in enumerate(image_tag_list): + if slot_subject.value in image_tag.get("subject", []): + space_type_match_list[idx] = True + for slot_tag_cate, slot_tag_item in slot_tag_dict.items(): if slot_tag_cate == "narrative_preference": slot_tag_narrative = slot_tag_item @@ -839,7 +853,7 @@ class CreatomateService: case "space_type": weight = 2.0 case "subject": - weight = 2.0 + weight = 3.0 case "camera": weight = 1.0 case "motion_recommended":