diff --git a/app/utils/suno.py b/app/utils/suno.py index 88d2744..cfcd530 100644 --- a/app/utils/suno.py +++ b/app/utils/suno.py @@ -179,7 +179,7 @@ class SunoService: raise ValueError("Suno API returned empty response for task status") return data - + async def get_lyric_timestamp(self, task_id: str, audio_id: str) -> dict[str, Any]: """ 음악 타임스탬프 정보 추출 @@ -189,27 +189,24 @@ class SunoService: audio_id: 사용할 audio id Returns: - data.alignedWords: 수노 가사 input - startS endS 시간 데이터 매핑 + data.alignedWords: 수노 가사 input - startS endS 시간 데이터 매핑 """ - payload = { - "task_id" : task_id, - "audio_id" : audio_id - } + payload = {"task_id": task_id, "audio_id": audio_id} async with httpx.AsyncClient() as client: response = await client.post( f"{self.BASE_URL}/generate/get-timestamped-lyrics", headers=self.headers, json=payload, - timeout=30.0, + timeout=120.0, ) response.raise_for_status() data = response.json() - if not data or not data['data']: + if not data or not data["data"]: raise ValueError("Suno API returned empty response for task status") - return data['data']['alignedWords'] + return data["data"]["alignedWords"] def parse_status_response(self, result: dict | None) -> PollingSongResponse: """Suno API 상태 응답을 파싱하여 PollingSongResponse로 변환합니다. @@ -284,83 +281,85 @@ class SunoService: message=status_messages.get(status, f"상태: {status}"), error_message=None, ) - + def align_lyrics(self, word_data: list[dict], sentences: list[str]) -> list[dict]: """ word의 시작/끝 포지션만 저장하고, 시간은 word에서 참조 """ - + # Step 1: 전체 텍스트 + word별 포지션 범위 저장 full_text = "" word_ranges = [] # [(start_pos, end_pos, entry), ...] - + for entry in word_data: - word = entry['word'] + word = entry["word"] start_pos = len(full_text) full_text += word end_pos = len(full_text) - 1 - + word_ranges.append((start_pos, end_pos, entry)) - + # Step 2: 메타데이터 제거 + 포지션 재매핑 meta_ranges = [] i = 0 while i < len(full_text): - if full_text[i] == '[': + if full_text[i] == "[": start = i - while i < len(full_text) and full_text[i] != ']': + while i < len(full_text) and full_text[i] != "]": i += 1 meta_ranges.append((start, i + 1)) i += 1 - + clean_text = "" new_to_old = {} # 클린 포지션 -> 원본 포지션 - + for old_pos, char in enumerate(full_text): in_meta = any(s <= old_pos < e for s, e in meta_ranges) if not in_meta: new_to_old[len(clean_text)] = old_pos clean_text += char - + # Step 3: 포지션으로 word 찾기 def get_word_at(old_pos: int): for start, end, entry in word_ranges: if start <= old_pos <= end: return entry return None - + # Step 4: 문장 매칭 def normalize(text): - return ''.join(c for c in text if c not in ' \n\t-') - + return "".join(c for c in text if c not in " \n\t-") + norm_clean = normalize(clean_text) - norm_to_clean = [i for i, c in enumerate(clean_text) if c not in ' \n\t-'] - + norm_to_clean = [i for i, c in enumerate(clean_text) if c not in " \n\t-"] + results = [] search_pos = 0 - + for sentence in sentences: norm_sentence = normalize(sentence) found_pos = norm_clean.find(norm_sentence, search_pos) - + if found_pos != -1: clean_start = norm_to_clean[found_pos] clean_end = norm_to_clean[found_pos + len(norm_sentence) - 1] - + old_start = new_to_old[clean_start] old_end = new_to_old[clean_end] - + word_start = get_word_at(old_start) word_end = get_word_at(old_end) - - results.append({ - 'text': sentence, - 'start_sec': word_start['startS'], - 'end_sec': word_end['endS'], - }) - + + results.append( + { + "text": sentence, + "start_sec": word_start["startS"], + "end_sec": word_end["endS"], + } + ) + search_pos = found_pos + len(norm_sentence) else: - results.append({'text': sentence, 'start_sec': None, 'end_sec': None}) - - return results \ No newline at end of file + results.append({"text": sentence, "start_sec": None, "end_sec": None}) + + return results