lyric 생성 시점에 subtitle 생성하도록 DB 및 코드 변경

subtitle
jaehwang 2026-03-24 06:43:32 +00:00
parent 7da6ab6ec0
commit 395b4dbbfb
6 changed files with 114 additions and 22 deletions

View File

@ -300,6 +300,12 @@ class MarketingIntel(Base):
comment="마케팅 인텔리전스 결과물", comment="마케팅 인텔리전스 결과물",
) )
subtitle : Mapped[dict[str, Any]] = mapped_column(
JSON,
nullable=True,
comment="자막 정보 생성 결과물",
)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
nullable=False, nullable=False,

View File

@ -41,7 +41,7 @@ from app.lyric.schemas.lyric import (
LyricListItem, LyricListItem,
LyricStatusResponse, 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.chatgpt_prompt import ChatgptService
from app.utils.logger import get_logger from app.utils.logger import get_logger
from app.utils.pagination import PaginatedResponse, get_paginated from app.utils.pagination import PaginatedResponse, get_paginated
@ -351,7 +351,7 @@ async def generate_lyric(
# ========== Step 4: 백그라운드 태스크 스케줄링 ========== # ========== Step 4: 백그라운드 태스크 스케줄링 ==========
step4_start = time.perf_counter() step4_start = time.perf_counter()
logger.debug(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...") logger.debug(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...")
orientation = request_body.orientation
background_tasks.add_task( background_tasks.add_task(
generate_lyric_background, generate_lyric_background,
task_id=task_id, task_id=task_id,
@ -360,6 +360,12 @@ async def generate_lyric(
lyric_id=lyric.id, 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 step4_elapsed = (time.perf_counter() - step4_start) * 1000
logger.debug(f"[generate_lyric] Step 4 완료 ({step4_elapsed:.1f}ms)") logger.debug(f"[generate_lyric] Step 4 완료 ({step4_elapsed:.1f}ms)")

View File

@ -23,7 +23,7 @@ Lyric API Schemas
""" """
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Literal
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
@ -42,7 +42,8 @@ class GenerateLyricRequest(BaseModel):
"region": "군산", "region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을", "detail_region_info": "군산 신흥동 말랭이 마을",
"language": "Korean", "language": "Korean",
"m_id" : 1 "m_id" : 1,
"orientation" : "vertical"
} }
""" """
@ -54,7 +55,8 @@ class GenerateLyricRequest(BaseModel):
"region": "군산", "region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을", "detail_region_info": "군산 신흥동 말랭이 마을",
"language": "Korean", "language": "Korean",
"m_id" : 1 "m_id" : 1,
"orientation" : "vertical"
} }
} }
) )
@ -68,7 +70,11 @@ class GenerateLyricRequest(BaseModel):
language: str = Field( language: str = Field(
default="Korean", default="Korean",
description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)", 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 값") m_id : Optional[int] = Field(None, description="마케팅 인텔리전스 ID 값")

View File

@ -7,11 +7,15 @@ Lyric Background Tasks
import traceback import traceback
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.database.session import BackgroundSessionLocal from app.database.session import BackgroundSessionLocal
from app.home.models import Image, Project, MarketingIntel
from app.lyric.models import Lyric from app.lyric.models import Lyric
from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError 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.prompts.prompts import Prompt
from app.utils.logger import get_logger from app.utils.logger import get_logger
@ -158,3 +162,55 @@ async def generate_lyric_background(
elapsed = (time.perf_counter() - task_start) * 1000 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) 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) 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

View File

@ -239,6 +239,8 @@ def select_template(orientation:OrientationType):
return DHST0001 return DHST0001
elif orientation == "vertical": elif orientation == "vertical":
return DVST0001 return DVST0001
else:
raise
async def get_shared_client() -> httpx.AsyncClient: async def get_shared_client() -> httpx.AsyncClient:
"""공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다.""" """공유 HTTP 클라이언트를 반환합니다. 없으면 생성합니다."""

View File

@ -14,6 +14,8 @@ Video API Router
""" """
import json import json
import asyncio
from typing import Literal from typing import Literal
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
@ -145,6 +147,34 @@ async def generate_video(
image_urls: list[str] = [] image_urls: list[str] = []
try: 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 작업 후 바로 닫음 # 세션을 명시적으로 열고 DB 작업 후 바로 닫음
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
# ===== 순차 쿼리 실행: Project, Lyric, Song, Image ===== # ===== 순차 쿼리 실행: Project, Lyric, Song, Image =====
@ -198,10 +228,8 @@ async def generate_video(
detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.", detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.",
) )
project_id = project.id project_id = project.id
marketing_intelligence = project.marketing_intelligence
store_address = project.detail_region_info store_address = project.detail_region_info
customer_name = project.store_name # customer_name = project.store_name
marketing_intelligence = project.marketing_intelligence
marketing_result = await session.execute( marketing_result = await session.execute(
select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence) select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence)
@ -297,8 +325,6 @@ async def generate_video(
# ========================================================================== # ==========================================================================
stage2_start = time.perf_counter() stage2_start = time.perf_counter()
subtitle_generator = SubtitleContentsGenerator()
try: try:
logger.info( logger.info(
f"[generate_video] Stage 2 START - Creatomate API - task_id: {task_id}" f"[generate_video] Stage 2 START - Creatomate API - task_id: {task_id}"
@ -325,17 +351,7 @@ async def generate_video(
) )
logger.debug(f"[generate_video] Modifications created - task_id: {task_id}") logger.debug(f"[generate_video] Modifications created - task_id: {task_id}")
pitchings = creatomate_service.extract_text_format_from_template(template) subtitle_modifications = marketing_intelligence.subtitle
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}
modifications.update(subtitle_modifications) modifications.update(subtitle_modifications)
# 6-3. elements 수정 # 6-3. elements 수정