652 lines
24 KiB
Python
652 lines
24 KiB
Python
"""
|
|
Lyric API Router
|
|
|
|
이 모듈은 가사 관련 API 엔드포인트를 정의합니다.
|
|
모든 엔드포인트는 재사용 가능하도록 설계되었습니다.
|
|
|
|
엔드포인트 목록:
|
|
- POST /lyric/generate: 가사 생성
|
|
- GET /lyric/status/{task_id}: 가사 생성 상태 조회
|
|
- GET /lyric/{task_id}: 가사 상세 조회
|
|
- GET /lyric/list: 가사 목록 조회 (페이지네이션)
|
|
|
|
사용 예시:
|
|
from app.lyric.api.routers.v1.lyric import router
|
|
app.include_router(router)
|
|
|
|
다른 서비스에서 재사용:
|
|
# 이 파일의 헬퍼 함수들을 import하여 사용 가능
|
|
from app.lyric.api.routers.v1.lyric import (
|
|
get_lyric_status_by_task_id,
|
|
get_lyric_by_task_id,
|
|
)
|
|
|
|
# 페이지네이션은 pagination 모듈 사용
|
|
from app.utils.pagination import PaginatedResponse, get_paginated
|
|
"""
|
|
|
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database.session import get_session
|
|
from app.home.models import Project, MarketingIntel
|
|
from app.user.dependencies.auth import get_current_user
|
|
from app.user.models import User
|
|
from app.lyric.models import Lyric
|
|
from app.lyric.schemas.lyric import (
|
|
GenerateLyricRequest,
|
|
GenerateLyricResponse,
|
|
LyricDetailResponse,
|
|
LyricListItem,
|
|
LyricStatusResponse,
|
|
SubtitleStatusResponse,
|
|
)
|
|
from app.lyric.worker.lyric_task import generate_lyric_background
|
|
from app.video.worker.creative_assets_task import generate_subtitle_background
|
|
from app.utils.prompts.chatgpt_prompt import ChatgptService
|
|
from app.utils.logger import get_logger
|
|
from app.utils.pagination import PaginatedResponse, get_paginated
|
|
|
|
from app.utils.prompts.prompts import lyric_prompt
|
|
import traceback as tb
|
|
import json
|
|
# 로거 설정
|
|
logger = get_logger("lyric")
|
|
|
|
router = APIRouter(prefix="/lyric", tags=["Lyric"])
|
|
|
|
|
|
# =============================================================================
|
|
# Reusable Service Functions (다른 모듈에서 import하여 사용 가능)
|
|
# =============================================================================
|
|
|
|
|
|
async def get_lyric_status_by_task_id(
|
|
session: AsyncSession, task_id: str
|
|
) -> LyricStatusResponse:
|
|
"""task_id로 가사 생성 작업의 상태를 조회합니다.
|
|
|
|
Args:
|
|
session: SQLAlchemy AsyncSession
|
|
task_id: 작업 고유 식별자
|
|
|
|
Returns:
|
|
LyricStatusResponse: 상태 정보
|
|
|
|
Raises:
|
|
HTTPException: 404 - task_id에 해당하는 가사가 없는 경우
|
|
|
|
Usage:
|
|
# 다른 서비스에서 사용
|
|
from app.lyric.api.routers.v1.lyric import get_lyric_status_by_task_id
|
|
|
|
status_info = await get_lyric_status_by_task_id(session, "some-task-id")
|
|
if status_info.status == "completed":
|
|
# 완료 처리
|
|
"""
|
|
logger.info(f"[get_lyric_status_by_task_id] START - task_id: {task_id}")
|
|
result = await session.execute(
|
|
select(Lyric)
|
|
.where(Lyric.task_id == task_id)
|
|
.order_by(Lyric.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
lyric = result.scalar_one_or_none()
|
|
|
|
if not lyric:
|
|
logger.warning(f"[get_lyric_status_by_task_id] NOT FOUND - task_id: {task_id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.",
|
|
)
|
|
|
|
status_messages = {
|
|
"processing": "가사 생성 중입니다.",
|
|
"completed": "가사 생성이 완료되었습니다.",
|
|
"failed": "가사 생성에 실패했습니다.",
|
|
}
|
|
|
|
logger.info(
|
|
f"[get_lyric_status_by_task_id] SUCCESS - task_id: {task_id}, status: {lyric.status}"
|
|
)
|
|
return LyricStatusResponse(
|
|
task_id=lyric.task_id,
|
|
status=lyric.status,
|
|
message=status_messages.get(lyric.status, "알 수 없는 상태입니다."),
|
|
)
|
|
|
|
|
|
async def get_lyric_by_task_id(
|
|
session: AsyncSession, task_id: str
|
|
) -> LyricDetailResponse:
|
|
"""task_id로 생성된 가사 상세 정보를 조회합니다.
|
|
|
|
Args:
|
|
session: SQLAlchemy AsyncSession
|
|
task_id: 작업 고유 식별자
|
|
|
|
Returns:
|
|
LyricDetailResponse: 가사 상세 정보
|
|
|
|
Raises:
|
|
HTTPException: 404 - task_id에 해당하는 가사가 없는 경우
|
|
|
|
Usage:
|
|
# 다른 서비스에서 사용
|
|
from app.lyric.api.routers.v1.lyric import get_lyric_by_task_id
|
|
|
|
lyric = await get_lyric_by_task_id(session, task_id)
|
|
"""
|
|
logger.info(f"[get_lyric_by_task_id] START - task_id: {task_id}")
|
|
result = await session.execute(
|
|
select(Lyric)
|
|
.where(Lyric.task_id == task_id)
|
|
.order_by(Lyric.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
lyric = result.scalar_one_or_none()
|
|
|
|
if not lyric:
|
|
logger.warning(f"[get_lyric_by_task_id] NOT FOUND - task_id: {task_id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"task_id '{task_id}'에 해당하는 가사를 찾을 수 없습니다.",
|
|
)
|
|
|
|
logger.info(f"[get_lyric_by_task_id] SUCCESS - task_id: {task_id}, lyric_id: {lyric.id}")
|
|
return LyricDetailResponse(
|
|
id=lyric.id,
|
|
task_id=lyric.task_id,
|
|
project_id=lyric.project_id,
|
|
status=lyric.status,
|
|
lyric_result=lyric.lyric_result,
|
|
genre=lyric.genre,
|
|
created_at=lyric.created_at,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# API Endpoints
|
|
# =============================================================================
|
|
|
|
|
|
@router.post(
|
|
"/generate",
|
|
summary="가사 생성",
|
|
description="""
|
|
고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다.
|
|
백그라운드에서 비동기로 처리되며, 즉시 task_id를 반환합니다.
|
|
|
|
## 인증
|
|
**Bearer 토큰 필수** - `Authorization: Bearer {access_token}` 헤더를 포함해야 합니다.
|
|
|
|
## 요청 필드
|
|
- **task_id**: 작업 고유 식별자 (이미지 업로드 시 생성된 task_id, 필수)
|
|
- **customer_name**: 고객명/가게명 (필수)
|
|
- **region**: 지역명 (필수)
|
|
- **detail_region_info**: 상세 지역 정보 (선택)
|
|
- **language**: 가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)
|
|
|
|
## 반환 정보
|
|
- **success**: 요청 접수 성공 여부
|
|
- **task_id**: 작업 고유 식별자
|
|
- **lyric**: null (백그라운드 처리 중)
|
|
- **language**: 가사 언어
|
|
- **error_message**: 에러 메시지 (요청 접수 실패 시)
|
|
|
|
## 상태 확인
|
|
- GET /lyric/status/{task_id} 로 처리 상태 확인
|
|
- GET /lyric/{task_id} 로 생성된 가사 조회
|
|
|
|
## 사용 예시 (cURL)
|
|
```bash
|
|
curl -X POST "http://localhost:8000/lyric/generate" \\
|
|
-H "Authorization: Bearer {access_token}" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{
|
|
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
|
"customer_name": "스테이 머뭄",
|
|
"region": "군산",
|
|
"detail_region_info": "군산 신흥동 말랭이 마을",
|
|
"language": "Korean"
|
|
}'
|
|
```
|
|
|
|
## 응답 예시
|
|
```json
|
|
{
|
|
"success": true,
|
|
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
|
|
"lyric": null,
|
|
"language": "Korean",
|
|
"error_message": null
|
|
}
|
|
```
|
|
""",
|
|
response_model=GenerateLyricResponse,
|
|
responses={
|
|
200: {"description": "가사 생성 요청 접수 성공"},
|
|
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
|
500: {"description": "서버 내부 오류"},
|
|
},
|
|
)
|
|
async def generate_lyric(
|
|
request_body: GenerateLyricRequest,
|
|
background_tasks: BackgroundTasks,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> GenerateLyricResponse:
|
|
"""고객 정보를 기반으로 가사를 생성합니다. (백그라운드 처리)"""
|
|
import time
|
|
|
|
request_start = time.perf_counter()
|
|
task_id = request_body.task_id
|
|
|
|
user = (await session.execute(
|
|
select(User).where(User.user_uuid == current_user.user_uuid)
|
|
)).scalar_one()
|
|
|
|
if user.credits <= 0:
|
|
logger.info(
|
|
f"크레딧 부족, user_uuid: {current_user.user_uuid}, credits: {current_user.credits}"
|
|
)
|
|
return GenerateLyricResponse(
|
|
success=False,
|
|
task_id=task_id,
|
|
lyric=None,
|
|
language=request_body.language,
|
|
error_message="No credits remaining.",
|
|
)
|
|
|
|
logger.info(f"[generate_lyric] ========== START ==========")
|
|
logger.info(
|
|
f"[generate_lyric] task_id: {task_id}, "
|
|
f"customer_name: {request_body.customer_name}, "
|
|
f"region: {request_body.region}"
|
|
)
|
|
|
|
try:
|
|
# ========== Step 1: ChatGPT 서비스 초기화 및 프롬프트 생성 ==========
|
|
step1_start = time.perf_counter()
|
|
logger.debug(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
|
|
|
marketing_intel_result = await session.execute(select(MarketingIntel).where(MarketingIntel.id == request_body.m_id))
|
|
marketing_intel = marketing_intel_result.scalar_one_or_none()
|
|
|
|
# 자동 선택(genre 미지정) 재생성 시, 직전에 사용된 장르를 조회해 이번엔 다른 장르가 나오도록 제외 처리
|
|
exclude_genre = ""
|
|
if not request_body.genre:
|
|
previous_lyric_result = await session.execute(
|
|
select(Lyric)
|
|
.where(Lyric.task_id == task_id, Lyric.genre.is_not(None))
|
|
.order_by(Lyric.created_at.desc())
|
|
.limit(1)
|
|
)
|
|
previous_lyric = previous_lyric_result.scalar_one_or_none()
|
|
if previous_lyric:
|
|
exclude_genre = previous_lyric.genre
|
|
|
|
lyric_input_data = {
|
|
"customer_name" : request_body.customer_name,
|
|
"region" : request_body.region,
|
|
"detail_region_info" : request_body.detail_region_info or "",
|
|
"marketing_intelligence_summary" : json.dumps(marketing_intel.intel_result, ensure_ascii = False),
|
|
"language" : request_body.language,
|
|
"genre": request_body.genre or "", # 비어있으면 GPT가 가사 무드에 맞춰 장르를 직접 추천
|
|
"exclude_genre": exclude_genre, # 자동 선택 재생성 시 직전 장르 제외
|
|
"industry": request_body.industry, # 크롤 응답에서 받아 전달된 업종 enum
|
|
}
|
|
|
|
# 업종 분기는 프롬프트 내부 {industry}로 처리하므로 단일 프롬프트 사용
|
|
selected_lyric_prompt = lyric_prompt
|
|
|
|
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
|
#logger.debug(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)")
|
|
|
|
# ========== Step 2: Project 조회 또는 생성 ==========
|
|
step2_start = time.perf_counter()
|
|
logger.debug(f"[generate_lyric] Step 2: Project 조회 또는 생성...")
|
|
|
|
# 기존 Project가 있는지 확인 (재생성 시 재사용)
|
|
existing_project_result = await session.execute(
|
|
select(Project).where(Project.task_id == task_id).limit(1)
|
|
)
|
|
project = existing_project_result.scalar_one_or_none()
|
|
|
|
if project:
|
|
# 기존 Project 재사용 (재생성 케이스)
|
|
logger.info(f"[generate_lyric] 기존 Project 재사용 - project_id: {project.id}, task_id: {task_id}")
|
|
else:
|
|
# 새 Project 생성 (최초 생성 케이스)
|
|
project = Project(
|
|
store_name=request_body.customer_name,
|
|
region=request_body.region,
|
|
task_id=task_id,
|
|
detail_region_info=request_body.detail_region_info,
|
|
language=request_body.language,
|
|
user_uuid=current_user.user_uuid,
|
|
marketing_intelligence=request_body.m_id,
|
|
industry=request_body.industry,
|
|
)
|
|
session.add(project)
|
|
await session.commit()
|
|
await session.refresh(project)
|
|
logger.info(f"[generate_lyric] 새 Project 생성 - project_id: {project.id}, task_id: {task_id}")
|
|
|
|
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
|
logger.debug(f"[generate_lyric] Step 2 완료 - project_id: {project.id} ({step2_elapsed:.1f}ms)")
|
|
|
|
# ========== Step 3: Lyric 테이블에 데이터 저장 ==========
|
|
step3_start = time.perf_counter()
|
|
logger.debug(f"[generate_lyric] Step 3: Lyric 저장 (processing)...")
|
|
|
|
estimated_prompt = selected_lyric_prompt.build_prompt(lyric_input_data)
|
|
lyric = Lyric(
|
|
project_id=project.id,
|
|
task_id=task_id,
|
|
status="processing",
|
|
lyric_prompt=estimated_prompt,
|
|
lyric_result=None,
|
|
language=request_body.language,
|
|
genre=request_body.genre or None, # 자동 선택인 경우 GPT 응답 완료 후 채워짐
|
|
)
|
|
session.add(lyric)
|
|
await session.commit()
|
|
await session.refresh(lyric)
|
|
|
|
step3_elapsed = (time.perf_counter() - step3_start) * 1000
|
|
logger.debug(f"[generate_lyric] Step 3 완료 - lyric_id: {lyric.id} ({step3_elapsed:.1f}ms)")
|
|
|
|
# ========== Step 4: 백그라운드 태스크 스케줄링 ==========
|
|
step4_start = time.perf_counter()
|
|
logger.debug(f"[generate_lyric] Step 4: 백그라운드 태스크 스케줄링...")
|
|
orientation = request_body.orientation
|
|
|
|
if request_body.instrumental:
|
|
# BGM 모드: ChatGPT 가사 생성 없이 Lyric을 즉시 completed로 마무리
|
|
lyric.status = "completed"
|
|
lyric.lyric_result = ""
|
|
await session.commit()
|
|
logger.info(f"[generate_lyric] BGM 모드 - 가사 생성 스킵, lyric_id: {lyric.id}")
|
|
else:
|
|
background_tasks.add_task(
|
|
generate_lyric_background,
|
|
task_id=task_id,
|
|
prompt=selected_lyric_prompt,
|
|
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)")
|
|
|
|
# ========== 완료 ==========
|
|
total_elapsed = (time.perf_counter() - request_start) * 1000
|
|
logger.info(f"[generate_lyric] ========== COMPLETE ==========")
|
|
logger.info(f"[generate_lyric] API 응답 소요시간: {total_elapsed:.1f}ms")
|
|
logger.debug(f"[generate_lyric] - Step 1 (프롬프트 생성): {step1_elapsed:.1f}ms")
|
|
logger.debug(f"[generate_lyric] - Step 2 (Project 저장): {step2_elapsed:.1f}ms")
|
|
logger.debug(f"[generate_lyric] - Step 3 (Lyric 저장): {step3_elapsed:.1f}ms")
|
|
logger.debug(f"[generate_lyric] - Step 4 (태스크 스케줄링): {step4_elapsed:.1f}ms")
|
|
logger.debug(f"[generate_lyric] (GPT API 호출은 백그라운드에서 별도 진행)")
|
|
|
|
# 5. 즉시 응답 반환
|
|
return GenerateLyricResponse(
|
|
success=True,
|
|
task_id=task_id,
|
|
lyric=None,
|
|
language=request_body.language,
|
|
error_message=None,
|
|
)
|
|
|
|
except Exception as e:
|
|
elapsed = (time.perf_counter() - request_start) * 1000
|
|
logger.error(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e} ({elapsed:.1f}ms)")
|
|
await session.rollback()
|
|
return GenerateLyricResponse(
|
|
success=False,
|
|
task_id=task_id,
|
|
lyric=None,
|
|
language=request_body.language,
|
|
error_message=''.join(tb.format_exception(None, e, e.__traceback__)),
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/status/{task_id}",
|
|
summary="가사 생성 상태 조회",
|
|
description="""
|
|
task_id로 가사 생성 작업의 현재 상태를 조회합니다.
|
|
|
|
## 인증
|
|
**Bearer 토큰 필수** - `Authorization: Bearer {access_token}` 헤더를 포함해야 합니다.
|
|
|
|
## 상태 값
|
|
- **processing**: 가사 생성 중
|
|
- **completed**: 가사 생성 완료
|
|
- **failed**: 가사 생성 실패
|
|
|
|
## 사용 예시 (cURL)
|
|
```bash
|
|
curl -X GET "http://localhost:8000/lyric/status/019123ab-cdef-7890-abcd-ef1234567890" \\
|
|
-H "Authorization: Bearer {access_token}"
|
|
```
|
|
""",
|
|
response_model=LyricStatusResponse,
|
|
responses={
|
|
200: {"description": "상태 조회 성공"},
|
|
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
|
404: {"description": "해당 task_id를 찾을 수 없음"},
|
|
},
|
|
)
|
|
async def get_lyric_status(
|
|
task_id: str,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> LyricStatusResponse:
|
|
"""task_id로 가사 생성 작업 상태를 조회합니다."""
|
|
return await get_lyric_status_by_task_id(session, task_id)
|
|
|
|
|
|
@router.get(
|
|
"/list",
|
|
summary="가사 목록 조회 (페이지네이션)",
|
|
description="""
|
|
생성 완료된 가사를 페이지네이션으로 조회합니다.
|
|
|
|
## 인증
|
|
**Bearer 토큰 필수** - `Authorization: Bearer {access_token}` 헤더를 포함해야 합니다.
|
|
|
|
## 파라미터
|
|
- **page**: 페이지 번호 (1부터 시작, 기본값: 1)
|
|
- **page_size**: 페이지당 데이터 수 (기본값: 20, 최대: 100)
|
|
|
|
## 반환 정보
|
|
- **items**: 가사 목록 (completed 상태만)
|
|
- **total**: 전체 데이터 수
|
|
- **page**: 현재 페이지
|
|
- **page_size**: 페이지당 데이터 수
|
|
- **total_pages**: 전체 페이지 수
|
|
- **has_next**: 다음 페이지 존재 여부
|
|
- **has_prev**: 이전 페이지 존재 여부
|
|
|
|
## 사용 예시 (cURL)
|
|
```bash
|
|
# 기본 조회 (1페이지, 20개)
|
|
curl -X GET "http://localhost:8000/lyric/list" \\
|
|
-H "Authorization: Bearer {access_token}"
|
|
|
|
# 2페이지 조회
|
|
curl -X GET "http://localhost:8000/lyric/list?page=2" \\
|
|
-H "Authorization: Bearer {access_token}"
|
|
|
|
# 50개씩 조회
|
|
curl -X GET "http://localhost:8000/lyric/list?page=1&page_size=50" \\
|
|
-H "Authorization: Bearer {access_token}"
|
|
```
|
|
|
|
## 참고
|
|
- 생성 완료(completed)된 가사만 조회됩니다.
|
|
- processing, failed 상태의 가사는 조회되지 않습니다.
|
|
""",
|
|
response_model=PaginatedResponse[LyricListItem],
|
|
responses={
|
|
200: {"description": "가사 목록 조회 성공"},
|
|
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
|
},
|
|
)
|
|
async def list_lyrics(
|
|
page: int = Query(1, ge=1, description="페이지 번호 (1부터 시작)"),
|
|
page_size: int = Query(20, ge=1, le=100, description="페이지당 데이터 수"),
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> PaginatedResponse[LyricListItem]:
|
|
"""페이지네이션으로 완료된 가사 목록을 조회합니다."""
|
|
return await get_paginated(
|
|
session=session,
|
|
model=Lyric,
|
|
item_schema=LyricListItem,
|
|
page=page,
|
|
page_size=page_size,
|
|
filters={"status": "completed"},
|
|
order_by="created_at",
|
|
order_desc=True,
|
|
)
|
|
|
|
|
|
@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}'에 해당하는 마케팅 인텔리전스를 찾을 수 없습니다.",
|
|
)
|
|
|
|
# 자막과 이미지 배정 모두 완료돼야 영상 생성 가능
|
|
subtitle_done = bool(intel.subtitle)
|
|
image_match_done = bool(intel.image_match)
|
|
|
|
if subtitle_done and image_match_done:
|
|
logger.info(f"[get_subtitle_status] completed (subtitle+image_match) - task_id: {task_id}")
|
|
return SubtitleStatusResponse(
|
|
task_id=task_id,
|
|
status="completed",
|
|
message="자막 생성 및 이미지 배정이 완료되었습니다.",
|
|
)
|
|
|
|
pending_parts = []
|
|
if not subtitle_done:
|
|
pending_parts.append("자막")
|
|
if not image_match_done:
|
|
pending_parts.append("이미지 배정")
|
|
pending_msg = ", ".join(pending_parts)
|
|
logger.info(f"[get_subtitle_status] pending ({pending_msg}) - task_id: {task_id}")
|
|
return SubtitleStatusResponse(
|
|
task_id=task_id,
|
|
status="pending",
|
|
message=f"{pending_msg} 처리가 진행 중입니다. 잠시 후 다시 확인해주세요.",
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/{task_id}",
|
|
summary="가사 상세 조회",
|
|
description="""
|
|
task_id로 생성된 가사의 상세 정보를 조회합니다.
|
|
|
|
## 인증
|
|
**Bearer 토큰 필수** - `Authorization: Bearer {access_token}` 헤더를 포함해야 합니다.
|
|
|
|
## 반환 정보
|
|
- **id**: 가사 ID
|
|
- **task_id**: 작업 고유 식별자
|
|
- **project_id**: 프로젝트 ID
|
|
- **status**: 처리 상태
|
|
- **lyric_prompt**: 가사 생성에 사용된 프롬프트
|
|
- **lyric_result**: 생성된 가사 (완료 시)
|
|
- **created_at**: 생성 일시
|
|
|
|
## 사용 예시 (cURL)
|
|
```bash
|
|
curl -X GET "http://localhost:8000/lyric/019123ab-cdef-7890-abcd-ef1234567890" \\
|
|
-H "Authorization: Bearer {access_token}"
|
|
```
|
|
""",
|
|
response_model=LyricDetailResponse,
|
|
responses={
|
|
200: {"description": "가사 조회 성공"},
|
|
401: {"description": "인증 실패 (토큰 없음/만료)"},
|
|
404: {"description": "해당 task_id를 찾을 수 없음"},
|
|
},
|
|
)
|
|
async def get_lyric_detail(
|
|
task_id: str,
|
|
current_user: User = Depends(get_current_user),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> LyricDetailResponse:
|
|
"""task_id로 생성된 가사를 조회합니다."""
|
|
return await get_lyric_by_task_id(session, task_id)
|