lyric 생성 시점에 subtitle 생성하도록 DB 및 코드 변경
parent
7da6ab6ec0
commit
395b4dbbfb
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -359,6 +359,12 @@ async def generate_lyric(
|
||||||
lyric_input_data=lyric_input_data,
|
lyric_input_data=lyric_input_data,
|
||||||
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)")
|
||||||
|
|
|
||||||
|
|
@ -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 값")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 클라이언트를 반환합니다. 없으면 생성합니다."""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -296,8 +324,6 @@ async def generate_video(
|
||||||
# 2단계: 외부 API 호출 (세션 사용 안함 - 커넥션 풀 점유 없음)
|
# 2단계: 외부 API 호출 (세션 사용 안함 - 커넥션 풀 점유 없음)
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
stage2_start = time.perf_counter()
|
stage2_start = time.perf_counter()
|
||||||
|
|
||||||
subtitle_generator = SubtitleContentsGenerator()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -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 수정
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue