v1.14.1: Add silence gap between TTS segments & fix Qwen3 TTS stitching

- CosyVoice: replace overlapping crossfade with fade-out + 100ms silence
  gap + fade-in for easier downstream splitting
- Qwen3 TTS: fix bare torch.cat in multi-emotion segment stitching,
  apply same fade-out + silence + fade-in approach
This commit is contained in:
Hawk Lee
2026-02-17 16:13:08 +08:00
parent f95f2a9832
commit fb0c84bfae
3 changed files with 51 additions and 26 deletions
+30 -23
View File
@@ -586,41 +586,48 @@ class AIIA_CosyVoice_TTS:
return segments
@staticmethod
def _crossfade_segments(waveforms, sample_rate, crossfade_ms=50):
def _crossfade_segments(waveforms, sample_rate, crossfade_ms=50, silence_ms=100):
"""
Join waveform segments with a cosine crossfade to eliminate clicks/pops.
Each waveform is (1, N) or (C, N). crossfade_ms is the overlap duration.
Uses the same cosine fade approach as the Voice Conversion stitcher.
Join waveform segments with cosine fade-out / silence gap / fade-in.
Each waveform is (1, N) or (C, N).
- crossfade_ms: cosine fade duration applied to each segment's edge.
- silence_ms: silence gap inserted between segments for easy splitting.
"""
if not waveforms or len(waveforms) == 1:
return torch.cat(waveforms, dim=-1) if waveforms else torch.zeros(1, 0)
xfade_samples = int(sample_rate * crossfade_ms / 1000)
# Pre-compute fade curves (cosine)
t = torch.linspace(0, np.pi, xfade_samples, device=waveforms[0].device)
fade_out = 0.5 * (1.0 + torch.cos(t)) # 1 -> 0
fade_in = 1.0 - fade_out # 0 -> 1
silence_samples = int(sample_rate * silence_ms / 1000)
result = waveforms[0]
for i in range(1, len(waveforms)):
curr = waveforms[i]
actual_xfade = min(xfade_samples, result.shape[-1], curr.shape[-1])
if actual_xfade < 2:
# Too short for crossfade, just concatenate
result = torch.cat([result, curr], dim=-1)
continue
# Trim fade curves to actual overlap
fo = fade_out[:actual_xfade]
fi = fade_in[:actual_xfade]
# --- Fade out the tail of previous segment ---
fo_len = min(xfade_samples, result.shape[-1])
if fo_len >= 2:
t_fo = torch.linspace(0, np.pi, fo_len, device=result.device)
fade_out = 0.5 * (1.0 + torch.cos(t_fo)) # 1 -> 0
result = torch.cat([
result[..., :-fo_len],
result[..., -fo_len:] * fade_out,
], dim=-1)
overlap = result[..., -actual_xfade:] * fo + curr[..., :actual_xfade] * fi
result = torch.cat([
result[..., :-actual_xfade],
overlap,
curr[..., actual_xfade:],
], dim=-1)
# --- Insert silence gap ---
channels = result.shape[0] if result.dim() >= 2 else 1
silence = torch.zeros(channels, silence_samples, device=result.device)
# --- Fade in the head of next segment ---
fi_len = min(xfade_samples, curr.shape[-1])
if fi_len >= 2:
t_fi = torch.linspace(0, np.pi, fi_len, device=curr.device)
fade_in = 1.0 - 0.5 * (1.0 + torch.cos(t_fi)) # 0 -> 1
curr = torch.cat([
curr[..., :fi_len] * fade_in,
curr[..., fi_len:],
], dim=-1)
result = torch.cat([result, silence, curr], dim=-1)
return result
+20 -2
View File
@@ -401,8 +401,26 @@ class AIIA_Qwen_TTS:
sr_final = seg_result[0]["sample_rate"]
all_segment_wavs.append(seg_wav.squeeze(0)) # [C, T]
# Concatenate all segments
combined = torch.cat(all_segment_wavs, dim=-1) # [C, T_total]
# Stitch segments: fade-out + silence gap + fade-in (same as CosyVoice)
XFADE_MS, SILENCE_MS = 50, 100
xfade_samples = int(sr_final * XFADE_MS / 1000)
silence_samples = int(sr_final * SILENCE_MS / 1000)
combined = all_segment_wavs[0]
for _si in range(1, len(all_segment_wavs)):
_curr = all_segment_wavs[_si]
# Fade out tail
fo_len = min(xfade_samples, combined.shape[-1])
if fo_len >= 2:
t_fo = torch.linspace(0, np.pi, fo_len, device=combined.device)
combined = torch.cat([combined[..., :-fo_len], combined[..., -fo_len:] * (0.5 * (1.0 + torch.cos(t_fo)))], dim=-1)
# Silence gap
silence = torch.zeros(combined.shape[0], silence_samples, device=combined.device)
# Fade in head
fi_len = min(xfade_samples, _curr.shape[-1])
if fi_len >= 2:
t_fi = torch.linspace(0, np.pi, fi_len, device=_curr.device)
_curr = torch.cat([_curr[..., :fi_len] * (1.0 - 0.5 * (1.0 + torch.cos(t_fi))), _curr[..., fi_len:]], dim=-1)
combined = torch.cat([combined, silence, _curr], dim=-1)
return ({"waveform": combined.unsqueeze(0), "sample_rate": sr_final},)
# --- Single segment path (original logic) ---
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "aiia"
description = "Advanced AI Audio/Video toolkit for ComfyUI. Features Multi-Role Podcast/Dialogue Generation, High-Fidelity Voice Cloning (CosyVoice/VibeVoice), TTS, Media Management, and efficient Video tools."
version = "1.14.0"
version = "1.14.1"
license = {file = "LICENSE"}
readme = "README.md"
authors = [