From e53084ed175cc8c640f40cd3903e4a2b12034bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Thu, 9 Jul 2026 14:19:48 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=B4=ED=8E=98,=20=EC=8B=9D=EB=8B=B9=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/utils/creatomate.py | 46 +++++++++++++++++------- app/utils/nvMapPwScraper.py | 7 ++-- app/video/api/routers/v1/video.py | 4 ++- app/video/worker/creative_assets_task.py | 7 ++-- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/app/utils/creatomate.py b/app/utils/creatomate.py index e081a8e..6315b79 100644 --- a/app/utils/creatomate.py +++ b/app/utils/creatomate.py @@ -225,29 +225,48 @@ autotext_template_h_1 = { "stroke_color": "#333333", "stroke_width": "0.2 vmin" } -DVST0001 = "75161273-0422-4771-adeb-816bd7263fb0" -DVST0002 = "c68cf750-bc40-485a-a2c5-3f9fe301e386" -DVST0003 = "e1fb5b00-1f02-4f63-99fa-7524b433ba47" +DVST0001 = "fe11aeab-ff29-4bc8-9f75-c695c7e243e6" +DVRT0001 = "3c4b74c4-82d7-4742-9b05-4b999fef4cbd" +DVCF0001 = "ebb532ec-e49a-4a7e-b0e2-a8132f1bf1f7" DHST0001 = "660be601-080a-43ea-bf0f-adcf4596fa98" DHST0002 = "3f194cc7-464e-4581-9db2-179d42d3e40f" DHST0003 = "f45df555-2956-4a13-9004-ead047070b3d" -DVST0001T = "fe11aeab-ff29-4bc8-9f75-c695c7e243e6" HST_LIST = [DHST0001,DHST0002,DHST0003] -VST_LIST = [DVST0001T] +VST_LIST = [DVST0001,DVRT0001,DVCF0001] + +# industry → 세로형 템플릿 매핑 (신규 세로형 템플릿 추가 시 여기에만 등록) +VERTICAL_INDUSTRY_TEMPLATE_MAP: dict[str, str] = { + "stay": DVST0001, + "restaurant": DVRT0001, + "cafe": DVCF0001, +} + +# 매핑에 없는 업종(salon, clinic 등)의 폴백 템플릿 +DEFAULT_VERTICAL_TEMPLATE = DVST0001 SCENE_TRACK = 1 AUDIO_TRACK = 2 SUBTITLE_TRACK = 3 KEYWORD_TRACK = 4 -def select_template(orientation:OrientationType): +def select_template(orientation: OrientationType, industry: str | None = None) -> str: + """orientation과 industry에 따라 Creatomate 템플릿 ID를 선택합니다. + + Args: + orientation: 영상 방향 ("horizontal" 또는 "vertical") + industry: 업종 분류 (stay|restaurant|cafe 등). + 세로형에서만 사용되며, 매핑에 없으면 기본 템플릿으로 폴백합니다. + + Returns: + 선택된 템플릿 ID + """ if orientation == "horizontal": template_id = DHST0001 elif orientation == "vertical": - template_id = random.choice(VST_LIST) + template_id = VERTICAL_INDUSTRY_TEMPLATE_MAP.get(industry, DEFAULT_VERTICAL_TEMPLATE) else: raise - logger.info(f"[select_template] orientation={orientation}, template_id={template_id}") + logger.info(f"[select_template] orientation={orientation}, industry={industry}, template_id={template_id}") return template_id async def get_shared_client() -> httpx.AsyncClient: @@ -296,21 +315,22 @@ class CreatomateService: def __init__( self, api_key: str | None = None, - orientation: OrientationType = "vertical" + orientation: OrientationType = "vertical", + industry: str | None = None, ): """ Args: api_key: Creatomate API 키 (Bearer token으로 사용) None일 경우 config에서 자동으로 가져옴 orientation: 영상 방향 ("horizontal" 또는 "vertical", 기본값: "vertical") - target_duration: 목표 영상 길이 (초) - None일 경우 orientation에 해당하는 기본값 사용 + industry: 업종 분류 (stay|restaurant|cafe 등). 세로형 템플릿 선택에 사용되며, + 매핑에 없거나 None이면 기본 세로형 템플릿으로 폴백 """ self.api_key = api_key or apikey_settings.CREATOMATE_API_KEY self.orientation = orientation - # orientation에 따른 템플릿 설정 가져오기 - self.template_id = select_template(orientation) + # orientation·industry에 따른 템플릿 설정 가져오기 + self.template_id = select_template(orientation, industry=industry) self.headers = { "Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}", diff --git a/app/utils/nvMapPwScraper.py b/app/utils/nvMapPwScraper.py index 039e85f..8e38d99 100644 --- a/app/utils/nvMapPwScraper.py +++ b/app/utils/nvMapPwScraper.py @@ -18,8 +18,9 @@ class NvMapPwScraper(): _context = None _win_width = 1280 _win_height = 720 - _max_retry = 3 - _timeout = 60 # place id timeout threshold seconds + _max_retry = 3 + _timeout = 30 # place id timeout threshold seconds + _retry_delay_ms = 1500 # 검색 실패 후 재시도 전 대기시간 (네이버 요청 밀도 완화) # instance var page = None @@ -422,12 +423,14 @@ patchedGetter.toString();''') # 2차 시도: 정제 주소 + 업체명 refined = self._refine_address(address) if refined != address: + await self.page.wait_for_timeout(self._retry_delay_ms) logger.info(f"[REFINE] 주소 정제: '{address}' → '{refined}'") result = await self._try_search(refined, title) if result: return result # 3차 시도: 업체명만으로 검색 + await self.page.wait_for_timeout(self._retry_delay_ms) logger.info(f"[RETRY] 업체명만으로 재시도: '{title}'") result = await self._try_search("", title) if result: diff --git a/app/video/api/routers/v1/video.py b/app/video/api/routers/v1/video.py index 8e2265d..ddb3f8a 100644 --- a/app/video/api/routers/v1/video.py +++ b/app/video/api/routers/v1/video.py @@ -191,6 +191,7 @@ async def generate_video( store_address = project.detail_region_info brand_name = project.store_name region = project.region + industry = project.industry # MarketingIntel 조회 marketing_result = await session.execute( @@ -357,7 +358,8 @@ async def generate_video( f"[generate_video] Stage 2 START - Creatomate API - task_id: {task_id}" ) creatomate_service = CreatomateService( - orientation=orientation + orientation=orientation, + industry=industry, ) logger.debug( f"[generate_video] Using template_id: {creatomate_service.template_id}, (song duration: {song_duration})" diff --git a/app/video/worker/creative_assets_task.py b/app/video/worker/creative_assets_task.py index 5c20e89..4f87732 100644 --- a/app/video/worker/creative_assets_task.py +++ b/app/video/worker/creative_assets_task.py @@ -57,10 +57,8 @@ async def generate_creative_assets_background( for attempt in range(1, max_retries + 1): try: - creatomate_service = CreatomateService(orientation=orientation) - template = await creatomate_service.get_one_template_data(creatomate_service.template_id) - # ── 프로젝트 / 마케팅 인텔 로드 ────────────────────────────────── + # 템플릿 선택에 project.industry가 필요하므로 서비스 생성보다 먼저 로드 async with BackgroundSessionLocal() as session: project_result = await session.execute( select(Project) @@ -74,6 +72,9 @@ async def generate_creative_assets_background( ) marketing_intelligence = marketing_result.scalar_one_or_none() + creatomate_service = CreatomateService(orientation=orientation, industry=project.industry) + template = await creatomate_service.get_one_template_data(creatomate_service.template_id) + store_address = project.detail_region_info customer_name = project.store_name language = project.language or "Korean"