import os import shutil import torch import tempfile import numpy as np import folder_paths from .tools.seeder import TorchSeedContext from .tools.logger import get_logger from .ChatTTS.core import Chat from .ChatTTS.utils import download_all_assets logger = get_logger("ChatTTS") def clean_corrupted_cache(): """Clear corrupted HuggingFace cache""" try: cache_path = os.path.expanduser( "~/.cache/huggingface/hub/models--2Noise--ChatTTS" ) if os.path.exists(cache_path): logger.info(f"Cleaning corrupted cache: {cache_path}") shutil.rmtree(cache_path, ignore_errors=True) return True except Exception as e: logger.error(f"Failed to clean cache: {e}") return False class ChatTTSLoader: @classmethod def INPUT_TYPES(cls): # No inputs are needed as the path is determined automatically. return {"required": {}} RETURN_TYPES = ("MODEL",) FUNCTION = "load" CATEGORY = "chattts" def load(self): # Get model directory path chattts_dir = os.path.join(folder_paths.models_dir, "chattts") os.makedirs(chattts_dir, exist_ok=True) # Ensure the directory exists model_path = None files = [ f for f in os.listdir(chattts_dir) if f.endswith(('.pth', '.pt', '.ckpt', '.safetensors')) ] if files: # If there are model files in the directory, use the first one model_path = os.path.join(chattts_dir, files[0]) logger.info(f"Using local model: {model_path}") source = "custom" # Treat local files as custom source else: # No local model found, use ChatTTS download mechanism logger.info( "No local model found in models/chattts, attempting download..." ) source = "huggingface" # Default to huggingface download model_path = chattts_dir # Pass the directory for download try: # Attempt download using ChatTTS built-in downloader first # Note: download_all_assets might not be the intended function here, # as Chat.load handles download internally based on source. # We rely on Chat.load('huggingface') to handle the download. logger.info(f"Attempting download to: {chattts_dir}") # The actual download happens within chat.load if source is 'huggingface' except Exception as e: logger.error(f"Initial download check failed: {e}") raise RuntimeError( "Failed to prepare for download. Check permissions or network." ) # Load model using the determined source and path chat = Chat(logger) logger.info(f"Loading model using source='{source}' and path='{model_path}'") # Use custom_path for both local files and download directory loaded = chat.load(source=source, custom_path=model_path) if not loaded: # Check if download failed or local file loading failed if source == "huggingface": logger.error( f"Model download/load failed from HuggingFace to {model_path}" ) # Clean potentially corrupted cache if download failed clean_corrupted_cache() raise RuntimeError( f"Download/load failed. Please check network or manually place model files in {chattts_dir}" ) else: # source == "custom" logger.error(f"Failed to load local model file: {model_path}") raise RuntimeError(f"Failed to load ChatTTS model from {model_path}") logger.info( f"ChatTTS model loaded successfully from: {model_path if source == 'custom' else chattts_dir}" ) return (chat,) class ChatTTS_SeedBasedSpeaker: @classmethod def INPUT_TYPES(cls): return { "required": { "model": ("MODEL",), "seed": ("INT", {"default": 42, "min": 0, "max": 2147483647}), } } RETURN_TYPES = ("DICT",) FUNCTION = "generate" CATEGORY = "chattts" def generate(self, model, seed): try: # Generate speaker embedding using a fixed seed with TorchSeedContext(seed): spk_emb = model.sample_random_speaker() # Debug info: print the first 50 characters and length of the generated speaker embedding logger.info(f"SeedBasedSpeaker - spk_emb prefix: {spk_emb[:50]}...") logger.info(f"SeedBasedSpeaker - spk_emb length: {len(spk_emb)}") # Attempt decoding for verification try: decoded = model.speaker._decode(spk_emb) logger.info( f"SeedBasedSpeaker - decoded shape: {decoded.shape}, type: {type(decoded)}" ) except Exception as decode_err: logger.error( f"SeedBasedSpeaker - decode verification failed: {decode_err}" ) return ({"spk_emb": spk_emb, "seed": seed, "source": "seed_based"},) except Exception as e: logger.error(f"Error generating seed-based speaker: {e}") return ({"error": str(e)},) class ChatTTS_Sampler: @classmethod def INPUT_TYPES(cls): return { "required": { "model": ("MODEL",), "text": ("STRING", {"multiline": True, "dynamicPrompts": False}), }, "optional": { "speaker_params": ("DICT", {}), "seed": ("INT", {"default": 2, "min": 1, "max": 4294967295}), "temperature": ( "FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.01}, ), "top_P": ( "FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}, ), "top_K": ("INT", {"default": 20, "min": 1, "max": 100}), "split_batch": ("INT", {"default": 0, "min": 0, "max": 32}), }, } RETURN_TYPES = ("AUDIO", "DICT") FUNCTION = "synthesize" CATEGORY = "chattts" def synthesize( self, model, text, speaker_params=None, seed=2, temperature=0.3, top_P=0.7, top_K=20, split_batch=0, ): # Get speaker embedding (if available) spk_emb = speaker_params.get("spk_emb", "") if speaker_params else "" spk_seed = speaker_params.get("seed", seed) if speaker_params else seed source = speaker_params.get("source", "unknown") if speaker_params else "none" logger.info(f"ChatTTS_Sampler - received speaker from source: {source}") logger.info(f"ChatTTS_Sampler - using seed: {spk_seed}") try: # First method: Manually modify ChatTTS internal state to ensure seed consistency # This is a direct intervention into the internal mechanism of ChatTTS with TorchSeedContext(spk_seed): # First, fix the random state import random random_state = random.getstate() torch_state = torch.get_rng_state() # 1. If embedding exists and is decodable, use it if spk_emb: try: test_decode = model.speaker._decode(spk_emb) logger.info( f"Using the provided spk_emb, shape: {test_decode.shape}" ) except Exception as decode_error: logger.error(f"spk_emb decoding failed: {decode_error}") spk_emb = model.sample_random_speaker() logger.info(f"Switched to using randomly generated spk_emb") # 2. Otherwise, generate a new embedding else: spk_emb = model.sample_random_speaker() logger.info(f"Using spk_emb generated with seed={spk_seed}") # Build parameters - Key: use the same seed as used for spk_emb generation params_infer_code = Chat.InferCodeParams( spk_emb=spk_emb, temperature=temperature, top_P=top_P, top_K=top_K, manual_seed=spk_seed, # Actively set the seed, same as the seed used previously for spk_emb generation ) # Restore random state to ensure the following infer uses a clean state random.setstate(random_state) torch.set_rng_state(torch_state) # Generate audio logger.info(f"Starting inference: split_batch={split_batch}") wav = model.infer( text, skip_refine_text=True, params_infer_code=params_infer_code, stream=False, split_text=split_batch > 0, max_split_batch=split_batch, ) logger.info( f"Inference completed: wav type={type(wav)}, length={len(wav) if wav is not None else 'None'}" ) # Defensive check - ensure wav is not None and has at least one element if wav is None or len(wav) == 0: logger.error("Model returned empty wav or None") # Create default empty audio - note the format! empty_audio = { "waveform": torch.zeros( (1, 1, 100) ), # [batch=1, channels=1, samples=100] "sample_rate": 24000, } meta = { "seed": seed, "spk_emb": spk_emb, "error": "Model returned empty audio", } return (empty_audio, meta) # Ensure wav[0] exists and has the correct format if not isinstance(wav[0], (list, tuple, np.ndarray)): logger.error(f"wav[0] has incorrect type: {type(wav[0])}") empty_audio = { "waveform": torch.zeros((1, 1, 100)), # Consistent 3D tensor format "sample_rate": 24000, } meta = { "seed": seed, "spk_emb": spk_emb, "error": f"Audio data format error: {type(wav[0])}", } return (empty_audio, meta) # Process return value - ensure the tensor has the correct format tensor = torch.tensor(wav[0], dtype=torch.float32) logger.info( f"Audio tensor: shape={tensor.shape}, dimensions={tensor.dim()}" ) # Ensure audio data format meets torchaudio.save requirements [batch_size, channels, samples] if tensor.dim() == 1: # 1D tensor [samples] -> 3D tensor [1, 1, samples] tensor = tensor.reshape(1, 1, -1).contiguous() logger.info( f"Adjusted 1D tensor to 3D [1, 1, samples], new shape={tensor.shape}" ) elif tensor.dim() == 2: # 2D tensor [channels, samples] -> 3D tensor [1, channels, samples] tensor = tensor.unsqueeze(0).contiguous() logger.info( f"Adjusted 2D tensor to 3D [1, channels, samples], new shape={tensor.shape}" ) else: # Ensure high-dimensional tensor format is correct tensor = tensor.view(1, 1, -1).contiguous() logger.info( f"Adjusted high-dimension tensor to 3D, new shape={tensor.shape}" ) # Verify tensor integrity if not tensor.is_contiguous(): logger.warning("Tensor not contiguous, forcing contiguous") tensor = tensor.contiguous() logger.info( f"Final audio tensor: shape={tensor.shape}, dtype={tensor.dtype}, is_contiguous={tensor.is_contiguous()}" ) audio = { "waveform": tensor.detach().clone(), # Create a deep copy "sample_rate": 24000, } # Ensure the returned data is correct meta = {"seed": seed, "spk_emb": spk_emb} logger.info(f"Successfully generated audio: shape={tensor.shape}") return (audio, meta) except Exception as e: logger.error(f"Error during audio synthesis: {e}", exc_info=True) # Return empty but correctly formatted audio data - key fix here! empty_audio = { "waveform": torch.zeros( (1, 1, 100) ), # Empty audio in 3D format [batch, channels, samples] "sample_rate": 24000, } meta = {"seed": seed, "spk_emb": spk_emb, "error": str(e)} return (empty_audio, meta)