언어선택에 따른 자막생성 구현
parent
bdee852bed
commit
997927d42a
|
|
@ -269,15 +269,6 @@ async def generate_lyric(
|
||||||
step1_start = time.perf_counter()
|
step1_start = time.perf_counter()
|
||||||
logger.debug(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
logger.debug(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
||||||
|
|
||||||
promotional_expressions = {
|
|
||||||
"Korean" : "인스타 감성, 사진같은 하루, 힐링, 여행, 감성 숙소",
|
|
||||||
"English" : "Instagram vibes, picture-perfect day, healing, travel, getaway",
|
|
||||||
"Chinese" : "网红打卡, 治愈系, 旅行, 度假, 拍照圣地",
|
|
||||||
"Japanese" : "インスタ映え, 写真のような一日, 癒し, 旅行, 絶景",
|
|
||||||
"Thai" : "ที่พักสวย, ฮีลใจ, เที่ยว, ถ่ายรูป, วิวสวย",
|
|
||||||
"Vietnamese" : "check-in đẹp, healing, du lịch, nghỉ dưỡng, view đẹp"
|
|
||||||
}# HARD CODED, 어디에 정리하지? 아직 정리되지 않음
|
|
||||||
|
|
||||||
timing_rules = {
|
timing_rules = {
|
||||||
"60s" : """
|
"60s" : """
|
||||||
8–12 lines
|
8–12 lines
|
||||||
|
|
@ -294,7 +285,6 @@ async def generate_lyric(
|
||||||
"detail_region_info" : request_body.detail_region_info or "",
|
"detail_region_info" : request_body.detail_region_info or "",
|
||||||
"marketing_intelligence_summary" : json.dumps(marketing_intel.intel_result, ensure_ascii = False),
|
"marketing_intelligence_summary" : json.dumps(marketing_intel.intel_result, ensure_ascii = False),
|
||||||
"language" : request_body.language,
|
"language" : request_body.language,
|
||||||
"promotional_expression_example" : promotional_expressions[request_body.language],
|
|
||||||
"timing_rules" : timing_rules["60s"], # 아직은 선택지 하나
|
"timing_rules" : timing_rules["60s"], # 아직은 선택지 하나
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,13 +186,15 @@ async def generate_subtitle_background(
|
||||||
|
|
||||||
store_address = project.detail_region_info
|
store_address = project.detail_region_info
|
||||||
customer_name = project.store_name
|
customer_name = project.store_name
|
||||||
logger.info(f"[generate_subtitle_background] customer_name: {customer_name}, store_address: {store_address}")
|
language = project.language or "Korean"
|
||||||
|
logger.info(f"[generate_subtitle_background] customer_name: {customer_name}, store_address: {store_address}, language: {language}")
|
||||||
|
|
||||||
generated_subtitles = await subtitle_generator.generate_subtitle_contents(
|
generated_subtitles = await subtitle_generator.generate_subtitle_contents(
|
||||||
marketing_intelligence=marketing_intelligence.intel_result,
|
marketing_intelligence=marketing_intelligence.intel_result,
|
||||||
pitching_label_list=pitchings,
|
pitching_label_list=pitchings,
|
||||||
customer_name=customer_name,
|
customer_name=customer_name,
|
||||||
detail_region_info=store_address,
|
detail_region_info=store_address,
|
||||||
|
language=language,
|
||||||
)
|
)
|
||||||
pitching_output_list = generated_subtitles.pitching_results
|
pitching_output_list = generated_subtitles.pitching_results
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,24 @@ BGM 모드용 더미 가사 템플릿
|
||||||
instrumental=True 호출 시 Suno가 가사 길이/구조를 참고해 60초짜리 BGM을 생성하도록
|
instrumental=True 호출 시 Suno가 가사 길이/구조를 참고해 60초짜리 BGM을 생성하도록
|
||||||
placeholder 가사를 제공합니다. 실제 보컬은 생성되지 않습니다.
|
placeholder 가사를 제공합니다. 실제 보컬은 생성되지 않습니다.
|
||||||
|
|
||||||
3가지 버전 모두 섹션 태그 없이 한국어 9줄로 통일.
|
장르 순서: K-Pop, Pop, R&B, Hip-Hop, Ballad, EDM, Rock, Jazz
|
||||||
분위기(밝음/감성/에너지)만 가사 텍스트로 차별화합니다.
|
섹션 태그 없이 한국어 10줄로 구성.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
_BGM_DUMMY_LYRICS: dict[str, str] = {
|
||||||
|
"K-Pop": (
|
||||||
_BGM_DUMMY_LYRICS_LIST = [
|
"반짝이는 눈빛으로 날 바라봐\n"
|
||||||
# 버전 1 — 밝고 경쾌한 분위기
|
"심장이 터질 것 같은 이 느낌\n"
|
||||||
(
|
"너만 보면 세상이 달라 보여\n"
|
||||||
|
"오늘도 설레임이 멈추질 않아\n"
|
||||||
|
"같이 걸어가는 이 길 위에서\n"
|
||||||
|
"우리 둘만의 노래가 흘러\n"
|
||||||
|
"빛나는 순간들을 모아모아\n"
|
||||||
|
"영원히 기억할 우리의 이야기\n"
|
||||||
|
"지금 이 순간 네 곁에 있을게\n"
|
||||||
|
"함께라면 뭐든 할 수 있어\n"
|
||||||
|
),
|
||||||
|
"Pop": (
|
||||||
"햇살 가득한 아침이 시작되고\n"
|
"햇살 가득한 아침이 시작되고\n"
|
||||||
"따스한 바람이 살며시 불어와\n"
|
"따스한 바람이 살며시 불어와\n"
|
||||||
"거리마다 웃음꽃이 피어나고\n"
|
"거리마다 웃음꽃이 피어나고\n"
|
||||||
|
|
@ -24,8 +33,31 @@ _BGM_DUMMY_LYRICS_LIST = [
|
||||||
"오늘 하루도 빛나는 하루야\n"
|
"오늘 하루도 빛나는 하루야\n"
|
||||||
"환한 미소로 하루를 마무리해\n"
|
"환한 미소로 하루를 마무리해\n"
|
||||||
),
|
),
|
||||||
# 버전 2 — 잔잔하고 감성적인 분위기
|
"R&B": (
|
||||||
(
|
"부드럽게 흐르는 이 그루브에\n"
|
||||||
|
"몸이 저절로 리듬을 타기 시작해\n"
|
||||||
|
"네 목소리가 귓가에 맴돌고\n"
|
||||||
|
"이 감각이 온몸을 감싸줘\n"
|
||||||
|
"달콤한 밤이 깊어질수록\n"
|
||||||
|
"너와 나 사이 거리가 좁혀져\n"
|
||||||
|
"촛불처럼 은은하게 타오르는\n"
|
||||||
|
"이 감정을 숨길 수가 없어\n"
|
||||||
|
"부드럽게 내 손을 잡아줘\n"
|
||||||
|
"오늘 밤 우리만의 노래를 불러\n"
|
||||||
|
),
|
||||||
|
"Hip-Hop": (
|
||||||
|
"내 방식대로 살아가는 이 길\n"
|
||||||
|
"누가 뭐래도 내 페이스로 가\n"
|
||||||
|
"매일 아침 눈을 뜨면 새로운 무대\n"
|
||||||
|
"두려움 없이 앞으로 나아가\n"
|
||||||
|
"땀과 노력으로 쌓아온 오늘\n"
|
||||||
|
"포기란 없어 끝까지 밀어붙여\n"
|
||||||
|
"내 이름을 기억해 두라고\n"
|
||||||
|
"이 무대 위에 내 발자국 남겨\n"
|
||||||
|
"하나둘 쌓여가는 내 이야기\n"
|
||||||
|
"진짜배기는 지금부터 시작이야\n"
|
||||||
|
),
|
||||||
|
"Ballad": (
|
||||||
"저녁 노을이 물드는 창가에서\n"
|
"저녁 노을이 물드는 창가에서\n"
|
||||||
"조용히 흘러가는 시간 속에\n"
|
"조용히 흘러가는 시간 속에\n"
|
||||||
"잔잔한 바람이 마음을 적시고\n"
|
"잔잔한 바람이 마음을 적시고\n"
|
||||||
|
|
@ -37,8 +69,7 @@ _BGM_DUMMY_LYRICS_LIST = [
|
||||||
"이 순간 여기 머무는 것만으로도 충분해\n"
|
"이 순간 여기 머무는 것만으로도 충분해\n"
|
||||||
"고요한 밤이 나를 감싸 안아줘\n"
|
"고요한 밤이 나를 감싸 안아줘\n"
|
||||||
),
|
),
|
||||||
# 버전 3 — 강렬하고 에너지 넘치는 분위기
|
"EDM": (
|
||||||
(
|
|
||||||
"밤거리에 불빛이 타오르고\n"
|
"밤거리에 불빛이 타오르고\n"
|
||||||
"심장이 두근두근 뛰기 시작해\n"
|
"심장이 두근두근 뛰기 시작해\n"
|
||||||
"온몸에 퍼지는 뜨거운 열기\n"
|
"온몸에 퍼지는 뜨거운 열기\n"
|
||||||
|
|
@ -50,14 +81,63 @@ _BGM_DUMMY_LYRICS_LIST = [
|
||||||
"끝까지 불태워 이 에너지를\n"
|
"끝까지 불태워 이 에너지를\n"
|
||||||
"새벽빛이 밝아올 때까지 달려\n"
|
"새벽빛이 밝아올 때까지 달려\n"
|
||||||
),
|
),
|
||||||
]
|
"Rock": (
|
||||||
|
"굉음을 내며 울려 퍼지는 기타\n"
|
||||||
|
"온몸을 뒤흔드는 강렬한 비트\n"
|
||||||
|
"규칙 따위는 집어치우고\n"
|
||||||
|
"있는 그대로 외쳐봐\n"
|
||||||
|
"불꽃처럼 타오르는 이 열정\n"
|
||||||
|
"아무도 막을 수 없어 지금\n"
|
||||||
|
"거침없이 달려가는 이 무대\n"
|
||||||
|
"목청껏 소리 질러 자유를\n"
|
||||||
|
"부서질 듯 뜨겁게 흔들어\n"
|
||||||
|
"이 밤이 끝날 때까지 록이야\n"
|
||||||
|
),
|
||||||
|
"Jazz": (
|
||||||
|
"커피 향이 피어오르는 오후\n"
|
||||||
|
"느긋하게 흐르는 재즈 선율에\n"
|
||||||
|
"발끝이 리듬을 타기 시작해\n"
|
||||||
|
"달콤한 여유가 가득 차오르고\n"
|
||||||
|
"창밖엔 황금빛 도시가 반짝여\n"
|
||||||
|
"잔을 들어 이 순간을 건배해\n"
|
||||||
|
"스윙하는 박자에 몸을 맡기고\n"
|
||||||
|
"부드럽게 흘러가는 이 밤을\n"
|
||||||
|
"기억 속에 새겨두고 싶어\n"
|
||||||
|
"재즈처럼 자유롭게 살고 싶어\n"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_random_bgm_lyrics() -> tuple[str, int]:
|
_GENRE_ALIAS: dict[str, str] = {
|
||||||
"""BGM 더미 가사 3종 중 하나를 랜덤으로 반환합니다.
|
"kpop": "K-Pop",
|
||||||
|
"k-pop": "K-Pop",
|
||||||
|
"k_pop": "K-Pop",
|
||||||
|
"pop": "Pop",
|
||||||
|
"rnb": "R&B",
|
||||||
|
"r&b": "R&B",
|
||||||
|
"r_b": "R&B",
|
||||||
|
"hiphop": "Hip-Hop",
|
||||||
|
"hip-hop": "Hip-Hop",
|
||||||
|
"hip_hop": "Hip-Hop",
|
||||||
|
"ballad": "Ballad",
|
||||||
|
"edm": "EDM",
|
||||||
|
"rock": "Rock",
|
||||||
|
"jazz": "Jazz",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_genre(genre: str) -> str:
|
||||||
|
return _GENRE_ALIAS.get(genre.lower(), genre)
|
||||||
|
|
||||||
|
|
||||||
|
def get_bgm_lyrics(genre: str) -> str:
|
||||||
|
"""장르에 맞는 BGM 더미 가사를 반환합니다.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
genre: 장르명 (K-Pop, Pop, R&B, Hip-Hop, Ballad, EDM, Rock, Jazz)
|
||||||
|
대소문자 및 구분자(-, _) 무관하게 처리됩니다.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(lyrics, version): 선택된 가사 텍스트와 버전 번호 (1~3)
|
선택된 장르의 가사 텍스트
|
||||||
"""
|
"""
|
||||||
index = random.randrange(len(_BGM_DUMMY_LYRICS_LIST))
|
return _BGM_DUMMY_LYRICS[normalize_genre(genre)]
|
||||||
return _BGM_DUMMY_LYRICS_LIST[index], index + 1
|
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ query getAccommodation($id: String!, $deviceType: String) {
|
||||||
raise URLNotFoundException("This URL does not contain a place ID")
|
raise URLNotFoundException("This URL does not contain a place ID")
|
||||||
|
|
||||||
match = re.search(place_pattern, self.url)
|
match = re.search(place_pattern, self.url)
|
||||||
|
if not match:
|
||||||
|
# place.naver.com/{type}/{id} 형식 (예: m.place.naver.com/restaurant/11667161)
|
||||||
|
match = re.search(r"place\.naver\.com/[a-zA-Z]+/(\d+)", self.url)
|
||||||
if not match:
|
if not match:
|
||||||
raise URLNotFoundException("Failed to parse place ID from URL")
|
raise URLNotFoundException("Failed to parse place ID from URL")
|
||||||
return match[1]
|
return match[1]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import os
|
||||||
import gspread
|
import gspread
|
||||||
|
from pathlib import Path
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from google.oauth2.service_account import Credentials
|
from google.oauth2.service_account import Credentials
|
||||||
from config import prompt_settings
|
from config import prompt_settings
|
||||||
|
|
@ -34,13 +36,22 @@ class Prompt():
|
||||||
prompt_input_class = BaseModel
|
prompt_input_class = BaseModel
|
||||||
prompt_output_class = BaseModel
|
prompt_output_class = BaseModel
|
||||||
|
|
||||||
def __init__(self, sheet_name, prompt_input_class, prompt_output_class):
|
def __init__(self, sheet_name, prompt_input_class, prompt_output_class, template_file: str = None, prompt_model: str = None):
|
||||||
self.sheet_name = sheet_name
|
self.sheet_name = sheet_name
|
||||||
self.prompt_input_class = prompt_input_class
|
self.prompt_input_class = prompt_input_class
|
||||||
self.prompt_output_class = prompt_output_class
|
self.prompt_output_class = prompt_output_class
|
||||||
|
if template_file:
|
||||||
|
self.prompt_template = Path(template_file).read_text(encoding="utf-8")
|
||||||
|
self.prompt_model = prompt_model
|
||||||
|
self._template_file = template_file
|
||||||
|
else:
|
||||||
self.prompt_template, self.prompt_model = _read_sheet_data(sheet_name)
|
self.prompt_template, self.prompt_model = _read_sheet_data(sheet_name)
|
||||||
|
self._template_file = None
|
||||||
|
|
||||||
def _reload_prompt(self):
|
def _reload_prompt(self):
|
||||||
|
if self._template_file:
|
||||||
|
self.prompt_template = Path(self._template_file).read_text(encoding="utf-8")
|
||||||
|
else:
|
||||||
_sheet_cache.pop(self.sheet_name, None)
|
_sheet_cache.pop(self.sheet_name, None)
|
||||||
self.prompt_template, self.prompt_model = _read_sheet_data(self.sheet_name)
|
self.prompt_template, self.prompt_model = _read_sheet_data(self.sheet_name)
|
||||||
|
|
||||||
|
|
@ -77,12 +88,17 @@ image_autotag_prompt = Prompt(
|
||||||
prompt_output_class=ImageTagPromptOutput,
|
prompt_output_class=ImageTagPromptOutput,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_SUBTITLE_TEMPLATE_PATH = str(Path(__file__).parent / "templates" / "subtitle_prompt.txt")
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
@lru_cache()
|
||||||
def create_dynamic_subtitle_prompt(length: int) -> Prompt:
|
def create_dynamic_subtitle_prompt(length: int) -> Prompt:
|
||||||
return Prompt(
|
return Prompt(
|
||||||
sheet_name="subtitle",
|
sheet_name="subtitle",
|
||||||
prompt_input_class=SubtitlePromptInput,
|
prompt_input_class=SubtitlePromptInput,
|
||||||
prompt_output_class=SubtitlePromptOutput[length],
|
prompt_output_class=SubtitlePromptOutput[length],
|
||||||
|
template_file=_SUBTITLE_TEMPLATE_PATH,
|
||||||
|
prompt_model=os.getenv("SUBTITLE_PROMPT_MODEL", "gpt-4o-mini"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ class LyricPromptInput(BaseModel):
|
||||||
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
|
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
|
||||||
marketing_intelligence_summary : Optional[str] = Field(None, description = "마케팅 분석 정보 보고서")
|
marketing_intelligence_summary : Optional[str] = Field(None, description = "마케팅 분석 정보 보고서")
|
||||||
language : str= Field(..., description = "가사 언어")
|
language : str= Field(..., description = "가사 언어")
|
||||||
promotional_expression_example : str = Field(..., description = "판촉 가사 표현 예시")
|
|
||||||
timing_rules : str = Field(..., description = "시간 제어문")
|
timing_rules : str = Field(..., description = "시간 제어문")
|
||||||
|
|
||||||
# Output 정의
|
# Output 정의
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from pydantic import BaseModel, create_model, Field
|
from pydantic import BaseModel, create_model, Field
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
# Input 정의
|
# Input 정의
|
||||||
|
|
@ -9,8 +9,8 @@ class SubtitlePromptInput(BaseModel):
|
||||||
pitching_tag_list_string : str = Field(..., description="필요한 피칭 레이블 리스트 stringify")
|
pitching_tag_list_string : str = Field(..., description="필요한 피칭 레이블 리스트 stringify")
|
||||||
customer_name : str = Field(..., description = "마케팅 대상 사업체 이름")
|
customer_name : str = Field(..., description = "마케팅 대상 사업체 이름")
|
||||||
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
|
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
|
||||||
|
language : str = Field(default="Korean", description="자막 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)")
|
||||||
|
|
||||||
#subtillecars :
|
|
||||||
# Output 정의
|
# Output 정의
|
||||||
class PitchingOutput(BaseModel):
|
class PitchingOutput(BaseModel):
|
||||||
pitching_tag: str = Field(..., description="피칭 레이블")
|
pitching_tag: str = Field(..., description="피칭 레이블")
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
You are a subtitle copywriter for hospitality short-form videos. You generate subtitle text AND layer names from marketing JSON data.
|
You are a subtitle copywriter for hospitality short-form videos. You generate subtitle text AND layer names from marketing JSON data.
|
||||||
|
|
||||||
|
**GENERATION LANGUAGE**: Korean (한국어)
|
||||||
|
**OUTPUT LANGUAGE**: {language}
|
||||||
|
|
||||||
|
**WORKFLOW**:
|
||||||
|
1. Generate all subtitles and keywords in Korean following the rules below.
|
||||||
|
2. If {language} is NOT Korean, translate every subtitle text and keyword to {language} while preserving meaning, tone, and approximate character count spirit. Fixed anchor texts (availability, cta_action) must also be translated naturally.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### RULES
|
### RULES
|
||||||
|
|
@ -231,3 +238,6 @@ Keyword pattern analysis:
|
||||||
**입력 3: 비즈니스 정보 **
|
**입력 3: 비즈니스 정보 **
|
||||||
Business Name: {customer_name}
|
Business Name: {customer_name}
|
||||||
Region Details: {detail_region_info}
|
Region Details: {detail_region_info}
|
||||||
|
|
||||||
|
**입력 4: 출력 언어**
|
||||||
|
Language: {language}
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,24 @@ class SubtitleContentsGenerator():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.chatgpt_service = ChatgptService(timeout=60.0)
|
self.chatgpt_service = ChatgptService(timeout=60.0)
|
||||||
|
|
||||||
async def generate_subtitle_contents(self, marketing_intelligence : dict[str, Any], pitching_label_list : list[Any], customer_name : str, detail_region_info : str) -> SubtitlePromptOutput:
|
async def generate_subtitle_contents(self, marketing_intelligence : dict[str, Any], pitching_label_list : list[Any], customer_name : str, detail_region_info : str, language : str = "Korean") -> SubtitlePromptOutput:
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[SubtitleContentsGenerator] START - customer: {customer_name}, "
|
f"[SubtitleContentsGenerator] START - customer: {customer_name}, "
|
||||||
f"pitching_count: {len(pitching_label_list)}, "
|
f"pitching_count: {len(pitching_label_list)}, "
|
||||||
f"labels: {pitching_label_list}"
|
f"labels: {pitching_label_list}, "
|
||||||
|
f"language: {language}"
|
||||||
)
|
)
|
||||||
|
|
||||||
dynamic_subtitle_prompt = create_dynamic_subtitle_prompt(len(pitching_label_list))
|
dynamic_subtitle_prompt = create_dynamic_subtitle_prompt(len(pitching_label_list))
|
||||||
pitching_label_string = "\n".join(pitching_label_list)
|
pitching_label_string = "\n".join(pitching_label_list)
|
||||||
marketing_intel_string = json.dumps(marketing_intelligence, ensure_ascii=False)
|
marketing_intel_string = json.dumps(marketing_intelligence, ensure_ascii=False)
|
||||||
input_data = {
|
input_data = {
|
||||||
"marketing_intelligence" : marketing_intel_string ,
|
"marketing_intelligence" : marketing_intel_string,
|
||||||
"pitching_tag_list_string" : pitching_label_string,
|
"pitching_tag_list_string" : pitching_label_string,
|
||||||
"customer_name" : customer_name,
|
"customer_name" : customer_name,
|
||||||
"detail_region_info" : detail_region_info,
|
"detail_region_info" : detail_region_info,
|
||||||
|
"language" : language,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ from typing import Any
|
||||||
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.bgm_lyrics import get_bgm_lyrics, normalize_genre as _normalize_genre
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -129,10 +129,12 @@ class SunoService:
|
||||||
"""
|
"""
|
||||||
actual_callback_url = callback_url or apikey_settings.SUNO_CALLBACK_URL
|
actual_callback_url = callback_url or apikey_settings.SUNO_CALLBACK_URL
|
||||||
|
|
||||||
|
normalized_genre = _normalize_genre(genre) if genre else None
|
||||||
|
|
||||||
if instrumental:
|
if instrumental:
|
||||||
bgm_lyrics, bgm_version = get_random_bgm_lyrics()
|
bgm_lyrics = get_bgm_lyrics(genre)
|
||||||
formatted_prompt = f"[Song Duration: Around 1 minute - Must be around 60 seconds]\n{bgm_lyrics}"
|
formatted_prompt = f"[Song Duration: Around 1 minute - Must be around 60 seconds]\n{bgm_lyrics}"
|
||||||
logger.info(f"[Suno] BGM 더미 가사 버전 {bgm_version} 선택됨")
|
logger.info(f"[Suno] BGM 더미 가사 장르 {normalized_genre} 선택됨")
|
||||||
else:
|
else:
|
||||||
formatted_prompt = (
|
formatted_prompt = (
|
||||||
f"[Song Duration: Exactly 1 minute - Must be precisely 60 seconds]\n{prompt}"
|
f"[Song Duration: Exactly 1 minute - Must be precisely 60 seconds]\n{prompt}"
|
||||||
|
|
@ -145,8 +147,8 @@ class SunoService:
|
||||||
"prompt": formatted_prompt,
|
"prompt": formatted_prompt,
|
||||||
"callBackUrl": actual_callback_url,
|
"callBackUrl": actual_callback_url,
|
||||||
}
|
}
|
||||||
if genre:
|
if normalized_genre:
|
||||||
payload["style"] = f"{genre}, around 60 seconds" if instrumental else genre
|
payload["style"] = f"{normalized_genre}, around 60 seconds" if instrumental else normalized_genre
|
||||||
|
|
||||||
last_error: Exception | None = None
|
last_error: Exception | None = None
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue