Qwen3VoiceClone phoneme bleed 해결 — x_vector_only_mode=True
개요: Qwen3-TTS Base 모델의 VoiceClone에서 reference 오디오의 끝음절이 생성 음성 앞부분에 묻어나는(bleed) 현상이 자주 발생한다. ICL(In-Context Learning) 모드의 구조적 결함으로,
x_vector_only_mode=True로 ICL을 끄면 깨끗하게 해결된다. ComfyUI-Qwen3-TTS 커스텀 노드는 기본적으로 이 옵션을 노출하지 않으므로 직접 패치해야 한다. 2026-05-20 예나샘 보이스 클로닝 작업에서 검증.
증상
ref_audio 의 마지막 음절이 합성 결과 앞부분에 묻어 나옴. 예시:
| ref_audio 끝 발화 | 합성 목표 시작 | 실제 결과 |
|---|---|---|
| ”…있어요" | "구글이 검색을…" | "요구글이…”, “여구글이…" |
| "…쓰고 있어” (트림) | “구글이 검색을…" | "쓰고 있어 구글이…” (음절 그대로 묻음) |
ref_audio 를 트림하든, 다른 시드를 쓰든, target 앞에 마침표/호흡어를 넣든 — ICL 모드를 쓰는 한 끝음 leakage 는 사라지지 않는다. 시드 변주는 빈도만 줄일 뿐.
원인 (Notion 가이드 + 코드 검증)
Qwen3-TTS Base 모델(Qwen3-TTS-12Hz-1.7B-Base 등)의 ICL 모드는 reference 오디오를 prompt/context 로 사용한다. 이때:
ref_audio가speech_tokenizer로 토큰화돼ref_code생성- ICL 모드는
ref_code+ref_text를 prompt 로 박아 모델이 그 뒤를 “이어서 말하는” 식으로 합성 - → reference 의 마지막 acoustic token(음소) 이 새 합성의 첫 토큰에 직접 영향을 줌
- → 끝음절 bleed 발생
이건 Qwen3 만의 문제가 아니라 CosyVoice 계열 모델 전반의 전형적 phoneme bleed / tail leakage 패턴.
해결책 (효과 순)
1. ★ x_vector_only_mode=True (정공)
ICL 자체를 끄고 speaker embedding 만 추출해서 클로닝.
ref_text불필요 (어차피 무시됨)- ref 음소가 prompt 로 안 들어가니까 leakage 거의 0
- timbre(음색) 재현은 비슷하거나 더 안정적
- prosody(억양·리듬) 재현력은 ICL 보다 약간 떨어질 수 있으나, 키노트 내레이션 같은 차분한 발화에선 차이 미세
라이브러리 시그니처 (qwen_tts/inference/qwen3_tts_model.py:476):
def generate_voice_clone(
self,
text, language=None,
ref_audio=None,
ref_text=None,
x_vector_only_mode: Union[bool, List[bool]] = False, # ← 이거
voice_clone_prompt=None,
...
):2. silence padding (완화책)
ref_audio 끝에 0.5초 무음을 붙이면 leakage 빈도가 줄어든다. 하지만 발화 내용에 따라 여전히 발생.
- ICL 모드 자체의 결함이라 padding 만으로 완전 해결 안 됨
- timbre 보존이 더 중요한 케이스에선 padding + ICL 조합
3. 생성 결과 앞부분 후처리 트림 (땜빵)
생성된 wav 의 앞 0.3~0.5초를 잘라내는 후처리. 결과물에 따라 자연스러운 시작이 잘리기도 함 → 비추천.
ComfyUI-Qwen3-TTS 노드 패치 (필수)
DarioFT/ComfyUI-Qwen3-TTS 커스텀 노드는 x_vector_only_mode 를 INPUT_TYPES 에 노출 안 함. 직접 패치 필요.
위치: C:\ComfyUI_windows_portable\ComfyUI\custom_nodes\ComfyUI-Qwen3-TTS\nodes.py (class Qwen3VoiceClone, 라인 ~696)
패치 3곳:
# 1) INPUT_TYPES optional 에 추가
"x_vector_only_mode": ("BOOLEAN", {"default": False}),
# 2) IS_CHANGED + generate() 시그니처에 x_vector_only_mode=False 추가
# 3) generate() 안에서 ref_audio 분기 조건과 호출 수정
elif ref_audio is not None and (x_vector_only_mode or (ref_text and ref_text.strip())):
...
wavs, sr = model.generate_voice_clone(
text=text,
language=lang,
ref_audio=audio_tuple,
ref_text=ref_text if not x_vector_only_mode else None,
x_vector_only_mode=bool(x_vector_only_mode),
max_new_tokens=max_new_tokens
)패치 후 ComfyUI 서버 재시작 필수 (custom_node 핫 리로드 안 됨).
워크플로우 (API form) 패턴
{
"1": {"class_type": "Qwen3Loader",
"inputs": {"repo_id": "Qwen/Qwen3-TTS-12Hz-1.7B-Base", ...}},
"2": {"class_type": "LoadAudio", "inputs": {"audio": "yena_ref_audio.wav"}},
"3": {"class_type": "Qwen3VoiceClone",
"inputs": {
"model": ["1", 0],
"ref_audio": ["2", 0],
"text": "합성할 한국어 텍스트",
"language": "Korean",
"seed": 12345,
"max_new_tokens": 2048,
"ref_audio_max_seconds": 30.0,
"x_vector_only_mode": True, # ← 핵심
# ref_text 는 입력 안 함 (어차피 무시됨)
}},
"4": {"class_type": "SaveAudio",
"inputs": {"audio": ["3", 0], "filename_prefix": "out"}},
}검증 사례 (2026-05-20, 예나샘 보이스 클로닝)
- 작업: Google I/O 2026 keynote 4분 27초 요약 영상 46컷 한국어 내레이션
- ref_audio:
yena_ref_audio.wav(LTX 컷1 v3 추출 5초, 끝 “…있어요”) - 시도 순서:
- ICL 모드 + 시드 변주 → 46컷 중 절반 가까이 “요/여 구글” leakage
- ref_audio 트림 (끝 0.6초 제거) → 다른 끝음 “쓰고 있어” 가 leak
- target prefix “음, ” / 마침표 추가 → 효과 미미
x_vector_only_mode=True→ leakage 완전 소거 (제이크 검증)
함정
x_vector_only_mode=True일 때 ref_text 를 같이 넘기면 라이브러리가 무시함. 패치 코드에선 명시적으로ref_text=None으로 비워 보냄.- 모델은 반드시 Base 계열 (
Qwen3-TTS-12Hz-1.7B-Base). VoiceDesign / CustomVoice 는 voice clone 자체 미지원. - 패치 후 서버 재시작 안 하면 ComfyUI 가 옛 INPUT_TYPES 로 캐시 —
/object_info/Qwen3VoiceClone로x_vector_only_mode키 보이는지 확인 후 사용. - prosody 보존이 결정적인 케이스(감정 연기, 호흡 디테일)면 ICL + silence padding 도 고려. 정보 전달 내레이션에선 xvec only 로 충분.
한 줄 요약
Qwen3VoiceClone 첫 음 묻음 = ICL 결함.
x_vector_only_mode=True가 정공. ComfyUI 노드는 패치 + 재시작.
