Compare commits
No commits in common. "ec3e0159e8ca59aecf78116c080dc887db2fa7ee" and "97a6384a545959dddb50936b200c79cee93f04a8" have entirely different histories.
ec3e0159e8
...
97a6384a54
|
|
@ -1,13 +1,12 @@
|
||||||
import time
|
import time
|
||||||
import traceback
|
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
from app.utils.logger import get_logger
|
from app.utils.logger import get_logger
|
||||||
from config import db_settings
|
from config import db_settings
|
||||||
|
import traceback
|
||||||
|
|
||||||
logger = get_logger("database")
|
logger = get_logger("database")
|
||||||
|
|
||||||
|
|
@ -135,16 +134,15 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
# )
|
# )
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
logger.error(
|
logger.error(
|
||||||
f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, "
|
f"[get_session] ROLLBACK - error: {type(e).__name__}: {e}, "
|
||||||
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
|
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
|
||||||
)
|
)
|
||||||
raise
|
raise e
|
||||||
finally:
|
finally:
|
||||||
total_time = time.perf_counter() - start_time
|
total_time = time.perf_counter() - start_time
|
||||||
# logger.debug(
|
# logger.debug(
|
||||||
|
|
@ -172,8 +170,6 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
# )
|
# )
|
||||||
try:
|
try:
|
||||||
yield session
|
yield session
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
@ -182,7 +178,7 @@ async def get_background_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
|
f"duration: {(time.perf_counter() - start_time)*1000:.1f}ms"
|
||||||
)
|
)
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
raise
|
raise e
|
||||||
finally:
|
finally:
|
||||||
total_time = time.perf_counter() - start_time
|
total_time = time.perf_counter() - start_time
|
||||||
# logger.debug(
|
# logger.debug(
|
||||||
|
|
|
||||||
|
|
@ -356,14 +356,6 @@ async def generate_lyric(
|
||||||
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
|
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(
|
background_tasks.add_task(
|
||||||
generate_lyric_background,
|
generate_lyric_background,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
|
|
@ -374,8 +366,8 @@ async def generate_lyric(
|
||||||
|
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
generate_subtitle_background,
|
generate_subtitle_background,
|
||||||
orientation=orientation,
|
orientation = orientation,
|
||||||
task_id=task_id,
|
task_id=task_id
|
||||||
)
|
)
|
||||||
|
|
||||||
step4_elapsed = (time.perf_counter() - step4_start) * 1000
|
step4_elapsed = (time.perf_counter() - step4_start) * 1000
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ class GenerateLyricRequest(BaseModel):
|
||||||
description="영상 방향 (horizontal: 가로형, vertical: 세로형)",
|
description="영상 방향 (horizontal: 가로형, vertical: 세로형)",
|
||||||
),
|
),
|
||||||
m_id : Optional[int] = Field(None, description="마케팅 인텔리전스 ID 값")
|
m_id : Optional[int] = Field(None, description="마케팅 인텔리전스 ID 값")
|
||||||
instrumental: bool = Field(default=False, description="BGM 전용 모드 (가사 생성 안 함)")
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateLyricResponse(BaseModel):
|
class GenerateLyricResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ class SocialAccountService:
|
||||||
else:
|
else:
|
||||||
# DB datetime은 naive, now()는 aware이므로 naive로 통일하여 비교
|
# DB datetime은 naive, now()는 aware이므로 naive로 통일하여 비교
|
||||||
current_time = now().replace(tzinfo=None)
|
current_time = now().replace(tzinfo=None)
|
||||||
buffer_time = current_time + timedelta(minutes=20)
|
buffer_time = current_time + timedelta(hours=1)
|
||||||
if account.token_expires_at <= buffer_time:
|
if account.token_expires_at <= buffer_time:
|
||||||
should_refresh = True
|
should_refresh = True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,10 +185,9 @@ async def generate_song(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Song 테이블에 초기 데이터 저장
|
# Song 테이블에 초기 데이터 저장
|
||||||
if request_body.instrumental:
|
song_prompt = (
|
||||||
song_prompt = f"[Instrumental]\n[Genre]\n{request_body.genre}"
|
f"[Lyrics]\n{request_body.lyrics}\n\n[Genre]\n{request_body.genre}"
|
||||||
else:
|
)
|
||||||
song_prompt = f"[Lyrics]\n{request_body.lyrics}\n\n[Genre]\n{request_body.genre}"
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[generate_song] Lyrics comparison - task_id: {task_id}\n"
|
f"[generate_song] Lyrics comparison - task_id: {task_id}\n"
|
||||||
f"{'=' * 60}\n"
|
f"{'=' * 60}\n"
|
||||||
|
|
@ -250,7 +249,6 @@ async def generate_song(
|
||||||
suno_task_id = await suno_service.generate(
|
suno_task_id = await suno_service.generate(
|
||||||
prompt=request_body.lyrics,
|
prompt=request_body.lyrics,
|
||||||
genre=request_body.genre,
|
genre=request_body.genre,
|
||||||
instrumental=request_body.instrumental,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stage2_time = time.perf_counter()
|
stage2_time = time.perf_counter()
|
||||||
|
|
@ -454,18 +452,6 @@ async def get_song_status(
|
||||||
)
|
)
|
||||||
|
|
||||||
suno_audio_id = first_clip.get("id")
|
suno_audio_id = first_clip.get("id")
|
||||||
|
|
||||||
# BGM 모드(lyric_result가 비어 있음)에서는 타임스탬프 생성 스킵
|
|
||||||
lyric_result = await session.execute(
|
|
||||||
select(Lyric)
|
|
||||||
.where(Lyric.task_id == song.task_id)
|
|
||||||
.order_by(Lyric.created_at.desc())
|
|
||||||
.limit(1)
|
|
||||||
)
|
|
||||||
lyric = lyric_result.scalar_one_or_none()
|
|
||||||
gt_lyric = lyric.lyric_result if lyric else None
|
|
||||||
|
|
||||||
if gt_lyric:
|
|
||||||
word_data = await suno_service.get_lyric_timestamp(
|
word_data = await suno_service.get_lyric_timestamp(
|
||||||
suno_task_id, suno_audio_id
|
suno_task_id, suno_audio_id
|
||||||
)
|
)
|
||||||
|
|
@ -474,6 +460,14 @@ async def get_song_status(
|
||||||
f"suno_task_id: {suno_task_id}, suno_audio_id: {suno_audio_id}, "
|
f"suno_task_id: {suno_task_id}, suno_audio_id: {suno_audio_id}, "
|
||||||
f"word_data: {word_data}"
|
f"word_data: {word_data}"
|
||||||
)
|
)
|
||||||
|
lyric_result = await session.execute(
|
||||||
|
select(Lyric)
|
||||||
|
.where(Lyric.task_id == song.task_id)
|
||||||
|
.order_by(Lyric.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
lyric = lyric_result.scalar_one_or_none()
|
||||||
|
gt_lyric = lyric.lyric_result
|
||||||
lyric_line_list = gt_lyric.split("\n")
|
lyric_line_list = gt_lyric.split("\n")
|
||||||
sentences = [
|
sentences = [
|
||||||
lyric_line.strip(",. ")
|
lyric_line.strip(",. ")
|
||||||
|
|
@ -488,10 +482,16 @@ async def get_song_status(
|
||||||
timestamped_lyrics = suno_service.align_lyrics(
|
timestamped_lyrics = suno_service.align_lyrics(
|
||||||
word_data, sentences
|
word_data, sentences
|
||||||
)
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"[get_song_status] sentences from lyric - "
|
||||||
|
f"sentences: {sentences}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO : DB upload timestamped_lyrics
|
||||||
for order_idx, timestamped_lyric in enumerate(
|
for order_idx, timestamped_lyric in enumerate(
|
||||||
timestamped_lyrics
|
timestamped_lyrics
|
||||||
):
|
):
|
||||||
|
# start_sec 또는 end_sec가 None인 경우 건너뛰기
|
||||||
if (
|
if (
|
||||||
timestamped_lyric["start_sec"] is None
|
timestamped_lyric["start_sec"] is None
|
||||||
or timestamped_lyric["end_sec"] is None
|
or timestamped_lyric["end_sec"] is None
|
||||||
|
|
@ -512,11 +512,6 @@ async def get_song_status(
|
||||||
end_time=timestamped_lyric["end_sec"],
|
end_time=timestamped_lyric["end_sec"],
|
||||||
)
|
)
|
||||||
session.add(song_timestamp)
|
session.add(song_timestamp)
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f"[get_song_status] BGM 모드 - 타임스탬프 생성 스킵, "
|
|
||||||
f"suno_task_id: {suno_task_id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
parsed_response.status = "processing"
|
parsed_response.status = "processing"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -33,7 +33,7 @@ class GenerateSongRequest(BaseModel):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lyrics: Optional[str] = Field(None, description="노래에 사용할 가사 (instrumental=True이면 생략 가능)")
|
lyrics: str = Field(..., description="노래에 사용할 가사")
|
||||||
genre: str = Field(
|
genre: str = Field(
|
||||||
...,
|
...,
|
||||||
description="음악 장르 (K-Pop, Pop, R&B, Hip-Hop, Ballad, EDM, Rock, Jazz 등)",
|
description="음악 장르 (K-Pop, Pop, R&B, Hip-Hop, Ballad, EDM, Rock, Jazz 등)",
|
||||||
|
|
@ -42,15 +42,6 @@ class GenerateSongRequest(BaseModel):
|
||||||
default="Korean",
|
default="Korean",
|
||||||
description="노래 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)",
|
description="노래 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)",
|
||||||
)
|
)
|
||||||
instrumental: bool = Field(default=False, description="BGM 전용 모드 (가사 없이 음악만 생성)")
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def validate_lyrics_required(self) -> "GenerateSongRequest":
|
|
||||||
if not self.instrumental and not self.lyrics:
|
|
||||||
raise ValueError("instrumental=False일 때 lyrics는 필수입니다.")
|
|
||||||
if self.instrumental:
|
|
||||||
self.lyrics = None
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateSongResponse(BaseModel):
|
class GenerateSongResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,10 @@ from app.user.services.auth import (
|
||||||
AdminRequiredError,
|
AdminRequiredError,
|
||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
MissingTokenError,
|
MissingTokenError,
|
||||||
TokenExpiredError,
|
|
||||||
UserInactiveError,
|
UserInactiveError,
|
||||||
UserNotFoundError,
|
UserNotFoundError,
|
||||||
)
|
)
|
||||||
from app.user.services.jwt import decode_token, is_token_expired
|
from app.user.services.jwt import decode_token
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -59,9 +58,6 @@ async def get_current_user(
|
||||||
|
|
||||||
payload = decode_token(token)
|
payload = decode_token(token)
|
||||||
if payload is None:
|
if payload is None:
|
||||||
if is_token_expired(token):
|
|
||||||
logger.info(f"[AUTH-DEP] Access Token 만료 - token: ...{token[-20:]}")
|
|
||||||
raise TokenExpiredError()
|
|
||||||
logger.warning(f"[AUTH-DEP] Access Token 디코딩 실패 - token: ...{token[-20:]}")
|
logger.warning(f"[AUTH-DEP] Access Token 디코딩 실패 - token: ...{token[-20:]}")
|
||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ from app.user.services.jwt import (
|
||||||
get_access_token_expire_seconds,
|
get_access_token_expire_seconds,
|
||||||
get_refresh_token_expires_at,
|
get_refresh_token_expires_at,
|
||||||
get_token_hash,
|
get_token_hash,
|
||||||
is_token_expired,
|
|
||||||
)
|
)
|
||||||
from app.user.services.kakao import kakao_client
|
from app.user.services.kakao import kakao_client
|
||||||
|
|
||||||
|
|
@ -213,9 +212,6 @@ class AuthService:
|
||||||
# 1. 토큰 디코딩 및 검증
|
# 1. 토큰 디코딩 및 검증
|
||||||
payload = decode_token(refresh_token)
|
payload = decode_token(refresh_token)
|
||||||
if payload is None:
|
if payload is None:
|
||||||
if is_token_expired(refresh_token):
|
|
||||||
logger.info(f"[AUTH] 토큰 갱신 실패 [1/8 만료] - token: ...{refresh_token[-20:]}")
|
|
||||||
raise TokenExpiredError()
|
|
||||||
logger.warning(f"[AUTH] 토큰 갱신 실패 [1/8 디코딩] - token: ...{refresh_token[-20:]}")
|
logger.warning(f"[AUTH] 토큰 갱신 실패 [1/8 디코딩] - token: ...{refresh_token[-20:]}")
|
||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,28 +116,6 @@ def decode_token(token: str) -> Optional[dict]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_token_expired(token: str) -> bool:
|
|
||||||
"""
|
|
||||||
토큰이 만료됐는지 확인 (서명/형식은 유효하지만 exp 초과인 경우)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True: 서명은 유효하나 만료된 토큰, False: 형식/서명 자체가 잘못된 토큰
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(
|
|
||||||
token,
|
|
||||||
jwt_settings.JWT_SECRET,
|
|
||||||
algorithms=[jwt_settings.JWT_ALGORITHM],
|
|
||||||
options={"verify_exp": False},
|
|
||||||
)
|
|
||||||
exp = payload.get("exp")
|
|
||||||
if exp is None:
|
|
||||||
return False
|
|
||||||
return datetime.fromtimestamp(exp) < datetime.now()
|
|
||||||
except JWTError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_token_hash(token: str) -> str:
|
def get_token_hash(token: str) -> str:
|
||||||
"""
|
"""
|
||||||
토큰의 SHA-256 해시값 생성
|
토큰의 SHA-256 해시값 생성
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ async def autotag_images(image_url_list : list[str]) -> list[dict]: #tag_list
|
||||||
|
|
||||||
image_result_tasks = [chatgpt.generate_structured_output(image_autotag_prompt, image_input_data, image_input_data['img_url'], False, silent = True) for image_input_data in image_input_data_list]
|
image_result_tasks = [chatgpt.generate_structured_output(image_autotag_prompt, image_input_data, image_input_data['img_url'], False, silent = True) for image_input_data in image_input_data_list]
|
||||||
image_result_list: list[BaseModel | BaseException] = await asyncio.gather(*image_result_tasks, return_exceptions=True)
|
image_result_list: list[BaseModel | BaseException] = await asyncio.gather(*image_result_tasks, return_exceptions=True)
|
||||||
MAX_RETRY = 2 # 하드코딩, 어떻게 처리할지는 나중에
|
MAX_RETRY = 3 # 하드코딩, 어떻게 처리할지는 나중에
|
||||||
for _ in range(MAX_RETRY):
|
for _ in range(MAX_RETRY):
|
||||||
failed_idx = [i for i, r in enumerate(image_result_list) if isinstance(r, Exception)]
|
failed_idx = [i for i, r in enumerate(image_result_list) if isinstance(r, Exception)]
|
||||||
print("Failed", failed_idx)
|
print("Failed", failed_idx)
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
"""
|
|
||||||
BGM 모드용 더미 가사 템플릿
|
|
||||||
|
|
||||||
instrumental=True 호출 시 Suno가 가사 길이/구조를 참고해 60초짜리 BGM을 생성하도록
|
|
||||||
placeholder 가사를 제공합니다. 실제 보컬은 생성되지 않습니다.
|
|
||||||
|
|
||||||
3가지 버전 모두 섹션 태그 없이 한국어 9줄로 통일.
|
|
||||||
분위기(밝음/감성/에너지)만 가사 텍스트로 차별화합니다.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
_BGM_DUMMY_LYRICS_LIST = [
|
|
||||||
# 버전 1 — 밝고 경쾌한 분위기
|
|
||||||
(
|
|
||||||
"햇살 가득한 아침이 시작되고\n"
|
|
||||||
"따스한 바람이 살며시 불어와\n"
|
|
||||||
"거리마다 웃음꽃이 피어나고\n"
|
|
||||||
"오늘도 설레는 하루가 열려\n"
|
|
||||||
"가볍게 발걸음을 내딛으며\n"
|
|
||||||
"환한 빛 속으로 걸어가는 길\n"
|
|
||||||
"두근두근 설레는 이 순간을\n"
|
|
||||||
"온 마음 가득 담아 느껴봐\n"
|
|
||||||
"오늘 하루도 빛나는 하루야\n"
|
|
||||||
"환한 미소로 하루를 마무리해\n"
|
|
||||||
),
|
|
||||||
# 버전 2 — 잔잔하고 감성적인 분위기
|
|
||||||
(
|
|
||||||
"저녁 노을이 물드는 창가에서\n"
|
|
||||||
"조용히 흘러가는 시간 속에\n"
|
|
||||||
"잔잔한 바람이 마음을 적시고\n"
|
|
||||||
"기억 속 풍경이 스쳐 지나가\n"
|
|
||||||
"부드럽게 감기는 이 느낌처럼\n"
|
|
||||||
"천천히 숨을 고르며 머물러\n"
|
|
||||||
"마음 깊은 곳에 스며드는 온기\n"
|
|
||||||
"조용히 눈을 감고 느껴봐\n"
|
|
||||||
"이 순간 여기 머무는 것만으로도 충분해\n"
|
|
||||||
"고요한 밤이 나를 감싸 안아줘\n"
|
|
||||||
),
|
|
||||||
# 버전 3 — 강렬하고 에너지 넘치는 분위기
|
|
||||||
(
|
|
||||||
"밤거리에 불빛이 타오르고\n"
|
|
||||||
"심장이 두근두근 뛰기 시작해\n"
|
|
||||||
"온몸에 퍼지는 뜨거운 열기\n"
|
|
||||||
"멈출 수 없는 이 흐름 속으로\n"
|
|
||||||
"있는 힘껏 달려가는 이 순간\n"
|
|
||||||
"모든 걸 내려놓고 느껴봐\n"
|
|
||||||
"짜릿하게 타오르는 지금 이 밤\n"
|
|
||||||
"온 세상이 하나로 움직여\n"
|
|
||||||
"끝까지 불태워 이 에너지를\n"
|
|
||||||
"새벽빛이 밝아올 때까지 달려\n"
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_random_bgm_lyrics() -> tuple[str, int]:
|
|
||||||
"""BGM 더미 가사 3종 중 하나를 랜덤으로 반환합니다.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(lyrics, version): 선택된 가사 텍스트와 버전 번호 (1~3)
|
|
||||||
"""
|
|
||||||
index = random.randrange(len(_BGM_DUMMY_LYRICS_LIST))
|
|
||||||
return _BGM_DUMMY_LYRICS_LIST[index], index + 1
|
|
||||||
|
|
@ -55,12 +55,11 @@ generate() 호출 시 callback_url 파라미터를 전달하면 생성 완료
|
||||||
- 동일 task_id에 대해 여러 콜백 수신 가능 (멱등성 처리 필요)
|
- 동일 task_id에 대해 여러 콜백 수신 가능 (멱등성 처리 필요)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from app.song.schemas.song_schema import PollingSongResponse, SongClipData
|
from app.song.schemas.song_schema import PollingSongResponse, SongClipData
|
||||||
from app.utils.bgm_lyrics import get_random_bgm_lyrics
|
|
||||||
from app.utils.logger import get_logger
|
from app.utils.logger import get_logger
|
||||||
from config import apikey_settings, recovery_settings
|
from config import apikey_settings, recovery_settings
|
||||||
|
|
||||||
|
|
@ -103,10 +102,9 @@ class SunoService:
|
||||||
|
|
||||||
async def generate(
|
async def generate(
|
||||||
self,
|
self,
|
||||||
prompt: str | None = None,
|
prompt: str,
|
||||||
genre: str | None = None,
|
genre: str | None = None,
|
||||||
callback_url: str | None = None,
|
callback_url: str | None = None,
|
||||||
instrumental: bool = False,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
음악 생성 요청
|
음악 생성 요청
|
||||||
|
|
@ -117,7 +115,6 @@ class SunoService:
|
||||||
genre: 음악 장르 (예: "K-Pop", "Pop", "R&B", "Hip-Hop", "Ballad", "EDM")
|
genre: 음악 장르 (예: "K-Pop", "Pop", "R&B", "Hip-Hop", "Ballad", "EDM")
|
||||||
None일 경우 style 파라미터를 전송하지 않음
|
None일 경우 style 파라미터를 전송하지 않음
|
||||||
callback_url: 생성 완료 시 알림 받을 URL (None일 경우 config에서 기본값 사용)
|
callback_url: 생성 완료 시 알림 받을 URL (None일 경우 config에서 기본값 사용)
|
||||||
instrumental: True이면 BGM 전용 — 더미 가사로 60초 길이를 유도하고 보컬 없이 생성
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
task_id: 작업 추적용 ID
|
task_id: 작업 추적용 ID
|
||||||
|
|
@ -127,26 +124,23 @@ class SunoService:
|
||||||
- 다운로드 URL: 2-3분 내 생성
|
- 다운로드 URL: 2-3분 내 생성
|
||||||
- 생성되는 노래는 약 1분 이내의 길이
|
- 생성되는 노래는 약 1분 이내의 길이
|
||||||
"""
|
"""
|
||||||
actual_callback_url = callback_url or apikey_settings.SUNO_CALLBACK_URL
|
# 정확히 1분 길이의 노래 생성을 위한 프롬프트 조건 추가
|
||||||
|
formatted_prompt = f"[Song Duration: Exactly 1 minute - Must be precisely 60 seconds]\n{prompt}"
|
||||||
|
|
||||||
if instrumental:
|
# callback_url이 없으면 config에서 기본값 사용 (Suno API 필수 파라미터)
|
||||||
bgm_lyrics, bgm_version = get_random_bgm_lyrics()
|
actual_callback_url = callback_url or apikey_settings.SUNO_CALLBACK_URL
|
||||||
formatted_prompt = f"[Song Duration: Around 1 minute - Must be around 60 seconds]\n{bgm_lyrics}"
|
|
||||||
logger.info(f"[Suno] BGM 더미 가사 버전 {bgm_version} 선택됨")
|
|
||||||
else:
|
|
||||||
formatted_prompt = (
|
|
||||||
f"[Song Duration: Exactly 1 minute - Must be precisely 60 seconds]\n{prompt}"
|
|
||||||
)
|
|
||||||
|
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"model": "V5",
|
"model": "V5",
|
||||||
"customMode": True,
|
"customMode": True,
|
||||||
"instrumental": instrumental,
|
"instrumental": False,
|
||||||
"prompt": formatted_prompt,
|
"prompt": formatted_prompt,
|
||||||
"callBackUrl": actual_callback_url,
|
"callBackUrl": actual_callback_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# genre가 있을 때만 style 추가
|
||||||
if genre:
|
if genre:
|
||||||
payload["style"] = f"{genre}, around 60 seconds" if instrumental else genre
|
payload["style"] = genre
|
||||||
|
|
||||||
last_error: Exception | None = None
|
last_error: Exception | None = None
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue