자막 생성 상태 조회를 위한 엔드포인트 추가 및 비동기 처리
parent
666115ae88
commit
346b461e9c
|
|
@ -40,6 +40,7 @@ from app.lyric.schemas.lyric import (
|
|||
LyricDetailResponse,
|
||||
LyricListItem,
|
||||
LyricStatusResponse,
|
||||
SubtitleStatusResponse,
|
||||
)
|
||||
from app.lyric.worker.lyric_task import generate_lyric_background, generate_subtitle_background
|
||||
from app.utils.prompts.chatgpt_prompt import ChatgptService
|
||||
|
|
@ -515,6 +516,86 @@ async def list_lyrics(
|
|||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/subtitle/status/{task_id}",
|
||||
summary="자막 생성 상태 조회",
|
||||
description="""
|
||||
자막(subtitle) 생성 완료 여부를 조회합니다.
|
||||
|
||||
## 인증
|
||||
**Bearer 토큰 필수** - `Authorization: Bearer {access_token}` 헤더를 포함해야 합니다.
|
||||
|
||||
## 경로 파라미터
|
||||
- **task_id**: 프로젝트 task_id (필수)
|
||||
|
||||
## 상태 값
|
||||
- **pending**: 자막 생성 진행 중 — 잠시 후 재요청
|
||||
- **completed**: 자막 생성 완료 — `/video/generate/{task_id}` 호출 가능
|
||||
|
||||
## 사용 예시 (cURL)
|
||||
```bash
|
||||
curl -X GET "http://localhost:8000/lyric/subtitle/status/019123ab-cdef-7890-abcd-ef1234567890" \\
|
||||
-H "Authorization: Bearer {access_token}"
|
||||
```
|
||||
|
||||
## 참고
|
||||
- 자막은 `/lyric/generate` 호출 시 백그라운드에서 자동 생성됩니다.
|
||||
- 클라이언트는 `completed` 상태 확인 후 `/video/generate`를 호출해야 합니다.
|
||||
""",
|
||||
response_model=SubtitleStatusResponse,
|
||||
responses={
|
||||
200: {"description": "상태 조회 성공"},
|
||||
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
||||
404: {"description": "해당 task_id에 해당하는 프로젝트를 찾을 수 없음"},
|
||||
},
|
||||
)
|
||||
async def get_subtitle_status(
|
||||
task_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> SubtitleStatusResponse:
|
||||
"""task_id로 자막 생성 상태를 조회합니다."""
|
||||
logger.info(f"[get_subtitle_status] START - task_id: {task_id}")
|
||||
|
||||
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()
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 프로젝트를 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
marketing_result = await session.execute(
|
||||
select(MarketingIntel).where(MarketingIntel.id == project.marketing_intelligence)
|
||||
)
|
||||
intel = marketing_result.scalar_one_or_none()
|
||||
if not intel:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 마케팅 인텔리전스를 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
if intel.subtitle:
|
||||
logger.info(f"[get_subtitle_status] completed - task_id: {task_id}")
|
||||
return SubtitleStatusResponse(
|
||||
task_id=task_id,
|
||||
status="completed",
|
||||
message="자막 생성이 완료되었습니다.",
|
||||
)
|
||||
|
||||
logger.info(f"[get_subtitle_status] pending - task_id: {task_id}")
|
||||
return SubtitleStatusResponse(
|
||||
task_id=task_id,
|
||||
status="pending",
|
||||
message="자막 생성이 진행 중입니다. 잠시 후 다시 확인해주세요.",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{task_id}",
|
||||
summary="가사 상세 조회",
|
||||
|
|
|
|||
|
|
@ -202,6 +202,46 @@ class LyricDetailResponse(BaseModel):
|
|||
created_at: Optional[datetime] = Field(None, description="생성 일시")
|
||||
|
||||
|
||||
class SubtitleStatusResponse(BaseModel):
|
||||
"""자막 생성 상태 조회 응답 스키마
|
||||
|
||||
Usage:
|
||||
GET /subtitle/status/{task_id}
|
||||
클라이언트가 subtitle 완료 여부를 polling할 때 사용합니다.
|
||||
|
||||
Status Values:
|
||||
- pending: 자막 생성 진행 중 (재시도 필요)
|
||||
- completed: 자막 생성 완료 (/video/generate 호출 가능)
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"examples": [
|
||||
{
|
||||
"summary": "생성 중",
|
||||
"value": {
|
||||
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
||||
"status": "pending",
|
||||
"message": "자막 생성이 진행 중입니다. 잠시 후 다시 확인해주세요.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"summary": "완료",
|
||||
"value": {
|
||||
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
||||
"status": "completed",
|
||||
"message": "자막 생성이 완료되었습니다.",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
task_id: str = Field(..., description="작업 고유 식별자")
|
||||
status: Literal["pending", "completed"] = Field(..., description="자막 생성 상태")
|
||||
message: str = Field(..., description="상태 메시지")
|
||||
|
||||
|
||||
class LyricListItem(BaseModel):
|
||||
"""가사 목록 아이템 스키마
|
||||
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@ async def generate_subtitle_background(
|
|||
orientation: str,
|
||||
task_id: str
|
||||
) -> None:
|
||||
logger.info(f"[generate_subtitle_background] task_id: {task_id}, {orientation}")
|
||||
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)
|
||||
|
|
@ -182,7 +183,7 @@ async def generate_subtitle_background(
|
|||
|
||||
store_address = project.detail_region_info
|
||||
customer_name = project.store_name
|
||||
logger.info(f"[generate_subtitle_background] customer_name: {customer_name}, {store_address}")
|
||||
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,
|
||||
|
|
@ -192,7 +193,10 @@ async def generate_subtitle_background(
|
|||
)
|
||||
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 = {
|
||||
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:
|
||||
|
|
@ -202,8 +206,11 @@ async def generate_subtitle_background(
|
|||
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")
|
||||
|
||||
logger.info(f"[generate_subtitle_background] DONE - task_id: {task_id}")
|
||||
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[generate_subtitle_background] FAILED - task_id: {task_id}, error: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ Video API Router
|
|||
"""
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
from typing import Literal
|
||||
|
||||
|
|
@ -148,8 +147,6 @@ 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)
|
||||
|
|
@ -159,21 +156,28 @@ async def generate_video(
|
|||
)
|
||||
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}")
|
||||
if not project:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"task_id '{task_id}'에 해당하는 Project를 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
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 > 60 :
|
||||
raise Exception("subtitle 결과 생성 실패")
|
||||
count += 1
|
||||
|
||||
# subtitle 미완료 시 즉시 202 반환 — 클라이언트가 /lyric/subtitle/status/{task_id} 폴링 후 재시도
|
||||
if not marketing_intelligence.subtitle:
|
||||
logger.info(f"[generate_video] subtitle pending - task_id: {task_id}")
|
||||
return GenerateVideoResponse(
|
||||
success=False,
|
||||
status="subtitle_pending",
|
||||
task_id=task_id,
|
||||
creatomate_render_id=None,
|
||||
message="자막 생성이 아직 완료되지 않았습니다. /lyric/subtitle/status/{task_id}로 완료 확인 후 재요청하세요.",
|
||||
error_message=None,
|
||||
)
|
||||
|
||||
|
||||
# 세션을 명시적으로 열고 DB 작업 후 바로 닫음
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class GenerateVideoResponse(BaseModel):
|
|||
)
|
||||
|
||||
success: bool = Field(..., description="요청 성공 여부")
|
||||
status: Optional[str] = Field(None, description="처리 상태 (subtitle_pending: 자막 미완료, completed: 정상 접수)")
|
||||
task_id: Optional[str] = Field(None, description="내부 작업 ID (Project task_id)")
|
||||
creatomate_render_id: Optional[str] = Field(None, description="Creatomate 렌더 ID")
|
||||
message: str = Field(..., description="응답 메시지")
|
||||
|
|
|
|||
Loading…
Reference in New Issue