feat: update song model and related routers, schemas, worker

insta
Dohyun Lim 2026-01-20 14:40:58 +09:00
parent ece201f92b
commit ee6069e5d5
7 changed files with 222 additions and 148 deletions

View File

@ -379,7 +379,7 @@ print(response.json())
200: {"description": "이미지 업로드 성공"}, 200: {"description": "이미지 업로드 성공"},
400: {"description": "이미지가 제공되지 않음", "model": ErrorResponse}, 400: {"description": "이미지가 제공되지 않음", "model": ErrorResponse},
}, },
tags=["image"], tags=["Image"],
) )
async def upload_images( async def upload_images(
images_json: Optional[str] = Form( images_json: Optional[str] = Form(

View File

@ -418,9 +418,9 @@ async def get_lyric_status(
## 사용 예시 ## 사용 예시
``` ```
GET /lyrics # 기본 조회 (1페이지, 20개) GET /lyrics/ # 기본 조회 (1페이지, 20개)
GET /lyrics?page=2 # 2페이지 조회 GET /lyrics/?page=2 # 2페이지 조회
GET /lyrics?page=1&page_size=50 # 50개씩 조회 GET /lyrics/?page=1&page_size=50 # 50개씩 조회
``` ```
## 참고 ## 참고

View File

@ -13,7 +13,7 @@ Song API Router
app.include_router(router, prefix="/api/v1") app.include_router(router, prefix="/api/v1")
""" """
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -32,6 +32,7 @@ from app.song.schemas.song_schema import (
PollingSongResponse, PollingSongResponse,
SongListItem, SongListItem,
) )
from app.song.worker.song_task import download_and_upload_song_by_suno_task_id
from app.utils.logger import get_logger from app.utils.logger import get_logger
from app.utils.pagination import PaginatedResponse from app.utils.pagination import PaginatedResponse
from app.utils.suno import SunoService from app.utils.suno import SunoService
@ -294,17 +295,17 @@ async def generate_song(
@router.get( @router.get(
"/status/{song_id}", "/status/{song_id}",
summary="노래 생성 상태 조회", summary="노래 생성 상태 조회 (Suno API)",
description=""" description="""
Suno API를 통해 노래 생성 작업의 상태를 조회합니다. Suno API를 통해 노래 생성 작업의 상태를 조회합니다.
SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고 Azure Blob Storage에 업로드 Song 테이블을 업데이트합니다. SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고 Azure Blob Storage에 업로드시작합니다.
## 경로 파라미터 ## 경로 파라미터
- **song_id**: 노래 생성 반환된 작업 ID (필수) - **song_id**: 노래 생성 반환된 Suno API 작업 ID (필수)
## 반환 정보 ## 반환 정보
- **success**: 조회 성공 여부 - **success**: 조회 성공 여부
- **status**: 작업 상태 (PENDING, processing, SUCCESS, failed) - **status**: Suno API 작업 상태
- **message**: 상태 메시지 - **message**: 상태 메시지
## 사용 예시 ## 사용 예시
@ -312,27 +313,28 @@ SUCCESS 상태인 경우 백그라운드에서 MP3 파일을 다운로드하고
GET /song/status/abc123... GET /song/status/abc123...
``` ```
## 상태 값 ## 상태 값 (Suno API 응답)
- **PENDING**: 대기 - **PENDING**: Suno API 대기
- **processing**: 생성 - **processing**: Suno API에서 노래 생성
- **SUCCESS**: 생성 완료 - **SUCCESS**: Suno API 노래 생성 완료 (백그라운드 Blob 업로드 시작)
- **failed**: 생성 실패 - **TEXT_SUCCESS**: Suno API 노래 생성 완료
- **failed**: Suno API 노래 생성 실패
- **error**: API 조회 오류
## 참고 ## 참고
- 스트림 URL: 30-40 생성 - 엔드포인트는 Suno API의 상태를 반환합니다
- 다운로드 URL: 2-3 생성 - SUCCESS 응답 백그라운드에서 MP3 다운로드 Azure Blob Storage 업로드가 시작됩니다
- SUCCESS 백그라운드에서 MP3 다운로드 Azure Blob Storage 업로드 Song 테이블 업데이트 진행 - 최종 완료 상태는 `/song/download/{task_id}` 엔드포인트에서 확인하세요
- 저장 경로: Azure Blob Storage ({BASE_URL}/{task_id}/song/{store_name}.mp3) - Song 테이블 상태: processing uploading completed
- Song 테이블의 song_result_url에 Blob URL이 저장됩니다
""", """,
response_model=PollingSongResponse, response_model=PollingSongResponse,
responses={ responses={
200: {"description": "상태 조회 성공"}, 200: {"description": "상태 조회 성공"},
500: {"description": "상태 조회 실패"},
}, },
) )
async def get_song_status( async def get_song_status(
song_id: str, song_id: str,
background_tasks: BackgroundTasks,
session: AsyncSession = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> PollingSongResponse: ) -> PollingSongResponse:
"""song_id로 노래 생성 작업의 상태를 조회합니다. """song_id로 노래 생성 작업의 상태를 조회합니다.
@ -345,10 +347,11 @@ async def get_song_status(
try: try:
suno_service = SunoService() suno_service = SunoService()
result = await suno_service.get_task_status(song_id) result = await suno_service.get_task_status(song_id)
logger.debug(f"[get_song_status] Suno API raw response - song_id: {song_id}, result: {result}")
parsed_response = suno_service.parse_status_response(result) parsed_response = suno_service.parse_status_response(result)
logger.info(f"[get_song_status] Suno API response - song_id: {song_id}, status: {parsed_response.status}") logger.info(f"[get_song_status] Suno API response - song_id: {song_id}, status: {parsed_response.status}")
# SUCCESS 상태인 경우 첫 번째 클립 정보를 DB에 직접 저장 # SUCCESS 상태인 경우 백그라운드에서 MP3 다운로드 및 Blob 업로드 진행
if parsed_response.status == "SUCCESS" and result: if parsed_response.status == "SUCCESS" and result:
# result에서 직접 clips 데이터 추출 # result에서 직접 clips 데이터 추출
data = result.get("data", {}) data = result.get("data", {})
@ -372,14 +375,33 @@ async def get_song_status(
) )
song = song_result.scalar_one_or_none() song = song_result.scalar_one_or_none()
if song and song.status != "completed": # processing 상태인 경우에만 백그라운드 태스크 실행 (중복 방지)
# 첫 번째 클립의 audio_url과 duration을 직접 DB에 저장 if song and song.status == "processing":
song.status = "completed" # store_name 조회
song.song_result_url = audio_url project_result = await session.execute(
if clip_duration is not None: select(Project).where(Project.id == song.project_id)
song.duration = clip_duration )
project = project_result.scalar_one_or_none()
store_name = project.store_name if project else "song"
# 상태를 uploading으로 변경 (중복 호출 방지)
song.status = "uploading"
song.suno_audio_id = first_clip.get('id')
await session.commit() await session.commit()
logger.info(f"[get_song_status] Song updated - song_id: {song_id}, status: completed, song_result_url: {audio_url}, duration: {clip_duration}") logger.info(f"[get_song_status] Song status changed to uploading - song_id: {song_id}")
# 백그라운드 태스크로 MP3 다운로드 및 Blob 업로드 실행
background_tasks.add_task(
download_and_upload_song_by_suno_task_id,
suno_task_id=song_id,
audio_url=audio_url,
store_name=store_name,
duration=clip_duration,
)
logger.info(f"[get_song_status] Background task scheduled - song_id: {song_id}, store_name: {store_name}")
elif song and song.status == "uploading":
logger.info(f"[get_song_status] SKIPPED - Song is already uploading, song_id: {song_id}")
elif song and song.status == "completed": elif song and song.status == "completed":
logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {song_id}") logger.info(f"[get_song_status] SKIPPED - Song already completed, song_id: {song_id}")
@ -400,7 +422,7 @@ async def get_song_status(
@router.get( @router.get(
"/download/{task_id}", "/download/{task_id}",
summary="노래 생성 URL 조회", summary="노래 다운로드 상태 조회 (DB Polling)",
description=""" description="""
task_id를 기반으로 Song 테이블의 상태를 조회하고, task_id를 기반으로 Song 테이블의 상태를 조회하고,
completed인 경우 Project 정보와 노래 URL을 반환합니다. completed인 경우 Project 정보와 노래 URL을 반환합니다.
@ -410,31 +432,38 @@ completed인 경우 Project 정보와 노래 URL을 반환합니다.
## 반환 정보 ## 반환 정보
- **success**: 조회 성공 여부 - **success**: 조회 성공 여부
- **status**: 처리 상태 (processing, completed, failed, not_found) - **status**: DB 처리 상태 (processing, uploading, completed, failed, not_found, error)
- **message**: 응답 메시지 - **message**: 응답 메시지
- **store_name**: 업체명 - **store_name**: 업체명 (completed )
- **region**: 지역명 - **region**: 지역명 (completed )
- **detail_region_info**: 상세 지역 정보 - **detail_region_info**: 상세 지역 정보 (completed )
- **task_id**: 작업 고유 식별자 - **task_id**: 작업 고유 식별자
- **language**: 언어 - **language**: 언어 (completed )
- **song_result_url**: 노래 결과 URL (completed , Azure Blob Storage URL) - **song_result_url**: 노래 결과 URL (completed , Azure Blob Storage URL)
- **created_at**: 생성 일시 - **created_at**: 생성 일시 (completed )
- **error_message**: 에러 메시지 (실패 )
## 사용 예시 ## 사용 예시
``` ```
GET /song/download/019123ab-cdef-7890-abcd-ef1234567890 GET /song/download/019123ab-cdef-7890-abcd-ef1234567890
``` ```
## 상태 값 (DB 상태)
- **processing**: Suno API에서 노래 생성
- **uploading**: MP3 다운로드 Azure Blob 업로드
- **completed**: 모든 작업 완료, Blob URL 사용 가능
- **failed**: 노래 생성 또는 업로드 실패
- **not_found**: task_id에 해당하는 Song 없음
- **error**: 조회 오류 발생
## 참고 ## 참고
- processing 상태인 경우 song_result_url은 null입니다. - 엔드포인트는 DB의 Song 테이블 상태를 반환합니다
- completed 상태인 경우 Project 정보와 함께 song_result_url (Azure Blob URL) 반환합니다. - completed 상태인 경우 Project 정보와 함께 song_result_url (Azure Blob URL) 반환합니다
- song_result_url 형식: {AZURE_BLOB_BASE_URL}/{task_id}/song/{store_name}.mp3 - song_result_url 형식: {AZURE_BLOB_BASE_URL}/{task_id}/song/{store_name}.mp3
""", """,
response_model=DownloadSongResponse, response_model=DownloadSongResponse,
responses={ responses={
200: {"description": "조회 성공"}, 200: {"description": "조회 성공 (모든 상태에서 200 반환)"},
404: {"description": "Song을 찾을 수 없음"},
500: {"description": "조회 실패"},
}, },
) )
async def download_song( async def download_song(
@ -474,6 +503,16 @@ async def download_song(
task_id=task_id, task_id=task_id,
) )
# uploading 상태인 경우
if song.status == "uploading":
logger.info(f"[download_song] UPLOADING - task_id: {task_id}")
return DownloadSongResponse(
success=True,
status="uploading",
message="노래 파일을 업로드 중입니다.",
task_id=task_id,
)
# failed 상태인 경우 # failed 상태인 경우
if song.status == "failed": if song.status == "failed":
logger.warning(f"[download_song] FAILED - task_id: {task_id}") logger.warning(f"[download_song] FAILED - task_id: {task_id}")
@ -546,7 +585,6 @@ GET /songs/?page=1&page_size=10
response_model=PaginatedResponse[SongListItem], response_model=PaginatedResponse[SongListItem],
responses={ responses={
200: {"description": "노래 목록 조회 성공"}, 200: {"description": "노래 목록 조회 성공"},
500: {"description": "조회 실패"},
}, },
) )
async def get_songs( async def get_songs(

View File

@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, func from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database.session import Base from app.database.session import Base
@ -25,7 +25,7 @@ class Song(Base):
lyric_id: 연결된 Lyric의 id (외래키) lyric_id: 연결된 Lyric의 id (외래키)
task_id: 노래 생성 작업의 고유 식별자 (UUID 형식) task_id: 노래 생성 작업의 고유 식별자 (UUID 형식)
suno_task_id: Suno API 작업 고유 식별자 (선택) suno_task_id: Suno API 작업 고유 식별자 (선택)
status: 처리 상태 (pending, processing, completed, failed ) status: 처리 상태 (processing, uploading, completed, failed)
song_prompt: 노래 생성에 사용된 프롬프트 song_prompt: 노래 생성에 사용된 프롬프트
song_result_url: 생성 결과 URL (선택) song_result_url: 생성 결과 URL (선택)
language: 출력 언어 language: 출력 언어
@ -82,10 +82,16 @@ class Song(Base):
comment="Suno API 작업 고유 식별자", comment="Suno API 작업 고유 식별자",
) )
suno_audio_id: Mapped[Optional[str]] = mapped_column(
String(64),
nullable=True,
comment="Suno 첫번째 노래의 고유 식별자",
)
status: Mapped[str] = mapped_column( status: Mapped[str] = mapped_column(
String(50), String(50),
nullable=False, nullable=False,
comment="처리 상태 (processing, completed, failed)", comment="처리 상태 (processing, uploading, completed, failed)",
) )
song_prompt: Mapped[str] = mapped_column( song_prompt: Mapped[str] = mapped_column(
@ -150,3 +156,92 @@ class Song(Base):
f"status='{self.status}'" f"status='{self.status}'"
f")>" f")>"
) )
class SongTimestamp(Base):
"""
노래 타임스탬프 테이블
노래의 가사별 시작/종료 시간 정보를 저장합니다.
Suno API에서 반환된 타임스탬프 데이터를 기반으로 생성됩니다.
Attributes:
id: 고유 식별자 (자동 증가)
suno_audio_id: 가사의 원본 오디오 ID
order_idx: 오디오 내에서 가사의 순서
lyric_line: 가사 줄의 내용
start_time: 가사 시작 시점 ()
end_time: 가사 종료 시점 ()
created_at: 생성 일시 (자동 설정)
"""
__tablename__ = "song_timestamp"
__table_args__ = (
{
"mysql_engine": "InnoDB",
"mysql_charset": "utf8mb4",
"mysql_collate": "utf8mb4_unicode_ci",
},
)
id: Mapped[int] = mapped_column(
Integer,
primary_key=True,
nullable=False,
autoincrement=True,
comment="고유 식별자",
)
suno_audio_id: Mapped[str] = mapped_column(
String(64),
nullable=False,
index=True,
comment="가사의 원본 오디오 ID",
)
order_idx: Mapped[int] = mapped_column(
Integer,
nullable=False,
comment="오디오 내에서 가사의 순서",
)
lyric_line: Mapped[str] = mapped_column(
Text,
nullable=False,
comment="가사 한 줄의 내용",
)
start_time: Mapped[float] = mapped_column(
Float,
nullable=False,
comment="가사 시작 시점 (초)",
)
end_time: Mapped[float] = mapped_column(
Float,
nullable=False,
comment="가사 종료 시점 (초)",
)
created_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
server_default=func.now(),
comment="생성 일시",
)
def __repr__(self) -> str:
def truncate(value: str | None, max_len: int = 10) -> str:
if value is None:
return "None"
return (value[:max_len] + "...") if len(value) > max_len else value
return (
f"<SongTimestamp("
f"id={self.id}, "
f"suno_audio_id='{truncate(self.suno_audio_id)}', "
f"order_idx={self.order_idx}, "
f"start_time={self.start_time}, "
f"end_time={self.end_time}"
f")>"
)

View File

@ -114,24 +114,33 @@ class SongClipData(BaseModel):
class PollingSongResponse(BaseModel): class PollingSongResponse(BaseModel):
"""노래 생성 상태 조회 응답 스키마 """노래 생성 상태 조회 응답 스키마 (Suno API)
Usage: Usage:
GET /song/status/{song_id} GET /song/status/{song_id}
Suno API 작업 상태를 조회합니다. Suno API 작업 상태를 조회합니다.
Note: Note:
상태 : 상태 (Suno API 응답):
- PENDING: 대기 - PENDING: Suno API 대기
- processing: 생성 - processing: Suno API에서 노래 생성
- SUCCESS / TEXT_SUCCESS / complete: 생성 완료 - SUCCESS: Suno API 노래 생성 완료 (백그라운드 Blob 업로드 시작)
- failed: 생성 실패 - TEXT_SUCCESS: Suno API 노래 생성 완료
- failed: Suno API 노래 생성 실패
- error: API 조회 오류 - error: API 조회 오류
SUCCESS 상태 : SUCCESS 상태 :
- 백그라운드에서 MP3 파일 다운로드 시작 - 백그라운드에서 MP3 파일 다운로드 Azure Blob 업로드 시작
- Song 테이블의 status를 completed로 업데이트 - Song 테이블의 status가 uploading으로 변경
- song_result_url에 로컬 파일 경로 저장 - 업로드 완료 status가 completed로 변경, song_result_url에 Blob URL 저장
Example Response (Pending):
{
"success": true,
"status": "PENDING",
"message": "노래 생성 대기 중입니다.",
"error_message": null
}
Example Response (Processing): Example Response (Processing):
{ {
@ -160,7 +169,7 @@ class PollingSongResponse(BaseModel):
success: bool = Field(..., description="조회 성공 여부") success: bool = Field(..., description="조회 성공 여부")
status: Optional[str] = Field( status: Optional[str] = Field(
None, description="작업 상태 (PENDING, processing, SUCCESS, failed)" None, description="Suno API 작업 상태 (PENDING, processing, SUCCESS, TEXT_SUCCESS, failed, error)"
) )
message: str = Field(..., description="상태 메시지") message: str = Field(..., description="상태 메시지")
error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)") error_message: Optional[str] = Field(None, description="에러 메시지 (실패 시)")
@ -192,17 +201,18 @@ class SongListItem(BaseModel):
class DownloadSongResponse(BaseModel): class DownloadSongResponse(BaseModel):
"""노래 다운로드 응답 스키마 """노래 다운로드 응답 스키마 (DB Polling)
Usage: Usage:
GET /song/download/{task_id} GET /song/download/{task_id}
Polls for song completion and returns project info with song URL. DB의 Song 테이블 상태를 조회하고 완료 Project 정보와 노래 URL을 반환합니다.
Note: Note:
상태 : 상태 (DB 상태):
- processing: 노래 생성 진행 (song_result_url은 null) - processing: Suno API에서 노래 생성 (song_result_url은 null)
- completed: 노래 생성 완료 (song_result_url 포함) - uploading: MP3 다운로드 Azure Blob 업로드 (song_result_url은 null)
- failed: 노래 생성 실패 - completed: 모든 작업 완료 (song_result_url에 Azure Blob URL 포함)
- failed: 노래 생성 또는 업로드 실패
- not_found: task_id에 해당하는 Song 없음 - not_found: task_id에 해당하는 Song 없음
- error: 조회 오류 발생 - error: 조회 오류 발생
@ -221,6 +231,21 @@ class DownloadSongResponse(BaseModel):
"error_message": null "error_message": null
} }
Example Response (Uploading):
{
"success": true,
"status": "uploading",
"message": "노래 파일을 업로드 중입니다.",
"store_name": null,
"region": null,
"detail_region_info": null,
"task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"language": null,
"song_result_url": null,
"created_at": null,
"error_message": null
}
Example Response (Completed): Example Response (Completed):
{ {
"success": true, "success": true,
@ -231,7 +256,7 @@ class DownloadSongResponse(BaseModel):
"detail_region_info": "군산 신흥동 말랭이 마을", "detail_region_info": "군산 신흥동 말랭이 마을",
"task_id": "019123ab-cdef-7890-abcd-ef1234567890", "task_id": "019123ab-cdef-7890-abcd-ef1234567890",
"language": "Korean", "language": "Korean",
"song_result_url": "http://localhost:8000/media/2025-01-15/스테이머뭄.mp3", "song_result_url": "https://blob.azure.com/.../song.mp3",
"created_at": "2025-01-15T12:00:00", "created_at": "2025-01-15T12:00:00",
"error_message": null "error_message": null
} }
@ -252,8 +277,8 @@ class DownloadSongResponse(BaseModel):
} }
""" """
success: bool = Field(..., description="다운로드 성공 여부") success: bool = Field(..., description="조회 성공 여부")
status: str = Field(..., description="처리 상태 (processing, completed, failed, not_found, error)") status: str = Field(..., description="DB 처리 상태 (processing, uploading, completed, failed, not_found, error)")
message: str = Field(..., description="응답 메시지") message: str = Field(..., description="응답 메시지")
store_name: Optional[str] = Field(None, description="업체명") store_name: Optional[str] = Field(None, description="업체명")
region: Optional[str] = Field(None, description="지역명") region: Optional[str] = Field(None, description="지역명")

View File

@ -173,90 +173,6 @@ async def download_and_save_song(
await _update_song_status(task_id, "failed") await _update_song_status(task_id, "failed")
async def download_and_upload_song_to_blob(
task_id: str,
audio_url: str,
store_name: str,
) -> None:
"""백그라운드에서 노래를 다운로드하고 Azure Blob Storage에 업로드한 뒤 Song 테이블을 업데이트합니다.
Args:
task_id: 프로젝트 task_id
audio_url: 다운로드할 오디오 URL
store_name: 저장할 파일명에 사용할 업체명
"""
logger.info(f"[download_and_upload_song_to_blob] START - task_id: {task_id}, store_name: {store_name}")
temp_file_path: Path | None = None
try:
# 파일명에 사용할 수 없는 문자 제거
safe_store_name = "".join(
c for c in store_name if c.isalnum() or c in (" ", "_", "-")
).strip()
safe_store_name = safe_store_name or "song"
file_name = f"{safe_store_name}.mp3"
# 임시 저장 경로 생성
temp_dir = Path("media") / "temp" / task_id
temp_dir.mkdir(parents=True, exist_ok=True)
temp_file_path = temp_dir / file_name
logger.info(f"[download_and_upload_song_to_blob] Temp directory created - path: {temp_file_path}")
# 오디오 파일 다운로드
logger.info(f"[download_and_upload_song_to_blob] Downloading audio - task_id: {task_id}, url: {audio_url}")
content = await _download_audio(audio_url, task_id)
async with aiofiles.open(str(temp_file_path), "wb") as f:
await f.write(content)
logger.info(f"[download_and_upload_song_to_blob] File downloaded - task_id: {task_id}, path: {temp_file_path}")
# Azure Blob Storage에 업로드
uploader = AzureBlobUploader(task_id=task_id)
upload_success = await uploader.upload_music(file_path=str(temp_file_path))
if not upload_success:
raise Exception("Azure Blob Storage 업로드 실패")
# SAS 토큰이 제외된 public_url 사용
blob_url = uploader.public_url
logger.info(f"[download_and_upload_song_to_blob] Uploaded to Blob - task_id: {task_id}, url: {blob_url}")
# Song 테이블 업데이트
await _update_song_status(task_id, "completed", blob_url)
logger.info(f"[download_and_upload_song_to_blob] SUCCESS - task_id: {task_id}")
except httpx.HTTPError as e:
logger.error(f"[download_and_upload_song_to_blob] DOWNLOAD ERROR - task_id: {task_id}, error: {e}", exc_info=True)
await _update_song_status(task_id, "failed")
except SQLAlchemyError as e:
logger.error(f"[download_and_upload_song_to_blob] DB ERROR - task_id: {task_id}, error: {e}", exc_info=True)
await _update_song_status(task_id, "failed")
except Exception as e:
logger.error(f"[download_and_upload_song_to_blob] EXCEPTION - task_id: {task_id}, error: {e}", exc_info=True)
await _update_song_status(task_id, "failed")
finally:
# 임시 파일 삭제
if temp_file_path and temp_file_path.exists():
try:
temp_file_path.unlink()
logger.info(f"[download_and_upload_song_to_blob] Temp file deleted - path: {temp_file_path}")
except Exception as e:
logger.warning(f"[download_and_upload_song_to_blob] Failed to delete temp file: {e}")
# 임시 디렉토리 삭제 시도
temp_dir = Path("media") / "temp" / task_id
if temp_dir.exists():
try:
temp_dir.rmdir()
except Exception:
pass # 디렉토리가 비어있지 않으면 무시
async def download_and_upload_song_by_suno_task_id( async def download_and_upload_song_by_suno_task_id(
suno_task_id: str, suno_task_id: str,
audio_url: str, audio_url: str,

View File

@ -716,7 +716,7 @@ from app.services.image_processor import ImageProcessorService
from app.config import settings from app.config import settings
router = APIRouter(prefix="/images", tags=["images"]) router = APIRouter(prefix="/images", tags=["Images"])
# 외부 서비스 인스턴스 # 외부 서비스 인스턴스
processor_service = ImageProcessorService(settings.IMAGE_PROCESSOR_URL) processor_service = ImageProcessorService(settings.IMAGE_PROCESSOR_URL)