퀵포스팅/기타

[음성변환]3.유튜브 영상 속 원하는 인물에 내 목소리 입히기(AI 보이스 체인저 자동화 도구)

파기차차 2025. 12. 27. 11:09
728x90
반응형
SMALL

ㅁ 개요

 

O 프로그램 소개

 

 - 직접 내 목소리 음성 특징을 학습시켜 내 목소리의 모델을 만드는 방법에 대해 알아 보겠습니다. ([음성변환]2.내 목소리의 모델 만들기(직접 모델 학습하기)

이번 시간에는 이전 시간에 학습된 내 모델을 이용하여 유튜브 영상 속 원하는 인물이 내 목소리와 똑같이 나오도록 만드는 방법에 대해 알아 보겠습니다.

(다만, 이전 시간에 만든 저의 모델의 경우 제 PC의 GPU성능이 좋지 못하여 정확하고, 클린한 음성으로 만들기에는 조금 부족한 모델로 판단하여 일단 허깅페이스에서 제공하는 깨끗한 음성 모델인 Trump_RVC를 사용하여 글을 작성하였으니 참고하시기 바랍니다.)

 

 

 

O 진행 순서

 

진행 순서는 크게 아래와 같이 진행합니다.

 

1. 설정 및 입력 (Configuration)
 
-파이썬 프로그램을 실행하면 아래 내용을 입력 받아 어떤 영상도 사용할 수 있도록 범용적으로 만들었습니다.
 
>영상 파일: 작업할 원본 영상 파일명을 입력받습니다.
>모델 선택: 사용할 RVC 모델(pth)과 인덱스(index) 파일을 지정합니다.
>시간 설정: 변환할 구간의 시작 시간과 종료 시간을 설정합니다. (예: 00:00:00 ~ 00:40:00)
>핵심: 전체 영상을 다 분석하지 않고 필요한 구간만 핀포인트로 작업하여 속도를 획기적으로 높입니다.
 
 


2. 오디오 추출 (Audio Extraction)
 
-영상에서 음성부분만 따로 추출합니다.
 
>도구: FFmpeg
>작업: 설정한 시간 구간의 오디오만 잘라내어 임시 파일(temp_analysis_audio.wav)로 저장합니다.


3. 화자 분리 (Speaker Diarization)
 
-영상 속 인물이 2명이상인 경우 누구의 목소리를 내 목소리로 바꿀지를 결정해야 합니다. 이것을 구분하기 위한 분석 작업을 하게 되며, 아래와 같이 영상속 어느 구간이 누구인지를 판단하는 분석을 하게됩니다.
 
{
        "start": 0.03096875,
        "end": 16.230968750000002,
        "speaker": "SPEAKER_00"
    },
 
>도구: pyannote.audio (AI 모델)
>작업: 잘라낸 오디오에서 **"누가 언제 말했는지"**를 분석합니다.
>캐싱: 분석 결과는 diarization_result.json에 저장되어, 다음에 같은 구간을 작업할 때 분석 과정을 건너뛸 수 있습니다.


4. 화자 선택 (Speaker Selection)
 
-위의 3번에서 영상속 구간별 인물을 분석했다면, 여기서는 내 목소리로 바꾸고 싶은 인물을 지정하는 단계입니다.
예를 들어 영상에서 총 2명이 나온다면, SPEAKER_00, SPEAKER_01 과 같이 구분되는데, SPEAKER_01을 지정하는 것과 같은 내용입니다.
 
>작업: 분석된 화자들(SPEAKER_00, 01...)의 목소리 샘플을 speaker_samples 폴더에 생성합니다.
>입력: 사용자가 샘플을 들어보고 변환하고 싶은 화자의 번호를 입력합니다.


5. 음성 추출 (Target Extraction)
 
-위 4번에서 선택한 인물인 SPEAKER_01이 영상속에서 말한 구간을 모두 쪼개서 임시폴더에 저장합니다.
 
>작업: 선택한 화자가 말한 구간만 정밀하게 잘라내어 temp_clips 폴더에 저장합니다.


6. RVC 변환 (Voice Conversion)
 
-임시폴더에 저장한 쪼개진 원본 음성파일을 지정한 모델의 목소리로 변환 후 변환폴더에 저장합니다.
 
>도구: RVC (Retrieval-based Voice Conversion)
>작업: 잘라낸 음성 조각들을 사용자가 지정한 AI 모델의 목소리로 변환합니다.
>자동화: RVC 환경에 맞는 스크립트를 자동으로 생성하여 실행하므로 복잡한 설정이 필요 없습니다.


7. 합성 및 최종 영상 생성 (Merge & Render)
 
-변환된 쪼개진 음성파일을 합치고, 이 합쳐진 음성파일을 다시 원본영상에 그대로 끼워 넣습니다.
 
>합성: 변환된 목소리 조각들을 원래 시간 위치에 정확하게 끼워 넣습니다. (오프셋 보정 적용)
>영상 생성: 완성된 오디오를 원본 영상과 합쳐 **final_result_video.mp4**를 생성합니다.

 

전체 소스 : auto_voice_changer.py

import os
import sys
import json
import shutil
import subprocess
import soundfile as sf
import numpy as np
import warnings
import torch

# === 사용자 설정 ===
VIDEO_FILE = "original_video.mp4" #원본 영상 이름
RVC_DIR = r"C:\Users\munnt2\Downloads\RVC1006Nvidia" #RVC 기본 설치 디렉토리
MODEL_FILENAME = "MyVoice.pth" # 원하는 모델의 pth 파일 입력
INDEX_FILENAME = "added_IVF149_Flat_nprobe_1_MyVoice_v2.index" # 원하는 모델의 인덱스 입력
HF_TOKEN = "" #허깅페이스 access Tokens 입력

# === 내부 설정 ===
TEMP_AUDIO = "temp_analysis_audio.wav"
TEMP_CLIPS_DIR = "temp_clips"
CONVERTED_CLIPS_DIR = "converted_clips"
SAMPLES_DIR = "speaker_samples"
FINAL_AUDIO = "final_output_audio.wav"
FINAL_VIDEO = "final_result_video.mp4"

# === 유틸리티 함수 ===
def run_ffmpeg(args):
    subprocess.run(["ffmpeg", "-y"] + args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def setup_pyannote():
    import torchaudio
    if not hasattr(torchaudio, 'list_audio_backends'):
        torchaudio.list_audio_backends = lambda: []
    if not hasattr(torchaudio, 'get_audio_backend'):
        torchaudio.get_audio_backend = lambda: 'soundfile'
    if not hasattr(torchaudio, 'set_audio_backend'):
        torchaudio.set_audio_backend = lambda x: None
    
    import torch.serialization
    _original_load = torch.load
    def patched_load(*args, **kwargs):
        kwargs['weights_only'] = False
        return _original_load(*args, **kwargs)
    torch.load = patched_load

def create_rvc_script(target_model, target_index):
    script_content = f"""
import os
import sys
import glob
import torch
import warnings
warnings.filterwarnings('ignore')

SOURCE_DIR = r"{os.path.abspath(TEMP_CLIPS_DIR)}"
OUTPUT_DIR = r"{os.path.abspath(CONVERTED_CLIPS_DIR)}"
RVC_ROOT = os.getcwd()
MODEL_DIR = os.path.join(RVC_ROOT, r"assets\\weights")
MODEL_FILENAME = "{target_model}"

# 인덱스 파일 검색 (재귀 검색 및 예외 처리 추가)
found_indices = glob.glob(os.path.join(RVC_ROOT, r"logs\\**\\{target_index}"), recursive=True)
INDEX_PATH = found_indices[0] if found_indices and "{target_index}" else ""
if "{target_index}" and not INDEX_PATH:
    print(f"[경고] 인덱스 파일을 찾을 수 없습니다: {{target_index}} (인덱스 없이 진행합니다)")

F0_UP_KEY = 0
F0_METHOD = "rmvpe"
INDEX_RATE = 0.75
FILTER_RADIUS = 3
RESAMPLE_SR = 0
RMS_MIX_RATE = 0.25
PROTECT = 0.33

sys.path.append(RVC_ROOT)
os.environ["rmvpe_root"] = os.path.join(RVC_ROOT, "assets/rmvpe")
os.environ["weight_root"] = MODEL_DIR
os.environ["index_root"] = os.path.join(RVC_ROOT, "logs")

try:
    from configs.config import Config
    config = Config()
    config.weight_root = MODEL_DIR
except ImportError:
    class Config:
        def __init__(self):
            self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
            self.is_half = True
            self.n_cpu = 0
            self.gpu_name = None
            self.gpu_mem = None
            self.x_pad = 1
            self.x_query = 6
            self.x_center = 38
            self.x_max = 41
            self.weight_root = MODEL_DIR
    config = Config()

try:
    from infer.modules.vc.modules import VC
    def patched_get_vc(self, sid, *args, **kwargs):
        if os.path.exists(sid):
            self.cpt = torch.load(sid, map_location="cpu")
            self.tgt_sr = self.cpt["config"][-1]
            self.cpt["config"][-3] = self.cpt["weight"]["emb_g.weight"].shape[0]
            self.if_f0 = self.cpt.get("f0", 1)
            self.version = self.cpt.get("version", "v1")
            from infer.lib.infer_pack.models import SynthesizerTrnMs256NSFsid, SynthesizerTrnMs256NSFsid_nono, SynthesizerTrnMs768NSFsid, SynthesizerTrnMs768NSFsid_nono
            if self.version == "v1":
                cls = SynthesizerTrnMs256NSFsid if self.if_f0 == 1 else SynthesizerTrnMs256NSFsid_nono
            else:
                cls = SynthesizerTrnMs768NSFsid if self.if_f0 == 1 else SynthesizerTrnMs768NSFsid_nono
            self.net_g = cls(*self.cpt["config"], is_half=self.config.is_half)
            del self.net_g.enc_q
            self.net_g.load_state_dict(self.cpt["weight"], strict=False)
            self.net_g.eval().to(self.config.device)
            if self.config.is_half: self.net_g.half()
            else: self.net_g.float()
            from infer.modules.vc.pipeline import Pipeline
            self.pipeline = Pipeline(self.tgt_sr, self.config)
            self.pipeline.model = self.net_g
        else:
            print(f"[오류] 모델 없음: {{sid}}")
    VC.get_vc = patched_get_vc
except ImportError:
    sys.exit(1)

def main():
    if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR)
    full_model_path = os.path.join(MODEL_DIR, MODEL_FILENAME)
    vc = VC(config)
    vc.get_vc(full_model_path)
    wav_files = glob.glob(os.path.join(SOURCE_DIR, "*.wav"))
    print(f"총 {{len(wav_files)}}개 파일 변환 시작...")
    for wav_path in wav_files:
        filename = os.path.basename(wav_path)
        try:
            info, (tgt_sr, audio_opt) = vc.vc_single(0, wav_path, F0_UP_KEY, None, F0_METHOD, INDEX_PATH, "", INDEX_RATE, FILTER_RADIUS, RESAMPLE_SR, RMS_MIX_RATE, PROTECT)
            import soundfile as sf
            sf.write(os.path.join(OUTPUT_DIR, filename), audio_opt, tgt_sr)
            print(f"변환 완료: {{filename}}")
        except Exception as e:
            print(f"실패 ({{filename}}): {{e}}")

if __name__ == "__main__":
    main()
"""
    return script_content

def main():
    print("=== AI 보이스 체인저 자동화 도구 (최적화 버전) ===")
    
    # ==========================================
    # [Step 1] 설정 및 입력 (Configuration)
    # - 영상 파일, RVC 모델, 인덱스 파일 설정
    # - 작업할 시간 범위 설정 (시작~종료)
    # ==========================================
    global VIDEO_FILE, MODEL_FILENAME, INDEX_FILENAME
    
    print(f"\n[설정] 현재 영상 파일: {VIDEO_FILE}")
    new_video = input("변경하려면 영상 파일명을 입력하세요 (엔터: 유지): ").strip()
    if new_video: VIDEO_FILE = new_video
        
    print(f"[설정] 현재 모델: {MODEL_FILENAME}")
    new_model = input("변경하려면 모델 파일명을 입력하세요 (엔터: 유지): ").strip()
    if new_model: MODEL_FILENAME = new_model
        
    print(f"[설정] 현재 인덱스: {INDEX_FILENAME}")
    new_index = input("변경하려면 인덱스 파일명을 입력하세요 (엔터: 유지): ").strip()
    if new_index: INDEX_FILENAME = new_index

    if not os.path.exists(VIDEO_FILE):
        print(f"[오류] 영상 파일을 찾을 수 없습니다: {VIDEO_FILE}")
        return

    # 시간 범위 설정
    print("\n[설정] 작업할 시간 범위를 설정합니다 (엔터: 전체 영상)")
    start_time_str = input("시작 시간 (HH:MM:SS, 예: 00:34:36): ").strip()
    end_time_str = input("종료 시간 (HH:MM:SS, 예: 00:40:00): ").strip()
    
    start_sec = 0.0
    duration = None
    
    if start_time_str:
        try:
            h, m, s = map(int, start_time_str.split(':'))
            start_sec = h * 3600 + m * 60 + s
            print(f">> 시작 시간: {start_sec}초")
        except:
            print(">> 시간 형식 오류. 0초부터 시작합니다.")
            
    if end_time_str:
        try:
            h, m, s = map(int, end_time_str.split(':'))
            end_sec = h * 3600 + m * 60 + s
            print(f">> 종료 시간: {end_sec}초")
            if end_sec > start_sec:
                duration = end_sec - start_sec
        except:
            print(">> 시간 형식 오류. 끝까지 진행합니다.")

    # ==========================================
    # [Step 2] 오디오 추출 (Audio Extraction)
    # - FFmpeg를 사용하여 설정한 구간의 오디오만 추출
    # ==========================================
    print(f"\n[1/6] 분석할 구간의 오디오 추출 중... ({VIDEO_FILE})")
    ffmpeg_args = ["-i", VIDEO_FILE, "-vn", "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "1"]
    if start_sec > 0:
        ffmpeg_args.extend(["-ss", str(start_sec)])
    if duration:
        ffmpeg_args.extend(["-t", str(duration)])
    ffmpeg_args.append(TEMP_AUDIO)
    
    if os.path.exists(TEMP_AUDIO): os.remove(TEMP_AUDIO)
    run_ffmpeg(ffmpeg_args)
    
    # ==========================================
    # [Step 3] 화자 분리 (Speaker Diarization)
    # - pyannote.audio를 사용하여 화자 식별
    # - 결과 캐싱 기능 포함 (diarization_result.json)
    # ==========================================
    print("\n[2/6] 화자 분리 수행 중... (선택한 구간만 분석)")
    
    DIARIZATION_FILE = "diarization_result.json"
    diarization_data = []
    skip_analysis = False
    
    # 캐시 확인
    if os.path.exists(DIARIZATION_FILE):
        print(f"   - 이전 분석 결과 발견: {DIARIZATION_FILE}")
        use_cache = input("   - 이전 결과를 사용하여 분석을 건너뛰시겠습니까? (y/n, 기본값: y): ").strip().lower()
        if use_cache != 'n':
            try:
                with open(DIARIZATION_FILE, "r", encoding="utf-8") as f:
                    diarization_data = json.load(f)
                print("   >> 이전 결과를 로드했습니다. 분석을 건너뜁니다.")
                skip_analysis = True
                
                # 오디오 데이터는 샘플 생성 등을 위해 로드 필요
                print("   - 오디오 데이터 로딩 중...")
                waveform, sample_rate = sf.read(TEMP_AUDIO)
            except Exception as e:
                print(f"   [오류] 캐시 파일 로드 실패: {e}. 다시 분석합니다.")
                skip_analysis = False

    if not skip_analysis:
        setup_pyannote()
        from pyannote.audio import Pipeline
        pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", token=HF_TOKEN)
        
        print("   - 오디오 로딩 및 분석 중...")
        waveform, sample_rate = sf.read(TEMP_AUDIO)
        waveform_tensor = torch.from_numpy(waveform).float().unsqueeze(0)
        diarization = pipeline({"waveform": waveform_tensor, "sample_rate": sample_rate})
        
        # 결과 처리
        if hasattr(diarization, 'speaker_diarization'):
            annotation = diarization.speaker_diarization
        else:
            annotation = diarization
            
        for segment, _, speaker in annotation.itertracks(yield_label=True):
            diarization_data.append({
                "start": segment.start,
                "end": segment.end,
                "speaker": speaker
            })
            
        # 결과 파일 저장
        with open(DIARIZATION_FILE, "w", encoding="utf-8") as f:
            json.dump(diarization_data, f, indent=4)
        print(f"   >> 분석 완료! 결과가 {DIARIZATION_FILE}에 저장되었습니다.")
    
    # ==========================================
    # [Step 4] 화자 선택 (Speaker Selection)
    # - 화자별 샘플 생성 및 사용자 선택
    # ==========================================
    print("\n[3/6] 화자 분석 및 샘플 생성 중...")
    if os.path.exists(SAMPLES_DIR): shutil.rmtree(SAMPLES_DIR)
    os.makedirs(SAMPLES_DIR)
    
    speakers = set()
    speaker_segments = {}
    
    for item in diarization_data:
        speaker = item["speaker"]
        start = item["start"]
        end = item["end"]
        
        speakers.add(speaker)
        if speaker not in speaker_segments:
            speaker_segments[speaker] = []
        speaker_segments[speaker].append({"start": start, "end": end})
        
        sample_path = os.path.join(SAMPLES_DIR, f"sample_{speaker}.wav")
        if not os.path.exists(sample_path) and (end - start) > 2.0:
            start_sample = int(start * sample_rate)
            end_sample = int(end * sample_rate)
            sf.write(sample_path, waveform[start_sample:end_sample], sample_rate)
            
    if not speakers:
        print("[경고] 해당 구간에서 화자가 감지되지 않았습니다.")
        return

    print(f"발견된 화자: {', '.join(speakers)}")
    print(f"샘플 파일이 '{SAMPLES_DIR}' 폴더에 저장되었습니다. 들어보고 변환할 화자를 선택하세요.")
    
    while True:
        target_speaker = input("\n변환할 화자를 입력하세요 (예: SPEAKER_01): ").strip()
        if target_speaker in speakers:
            break
        print("잘못된 화자입니다. 다시 입력해주세요.")
        
    # ==========================================
    # [Step 5] 음성 추출 (Target Extraction)
    # - 선택한 화자의 음성 구간만 잘라내어 저장
    # ==========================================
    print(f"\n[4/6] {target_speaker} 음성 추출 및 RVC 변환 준비...")
    if os.path.exists(TEMP_CLIPS_DIR): shutil.rmtree(TEMP_CLIPS_DIR)
    os.makedirs(TEMP_CLIPS_DIR)
    
    clips_metadata = []
    count = 0
    
    for segment in speaker_segments[target_speaker]:
        start_sample = int(segment["start"] * sample_rate)
        end_sample = int(segment["end"] * sample_rate)
        
        filename = f"{count:04d}_{target_speaker}.wav"
        filepath = os.path.join(TEMP_CLIPS_DIR, filename)
        sf.write(filepath, waveform[start_sample:end_sample], sample_rate)
        
        clips_metadata.append({
            "filename": filename,
            "start_sample": start_sample,
            "end_sample": end_sample
        })
        count += 1
        
    # ==========================================
    # [Step 6] RVC 변환 (Voice Conversion)
    # - RVC 모델을 사용하여 목소리 변환 수행
    # ==========================================
    print(f"\n[5/6] RVC 변환 실행 중... (총 {count}개 클립)")
    if os.path.exists(CONVERTED_CLIPS_DIR): shutil.rmtree(CONVERTED_CLIPS_DIR)
    os.makedirs(CONVERTED_CLIPS_DIR)
    
    rvc_script_path = os.path.join(RVC_DIR, "auto_rvc_run.py")
    with open(rvc_script_path, "w", encoding="utf-8") as f:
        f.write(create_rvc_script(MODEL_FILENAME, INDEX_FILENAME))
        
    python_exe = os.path.join(RVC_DIR, "runtime", "python.exe")
    if not os.path.exists(python_exe): python_exe = "python"
    subprocess.run([python_exe, "auto_rvc_run.py"], cwd=RVC_DIR, check=True)
    
    # ==========================================
    # [Step 7] 합성 및 최종 영상 생성 (Merge & Render)
    # - 변환된 오디오 합성 및 영상 렌더링
    # ==========================================
    print("\n[6/6] 오디오 합성 및 최종 영상 생성 중...")
    
    # 전체 오디오 추출 (합성용)
    FULL_AUDIO_TEMP = "temp_full_original.wav"
    if not os.path.exists(FULL_AUDIO_TEMP):
        print("   - 전체 영상 오디오 추출 중...")
        run_ffmpeg(["-i", VIDEO_FILE, "-vn", "-acodec", "pcm_s16le", "-ar", "44100", "-ac", "1", FULL_AUDIO_TEMP])
    
    print("   - 전체 오디오 로딩 중...")
    full_waveform, full_sr = sf.read(FULL_AUDIO_TEMP)
    
    print("   - 변환된 목소리 합성 중...")
    offset_sample = int(start_sec * full_sr)
    
    success_cnt = 0
    for meta in clips_metadata:
        converted_path = os.path.join(CONVERTED_CLIPS_DIR, meta["filename"])
        if os.path.exists(converted_path):
            conv_wav, conv_sr = sf.read(converted_path)
            target_len = meta["end_sample"] - meta["start_sample"]
            if len(conv_wav) > target_len: conv_wav = conv_wav[:target_len]
            
            global_start = offset_sample + meta["start_sample"]
            global_end = global_start + len(conv_wav)
            
            if global_end <= len(full_waveform):
                full_waveform[global_start:global_end] = conv_wav
                success_cnt += 1
            
    print(f"합성 완료: {success_cnt}/{len(clips_metadata)} 클립")
    sf.write(FINAL_AUDIO, full_waveform, full_sr)
    
    print("   - 최종 영상 렌더링 중...")
    run_ffmpeg(["-i", VIDEO_FILE, "-i", FINAL_AUDIO, "-c:v", "copy", "-c:a", "aac", "-map", "0:v:0", "-map", "1:a:0", FINAL_VIDEO])
    
    if os.path.exists(TEMP_AUDIO): os.remove(TEMP_AUDIO)
    if os.path.exists(FINAL_AUDIO): os.remove(FINAL_AUDIO)
    if os.path.exists(FULL_AUDIO_TEMP): os.remove(FULL_AUDIO_TEMP)
    
    print(f"\n=== 모든 작업 완료! ===")
    print(f"최종 결과물: {FINAL_VIDEO}")

if __name__ == "__main__":
    main()

 

 

 

-실행방법

C:\Antigravity\imsi> python auto_voice_changer.py

 

 

-참고

 

>pip install yt-dlp

 

 

전체 음성 추출:
cmdyt-dlp -x --audio-format wav "https://www.youtube.com/watch?v=mkpaXeA-TxA" -o "video_audio.wav"

특정 구간만 추출 (00:00:18 초부터):
cmdyt-dlp -x --audio-format wav --postprocessor-args "-ss 00:00:18" "https://www.youtube.com/watch?v=mkpaXeA-TxA" -o "video_audio_34min.wav"

전체 영상 다운로드 (나중에 합성용):
cmdyt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]" "https://www.youtube.com/watch?v=mkpaXeA-TxA" -o "original_video.mp4"


 

 

 

1. 설정 및 입력 (Configuration)
 
-파이썬 프로그램을 실행하면 아래 내용을 입력 받아 음성 모델만 있으면 어떤 영상도 사용할 수 있도록 범용적으로 만들었습니다.
 
 
 
 
 
아래 '사용자 설정'은 본인의 환경에 맞게 변경해 주시기 바랍니다.
 
 
 
 
위 작업 전 아래 사전 작업을 수행해야 합니다.
 
 
1.관련 모델 사용을 위한 동의 및 엑세스 요청
 
 
 
 
 
2.허깅페이스 API 토큰 발급
 
 
 


2. 오디오 추출 (Audio Extraction)
 
-영상에서 음성부분만 따로 추출합니다.
 
original_video.mp4 -> temp_analysis_audio.wav



3. 화자 분리 (Speaker Diarization)
 
-영상 속 인물이 2명이상인 경우 누구의 목소리를 내 목소리로 바꿀지를 결정해야 합니다. 이것을 구분하기 위한 분석 작업을 하게 되며, 아래와 같이 영상속 어느 구간이 누구인지를 판단하는 분석을 하게됩니다.
 
{
        "start": 0.03096875,
        "end": 16.230968750000002,
        "speaker": "SPEAKER_00"
    },
 
[diarization_result.json파일 일부 화면]
 


4. 화자 선택 (Speaker Selection)
 
-위의 3번에서 영상속 구간별 인물을 분석했다면, 여기서는 내 목소리로 바꾸고 싶은 인물을 지정하는 단계입니다.
예를 들어 영상에서 총 2명이 나온다면, SPEAKER_00(질문자), SPEAKER_01(톰리) 과 같이 구분되는데, SPEAKER_01을 지정하는 것과 같은 내용입니다.
 
내가 지정하고 싶은 인물이 영상 속 그 인물이 맞는지를 알아내기 위해서 speaker_samples 폴더에 해당 인물들의 짧은 음성을 생성하였으며, 이를 확인함으로써 내가 원하는 인물을 선택할 수 있습니다.
 
 
 
아래에서는 SPEAKER_01(Tom lee)을 선택했습니다.


5. 음성 추출 (Target Extraction)
 
-위 4번에서 선택한 인물인 SPEAKER_01이 영상속에서 말한 구간을 모두 쪼개서 임시폴더에 저장합니다.
 
아래에서는 총 30개의 파일이 temp_clips 폴더에 쪼개서 저장하였습니다.
 


6. RVC 변환 (Voice Conversion)
 
-임시폴더에 저장한 쪼개진 원본 음성파일을 지정한 모델을 통해 내 목소리로 변환 후 변환폴더에 저장합니다.
 
아래에서는 위 5번의 총 30개의 파일이 converted_clips 폴더에 쪼개서 변환 후 저장되었습니다.
 
 


7. 합성 및 최종 영상 생성 (Merge & Render)
 
-변환된 쪼개진 음성파일을 합치고, 이 합쳐진 음성파일을 다시 원본영상에 그대로 끼워 넣습니다.
 
 
최종적으로 아래와 같은 파일('final_result_video.mp4)이 생성됩니다.

 

 

파일을 실행하면 아래와 같이 원본영상과 동일하게 실행되나, 내가 선택한 인물의 목소리는 변환된 목소리로 영상이 나오게 됩니다.

 

 

 

-이해를 돕기 위해 최종 결과를 아래 올려 드리오니 참고하시기 바랍니다.

 

-변환 전 원본영상

original_video.mp4
17.07MB

 

 

-변환 후 영상

final_result_video.mp4
15.55MB

 

 

 

유튜브 영상 속 인물에 내 목소리 입히기 관련 글은 여기까지 입니다.

그 동안 긴 글 읽어 주셔서 감사합니다.

 

 

 

 

728x90
반응형
LIST