o2o-castad-backend/app/lyric/api/routers/v1/lyric.py

448 lines
14 KiB
Python

"""
Lyric API Router
이 모듈은 가사 관련 API 엔드포인트를 정의합니다.
모든 엔드포인트는 재사용 가능하도록 설계되었습니다.
엔드포인트 목록:
- POST /lyric/generate: 가사 생성
- GET /lyric/status/{task_id}: 가사 생성 상태 조회
- GET /lyric/{task_id}: 가사 상세 조회
- GET /lyrics: 가사 목록 조회 (페이지네이션)
사용 예시:
from app.lyric.api.routers.v1.lyric import router
app.include_router(router, prefix="/api/v1")
다른 서비스에서 재사용:
# 이 파일의 헬퍼 함수들을 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 typing import Optional
from fastapi import APIRouter, 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
from app.lyric.models import Lyric
from app.lyric.schemas.lyric import (
GenerateLyricRequest,
GenerateLyricResponse,
LyricDetailResponse,
LyricListItem,
LyricStatusResponse,
)
from app.utils.chatgpt_prompt import ChatgptService
from app.utils.pagination import PaginatedResponse, get_paginated
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":
# 완료 처리
"""
print(f"[get_lyric_status_by_task_id] START - task_id: {task_id}")
result = await session.execute(select(Lyric).where(Lyric.task_id == task_id))
lyric = result.scalar_one_or_none()
if not lyric:
print(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": "가사 생성에 실패했습니다.",
}
print(
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)
"""
print(f"[get_lyric_by_task_id] START - task_id: {task_id}")
result = await session.execute(select(Lyric).where(Lyric.task_id == task_id))
lyric = result.scalar_one_or_none()
if not lyric:
print(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}'에 해당하는 가사를 찾을 수 없습니다.",
)
print(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_prompt=lyric.lyric_prompt,
lyric_result=lyric.lyric_result,
created_at=lyric.created_at,
)
# =============================================================================
# API Endpoints
# =============================================================================
@router.post(
"/generate",
summary="가사 생성",
description="""
고객 정보를 기반으로 ChatGPT를 이용하여 가사를 생성합니다.
## 요청 필드
- **task_id**: 작업 고유 식별자 (이미지 업로드 시 생성된 task_id, 필수)
- **customer_name**: 고객명/가게명 (필수)
- **region**: 지역명 (필수)
- **detail_region_info**: 상세 지역 정보 (선택)
- **language**: 가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)
## 반환 정보
- **success**: 생성 성공 여부
- **task_id**: 작업 고유 식별자
- **lyric**: 생성된 가사 (성공 시)
- **language**: 가사 언어
- **error_message**: 에러 메시지 (실패 시)
## 실패 조건
- ChatGPT API 오류
- ChatGPT 거부 응답 (I'm sorry, I cannot 등)
- 응답에 ERROR: 포함
## 사용 예시
```
POST /lyric/generate
{
"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": "인스타 감성의 스테이 머뭄...",
"language": "Korean",
"error_message": null
}
```
## 응답 예시 (실패)
```json
{
"success": false,
"task_id": "0694b716-dbff-7219-8000-d08cb5fce431",
"lyric": null,
"language": "Korean",
"error_message": "I'm sorry, I can't comply with that request."
}
```
""",
response_model=GenerateLyricResponse,
responses={
200: {"description": "가사 생성 성공 또는 실패 (success 필드로 구분)"},
500: {"description": "서버 내부 오류"},
},
)
async def generate_lyric(
request_body: GenerateLyricRequest,
session: AsyncSession = Depends(get_session),
) -> GenerateLyricResponse:
"""고객 정보를 기반으로 가사를 생성합니다."""
task_id = request_body.task_id
print(
f"[generate_lyric] START - task_id: {task_id}, "
f"customer_name: {request_body.customer_name}, region: {request_body.region}"
)
try:
# 1. ChatGPT 서비스 초기화 및 프롬프트 생성
service = ChatgptService(
customer_name=request_body.customer_name,
region=request_body.region,
detail_region_info=request_body.detail_region_info or "",
language=request_body.language,
)
prompt = service.build_lyrics_prompt()
# 2. 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,
)
session.add(project)
await session.commit()
await session.refresh(project) # commit 후 project.id 동기화
print(
f"[generate_lyric] Project saved - project_id: {project.id}, task_id: {task_id}"
)
# 3. Lyric 테이블에 데이터 저장 (status: processing)
lyric = Lyric(
project_id=project.id,
task_id=task_id,
status="processing",
lyric_prompt=prompt,
lyric_result=None,
language=request_body.language,
)
session.add(lyric)
await (
session.commit()
) # processing 상태를 확실히 저장 (다른 트랜잭션에서 조회 가능)
await session.refresh(lyric) # commit 후 객체 상태 동기화
print(
f"[generate_lyric] Lyric saved (processing) - lyric_id: {lyric.id}, task_id: {task_id}"
)
# 4. ChatGPT를 통해 가사 생성
print(f"[generate_lyric] ChatGPT generation started - task_id: {task_id}")
result = await service.generate(prompt=prompt)
print(f"[generate_lyric] ChatGPT generation completed - task_id: {task_id}")
# 5. 실패 응답 검사 (ERROR 또는 ChatGPT 거부 응답)
failure_patterns = [
"ERROR:",
"I'm sorry",
"I cannot",
"I can't",
"I apologize",
"I'm unable",
"I am unable",
"I'm not able",
"I am not able",
]
is_failure = any(
pattern.lower() in result.lower() for pattern in failure_patterns
)
if is_failure:
print(f"[generate_lyric] FAILED - task_id: {task_id}, error: {result}")
lyric.status = "failed"
lyric.lyric_result = result
await session.commit()
return GenerateLyricResponse(
success=False,
task_id=task_id,
lyric=None,
language=request_body.language,
error_message=result,
)
# 6. 성공 시 Lyric 테이블 업데이트 (status: completed)
lyric.status = "completed"
lyric.lyric_result = result
await session.commit()
print(f"[generate_lyric] SUCCESS - task_id: {task_id}")
return GenerateLyricResponse(
success=True,
task_id=task_id,
lyric=result,
language=request_body.language,
error_message=None,
)
except Exception as e:
print(f"[generate_lyric] EXCEPTION - task_id: {task_id}, error: {e}")
await session.rollback()
return GenerateLyricResponse(
success=False,
task_id=task_id,
lyric=None,
language=request_body.language,
error_message=str(e),
)
@router.get(
"/status/{task_id}",
summary="가사 생성 상태 조회",
description="""
task_id로 가사 생성 작업의 현재 상태를 조회합니다.
## 상태 값
- **processing**: 가사 생성 중
- **completed**: 가사 생성 완료
- **failed**: 가사 생성 실패
## 사용 예시
```
GET /lyric/status/019123ab-cdef-7890-abcd-ef1234567890
```
""",
response_model=LyricStatusResponse,
responses={
200: {"description": "상태 조회 성공"},
404: {"description": "해당 task_id를 찾을 수 없음"},
},
)
async def get_lyric_status(
task_id: str,
session: AsyncSession = Depends(get_session),
) -> LyricStatusResponse:
"""task_id로 가사 생성 작업 상태를 조회합니다."""
return await get_lyric_status_by_task_id(session, task_id)
@router.get(
"s",
summary="가사 목록 조회 (페이지네이션)",
description="""
생성 완료된 가사를 페이지네이션으로 조회합니다.
## 파라미터
- **page**: 페이지 번호 (1부터 시작, 기본값: 1)
- **page_size**: 페이지당 데이터 수 (기본값: 20, 최대: 100)
## 반환 정보
- **items**: 가사 목록 (completed 상태만)
- **total**: 전체 데이터 수
- **page**: 현재 페이지
- **page_size**: 페이지당 데이터 수
- **total_pages**: 전체 페이지 수
- **has_next**: 다음 페이지 존재 여부
- **has_prev**: 이전 페이지 존재 여부
## 사용 예시
```
GET /lyrics # 기본 조회 (1페이지, 20개)
GET /lyrics?page=2 # 2페이지 조회
GET /lyrics?page=1&page_size=50 # 50개씩 조회
```
## 참고
- 생성 완료(completed)된 가사만 조회됩니다.
- processing, failed 상태의 가사는 조회되지 않습니다.
""",
response_model=PaginatedResponse[LyricListItem],
responses={
200: {"description": "가사 목록 조회 성공"},
},
)
async def list_lyrics(
page: int = Query(1, ge=1, description="페이지 번호 (1부터 시작)"),
page_size: int = Query(20, ge=1, le=100, description="페이지당 데이터 수"),
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(
"/{task_id}",
summary="가사 상세 조회",
description="""
task_id로 생성된 가사의 상세 정보를 조회합니다.
## 반환 정보
- **id**: 가사 ID
- **task_id**: 작업 고유 식별자
- **project_id**: 프로젝트 ID
- **status**: 처리 상태
- **lyric_prompt**: 가사 생성에 사용된 프롬프트
- **lyric_result**: 생성된 가사 (완료 시)
- **created_at**: 생성 일시
## 사용 예시
```
GET /lyric/019123ab-cdef-7890-abcd-ef1234567890
```
""",
response_model=LyricDetailResponse,
responses={
200: {"description": "가사 조회 성공"},
404: {"description": "해당 task_id를 찾을 수 없음"},
},
)
async def get_lyric_detail(
task_id: str,
session: AsyncSession = Depends(get_session),
) -> LyricDetailResponse:
"""task_id로 생성된 가사를 조회합니다."""
return await get_lyric_by_task_id(session, task_id)