ㅁ 개요
O 프로그램 소개
- 직접 내 목소리 음성 특징을 학습시켜 내 목소리의 모델을 만드는 방법에 대해 알아 보겠습니다. ([음성변환]2.내 목소리의 모델 만들기(직접 모델 학습하기))
이번 시간에는 이전 시간에 학습된 내 모델을 이용하여 유튜브 영상 속 원하는 인물이 내 목소리와 똑같이 나오도록 만드는 방법에 대해 알아 보겠습니다.
(다만, 이전 시간에 만든 저의 모델의 경우 제 PC의 GPU성능이 좋지 못하여 정확하고, 클린한 음성으로 만들기에는 조금 부족한 모델로 판단하여 일단 허깅페이스에서 제공하는 깨끗한 음성 모델인 Trump_RVC를 사용하여 글을 작성하였으니 참고하시기 바랍니다.)
O 진행 순서
진행 순서는 크게 아래와 같이 진행합니다.
1. 설정 및 입력 (Configuration)
>모델 선택: 사용할 RVC 모델(pth)과 인덱스(index) 파일을 지정합니다.
>시간 설정: 변환할 구간의 시작 시간과 종료 시간을 설정합니다. (예: 00:00:00 ~ 00:40:00)
>핵심: 전체 영상을 다 분석하지 않고 필요한 구간만 핀포인트로 작업하여 속도를 획기적으로 높입니다.
2. 오디오 추출 (Audio Extraction)
>작업: 설정한 시간 구간의 오디오만 잘라내어 임시 파일(temp_analysis_audio.wav)로 저장합니다.
3. 화자 분리 (Speaker Diarization)
>캐싱: 분석 결과는 diarization_result.json에 저장되어, 다음에 같은 구간을 작업할 때 분석 과정을 건너뛸 수 있습니다.
4. 화자 선택 (Speaker Selection)

5. 음성 추출 (Target Extraction)
6. RVC 변환 (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)




2. 오디오 추출 (Audio Extraction)

3. 화자 분리 (Speaker Diarization)
4. 화자 선택 (Speaker Selection)


5. 음성 추출 (Target Extraction)

6. RVC 변환 (Voice Conversion)

7. 합성 및 최종 영상 생성 (Merge & Render)

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

-이해를 돕기 위해 최종 결과를 아래 올려 드리오니 참고하시기 바랍니다.
-변환 전 원본영상
-변환 후 영상
유튜브 영상 속 인물에 내 목소리 입히기 관련 글은 여기까지 입니다.
그 동안 긴 글 읽어 주셔서 감사합니다.
'퀵포스팅 > 기타' 카테고리의 다른 글
| 무료 AI API사용하기(무료 ai모델 라우터 frouter 사용하기) (0) | 2026.02.28 |
|---|---|
| 무료로 노트북lm 또는 제미나이로 ppt만들기 (0) | 2026.02.14 |
| [음성변환]2.내 목소리의 모델 만들기(직접 모델 학습하기) (1) | 2025.12.13 |
| [음성변환]1.10분(?)만에 내 목소리를 트럼프 목소리로 변환하기 (0) | 2025.12.07 |
| 구글 노트북LM 새기능 - 플래시카드, 퀴즈 (1) | 2025.09.10 |