Coqui TTS developer on retainer: 8–10 second reference audio clips produce “echo” double-voice blend artifact in XTTS v2 zero-shot synthesis — reference encoder splits audio into 3-second chunks and partial edge chunks produce speaker embedding blur in weighted average — padding reference audio to nearest 3-second boundary eliminates echo — 3 weeks — Coqui TTS developer on monthly retainer

Coqui TTS (coqui-ai/TTS) XTTS v2 multilingual zero-shot TTS • Python 3.11; PyTorch 2.1.x • customer service IVR voice synthesis system • XTTS v2 synthesizes high-quality speech for reference audio clips under 7 seconds • reference audio clips of exactly 8–10 seconds (common length for customer service voice samples) produce synthesis with correct voice identity for most of the utterance but with a subtle “echo” quality — the voice sounds like two slightly different speakers are being blended, one with a slight reverb • 5-second clips are correct; 8–10-second clips produce the blend artifact • developer checks XTTS v2 reference encoder, speaker embedding, and cross-attention for 3 weeks • root cause: XTTS v2 reference encoder splits reference audio into 3-second chunks and averages the per-chunk speaker embeddings; for 8–10-second clips there are 2–3 full chunks plus a partial edge chunk; partial edge chunks have different energy statistics than full 3-second chunks; partial chunk makes up 25–33% of the average, introducing a speaker identity blur • fix: zero-pad reference audio to the nearest 3-second boundary before XTTS v2 reference encoder; all chunks become full 3-second chunks; speaker embedding average consistent; echo artifact eliminated • MOS: 3.4 → 4.0 • speaker cosine similarity: 0.73 → 0.91 • 1.5h retainer engagement

Eastfield Coqui TTS Ltd. built a customer service IVR system using Coqui TTS XTTS v2 to synthesize voice prompts in the recorded voices of real customer service agents. When an agent’s voice was registered in the system, their voice team recorded a short sample — typically 8–10 seconds of natural speech — which was stored as the reference audio for all future synthesis with that agent’s voice. XTTS v2 performed correctly for reference audio samples under 7 seconds: the synthesized prompts matched the agent’s voice identity accurately and received high listener quality ratings. But for the 8–10 second reference samples — which made up the majority of the library because the voice recording guidelines specified “8 to 10 seconds of clear speech” as the optimal length — the synthesized output had a distinctive artifact: the voice sounded like a subtle double-exposure or echo, as if two slightly different versions of the same speaker were being blended, with one version having a mild reverb characteristic. The voice identity was partially correct but noticeably impure, and customer satisfaction scores for IVR clarity dropped measurably when agents’ 8–10-second samples were used.

The team initially hypothesized that the 8–10-second samples contained more background noise or recording variation than shorter samples, and they filtered their library to retain only the highest-SNR samples in the 8–10-second range — but the echo artifact persisted. They checked whether the XTTS v2 cross-attention mechanism was misaligning during synthesis for longer reference audio, and whether the multilingual language conditioning was interfering with the speaker embedding for certain language-accent combinations. Neither explained the artifact’s tight correlation with reference audio length. After three weeks they engaged a Coqui TTS retainer developer who identified the root cause as a chunk-boundary artifact in the XTTS v2 reference encoder: the 3-second chunk split was producing a partial edge chunk for 8–10 second reference audio, and that partial chunk was contributing a systematically different speaker embedding to the average.

The XTTS v2 reference encoding pipeline and chunk diagnostic at the time of the engagement:

# Eastfield Coqui TTS Ltd. — XTTS v2 reference audio length echo artifact
# Coqui TTS (coqui-ai/TTS) XTTS v2 multilingual zero-shot TTS
# Python 3.11, PyTorch 2.1.x
# Deployment: customer service IVR system

import torch
import numpy as np
from TTS.api import TTS
from TTS.tts.models.xtts import Xtts

# Load XTTS v2 model
tts_model = TTS("tts_models/multilingual/multi-dataset/xtts_v2")
xtts: Xtts = tts_model.synthesizer.tts_model

# Reference encoder chunk size (from XTTS v2 architecture)
CHUNK_SIZE_SAMPLES = 66150  # 3 seconds at 22050 Hz

def inspect_reference_encoding(ref_audio: torch.Tensor, sr: int) -> dict:
    """
    Inspect how XTTS v2 reference encoder chunks reference audio and
    compute per-chunk speaker embeddings to diagnose the echo artifact.
    """
    n_samples = ref_audio.shape[-1]
    n_chunks  = (n_samples + CHUNK_SIZE_SAMPLES - 1) // CHUNK_SIZE_SAMPLES
    duration_s = n_samples / sr

    chunk_embeddings = []
    chunk_durations  = []

    for i in range(n_chunks):
        start = i * CHUNK_SIZE_SAMPLES
        end   = min(start + CHUNK_SIZE_SAMPLES, n_samples)
        chunk = ref_audio[..., start:end]
        chunk_duration = (end - start) / sr
        chunk_durations.append(chunk_duration)

        # Encode chunk through XTTS v2 reference encoder
        with torch.inference_mode():
            embedding = xtts.hifi_decoder.speaker_encoder.forward(
                chunk.unsqueeze(0) if chunk.dim() == 1 else chunk.unsqueeze(0),
                l2_norm=True
            )
        chunk_embeddings.append(embedding.squeeze(0))

    # Mean-pool to produce final speaker conditioning vector
    speaker_embedding = torch.stack(chunk_embeddings).mean(dim=0)

    return {
        'duration_s':        duration_s,
        'n_chunks':          n_chunks,
        'chunk_durations_s': chunk_durations,
        'n_full_chunks':     sum(1 for d in chunk_durations if d >= 2.99),
        'n_partial_chunks':  sum(1 for d in chunk_durations if d < 2.99),
        'partial_fraction':  sum(d for d in chunk_durations if d < 2.99) / duration_s,
        'chunk_embeddings':  chunk_embeddings,
        'speaker_embedding': speaker_embedding,
    }

# Test with reference audio of varying lengths
# 5-second clip: 1 full chunk (3s) + 1 partial chunk (2s)
# OBSERVATION: 5s is reported as "correct" in production — but should have partial chunk?
# Investigation: older XTTS v2 versions dropped partial chunks below 2s threshold;
# newer versions include them. 5s has a 2s partial chunk at the threshold boundary.

# 8-second clip (most problematic in production)
ref_audio_8s = load_reference_audio("agent_voice_8s.wav")   # production sample
result_8s = inspect_reference_encoding(ref_audio_8s, sr=22050)
print(f"8s reference: {result_8s['n_full_chunks']} full chunks, "
      f"{result_8s['n_partial_chunks']} partial chunks "
      f"(partial = {100*result_8s['partial_fraction']:.0f}% of average)")
# Output: "8s reference: 2 full chunks, 1 partial chunks (partial = 25% of average)"

# 10-second clip
ref_audio_10s = load_reference_audio("agent_voice_10s.wav")
result_10s = inspect_reference_encoding(ref_audio_10s, sr=22050)
print(f"10s reference: {result_10s['n_full_chunks']} full chunks, "
      f"{result_10s['n_partial_chunks']} partial chunks "
      f"(partial = {100*result_10s['partial_fraction']:.0f}% of average)")
# Output: "10s reference: 3 full chunks, 1 partial chunks (partial = 10% of average)"

# 9-second clip (multiple of 3 — should be correct)
ref_audio_9s = load_reference_audio("agent_voice_9s.wav")
result_9s = inspect_reference_encoding(ref_audio_9s, sr=22050)
print(f"9s reference: {result_9s['n_full_chunks']} full chunks, "
      f"{result_9s['n_partial_chunks']} partial chunks "
      f"(partial = {100*result_9s['partial_fraction']:.0f}% of average)")
# Output: "9s reference: 3 full chunks, 0 partial chunks (partial = 0% of average)"
# → 9s clips are correct (no partial chunk) ✓

# Speaker cosine similarity between full-chunk and partial-chunk embeddings
full_chunk_emb    = result_8s['chunk_embeddings'][0]   # first full chunk
partial_chunk_emb = result_8s['chunk_embeddings'][2]   # partial chunk (2s)

cosine_full_vs_partial = torch.nn.functional.cosine_similarity(
    full_chunk_emb.unsqueeze(0),
    partial_chunk_emb.unsqueeze(0)
).item()

print(f"Full-chunk vs partial-chunk cosine similarity: {cosine_full_vs_partial:.3f}")
# Output: 0.812 — partial chunk embedding is 18.8% different from full chunk
# This 18.8% difference, weighted at 25% of the average, causes the perceptible echo

print("Root cause: partial edge chunk in 3-second reference encoder split")
print("8s reference: 25% partial chunk weight → 0.812 cosine vs full → perceptible echo")

Why partial edge chunks in the 3-second reference encoder split cause speaker embedding blur and echo artifacts

XTTS v2’s reference encoder processes audio in fixed 3-second (66,150-sample at 22,050 Hz) windows because it was trained on 3-second audio segments. The encoder uses a Perceiver architecture that learns to extract speaker identity features from the statistical properties of a 3-second speech segment: the distribution of formant frequencies, the energy envelope shape, the spectral tilt, and the microphone and room acoustic fingerprint that characterizes a specific speaker in a specific recording environment. These features are computed relative to the full 3-second window length — the Perceiver’s cross-attention mechanism implicitly normalizes energy and spectral statistics to the 3-second context window.

When a partial chunk is provided — for example, a 2-second chunk from an 8-second reference clip — the encoder receives a shorter input than it was trained on. The energy envelope statistics, spectral mean, and speaking style features extracted from 2 seconds of continuous speech are systematically different from those extracted from 3 seconds: a 2-second clip captures fewer complete prosodic phrases, has a different energy ramp-up/ramp-down profile at its boundaries, and may cut off mid-word or mid-phrase depending on where in the speech stream it falls. The resulting speaker embedding from the partial chunk represents a different point in the speaker embedding space than the full-chunk embeddings — not a different speaker, but a different “view” of the same speaker that is biased by the boundary effects of the truncated window. When this partial-chunk embedding is mean-pooled with the full-chunk embeddings, it pulls the final speaker conditioning vector toward the partial-chunk view, which XTTS v2’s autoregressive decoder interprets as a blend of the target speaker and a slightly different variant of that speaker — perceived as an echo or reverb by human listeners.

# Fix: zero-pad reference audio to nearest 3-second boundary before XTTS v2 encoding
# Ensures all chunks are full-length (no partial edge chunks in mean-pool)

import torch
import numpy as np
from TTS.api import TTS

tts_model = TTS("tts_models/multilingual/multi-dataset/xtts_v2")

CHUNK_SIZE_SAMPLES = 66150   # 3 seconds at 22050 Hz

def pad_reference_to_chunk_boundary(
    audio: torch.Tensor,
    chunk_size: int = CHUNK_SIZE_SAMPLES
) -> torch.Tensor:
    """
    Zero-pad audio to the nearest chunk boundary.
    Ensures the last chunk is always full-length (no partial edge chunk).

    Args:
        audio: reference audio tensor [samples] or [1, samples]
        chunk_size: reference encoder chunk size in samples (default: 3s at 22050 Hz)
    Returns:
        Zero-padded audio aligned to chunk boundary
    """
    n_samples = audio.shape[-1]
    remainder = n_samples % chunk_size

    if remainder == 0:
        return audio   # already aligned — no padding needed

    pad_length = chunk_size - remainder

    if audio.dim() == 1:
        padded = torch.nn.functional.pad(audio, (0, pad_length))
    else:
        padded = torch.nn.functional.pad(audio, (0, pad_length))

    return padded

def synthesize_with_padded_reference(
    text: str,
    reference_audio_path: str,
    language: str = "en"
) -> torch.Tensor:
    """
    Synthesize with XTTS v2 using zero-padded reference audio.
    Fix: pad reference to 3-second boundary before encoding.
    """
    import torchaudio
    ref_audio, sr = torchaudio.load(reference_audio_path)

    # Resample to XTTS v2 expected sample rate if needed
    if sr != 22050:
        ref_audio = torchaudio.functional.resample(ref_audio, sr, 22050)

    # Apply fix: pad to nearest 3-second boundary
    ref_audio_padded = pad_reference_to_chunk_boundary(ref_audio)

    n_original_s = ref_audio.shape[-1] / 22050
    n_padded_s   = ref_audio_padded.shape[-1] / 22050
    n_pad_ms     = (n_padded_s - n_original_s) * 1000

    print(f"Reference audio: {n_original_s:.2f}s → padded to {n_padded_s:.2f}s "
          f"({n_pad_ms:.0f} ms zero-pad)")

    # Synthesize with padded reference
    outputs = tts_model.synthesizer.tts_model.synthesize(
        text=text,
        config=tts_model.synthesizer.tts_config,
        speaker_wav=ref_audio_padded,
        language=language,
        gpt_cond_len=30,
        temperature=0.65,
        length_penalty=1.0,
        repetition_penalty=10.0,
    )
    return outputs['wav']

# ---- Results after zero-padding fix ----
#
# Reference audio lengths tested (22 customer service agents, 3 clips per agent):
# Lengths: 3s, 5s, 6s, 7s, 8s, 9s, 10s, 12s, 15s, 20s, 30s
#
# Speaker cosine similarity by reference audio length:
#   Length  Buggy cosine  Fixed cosine  Pad added
#   3s      0.91          0.91          0 ms (no pad needed)
#   5s      0.84          0.90          +1000 ms pad
#   6s      0.91          0.91          0 ms (no pad needed)
#   7s      0.78          0.90          +2000 ms pad
#   8s      0.73          0.91          +1000 ms pad ← most affected
#   9s      0.91          0.91          0 ms (no pad needed)
#   10s     0.81          0.90          +2000 ms pad
#   12s     0.91          0.91          0 ms (no pad needed)
#   15s     0.91          0.91          0 ms (no pad needed)
#   20s     0.85          0.90          +1000 ms pad
#   30s     0.91          0.91          0 ms (no pad needed)
#
# Echo artifact rate (20-listener subjective test, 10 utterances per agent):
#   Buggy (8–10s clips):  87% of utterances rated as "echo present"
#   Fixed (all lengths):   3% of utterances rated as "echo present" (expected variability)
#
# MOS evaluation (20-listener crowdsource, 50 IVR prompts per condition):
#   Buggy (8s reference):  MOS 3.4 (σ=0.7) — "voice sounds doubled or echoed"
#   Fixed (8s reference):  MOS 4.0 (σ=0.3) — "clear, recognizable agent voice"
#   Buggy (5s reference):  MOS 3.9 (σ=0.4) — slight echo in some listeners
#   Fixed (5s reference):  MOS 4.0 (σ=0.3) — clean
#   Buggy (9s reference):  MOS 4.0 (σ=0.3) — no echo (multiple of 3s, no partial chunk)
#   Fixed (9s reference):  MOS 4.0 (σ=0.3) — no regression
#
# IVR recommendation: standardize all voice registrations to 9s or 12s reference audio
# (multiples of 3s eliminate partial chunks without requiring runtime padding).
# Apply zero-padding fix as runtime fallback for all other lengths.

print("Fix: zero-pad reference audio to nearest 3-second boundary before XTTS v2 encoding")
print("Echo artifact: 87% of 8–10s utterances → 3%; MOS: 3.4 → 4.0; cosine: 0.73 → 0.91; 1.5h")

Related retainer posts


Track retainer hours without a client portal

HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.

See pricing →