본문 바로가기
AI/Suno

SUNO 가사 다운로드 방법

by Ballenteine 2025. 12. 9.
반응형

 

가사가 포함된 영상을 다운로드 받는 방법 (유료만)

 

먼저, 공식적으로 Suno에서 가사 혹은 자막을 다운로드 하는 옵션을 제공하지 않습니다.

직접 작성된 가사를 하나하나 붙여가며 가사를 작성할 순 있지만, 그 과정이 상당히 오래 걸립니다.

가장 편한 방법부터 설명하자면, 가사가 들어간 영상을 다운로드 하는 방법입니다.

이 과정은 유료 버전을 구매한 사람들에게만 제공 되는 방법 입니다.

해당 방법을 사용한 경우에 아래와 같은 영상으로 출력 됩니다.

가사 싱크가 노래에 맞게 제공되기 때문에 정말 좋은 방법이라 생각은 합니다.

 

.Srt를 다운받아 직접 영상에 기입하는 방법 (복잡함)

 

이 과정은 무료 회원들도 이용이 가능하지만, 과정이 꽤나 번거롭습니다.

크롬 확장프로그램 다운받기 [링크]

해당 파일은 문장 단위로 쪼개어 .srt로 받을 수 있게 제공합니다.

다운 받은 이후, Suno에서 원하는 음악을 클릭하면, 위와 같이 자막을 다운 받을 수 있는 탭이 활성화 됩니다.

1
00:00:16,595 --> 00:00:17,074
[Verse 1]
Beyond 

2
00:00:17,180 --> 00:00:17,393
the 

3
00:00:17,482 --> 00:00:18,351
mountains 

4
00:00:18,398 --> 00:00:18,909
crowned 

5
00:00:19,009 --> 00:00:19,308
with 

6
00:00:19,348 --> 00:00:22,978
frost


7
00:00:23,058 --> 00:00:23,377
Where 

8
00:00:23,468 --> 00:00:24,175
ancient 

위와 같이 출력되는데, "단어" 단위가 아닌 "문장" 단위로 끊고 싶어하는 유저분들도 있을 겁니다.

다만, 계속 찾아봐도 관련 파일을 제공하는 경우는 거의 없었습니다.

야매적인 방법이지만, 다른 분들도 시도 할 수 있는 방법에 대해 제시 해보고자 합니다.

준비물

1. 위에서 다운 받은 .srt 파일

2. 자신의 노래의 가사파일을 메모장에 옮긴 .txt


"""
SRT 병합 스크립트 v2.1
단어 단위 SRT 파일을 가사 TXT 파일 기준으로 병합합니다.

사용법:
    1. 아래 설정 섹션에서 파일 경로와 옵션을 수정
    2. python merge_srt_final.py 실행
"""

import re
import sys
import os

# ============================================================================
# 설정 (여기를 수정하세요)
# ============================================================================

# 입력 파일 경로
SRT_PATH = r".srt경로"
TXT_PATH = r".txt경로"

# 출력 파일 경로
OUTPUT_PATH = r"추출된 .srt 경로"

# 괄호 섹션 처리 모드
# "keep_first" : 첫 번째 (간주중) 제외하고 나머지 괄호 삭제 (권장)
# "all"        : 모든 괄호 섹션 삭제
# "none"       : 괄호 섹션 유지
REMOVE_BRACKETS_MODE = "keep_first"

# ============================================================================
# 코드 (수정 불필요)
# ============================================================================

def parse_srt(srt_path):
    """단어 단위 SRT를 파싱하여 (start, end, text) 리스트 반환"""
    entries = []
    try:
        with open(srt_path, "r", encoding="utf-8") as f:
            content = f.read().strip()
            if not content:
                print(f"❌ 오류: SRT 파일이 비어있습니다.")
                return []
            blocks = content.split("\n\n")
    except FileNotFoundError:
        print(f"❌ 오류: SRT 파일을 찾을 수 없습니다: {srt_path}")
        return []
    except Exception as e:
        print(f"❌ 오류: SRT 파일 읽기 실패: {e}")
        return []
    
    for i, block in enumerate(blocks):
        lines = block.strip().split("\n")
        if len(lines) < 3:
            continue
        
        # 타임라인 추출
        time_line = lines[1]
        if " --> " not in time_line:
            print(f"⚠️ 경고: 블록 {i+1}에서 타임라인 형식 오류")
            continue
            
        start, end = time_line.split(" --> ")
        text = " ".join(lines[2:]).strip()
        entries.append((start, end, text))
    
    return entries

def read_lyrics(txt_path):
    """TXT 파일을 줄 단위로 읽어 반환하되 빈 줄 제외"""
    lines = []
    try:
        with open(txt_path, "r", encoding="utf-8") as f:
            for line in f:
                t = line.strip()
                if t:
                    lines.append(t)
    except FileNotFoundError:
        print(f"❌ 오류: 가사 파일을 찾을 수 없습니다: {txt_path}")
        return []
    except Exception as e:
        print(f"❌ 오류: 가사 파일 읽기 실패: {e}")
        return []
    
    return lines

def merge_by_lyrics(entries, lyrics):
    """TXT 기준으로 단어 SRT 결합"""
    merged = []
    idx = 0
    
    for i, line in enumerate(lyrics):
        words = line.split()
        word_count = len(words)
        
        # 인덱스 범위 체크
        if idx + word_count > len(entries):
            print(f"⚠️ 경고: 라인 {i+1} '{line[:40]}...'")
            print(f"   필요한 단어: {word_count}, 남은 엔트리: {len(entries) - idx}")
            # 남은 엔트리만 사용
            word_count = len(entries) - idx
            if word_count <= 0:
                print(f"   더 이상 사용 가능한 엔트리가 없습니다.")
                break
        
        srt_slice = entries[idx:idx + word_count]
        
        # 빈 슬라이스 체크
        if not srt_slice:
            print(f"⚠️ 경고: 라인 {i+1}에서 빈 슬라이스 발생")
            continue
        
        start = srt_slice[0][0]
        end = srt_slice[-1][1]
        merged.append([start, end, line])
        idx += word_count
    
    # 남은 엔트리 확인
    if idx < len(entries):
        remaining = len(entries) - idx
        print(f"\n⚠️ 주의: {remaining}개의 SRT 엔트리가 사용되지 않았습니다.")
        if remaining <= 5:
            print(f"   남은 엔트리:")
            for j, entry in enumerate(entries[idx:], 1):
                print(f"     [{j}] {entry[2]}")
    
    return merged

def remove_bracket_sections(merged, keep_first=True):
    """괄호로만 구성된 줄 삭제하고 삭제된 시간 범위 기록
    
    지원하는 괄호: (), [], <>
    
    Args:
        merged: 병합된 섹션 리스트
        keep_first: True이면 첫 번째 섹션은 유지, False이면 모든 괄호 섹션 삭제
    
    Returns:
        (정리된 섹션 리스트, 삭제된 섹션 정보 리스트)
    """
    if not merged:
        return [], []
    
    cleaned = []
    deleted_sections = []  # 삭제된 섹션의 (시작시간, 종료시간, 인덱스) 정보
    deleted_count = 0
    
    for i in range(len(merged)):
        start, end, text = merged[i]
        
        # 괄호로만 구성되어 있는지 체크 - (), [], <> 모두 지원
        is_round_bracket = bool(re.fullmatch(r"\s*\([^)]*\)\s*", text))      # ()
        is_square_bracket = bool(re.fullmatch(r"\s*\[[^\]]*\]\s*", text))   # []
        is_angle_bracket = bool(re.fullmatch(r"\s*<[^>]*>\s*", text))       # <>
        
        is_bracket_only = is_round_bracket or is_square_bracket or is_angle_bracket
        
        # 첫 번째 섹션이고 keep_first=True이면 무조건 유지
        if i == 0 and keep_first:
            cleaned.append([start, end, text])
            continue
        
        # 괄호만 있는 섹션이면 삭제
        if is_bracket_only:
            print(f"   삭제: [{i+1}] '{text}' (시간: {start} ~ {end})")
            deleted_sections.append((start, end, len(cleaned)))  # 삭제 위치는 현재 cleaned 길이
            deleted_count += 1
            continue
        
        cleaned.append([start, end, text])
    
    if deleted_count > 0:
        print(f"   → 총 {deleted_count}개 섹션 삭제됨")
    else:
        print(f"   → 삭제된 섹션 없음")
    
    return cleaned, deleted_sections

def fix_timing_gaps(merged, deleted_sections):
    """공백 제거 및 괄호 삭제 시간을 앞뒤로 균등 분배
    
    Args:
        merged: 병합된 섹션 리스트
        deleted_sections: 삭제된 섹션 정보 [(start, end, position), ...] (사용 안 함)
    
    괄호 섹션이 삭제된 경우, 그 시간을 이전 가사와 다음 가사가 50:50으로 나눠 차지합니다.
    """
    if len(merged) <= 1:
        return merged
    
    # 모든 갭을 찾아서 50:50 분배
    for i in range(len(merged) - 1):
        current_end = merged[i][1]
        next_start = merged[i + 1][0]
        
        # 갭이 있는 경우 (괄호가 삭제되거나 원래 공백이 있던 경우)
        if current_end != next_start:
            # 중간 지점 계산
            mid_time = calculate_mid_time(current_end, next_start)
            
            # 현재 섹션은 중간 지점까지 연장
            merged[i][1] = mid_time
            # 다음 섹션은 중간 지점부터 시작
            merged[i + 1][0] = mid_time
            
            print(f"   시간 분배: 섹션 {i+1} ({current_end}) + 섹션 {i+2} ({next_start}) → 중간점 {mid_time}")
    
    return merged

def calculate_mid_time(time1, time2):
    """두 타임스탬프의 중간 지점을 계산
    
    Args:
        time1: 시작 시간 (HH:MM:SS,mmm 형식)
        time2: 종료 시간 (HH:MM:SS,mmm 형식)
    
    Returns:
        중간 시간 (HH:MM:SS,mmm 형식)
    """
    # 타임스탬프를 밀리초로 변환
    ms1 = timestamp_to_ms(time1)
    ms2 = timestamp_to_ms(time2)
    
    # 중간 지점 계산
    mid_ms = (ms1 + ms2) // 2
    
    # 밀리초를 다시 타임스탬프로 변환
    return ms_to_timestamp(mid_ms)

def timestamp_to_ms(timestamp):
    """타임스탬프를 밀리초로 변환
    
    Args:
        timestamp: HH:MM:SS,mmm 형식의 문자열
    
    Returns:
        총 밀리초 (int)
    """
    # 00:01:23,456 형식 파싱
    time_part, ms_part = timestamp.strip().split(',')
    hours, minutes, seconds = map(int, time_part.split(':'))
    milliseconds = int(ms_part)
    
    total_ms = (hours * 3600 + minutes * 60 + seconds) * 1000 + milliseconds
    return total_ms

def ms_to_timestamp(ms):
    """밀리초를 타임스탬프로 변환
    
    Args:
        ms: 총 밀리초 (int)
    
    Returns:
        HH:MM:SS,mmm 형식의 문자열
    """
    milliseconds = ms % 1000
    total_seconds = ms // 1000
    
    seconds = total_seconds % 60
    total_minutes = total_seconds // 60
    
    minutes = total_minutes % 60
    hours = total_minutes // 60
    
    return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}"

def apply_first_section_rule(merged):
    """첫 번째 섹션을 0초부터 시작 + 텍스트를 (간주중)으로 변경"""
    if merged:
        merged[0][0] = "00:00:00,000"
        merged[0][2] = "(간주중)"
    return merged

def save_srt(merged, output_path):
    """SRT 포맷으로 저장"""
    try:
        # 출력 디렉토리 확인 및 생성
        output_dir = os.path.dirname(output_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir)
            
        with open(output_path, "w", encoding="utf-8") as f:
            for i, (start, end, text) in enumerate(merged, 1):
                f.write(f"{i}\n{start} --> {end}\n{text}\n\n")
        return True
    except Exception as e:
        print(f"❌ 오류: 파일 저장 실패: {e}")
        return False

def process_srt(srt_path, txt_path, output_path, remove_brackets_mode="keep_first"):
    """SRT 병합 메인 프로세스
    
    Args:
        srt_path: 입력 SRT 파일 경로
        txt_path: 입력 가사 TXT 파일 경로
        output_path: 출력 SRT 파일 경로
        remove_brackets_mode: 괄호 삭제 모드
            - "keep_first": 첫 번째 제외하고 삭제 (권장)
            - "all": 모든 괄호 섹션 삭제
            - "none": 괄호 섹션 유지
    """
    print("=" * 60)
    print("SRT 병합 프로세스 시작")
    print("=" * 60)
    
    # 설정 출력
    print("\n[설정]")
    print(f"입력 SRT: {srt_path}")
    print(f"입력 가사: {txt_path}")
    print(f"출력 파일: {output_path}")
    print(f"괄호 모드: {remove_brackets_mode}")
    
    # 1. 파일 읽기
    print("\n[1단계] 파일 읽기")
    entries = parse_srt(srt_path)
    if not entries:
        print("❌ SRT 파일 읽기 실패")
        return False
    
    lyrics = read_lyrics(txt_path)
    if not lyrics:
        print("❌ 가사 파일 읽기 실패")
        return False
    
    print(f"✓ SRT 엔트리: {len(entries)}개")
    print(f"✓ 가사 라인: {len(lyrics)}개")
    total_words = sum(len(line.split()) for line in lyrics)
    print(f"✓ 가사 총 단어: {total_words}개")
    
    if len(entries) != total_words:
        diff = abs(len(entries) - total_words)
        print(f"\n⚠️ 경고: 단어 수 불일치! (차이: {diff}개)")
        print(f"   이 경우 일부 가사가 잘리거나 매칭되지 않을 수 있습니다.")
    
    # 2. 병합
    print("\n[2단계] 가사 기준으로 병합")
    merged = merge_by_lyrics(entries, lyrics)
    if not merged:
        print("❌ 병합 실패")
        return False
    print(f"✓ {len(merged)}개 섹션 생성")
    
    # 3. 첫 번째 섹션 규칙 적용
    print("\n[3단계] 첫 번째 섹션 규칙 적용")
    merged = apply_first_section_rule(merged)
    print(f"✓ 첫 섹션: 0초 시작, 텍스트 '(간주중)'으로 변경")
    
    # 4. 괄호 섹션 처리
    print("\n[4단계] 괄호 섹션 처리")
    
    deleted_sections = []  # 삭제된 섹션 정보
    
    # 괄호 처리
    if remove_brackets_mode == "none":
        print("→ 괄호 섹션 유지")
    elif remove_brackets_mode == "keep_first":
        print("→ 첫 번째 제외하고 괄호 섹션 삭제")
        merged, deleted_sections = remove_bracket_sections(merged, keep_first=True)
    elif remove_brackets_mode == "all":
        print("→ 모든 괄호 섹션 삭제")
        merged, deleted_sections = remove_bracket_sections(merged, keep_first=False)
    else:
        print(f"❌ 오류: 알 수 없는 모드 '{remove_brackets_mode}'")
        print(f"   사용 가능한 모드: 'keep_first', 'all', 'none'")
        return False
    
    print(f"✓ 최종 섹션: {len(merged)}개")
    
    # 5. 타이밍 갭 조정
    print("\n[5단계] 타이밍 갭 조정")
    merged = fix_timing_gaps(merged, deleted_sections)
    print(f"✓ 갭 제거 완료")
    
    # 6. 저장
    print("\n[6단계] 파일 저장")
    if save_srt(merged, output_path):
        print(f"✓ 저장 완료: {output_path}")
        print(f"✓ 최종 섹션 수: {len(merged)}개")
    else:
        return False
    
    print("\n" + "=" * 60)
    print("✅ 모든 작업 완료!")
    print("=" * 60)
    return True

# ============================================================================
# 메인 실행부
# ============================================================================
if __name__ == "__main__":
    print("\n" + "="*60)
    print("SRT 병합 도구 v2.1")
    print("="*60 + "\n")
    
    # 처리 실행
    success = process_srt(
        srt_path=SRT_PATH,
        txt_path=TXT_PATH,
        output_path=OUTPUT_PATH,
        remove_brackets_mode=REMOVE_BRACKETS_MODE
    )
    
    if not success:
        print("\n❌ 작업 실패")
        sys.exit(1)
    else:
        print(f"\n💾 출력 파일: {OUTPUT_PATH}")
        print("\n📝 설정 변경:")
        print("   1. 스크립트 최상단의 '설정' 섹션에서 경로 수정")
        print("   2. REMOVE_BRACKETS_MODE를 변경하여 괄호 처리 방식 선택")
        print("      - 'keep_first' : 첫 번째 제외하고 삭제 (권장)")
        print("      - 'all'        : 모든 괄호 삭제")
        print("      - 'none'       : 괄호 유지")

간단히 설명하자면, 추출된 .srt는 타임라인을 갖고 있는데

이 타임라인과 .txt을 비교하여 문장단위로 끊어주는 Python 코드입니다.

자, "나는 이걸 어떻게 실행하는지 모르겠어요." 라고 하는 분들도 분명 계실겁니다.

위 코드를 복사하여, Chat GPT에서 붙여넣고 .srt와 .txt 파일을 제공한 후

" 제공한 .srt와 .txt파일 위 코드를 기반으로 하여 처리 해줘 "

라고 요청을 하면 알아서 수행 해 줍니다. (무료 버전 GPT도 해주는지는 모르겠습니다.)

위 방법을 통해 얻은 .srt 파일

1
00:00:00,000 --> 00:00:17,482
(간주중)

2
00:00:17,482 --> 00:00:24,275
Beyond the mountains crowned with frost
서리에 뒤덮인 산맥 너머로

3
00:00:24,275 --> 00:00:30,797
Where ancient echoes linger still
고대의 메아리가 아직도 머무는 그곳에서

4
00:00:30,797 --> 00:00:35,744
A quiet throne in ruins lies
폐허 속에 고요한 왕좌가 놓여 있고

 

이후 .srt를 동영상 편집 프로그램으로 편집하거나 하는 방법으로 기입하면 됩니다.

 

최종 영상

 

이제 제일 복잡한 자막 과정이 끝났으니, 입맛 대로 영상을 만들면 됩니다.

물론 쉽게 하는 방법도 있을지도 모르겠지만, 저의 짧은 생각으로는 지금이 한계 였습니다.

좋은 의견 있으신 분들은 댓글 남겨 주시면 감사하겠습니다.

※ 주의 Suno 정책에 따라 srt 파일 추출이 금지 될 수 있습니다. (아직은 괜찮은 거 같습니다.)

추후 업데이트를 통해 SRT를 직접 뽑아낼 수 있었으면 좋겠습니다. 감사합니다.

반응형

'AI > Suno' 카테고리의 다른 글

SUNO 음악 다운로드 방법  (0) 2025.12.05
SUNO 에서 음악 생성 하는 방법  (0) 2025.12.04