diff --git a/app/home/models.py b/app/home/models.py index ff55307..cdcae17 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -300,6 +300,12 @@ class MarketingIntel(Base): comment="마케팅 인텔리전스 결과물", ) + subtitle : Mapped[dict[str, Any]] = mapped_column( + JSON, + nullable=True, + comment="자막 정보 생성 결과물", + ) + created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, diff --git a/app/lyric/api/routers/v1/lyric.py b/app/lyric/api/routers/v1/lyric.py index ee06808..a5d0a91 100644 --- a/app/lyric/api/routers/v1/lyric.py +++ b/app/lyric/api/routers/v1/lyric.py @@ -41,7 +41,7 @@ from app.lyric.schemas.lyric import ( LyricListItem, LyricStatusResponse, ) -from app.lyric.worker.lyric_task import generate_lyric_background +from app.lyric.worker.lyric_task import generate_lyric_background, generate_subtitle_background from app.utils.chatgpt_prompt import ChatgptService from app.utils.logger import get_logger from app.utils.pagination import PaginatedResponse, get_paginated @@ -351,7 +351,7 @@ async def generate_lyric( # ========== Step 4: 백그라운드 태스크 스케줄링 ========== step4_start = time.perf_counter() logger.debug(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...") - + orientation = request_body.orientation background_tasks.add_task( generate_lyric_background, task_id=task_id, @@ -359,6 +359,12 @@ async def generate_lyric( lyric_input_data=lyric_input_data, lyric_id=lyric.id, ) + + background_tasks.add_task( + generate_subtitle_background, + orientation = orientation, + task_id=task_id + ) step4_elapsed = (time.perf_counter() - step4_start) * 1000 logger.debug(f"[generate_lyric] Step 4 완료 ({step4_elapsed:.1f}ms)") diff --git a/app/lyric/schemas/lyric.py b/app/lyric/schemas/lyric.py index 46d28c3..13a3100 100644 --- a/app/lyric/schemas/lyric.py +++ b/app/lyric/schemas/lyric.py @@ -23,7 +23,7 @@ Lyric API Schemas """ from datetime import datetime -from typing import Optional +from typing import Optional, Literal from pydantic import BaseModel, ConfigDict, Field @@ -42,7 +42,8 @@ class GenerateLyricRequest(BaseModel): "region": "군산", "detail_region_info": "군산 신흥동 말랭이 마을", "language": "Korean", - "m_id" : 1 + "m_id" : 1, + "orientation" : "vertical" } """ @@ -54,7 +55,8 @@ class GenerateLyricRequest(BaseModel): "region": "군산", "detail_region_info": "군산 신흥동 말랭이 마을", "language": "Korean", - "m_id" : 1 + "m_id" : 1, + "orientation" : "vertical" } } ) @@ -68,7 +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 값") diff --git a/app/lyric/worker/lyric_task.py b/app/lyric/worker/lyric_task.py index 8c1ce31..42c9321 100644 --- a/app/lyric/worker/lyric_task.py +++ b/app/lyric/worker/lyric_task.py @@ -7,11 +7,15 @@ Lyric Background Tasks import traceback from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import SQLAlchemyError from app.database.session import BackgroundSessionLocal +from app.home.models import Image, Project, MarketingIntel from app.lyric.models import Lyric from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError +from app.utils.subtitles import SubtitleContentsGenerator +from app.utils.creatomate import CreatomateService from app.utils.prompts.prompts import Prompt from app.utils.logger import get_logger @@ -158,3 +162,55 @@ async def generate_lyric_background( elapsed = (time.perf_counter() - task_start) * 1000 logger.error(f"[generate_lyric_background] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)", exc_info=True) await _update_lyric_status(task_id, "failed", f"Error: {str(e)}", lyric_id) + +async def generate_subtitle_background( + orientation: str, + task_id: str +) -> None: + logger.info(f"[generate_subtitle_background] task_id: {task_id}, {orientation}") + creatomate_service = CreatomateService(orientation=orientation) + template = await creatomate_service.get_one_template_data_async(creatomate_service.template_id) + pitchings = creatomate_service.extract_text_format_from_template(template) + + 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}") + + 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] task_id: {task_id} DONE") + + + + return diff --git a/app/utils/creatomate.py b/app/utils/creatomate.py index 0b86b18..990dc27 100644 --- a/app/utils/creatomate.py +++ b/app/utils/creatomate.py @@ -239,6 +239,8 @@ def select_template(orientation:OrientationType): return DHST0001 elif orientation == "vertical": return DVST0001 + else: + raise async def get_shared_client() -> httpx.AsyncClient: """공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다.""" diff --git a/app/video/api/routers/v1/video.py b/app/video/api/routers/v1/video.py index 5f48894..398d4aa 100644 --- a/app/video/api/routers/v1/video.py +++ b/app/video/api/routers/v1/video.py @@ -14,6 +14,8 @@ Video API Router """ import json +import asyncio + from typing import Literal from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query @@ -145,6 +147,34 @@ async def generate_video( image_urls: list[str] = [] try: + subtitle_done = False + count = 0 + async with AsyncSessionLocal() 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() + + while not subtitle_done: + async with AsyncSessionLocal() as session: + logger.info(f"[generate_video] Checking subtitle- task_id: {task_id}, count : {count}") + marketing_result = await session.execute( + select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) + ) + marketing_intelligence = marketing_result.scalar_one_or_none() + subtitle_done = bool(marketing_intelligence.subtitle) + if subtitle_done: + logger.info(f"[generate_video] Check subtitle done task_id: {task_id}") + break + await asyncio.sleep(5) + if count > 12 : + raise Exception("subtitle 결과 생성 실패") + count += 1 + + # 세션을 명시적으로 열고 DB 작업 후 바로 닫음 async with AsyncSessionLocal() as session: # ===== 순차 쿼리 실행: Project, Lyric, Song, Image ===== @@ -198,10 +228,8 @@ async def generate_video( detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.", ) project_id = project.id - marketing_intelligence = project.marketing_intelligence store_address = project.detail_region_info - customer_name = project.store_name - marketing_intelligence = project.marketing_intelligence + # customer_name = project.store_name marketing_result = await session.execute( select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) @@ -296,8 +324,6 @@ async def generate_video( # 2단계: 외부 API 호출 (세션 사용 안함 - 커넥션 풀 점유 없음) # ========================================================================== stage2_start = time.perf_counter() - - subtitle_generator = SubtitleContentsGenerator() try: logger.info( @@ -325,17 +351,7 @@ async def generate_video( ) logger.debug(f"[generate_video] Modifications created - task_id: {task_id}") - pitchings = creatomate_service.extract_text_format_from_template(template) - - 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} + subtitle_modifications = marketing_intelligence.subtitle modifications.update(subtitle_modifications) # 6-3. elements 수정