From 73e5da3f08639f20c8aed43d1d3984c2aeaa7f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Wed, 27 May 2026 15:44:44 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9E=90=EB=A7=89=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lyric/schemas/lyric.py | 11 +++- app/lyric/worker/lyric_task.py | 106 ++++++++++++++++++--------------- app/utils/subtitles.py | 21 ++++++- 3 files changed, 87 insertions(+), 51 deletions(-) diff --git a/app/lyric/schemas/lyric.py b/app/lyric/schemas/lyric.py index 4201dff..f90cbc8 100644 --- a/app/lyric/schemas/lyric.py +++ b/app/lyric/schemas/lyric.py @@ -212,6 +212,7 @@ class SubtitleStatusResponse(BaseModel): Status Values: - pending: 자막 생성 진행 중 (재시도 필요) - completed: 자막 생성 완료 (/video/generate 호출 가능) + - failed: 자막 생성 실패 (/lyric/generate 재호출 필요) """ model_config = ConfigDict( @@ -233,12 +234,20 @@ class SubtitleStatusResponse(BaseModel): "message": "자막 생성이 완료되었습니다.", }, }, + { + "summary": "실패", + "value": { + "task_id": "0694b716-dbff-7219-8000-d08cb5fce431", + "status": "failed", + "message": "자막 생성에 실패했습니다. 다시 시도해주세요.", + }, + }, ] } ) task_id: str = Field(..., description="작업 고유 식별자") - status: Literal["pending", "completed"] = Field(..., description="자막 생성 상태") + status: Literal["pending", "completed", "failed"] = Field(..., description="자막 생성 상태") message: str = Field(..., description="상태 메시지") diff --git a/app/lyric/worker/lyric_task.py b/app/lyric/worker/lyric_task.py index aa1f015..dd0620c 100644 --- a/app/lyric/worker/lyric_task.py +++ b/app/lyric/worker/lyric_task.py @@ -158,59 +158,67 @@ async def generate_lyric_background( async def generate_subtitle_background( orientation: str, - task_id: str + task_id: str, + max_retries: int = 3, ) -> None: logger.info(f"[generate_subtitle_background] START - task_id: {task_id}, orientation: {orientation}") - try: - creatomate_service = CreatomateService(orientation=orientation) - template = await creatomate_service.get_one_template_data(creatomate_service.template_id) - pitchings = creatomate_service.extract_text_format_from_template(template) - subtitle_generator = SubtitleContentsGenerator() + 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) + pitchings = creatomate_service.extract_text_format_from_template(template) - async with BackgroundSessionLocal() as session: - project_result = await session.execute( - select(Project) - .where(Project.task_id == task_id) - .order_by(Project.created_at.desc()) - .limit(1) + subtitle_generator = SubtitleContentsGenerator() + + async with BackgroundSessionLocal() as session: + project_result = await session.execute( + select(Project) + .where(Project.task_id == task_id) + .order_by(Project.created_at.desc()) + .limit(1) + ) + project = project_result.scalar_one_or_none() + marketing_result = await session.execute( + select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) + ) + marketing_intelligence = marketing_result.scalar_one_or_none() + + store_address = project.detail_region_info + customer_name = project.store_name + logger.info(f"[generate_subtitle_background] customer_name: {customer_name}, store_address: {store_address}") + + generated_subtitles = await subtitle_generator.generate_subtitle_contents( + marketing_intelligence=marketing_intelligence.intel_result, + pitching_label_list=pitchings, + customer_name=customer_name, + detail_region_info=store_address, ) - project = project_result.scalar_one_or_none() - marketing_result = await session.execute( - select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) + pitching_output_list = generated_subtitles.pitching_results + + subtitle_modifications = { + pitching_output.pitching_tag: pitching_output.pitching_data + for pitching_output in pitching_output_list + } + logger.info(f"[generate_subtitle_background] subtitle_modifications: {subtitle_modifications}") + + async with BackgroundSessionLocal() as session: + marketing_result = await session.execute( + select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) + ) + marketing_intelligence = marketing_result.scalar_one_or_none() + marketing_intelligence.subtitle = subtitle_modifications + await session.commit() + + logger.info(f"[generate_subtitle_background] DONE - task_id: {task_id} (attempt {attempt}/{max_retries})") + return + + except Exception as e: + logger.error( + f"[generate_subtitle_background] FAILED (attempt {attempt}/{max_retries}) - task_id: {task_id}, error: {e}", + exc_info=True, ) - marketing_intelligence = marketing_result.scalar_one_or_none() + if attempt < max_retries: + logger.info(f"[generate_subtitle_background] 재시도 중... ({attempt + 1}/{max_retries}) - task_id: {task_id}") - store_address = project.detail_region_info - customer_name = project.store_name - logger.info(f"[generate_subtitle_background] customer_name: {customer_name}, store_address: {store_address}") - - generated_subtitles = await subtitle_generator.generate_subtitle_contents( - marketing_intelligence=marketing_intelligence.intel_result, - pitching_label_list=pitchings, - customer_name=customer_name, - detail_region_info=store_address, - ) - pitching_output_list = generated_subtitles.pitching_results - - subtitle_modifications = { - pitching_output.pitching_tag: pitching_output.pitching_data - for pitching_output in pitching_output_list - } - logger.info(f"[generate_subtitle_background] subtitle_modifications: {subtitle_modifications}") - - async with BackgroundSessionLocal() as session: - marketing_result = await session.execute( - select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) - ) - marketing_intelligence = marketing_result.scalar_one_or_none() - marketing_intelligence.subtitle = subtitle_modifications - await session.commit() - - logger.info(f"[generate_subtitle_background] DONE - task_id: {task_id}") - - except Exception as e: - logger.error( - f"[generate_subtitle_background] FAILED - task_id: {task_id}, error: {e}", - exc_info=True, - ) + logger.error(f"[generate_subtitle_background] 모든 재시도 실패 - task_id: {task_id}") diff --git a/app/utils/subtitles.py b/app/utils/subtitles.py index 3a930dd..06c6a12 100644 --- a/app/utils/subtitles.py +++ b/app/utils/subtitles.py @@ -10,11 +10,20 @@ from app.utils.prompts.chatgpt_prompt import ChatgptService from app.utils.prompts.schemas import * from app.utils.prompts.prompts import * +logger = get_logger("subtitle") + class SubtitleContentsGenerator(): def __init__(self): - self.chatgpt_service = ChatgptService() + self.chatgpt_service = ChatgptService(timeout=60.0) async def generate_subtitle_contents(self, marketing_intelligence : dict[str, Any], pitching_label_list : list[Any], customer_name : str, detail_region_info : str) -> SubtitlePromptOutput: + start = time.perf_counter() + logger.info( + f"[SubtitleContentsGenerator] START - customer: {customer_name}, " + f"pitching_count: {len(pitching_label_list)}, " + f"labels: {pitching_label_list}" + ) + dynamic_subtitle_prompt = create_dynamic_subtitle_prompt(len(pitching_label_list)) pitching_label_string = "\n".join(pitching_label_list) marketing_intel_string = json.dumps(marketing_intelligence, ensure_ascii=False) @@ -24,7 +33,17 @@ class SubtitleContentsGenerator(): "customer_name" : customer_name, "detail_region_info" : detail_region_info, } + + logger.info( + f"[SubtitleContentsGenerator] GPT 호출 시작 - model: {dynamic_subtitle_prompt.prompt_model}" + ) output_data = await self.chatgpt_service.generate_structured_output(dynamic_subtitle_prompt, input_data) + + elapsed = (time.perf_counter() - start) * 1000 + logger.info( + f"[SubtitleContentsGenerator] DONE - 소요시간: {elapsed:.0f}ms, " + f"결과: {[r.pitching_tag for r in output_data.pitching_results]}" + ) return output_data