diff --git a/app/utils/suno.py b/app/utils/suno.py index 7db79b8..cc24e62 100644 --- a/app/utils/suno.py +++ b/app/utils/suno.py @@ -100,8 +100,8 @@ class SunoService: - 다운로드 URL: 2-3분 내 생성 - 생성되는 노래는 약 1분 이내의 길이 """ - # 1분 이내 노래 생성을 위한 프롬프트 조건 추가 - formatted_prompt = f"[Short Song - Under 1 minute]\n{prompt}" + # 정확히 1분 길이의 노래 생성을 위한 프롬프트 조건 추가 + formatted_prompt = f"[Song Duration: Exactly 1 minute - Must be precisely 60 seconds]\n{prompt}" # callback_url이 없으면 config에서 기본값 사용 (Suno API 필수 파라미터) actual_callback_url = callback_url or apikey_settings.SUNO_CALLBACK_URL diff --git a/poc/crawling/creatomate/creatomate.py b/poc/crawling/creatomate/creatomate.py new file mode 100644 index 0000000..2ec1e58 --- /dev/null +++ b/poc/crawling/creatomate/creatomate.py @@ -0,0 +1,206 @@ +import requests +import copy + +CREATOMATE_API_KEY = "df9e4382d7e84fe790bf8a2168152be195d5a3568524ceb66ed989a2dea809f7d3065d6803b2e3dd9d02b5e5ec1c9823" +#ACCOUNT_URL = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/" +# Creatomate 템플릿 정보 전부 가져오기 + +class Creatomate(): + base_url : str = "https://api.creatomate.com" + + def __init__(self, api_key): + self.api_key = api_key + + def get_all_templates_data(self) -> dict: + url = Creatomate.base_url + f"/v1/templates" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + response = requests.get(url, headers=headers) + return response.json() + + + # Creatomate 템플릿 ID 로부터 해당 템플릿 정보 가져오기 + def get_one_template_data(self, template_id:str) -> dict: + url = Creatomate.base_url + f"/v1/templates/{template_id}" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + response = requests.get(url, headers=headers) + return response.json() + + # 템플릿 정보 파싱하여 리소스 이름 추출하기 + def parse_template_component_name(self, template_source : dict) -> dict: + def recursive_parse_component(element:dict) -> dict: + if 'name' in element: + result_element_name_type = {element['name'] : element['type']} + else: + result_element_name_type = {} + + if element['type'] == "composition": + minor_component_list = [recursive_parse_component(minor) for minor in element['elements']] + for minor_component in minor_component_list: ## WARNING : Same name component should shroud other component. be aware + result_element_name_type.update(minor_component) + + return result_element_name_type + + result = {} + for result_element_dict in [recursive_parse_component(component) for component in template_source]: + result.update(result_element_dict) + + return result + + # 템플릿 정보 이미지/가사/음악 리소스와 매핑하기 + # 이미지는 순차적으로 집어넣기 + # 가사는 개행마다 한 텍스트 삽입 + # Template에 audio-music 항목이 있어야 함. (추가된 템플릿 Cafe뿐임) + def template_connect_resource_blackbox(self, template_id:str, image_url_list:list[str], lyric:str, music_url:str) -> dict: + template_data = self.get_one_template_data(template_id) + template_component_data = self.parse_template_component_name(template_data['source']['elements']) + + lyric.replace("\r", "") + lyric_splited = lyric.split("\n") + modifications = {} + for idx, (template_component_name, template_type) in enumerate(template_component_data.items()): + match template_type: + case 'image': + modifications[template_component_name] = image_url_list[idx % len(image_url_list)] + case 'text': + modifications[template_component_name] = lyric_splited[idx % len(lyric_splited)] + + modifications["audio-music"] = music_url + + return modifications + + def elements_connect_resource_blackbox(self, elements:list, image_url_list:list[str], lyric:str, music_url:str) -> dict: + template_component_data = self.parse_template_component_name(elements) + + lyric.replace("\r", "") + lyric_splited = lyric.split("\n") + modifications = {} + for idx, (template_component_name, template_type) in enumerate(template_component_data.items()): + match template_type: + case 'image': + modifications[template_component_name] = image_url_list[idx % len(image_url_list)] + case 'text': + modifications[template_component_name] = lyric_splited[idx % len(lyric_splited)] + + modifications["audio-music"] = music_url + + return modifications + + def modify_element(self, elements : list, modification : dict): + def recursive_modify(element:dict) -> dict: + if 'name' in element: + match element['type']: + case 'image': + element['source'] = modification[element['name']] + case 'audio': + element['source'] = modification.get(element['name'], "") + case 'video': + element['source'] = modification[element['name']] + case 'text': + element['source'] = modification.get(element['name'], "") + case 'composition': + for minor in element['elements']: + recursive_modify(minor) + + for minor in elements: + recursive_modify(minor) + + return elements + + # Creatomate에 생성 요청 + # response에 요청 정보 있으니 풀링 필요 + def make_creatomate_call(self, template_id:str, modifications:dict): + url = Creatomate.base_url + f"/v2/renders" + + data = { + "template_id": template_id, + "modifications": modifications + } + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + + response = requests.post(url, json=data, headers=headers) + return response + + # Creatomate에 생성 요청 without template + # response에 요청 정보 있으니 풀링 필요 + def make_creatomate_custom_call(self, source:str): + url = Creatomate.base_url + f"/v2/renders" + data = source + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + } + + response = requests.post(url, json=data, headers=headers) + return response + + def calc_scene_duration(self, template : dict): + total_template_duration = 0 + for elem in template['source']['elements']: + try: + if elem['type'] == 'audio': + continue + total_template_duration += elem['duration'] + if 'animations' not in elem: + continue + for animation in elem['animations']: + assert(animation['time'] == 0) # 0이 아닌 경우 확인 필요 + if animation['transition']: + total_template_duration -= animation['duration'] + except: + print(elem) + return total_template_duration + + def extend_template_duration(self, template : dict, target_duration : float): + template['duration'] = target_duration + total_template_duration = self.calc_scene_duration(template) + extend_rate = target_duration / total_template_duration + new_template = copy.deepcopy(template) + for elem in new_template['source']['elements']: + try: + if elem['type'] == 'audio': + continue + elem['duration'] = elem['duration'] * extend_rate + if 'animations' not in elem: + continue + for animation in elem['animations']: + assert(animation['time'] == 0) # 0이 아닌 경우 확인 필요 + animation['duration'] = animation['duration'] * extend_rate + except: + print(elem) + return new_template + + +# Azure사용한 legacy 코드 원본 +# def template_connect_resource_blackbox(template_id, user_idx, task_idx): +# secret_client = get_keyvault_client() +# account_url = secret_client.get_secret(BLOB_ACCOUNT_URL_KEY).value +# media_folder_path = f"{user_idx}/{task_idx}" +# lyric_path = f"{media_folder_path}/lyric.txt" +# lyric = az_storage.az_storage_read_ado2_media(lyric_path).readall().decode('UTF-8') +# media_list = az_storage.az_storage_get_ado2_media_list(media_folder_path) +# image_list = [media.name for media in media_list if '/crawling-images/' in media.name] +# template_data = get_one_template_data(template_id) +# template_component_data = parse_template_component_name(template_data['source']['elements']) +# lyric.replace("\r", "") +# lyric_splited = lyric.split("\n") +# modifications = {} +# for idx, (template_component_name, template_type) in enumerate(template_component_data.items()): +# match template_type: +# case 'image': +# modifications[template_component_name] = f"{account_url}/{BLOB_CONTAINER_NAME}/{image_list[idx % len(image_list)]}" +# case 'text': +# modifications[template_component_name] = lyric_splited[idx % len(lyric_splited)] + +# modifications["audio-music"] = f"{account_url}/{BLOB_CONTAINER_NAME}/{BLOB_MEDIA_FOLDER}/{media_folder_path}/music_mureka.mp3" +# print(modifications) + +# return modifications diff --git a/poc/crawling/creatomate/test.py b/poc/crawling/creatomate/test.py new file mode 100644 index 0000000..b391acc --- /dev/null +++ b/poc/crawling/creatomate/test.py @@ -0,0 +1,57 @@ +import creatomate +CREATOMATE_API_KEY = "df9e4382d7e84fe790bf8a2168152be195d5a3568524ceb66ed989a2dea809f7d3065d6803b2e3dd9d02b5e5ec1c9823" +shortform_4_template_id = "e8c7b43f-de4b-4ba3-b8eb-5df688569193" +target_duration = 90.0 # s + +creato = creatomate.Creatomate(CREATOMATE_API_KEY) + +template = creato.get_one_template_data(shortform_4_template_id) + +uploaded_image_url_list = ["https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/crawling-images/crawler4_img_1755818306_000_385523a5_99f2e8a8.jpg", + "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/crawling-images/crawler4_img_1755818306_001_d4cf6ec9_b81a1fdc.jpg", + "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/crawling-images/crawler4_img_1755818307_002_e4a0b276_680c5020.jpg", + "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/crawling-images/crawler4_img_1755818307_003_657f8c26_9f2c7168.jpg", + "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/crawling-images/crawler4_img_1755818307_004_9500e39d_24b9dad0.jpg", + "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/crawling-images/crawler4_img_1755818308_005_c3536641_9d490ccf.jpg"] + +lyric = """ +진짜 맛있는 추어탕의 향연 +청담추어정 본점이야 말로 +온 가족이 함께 먹는 그 맛 +여수동 맛집으로 명성을 떨쳐 + +주차 가능, 단체 이용도 OK +내 입맛을 사로잡는 맛 +청담추어정, 그 진정한 맛 +말복을 지나고 느껴보세요 + +한산한 분위기, 편안한 식사 +상황 추어탕으로 더욱 완벽 +톡톡 튀는 맛, 한 입에 느껴 +청담추어정에서 즐겨보세요 + +성남 출신의 맛집으로 +여수대로에서 빛나는 그곳 +청담추어정, 진짜 맛의 꿈 +여러분을 초대합니다 여기에 + +#청담추어정 #여수동맛집 +성남에서 만나는 진짜 맛 +""" + +song_url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/stay.mp3" + +modifications = creato.elements_connect_resource_blackbox( + template['source']['elements'], + uploaded_image_url_list, + lyric, + song_url +) + + +new_elements = creato.modify_element(template['source']['elements'], modifications) +template['source']['elements'] = new_elements +last_template = creato.extend_template_duration(template, target_duration) +creato.make_creatomate_custom_call(last_template['source']) + +