Download train.py from ViuAI/ViuAI_TTS_200M: direct link, hf CLI and curl.
- Browser
- Download file 45.9 kB
-
https://huggingface.co/ViuAI/ViuAI_TTS_200M/resolve/main/train.py
- Command line
-
hf download hf://ViuAI/ViuAI_TTS_200M/train.py
-
curl -L -o train.py https://huggingface.co/ViuAI/ViuAI_TTS_200M/resolve/main/train.py
45.9 kB
| """ | |
| Training Script for ViuAI_TTS_200M (SOTA Speech Generation & Zero-Shot Voice Cloning). | |
| Optimized for Full Continuous Training on Any GPU (T4, 3090, 4090, 5090, A100, H100/H200). | |
| Architectural Features: | |
| 1. In-Context Dynamic Same-Speaker Reference Conditioning (Zero-Shot Voice Cloning) | |
| 2. Decoupled High-Fidelity Studio Vocoder (100% GPU compute on 175M DiT Flow Matching) | |
| 3. Energy-Aware Realistic Duration Modeling (Dynamic vowel/consonant pacing & micro-pauses) | |
| 4. Linear Warmup (1,000 steps) + Cosine Annealing Learning Rate | |
| 5. Exponential Moving Average (EMA, decay 0.999) for smooth, artifact-free speech | |
| 6. Automated Golden Audio Benchmark Suite at Milestones (Hindi/English, Male/Female) | |
| """ | |
| import sys | |
| import os | |
| import time | |
| import math | |
| import csv | |
| import wave | |
| import random | |
| import argparse | |
| import glob | |
| from collections import Counter, defaultdict | |
| from typing import Optional, List, Dict, Tuple | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import torch.distributed as dist | |
| import torch.multiprocessing as mp | |
| # CUDA Memory Optimization: Prevent fragmentation with expandable_segments | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| # Docker/Rental Cloud SHM Guard: Use file_system to prevent /dev/shm shared memory exhaustion | |
| try: | |
| mp.set_sharing_strategy("file_system") | |
| except Exception: | |
| pass | |
| # Hardware Performance Accelerators: cuDNN autotuner and Tensor Core precision | |
| if torch.cuda.is_available(): | |
| torch.backends.cudnn.benchmark = True | |
| try: | |
| torch.set_float32_matmul_precision("high") | |
| except Exception: | |
| pass | |
| from torch.nn.parallel import DistributedDataParallel as DDP | |
| from torch.utils.data import DataLoader, Dataset | |
| from torch.utils.data.distributed import DistributedSampler | |
| # Setup encoding on Windows | |
| if sys.platform == "win32": | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| except Exception: | |
| pass | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from models import ViuAITTS200M | |
| from dataset.tokenizer import ViuAITokenizer | |
| from dataset.audio_processing import ( | |
| extract_mel_from_file, | |
| load_audio_wav, | |
| get_default_extractor, | |
| extract_pitch_and_energy, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Any-GPU auto configuration | |
| # --------------------------------------------------------------------------- | |
| GPU_PROFILES: List[Tuple[str, Dict]] = [ | |
| ("H200", {"batch_size": 32, "num_workers": 2, "precision": "bf16"}), | |
| ("H100", {"batch_size": 24, "num_workers": 2, "precision": "bf16"}), | |
| ("A100", {"batch_size": 24, "num_workers": 2, "precision": "bf16"}), | |
| ("5090", {"batch_size": 32, "num_workers": 2, "precision": "bf16"}), | |
| ("4090", {"batch_size": 12, "num_workers": 2, "precision": "bf16"}), | |
| ("3090", {"batch_size": 10, "num_workers": 2, "precision": "bf16"}), | |
| ("3080", {"batch_size": 8, "num_workers": 2, "precision": "bf16"}), | |
| ("A10", {"batch_size": 12, "num_workers": 2, "precision": "bf16"}), | |
| ("L4", {"batch_size": 12, "num_workers": 2, "precision": "bf16"}), | |
| ("A6000", {"batch_size": 16, "num_workers": 2, "precision": "bf16"}), | |
| ("T4", {"batch_size": 8, "num_workers": 2, "precision": "fp16"}), | |
| ("V100", {"batch_size": 8, "num_workers": 2, "precision": "fp16"}), | |
| ] | |
| DEFAULT_PROFILE = {"batch_size": 8, "num_workers": 2, "precision": "fp16"} | |
| def get_gpu_profile() -> Tuple[str, Dict]: | |
| """Returns (device_label, profile). Works on CPU too (no CUDA).""" | |
| if not torch.cuda.is_available(): | |
| return "cpu", {"batch_size": 2, "num_workers": 0, "precision": "fp32"} | |
| name = torch.cuda.get_device_name(0) | |
| for key, prof in GPU_PROFILES: | |
| if key.lower() in name.lower(): | |
| return name, dict(prof) | |
| try: | |
| major, _ = torch.cuda.get_device_capability(0) | |
| prec = "bf16" if major >= 8 else "fp16" | |
| except Exception: | |
| prec = "fp16" | |
| prof = dict(DEFAULT_PROFILE) | |
| prof["precision"] = prec | |
| return name, prof | |
| def setup_distributed() -> Tuple[bool, int, int, torch.device]: | |
| """Init torchrun/DDP if WORLD_SIZE>1. Returns (is_ddp, rank, world, device).""" | |
| world = int(os.environ.get("WORLD_SIZE", "1")) | |
| if world < 2 or not dist.is_available(): | |
| dev = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| return False, 0, 1, dev | |
| rank = int(os.environ.get("RANK", "0")) | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| backend = "nccl" if (torch.cuda.is_available() and sys.platform != "win32") else "gloo" | |
| dist.init_process_group(backend=backend) | |
| if torch.cuda.is_available(): | |
| torch.cuda.set_device(local_rank) | |
| dev = torch.device("cuda", local_rank) | |
| else: | |
| dev = torch.device("cpu") | |
| return True, rank, world, dev | |
| def resolve_precision(requested: str, profile_prec: str, device: torch.device) -> Tuple[str, torch.dtype, bool]: | |
| """Returns (name, amp_dtype, use_scaler).""" | |
| name = requested.lower() | |
| if name == "auto": | |
| name = profile_prec if device.type == "cuda" else "fp32" | |
| if device.type == "cpu": | |
| return "fp32", torch.float32, False | |
| if name == "bf16": | |
| try: | |
| major, _ = torch.cuda.get_device_capability(device) | |
| if major < 8: | |
| name = "fp16" | |
| except Exception: | |
| pass | |
| if name == "bf16": | |
| return "bf16", torch.bfloat16, False | |
| if name == "fp32": | |
| return "fp32", torch.float32, False | |
| return "fp16", torch.float16, True | |
| # --------------------------------------------------------------------------- | |
| # Exponential Moving Average (EMA) & Warmup Learning Rate Scheduler | |
| # --------------------------------------------------------------------------- | |
| class EMAModel: | |
| """Maintains Exponential Moving Average (EMA) of model weights for smooth, artifact-free speech.""" | |
| def __init__(self, model: nn.Module, decay: float = 0.999): | |
| self.decay = decay | |
| self.shadow: Dict[str, torch.Tensor] = {} | |
| for name, param in model.named_parameters(): | |
| if param.requires_grad: | |
| self.shadow[name] = param.data.clone().detach() | |
| def update(self, model: nn.Module): | |
| for name, param in model.named_parameters(): | |
| if param.requires_grad and name in self.shadow: | |
| self.shadow[name].lerp_(param.data, 1.0 - self.decay) | |
| def state_dict(self) -> Dict[str, torch.Tensor]: | |
| return self.shadow | |
| def copy_to(self, model: nn.Module): | |
| """Loads EMA weights into model.""" | |
| model_state = model.state_dict() | |
| for name, param in self.shadow.items(): | |
| if name in model_state: | |
| model_state[name].copy_(param) | |
| class WarmupCosineScheduler(torch.optim.lr_scheduler._LRScheduler): | |
| """Linear warmup for stability followed by Cosine Annealing decay.""" | |
| def __init__(self, optimizer, warmup_steps: int, total_steps: int, min_lr: float = 1e-6, last_epoch: int = -1): | |
| self.warmup_steps = max(1, warmup_steps) | |
| self.total_steps = max(self.warmup_steps + 1, total_steps) | |
| self.min_lr = min_lr | |
| super().__init__(optimizer, last_epoch) | |
| def get_lr(self): | |
| step = max(1, self.last_epoch) | |
| if step <= self.warmup_steps: | |
| alpha = step / float(self.warmup_steps) | |
| return [base_lr * alpha for base_lr in self.base_lrs] | |
| else: | |
| progress = (step - self.warmup_steps) / max(1, (self.total_steps - self.warmup_steps)) | |
| progress = min(max(progress, 0.0), 1.0) | |
| cosine_decay = 0.5 * (1.0 + math.cos(math.pi * progress)) | |
| return [self.min_lr + (base_lr - self.min_lr) * cosine_decay for base_lr in self.base_lrs] | |
| # --------------------------------------------------------------------------- | |
| # Rental GPU Cloud-Sync & Auto-Recovery Engine | |
| # --------------------------------------------------------------------------- | |
| def resolve_resume_checkpoint( | |
| resume_arg: Optional[str], | |
| checkpoint_dir: str = "checkpoints", | |
| repo_id: str = "ViuAI/ViuAI_TTS_200M", | |
| ) -> Optional[str]: | |
| """Resolves checkpoint path. If 'auto' or 'latest', checks locally first, then downloads from Hugging Face.""" | |
| if not resume_arg or resume_arg.lower() in ("none", "null", "false", "0"): | |
| return None | |
| def _is_healthy(path: str) -> bool: | |
| if not os.path.exists(path) or os.path.getsize(path) < 1000: | |
| return False | |
| try: | |
| torch.load(path, map_location="cpu", weights_only=True) | |
| return True | |
| except Exception: | |
| try: | |
| torch.load(path, map_location="cpu", weights_only=False) | |
| return True | |
| except Exception: | |
| return False | |
| # 1. If explicit existing file path | |
| if os.path.exists(resume_arg) and os.path.isfile(resume_arg): | |
| if _is_healthy(resume_arg): | |
| return resume_arg | |
| print(f"[!] Warning: Specified checkpoint {resume_arg} is corrupt/unreadable! Searching alternatives...") | |
| # 2. If 'auto' or 'latest' | |
| if resume_arg.lower() in ("auto", "latest", "true", "1") or not _is_healthy(resume_arg): | |
| # Check local latest | |
| latest_local = os.path.join(checkpoint_dir, "viuai_tts_latest.pt") | |
| if _is_healthy(latest_local): | |
| print(f"[*] 🔄 Found valid local latest checkpoint: {latest_local}") | |
| return latest_local | |
| # Search all healthy local step checkpoints (e.g. step_006000.pt) | |
| step_ckpts = sorted(glob.glob(os.path.join(checkpoint_dir, "viuai_tts_step_*.pt")), reverse=True) | |
| for s_ckpt in step_ckpts: | |
| if _is_healthy(s_ckpt): | |
| print(f"[*] 🔄 Restoring from latest healthy step checkpoint: {s_ckpt}") | |
| return s_ckpt | |
| # Search all healthy local epoch checkpoints | |
| epoch_ckpts = sorted(glob.glob(os.path.join(checkpoint_dir, "viuai_tts_epoch_*.pt")), reverse=True) | |
| for e_ckpt in epoch_ckpts: | |
| if _is_healthy(e_ckpt): | |
| print(f"[*] 🔄 Restoring from latest healthy epoch checkpoint: {e_ckpt}") | |
| return e_ckpt | |
| # Check Hugging Face for remote checkpoint | |
| print(f"[*] ☁️ Checking Hugging Face ({repo_id}) for remote checkpoint 'checkpoints/viuai_tts_latest.pt'...") | |
| try: | |
| from huggingface_hub import hf_hub_download, get_token | |
| token = get_token() or os.environ.get("HF_TOKEN") | |
| os.makedirs(checkpoint_dir, exist_ok=True) | |
| downloaded = hf_hub_download( | |
| repo_id=repo_id, | |
| filename="checkpoints/viuai_tts_latest.pt", | |
| local_dir=".", | |
| token=token, | |
| ) | |
| if _is_healthy(downloaded): | |
| print(f"[OK] ☁️ Restored latest checkpoint from Hugging Face: {downloaded}") | |
| return downloaded | |
| else: | |
| # Try step_006000 from HF if latest on HF had an issue | |
| s6000 = hf_hub_download( | |
| repo_id=repo_id, | |
| filename="checkpoints/viuai_tts_step_006000.pt", | |
| local_dir=".", | |
| token=token, | |
| ) | |
| if _is_healthy(s6000): | |
| print(f"[OK] ☁️ Restored step 6000 from Hugging Face: {s6000}") | |
| return s6000 | |
| except Exception as e: | |
| print(f"[*] Note: No existing cloud checkpoint found ({e}). Starting fresh training from Epoch 1.") | |
| return None | |
| return None | |
| def sync_checkpoint_to_hub( | |
| checkpoint_path: str, | |
| repo_id: str = "ViuAI/ViuAI_TTS_200M", | |
| benchmark_dir: Optional[str] = None, | |
| ): | |
| """Uploads milestone checkpoint and benchmark audio to Hugging Face so training progress is never lost.""" | |
| try: | |
| from huggingface_hub import HfApi, get_token | |
| token = get_token() or os.environ.get("HF_TOKEN") | |
| if not token: | |
| print(" [!] Warning: Hugging Face authentication token not found. Checkpoint saved locally only.") | |
| return | |
| api = HfApi(token=token) | |
| filename = os.path.basename(checkpoint_path) | |
| # 1. Upload milestone checkpoint | |
| print(f" [☁️ CLOUD SYNC] Uploading {filename} to Hugging Face ({repo_id})...") | |
| api.upload_file( | |
| path_or_fileobj=checkpoint_path, | |
| path_in_repo=f"checkpoints/{filename}", | |
| repo_id=repo_id, | |
| repo_type="model", | |
| ) | |
| # 2. Update 'viuai_tts_latest.pt' pointer on HF | |
| api.upload_file( | |
| path_or_fileobj=checkpoint_path, | |
| path_in_repo="checkpoints/viuai_tts_latest.pt", | |
| repo_id=repo_id, | |
| repo_type="model", | |
| ) | |
| # 3. Upload audio benchmark WAVs if present | |
| if benchmark_dir and os.path.exists(benchmark_dir): | |
| b_tag = os.path.basename(benchmark_dir) | |
| for wpath in glob.glob(os.path.join(benchmark_dir, "*.wav")): | |
| wname = os.path.basename(wpath) | |
| api.upload_file( | |
| path_or_fileobj=wpath, | |
| path_in_repo=f"eval_benchmarks/{b_tag}/{wname}", | |
| repo_id=repo_id, | |
| repo_type="model", | |
| ) | |
| print(f" [☁️ CLOUD BACKUP OK] {filename} & viuai_tts_latest.pt safely backed up to Hugging Face!") | |
| except Exception as e: | |
| print(f" [!] Cloud sync notice: {e}") | |
| # --------------------------------------------------------------------------- | |
| # High-Performance Dataset with Dynamic In-Context Reference Audio | |
| # --------------------------------------------------------------------------- | |
| class ViuAISpeechDataset(Dataset): | |
| """ | |
| High-Performance Balanced Dataset Loader for ViuAI_TTS_Dataset. | |
| Guarantees 50% Hindi / 50% English, balanced Gender priors, and same-speaker in-context reference pairing. | |
| """ | |
| def __init__(self, dataset_dir: str): | |
| self.dataset_dir = dataset_dir | |
| self.tokenizer = ViuAITokenizer() | |
| self.records = [] | |
| self.sample_weights = [] | |
| self.speaker_to_indices = defaultdict(list) | |
| meta_path = os.path.join(dataset_dir, "metadata.csv") | |
| if os.path.exists(meta_path): | |
| raw_items = [] | |
| with open(meta_path, "r", encoding="utf-8") as f: | |
| reader = csv.reader(f, delimiter="|") | |
| for row in reader: | |
| if len(row) >= 4 and row[0] != "audio_file": | |
| rel_path = row[0].strip() | |
| transcript = row[1].strip() | |
| speaker = row[2].strip() | |
| lang = row[3].strip() | |
| full_audio_path = os.path.join(dataset_dir, rel_path) | |
| if os.path.exists(full_audio_path): | |
| raw_items.append((full_audio_path, transcript, speaker, lang)) | |
| # Compute balanced language & gender weights | |
| spk_counts = Counter(item[2] for item in raw_items) | |
| lang_counts = Counter(item[3] for item in raw_items) | |
| for idx, (full_p, txt, spk, lang) in enumerate(raw_items): | |
| self.records.append((full_p, txt, spk, lang)) | |
| self.speaker_to_indices[spk].append(idx) | |
| # 50% mass to Hindi, 50% to English; balanced within speaker pools | |
| lang_w = 0.5 / max(1, lang_counts.get(lang, 1)) | |
| spk_w = 0.5 / max(1, spk_counts.get(spk, 1)) | |
| self.sample_weights.append(lang_w + spk_w) | |
| print(f"[*] ViuAISpeechDataset loaded {len(self.records)} verified audio-text pairs across {len(self.speaker_to_indices)} speakers from {dataset_dir}") | |
| if self.sample_weights: | |
| print(" - ⚖️ Balanced Sampler Active: 50% Hindi / 50% English & Balanced Gender priors!") | |
| print(" - 🎙️ In-Context Voice Conditioning Active: Dynamic same-speaker reference audio pairing enabled!") | |
| def __len__(self): | |
| return len(self.records) | |
| def __getitem__(self, idx): | |
| audio_path, text, speaker, lang = self.records[idx] | |
| tokens = torch.tensor(self.tokenizer.text_to_ids(text), dtype=torch.long) | |
| # 1. Load 24kHz audio waveform ONCE in-memory | |
| wav = load_audio_wav(audio_path, target_sr=24000) | |
| if wav is None: | |
| wav = torch.zeros(1, 24000) | |
| # OOM Guard: Cap audio duration to max 10.0s (240,000 samples) to prevent quadratic attention spikes | |
| max_samples = 24000 * 10 | |
| if wav.shape[-1] > max_samples: | |
| wav = wav[:, :max_samples] | |
| # 2. Extract 80-channel mel directly from in-memory waveform | |
| extractor = get_default_extractor(sample_rate=24000) | |
| mel = extractor(wav) | |
| if mel is None: | |
| mel = torch.randn(80, max(20, len(tokens) * 4)) | |
| else: | |
| mel = mel.squeeze(0) # [80, T_mel] | |
| # OOM Guard: Cap mel frames to max 800 frames (~9.3 seconds) | |
| if mel.shape[-1] > 800: | |
| mel = mel[:, :800] | |
| T_text = len(tokens) | |
| T_mel = mel.shape[-1] | |
| # Extract real acoustic pitch (F0) and RMS energy from speech waveform first | |
| frame_pitch, frame_energy = extract_pitch_and_energy(wav, sample_rate=24000, hop_length=256) | |
| if frame_pitch.numel() > 0: | |
| text_pitch = F.interpolate( | |
| frame_pitch.view(1, 1, -1), size=T_text, mode="linear", align_corners=False | |
| ).view(T_text) | |
| else: | |
| text_pitch = torch.zeros(T_text, dtype=torch.float32) | |
| if frame_energy.numel() > 0: | |
| text_energy = F.interpolate( | |
| frame_energy.view(1, 1, -1), size=T_text, mode="linear", align_corners=False | |
| ).view(T_text) | |
| else: | |
| text_energy = torch.zeros(T_text, dtype=torch.float32) | |
| # 3. Dynamic Energy-Aware Duration Allocation | |
| # Content characters receive more frames; vowels get high duration, spaces/punct get micro-pauses | |
| token_weights = torch.ones(T_text, dtype=torch.float32) | |
| for ti, ch_id in enumerate(tokens): | |
| if ch_id in (0, 1, 2, 3): # pad, unk, bos, eos | |
| token_weights[ti] = 0.4 | |
| elif ch_id in (4, 5, 6, 7, 8, 9, 10, 11): # punctuation/space | |
| token_weights[ti] = 0.9 | |
| else: | |
| token_weights[ti] = 2.4 # speech letters/Devanagari matras | |
| # Modulate content weights with active speech energy ratio | |
| if frame_energy.numel() > 0: | |
| speech_ratio = (frame_energy > 0.15).float().mean().item() | |
| token_weights[token_weights > 1.0] *= (1.0 + speech_ratio * 0.4) | |
| token_weights = token_weights / token_weights.sum() | |
| durations = torch.clamp(torch.round(token_weights * T_mel).long(), min=1) | |
| diff = T_mel - int(durations.sum().item()) | |
| # Distribute residual evenly across content tokens (NEVER dumping all diff into token 0!) | |
| if diff > 0: | |
| sort_indices = torch.argsort(token_weights, descending=True) | |
| for i in range(diff): | |
| durations[sort_indices[i % T_text]] += 1 | |
| elif diff < 0: | |
| sort_indices = torch.argsort(durations, descending=True) | |
| for i in range(abs(diff)): | |
| for idx in sort_indices: | |
| if durations[idx] > 1: | |
| durations[idx] -= 1 | |
| break | |
| # 4. In-Context Reference Audio from SAME Speaker for Zero-Shot Voice Conditioning | |
| # Fast RAM-cached path: avoids reloading duplicate reference audio files from disk | |
| ref_mel = None | |
| cached_refs = getattr(self, "speaker_ref_mels", {}).get(speaker) | |
| if cached_refs and len(cached_refs) > 0 and random.random() < 0.85: | |
| ref_mel = random.choice(cached_refs) | |
| else: | |
| spk_indices = self.speaker_to_indices.get(speaker, [idx]) | |
| ref_idx = random.choice(spk_indices) if len(spk_indices) > 1 else idx | |
| if ref_idx == idx: | |
| ref_wav = wav | |
| else: | |
| ref_audio_path, _, _, _ = self.records[ref_idx] | |
| ref_wav = load_audio_wav(ref_audio_path, target_sr=24000) | |
| if ref_wav is not None: | |
| # Cap reference audio to max ~3.5 seconds (84,000 samples) to prevent VRAM spikes | |
| max_ref_samples = 24000 * 3.5 | |
| if ref_wav.shape[-1] > max_ref_samples: | |
| ref_wav = ref_wav[:, :int(max_ref_samples)] | |
| ref_mel = extractor(ref_wav).squeeze(0) # [80, T_ref] | |
| else: | |
| ref_mel = mel[:, :min(T_mel, 150)] | |
| if not hasattr(self, "speaker_ref_mels"): | |
| self.speaker_ref_mels = {} | |
| if speaker not in self.speaker_ref_mels: | |
| self.speaker_ref_mels[speaker] = [] | |
| if len(self.speaker_ref_mels[speaker]) < 4: | |
| self.speaker_ref_mels[speaker].append(ref_mel.detach().clone()) | |
| # Return dummy wav to eliminate 30MB of /dev/shm IPC serialization per batch (vocoder is decoupled) | |
| dummy_wav = torch.zeros(1, dtype=torch.float32) | |
| return tokens, mel, durations, text_pitch, text_energy, dummy_wav, ref_mel | |
| def speech_collate_fn(batch): | |
| tokens_list, mels_list, durs_list, pitch_list, energy_list, audio_list, ref_mels_list = zip(*batch) | |
| # 1. Text lengths & padding | |
| text_lengths = torch.tensor([len(t) for t in tokens_list], dtype=torch.long) | |
| max_token_len = max(len(t) for t in tokens_list) | |
| padded_tokens = torch.zeros(len(tokens_list), max_token_len, dtype=torch.long) | |
| padded_durs = torch.zeros(len(durs_list), max_token_len, dtype=torch.long) | |
| padded_pitch = torch.zeros(len(pitch_list), max_token_len, dtype=torch.float32) | |
| padded_energy = torch.zeros(len(energy_list), max_token_len, dtype=torch.float32) | |
| for i in range(len(tokens_list)): | |
| t_len = len(tokens_list[i]) | |
| padded_tokens[i, :t_len] = tokens_list[i] | |
| padded_durs[i, :t_len] = durs_list[i] | |
| padded_pitch[i, :t_len] = pitch_list[i] | |
| padded_energy[i, :t_len] = energy_list[i] | |
| # 2. Mel lengths & padding: [B, 80, max_mel_len] | |
| mel_lengths = torch.tensor([m.shape[-1] for m in mels_list], dtype=torch.long) | |
| max_mel_len = max(m.shape[-1] for m in mels_list) | |
| padded_mels = torch.zeros(len(mels_list), 80, max_mel_len, dtype=torch.float32) | |
| for i, m in enumerate(mels_list): | |
| padded_mels[i, :, :m.shape[-1]] = m | |
| # 3. Reference Mels: [B, 80, max_ref_len] | |
| max_ref_len = max(r.shape[-1] for r in ref_mels_list) | |
| padded_ref_mels = torch.zeros(len(ref_mels_list), 80, max_ref_len, dtype=torch.float32) | |
| for i, r in enumerate(ref_mels_list): | |
| padded_ref_mels[i, :, :r.shape[-1]] = r | |
| # 4. Audio waveforms: Minimal dummy tensor to keep shared memory under 200KB per batch (vocoder is decoupled) | |
| padded_audios = torch.zeros(len(tokens_list), 1, 1, dtype=torch.float32) | |
| return padded_tokens, padded_mels, padded_durs, text_lengths, mel_lengths, padded_pitch, padded_energy, padded_audios, padded_ref_mels | |
| # --------------------------------------------------------------------------- | |
| # Golden Audio Benchmark Suite (Hindi & English, Male & Female) | |
| # --------------------------------------------------------------------------- | |
| def run_golden_benchmark(model, dataset_dir: str, tag: str, output_dir: str = "checkpoints/eval_benchmarks"): | |
| """ | |
| Evaluates 4 fixed audio benchmarks: | |
| 1. Hindi Male (Deep Baritone) | |
| 2. Hindi Female (Expressive Melodic) | |
| 3. English Male (Flagship Deep Adam style) | |
| 4. English Female (Clear Narrator) | |
| Saves WAV files directly into output_dir/{tag}/ | |
| """ | |
| model.eval() | |
| save_dir = os.path.join(output_dir, str(tag)) | |
| os.makedirs(save_dir, exist_ok=True) | |
| device = next(model.parameters()).device | |
| tokenizer = ViuAITokenizer() | |
| test_prompts = [ | |
| ("01_hindi_male", "भारतीय संस्कृति, संगीत और इतिहास की यह एक अनूठी गाथा है।", "indic_hi_male_1"), | |
| ("02_hindi_female", "नमस्ते! आज का मौसम बहुत ही सुहावना है, आप कैसे हैं?", "indic_hi_female_4"), | |
| ("03_english_male", "The future of artificial intelligence begins with human speech and deep resonance.", "en_flagship"), | |
| ("04_english_female", "Welcome to our studio, where advanced technology meets creative natural expression.", "en_female"), | |
| ] | |
| print(f"\n🎧 [GENERATING GOLDEN AUDIO BENCHMARK FOR {tag}]...") | |
| for file_tag, text, spk_pref in test_prompts: | |
| try: | |
| token_ids = tokenizer.text_to_ids(text) | |
| token_tensor = torch.tensor([token_ids], dtype=torch.long, device=device) | |
| ref_mel = None | |
| if dataset_dir: | |
| patterns = [ | |
| os.path.join(dataset_dir, "audio", spk_pref, "*.wav"), | |
| os.path.join(dataset_dir, "audio", f"*{spk_pref}*.wav"), | |
| os.path.join(dataset_dir, "**", f"*{spk_pref}*.wav"), | |
| os.path.join(dataset_dir, "audio", "*", "*.wav"), | |
| os.path.join(dataset_dir, "audio", "*.wav"), | |
| ] | |
| candidates = [] | |
| for p in patterns: | |
| candidates = glob.glob(p, recursive=True) | |
| if candidates: | |
| break | |
| if candidates: | |
| ref_mel = extract_mel_from_file(candidates[0], target_sr=24000) | |
| if ref_mel is not None: | |
| ref_mel = ref_mel.to(device) | |
| with torch.no_grad(): | |
| audio_out, _ = model.generate( | |
| text_tokens=token_tensor, | |
| ref_mel=ref_mel, | |
| num_steps=20, | |
| cfg_scale=1.6, | |
| speed=1.0, | |
| ) | |
| out_wav_path = os.path.join(save_dir, f"{file_tag}.wav") | |
| audio_flat = audio_out.squeeze().detach().cpu().float() | |
| # Step A: DC Offset Removal (Removes massive electrical hum/bias) | |
| audio_flat = audio_flat - audio_flat.mean() | |
| # Step B: High-pass filtering (Remove sub-audible rumble below 50Hz) | |
| try: | |
| import torchaudio | |
| audio_flat = torchaudio.functional.highpass_biquad(audio_flat.unsqueeze(0), 24000, cutoff_freq=50.0).squeeze(0) | |
| audio_flat = audio_flat - audio_flat.mean() | |
| except Exception: | |
| pass | |
| # Step C: Peak normalize to -1 dB (0.89) safely without amplifying DC noise | |
| max_amp = audio_flat.abs().max().item() | |
| if max_amp > 1e-5: | |
| audio_flat = audio_flat * (0.89 / max_amp) | |
| audio_flat = audio_flat.clamp(-1.0, 1.0) | |
| audio_int16 = (audio_flat * 32767.0).to(torch.int16).numpy().tobytes() | |
| with wave.open(out_wav_path, "wb") as wav_out: | |
| wav_out.setnchannels(1) | |
| wav_out.setsampwidth(2) | |
| wav_out.setframerate(24000) | |
| wav_out.writeframes(audio_int16) | |
| print(f" [OK] Saved {file_tag}.wav ({len(audio_flat)/24000:.1f}s)") | |
| except Exception as e: | |
| print(f" [!] Benchmark {file_tag} note: {e}") | |
| print(f" [🎧 BENCHMARKS READY] Audio files saved at: {save_dir}\n") | |
| model.train() | |
| # --------------------------------------------------------------------------- | |
| # Master Training Engine | |
| # --------------------------------------------------------------------------- | |
| def train( | |
| dataset_dir: str = None, | |
| epochs: int = 200, | |
| batch_size: int = 0, # 0/auto => GPU profile default | |
| lr: float = 1e-4, | |
| checkpoint_dir: str = "checkpoints", | |
| seed: int = 42, | |
| precision: str = "auto", # auto|fp16|bf16|fp32 | |
| grad_accum: int = 1, | |
| num_workers: int = -1, # -1/auto => GPU profile default | |
| max_steps: int = 0, # 0 => no cap, runs full training across all epochs | |
| warmup_steps: int = 1000, | |
| resume: str = "auto", # 'auto' automatically checks local & Hugging Face for latest checkpoint | |
| push_to_hub: bool = False, # True automatically backs up checkpoints & benchmarks to Hugging Face | |
| hf_repo: str = "ViuAI/ViuAI_TTS_200M", | |
| save_every: int = 5, | |
| save_every_steps: int = 0, | |
| log_every: int = 10, | |
| train_vocoder: bool = False, | |
| ): | |
| is_ddp, rank, world, device = setup_distributed() | |
| is_main = (rank == 0) | |
| torch.manual_seed(seed + rank) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed_all(seed + rank) | |
| gpu_label, profile = get_gpu_profile() | |
| if batch_size is None or batch_size <= 0: | |
| batch_size = profile["batch_size"] | |
| if num_workers is None or num_workers < 0: | |
| num_workers = profile["num_workers"] | |
| prec_name, amp_dtype, use_scaler = resolve_precision(precision, profile["precision"], device) | |
| if device.type == "cuda": | |
| try: | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| torch.backends.cudnn.benchmark = True | |
| except Exception: | |
| pass | |
| try: | |
| torch.set_float32_matmul_precision("high") | |
| except Exception: | |
| pass | |
| if is_main: | |
| print("=" * 70) | |
| print(f"🚀 Training ViuAI_TTS_200M (Master SOTA Mode) | device={device}") | |
| print(f" GPU: '{gpu_label}' | Precision: {prec_name}") | |
| print(f" Batch/GPU: {batch_size} x Accum: {grad_accum} x World: {world} = Effective Batch {batch_size * grad_accum * world}") | |
| print(f" In-Context Reference Conditioning: Enabled (Same-Speaker Zero-Shot Voice Cloning)") | |
| print(f" Decoupled Vocoder: Enabled (100% GPU compute dedicated to DiT Flow Matching)") | |
| print(f" Full Training Target: {epochs} Epochs (Continuous Training)") | |
| print("=" * 70) | |
| # 1. Initialize Model (~190.5M Parameters) | |
| model = ViuAITTS200M().to(device) | |
| if is_main: | |
| params_m = model.count_parameters()["Total Trainable Parameters"] / 1e6 | |
| print(f"[*] Model Loaded: {params_m:.2f}M Parameters") | |
| if is_ddp: | |
| model = DDP(model, device_ids=[device.index] if device.type == "cuda" else None) | |
| raw_model = model.module if is_ddp else model | |
| # Exponential Moving Average (EMA) for artifact-free audio | |
| ema = EMAModel(raw_model, decay=0.999) | |
| # 2. Locate Dataset | |
| possible_dirs = [ | |
| dataset_dir, | |
| "./dataset", | |
| "./colab_dataset", | |
| "../colab_dataset", | |
| "../ViuAI_TTS_Dataset", | |
| "../../ViuAI_TTS_Dataset", | |
| "../dataset", | |
| ] | |
| chosen_dir = None | |
| for d in possible_dirs: | |
| if d and os.path.exists(os.path.join(d, "metadata.csv")): | |
| chosen_dir = d | |
| break | |
| if not chosen_dir: | |
| if is_main: | |
| print("[*] No local dataset found. Auto-downloading ViuAI_TTS_Dataset from Hugging Face...") | |
| try: | |
| from huggingface_hub import snapshot_download, get_token | |
| chosen_dir = "./dataset" | |
| hf_tok = os.environ.get("HF_TOKEN") or get_token() | |
| snapshot_download( | |
| "ViuAI/ViuAI_TTS_Dataset", | |
| repo_type="dataset", | |
| local_dir=chosen_dir, | |
| token=hf_tok, | |
| max_workers=8, | |
| ) | |
| print(f"[OK] Dataset downloaded successfully to: {chosen_dir}") | |
| except Exception as e: | |
| print(f"[!] Failed to auto-download dataset: {e}") | |
| chosen_dir = None | |
| if is_ddp: | |
| dist.barrier() | |
| chosen_dir = "./dataset" | |
| if not chosen_dir or not os.path.exists(os.path.join(chosen_dir, "metadata.csv")): | |
| print("[!] No dataset folder found with metadata.csv. Exiting.") | |
| if is_ddp: | |
| dist.destroy_process_group() | |
| return | |
| dataset = ViuAISpeechDataset(chosen_dir) | |
| if is_ddp: | |
| sampler = DistributedSampler(dataset, shuffle=True) | |
| elif dataset.sample_weights: | |
| from torch.utils.data import WeightedRandomSampler | |
| sampler = WeightedRandomSampler( | |
| weights=dataset.sample_weights, | |
| num_samples=len(dataset), | |
| replacement=True, | |
| ) | |
| else: | |
| sampler = None | |
| dataloader = DataLoader( | |
| dataset, | |
| batch_size=batch_size, | |
| shuffle=(sampler is None), | |
| sampler=sampler, | |
| collate_fn=speech_collate_fn, | |
| drop_last=True, | |
| num_workers=num_workers, | |
| pin_memory=(device.type == "cuda"), | |
| persistent_workers=(num_workers > 0), | |
| prefetch_factor=(2 if num_workers > 0 else None), | |
| ) | |
| # 3. Setup Optimizer & Warmup Cosine Annealing LR Scheduler | |
| optimizer = torch.optim.AdamW( | |
| model.parameters(), | |
| lr=lr, | |
| betas=(0.9, 0.99), | |
| eps=1e-8, | |
| weight_decay=0.01, | |
| ) | |
| steps_per_epoch = max(1, len(dataloader)) | |
| total_steps = epochs * steps_per_epoch | |
| if max_steps and max_steps > 0: | |
| total_steps = min(total_steps, max_steps) | |
| scheduler = WarmupCosineScheduler( | |
| optimizer, | |
| warmup_steps=warmup_steps, | |
| total_steps=total_steps, | |
| min_lr=1e-6, | |
| ) | |
| scaler = torch.amp.GradScaler("cuda", enabled=(use_scaler and device.type == "cuda")) | |
| # 4. Checkpoints directory | |
| if is_main: | |
| os.makedirs(checkpoint_dir, exist_ok=True) | |
| if is_ddp: | |
| dist.barrier() | |
| # 5. Optional resume (supports local & Hugging Face auto-recovery) | |
| start_epoch, step = 1, 0 | |
| resolved_resume = resolve_resume_checkpoint(resume, checkpoint_dir=checkpoint_dir, repo_id=hf_repo) | |
| if resolved_resume and os.path.exists(resolved_resume): | |
| try: | |
| ckpt = torch.load(resolved_resume, map_location=device, weights_only=True) | |
| except Exception: | |
| ckpt = torch.load(resolved_resume, map_location=device, weights_only=False) | |
| sd = ckpt.get("model_state_dict", ckpt) | |
| raw_model.load_state_dict(sd, strict=False) | |
| if "ema_state_dict" in ckpt: | |
| for k, v in ckpt["ema_state_dict"].items(): | |
| if k in ema.shadow: | |
| ema.shadow[k].copy_(v) | |
| if "optimizer_state_dict" in ckpt: | |
| try: | |
| optimizer.load_state_dict(ckpt["optimizer_state_dict"]) | |
| except Exception as e: | |
| if is_main: | |
| print(f"[!] Optimizer state skipped: {e}") | |
| if "scheduler_state_dict" in ckpt: | |
| try: | |
| scheduler.load_state_dict(ckpt["scheduler_state_dict"]) | |
| except Exception as e: | |
| if is_main: | |
| print(f"[!] Scheduler state skipped: {e}") | |
| start_epoch = int(ckpt.get("epoch", 0)) + 1 | |
| step = int(ckpt.get("step", 0)) | |
| if is_main: | |
| print(f"[*] 🔄 Resumed seamlessly from {resolved_resume} (next epoch: {start_epoch}, next step: {step + 1})") | |
| if is_main: | |
| print(f"[*] Checkpoints will be saved locally to: {checkpoint_dir}") | |
| if push_to_hub: | |
| print(f" - ☁️ Auto-Cloud Backup Active: Every checkpoint will be backed up to Hugging Face ({hf_repo})") | |
| print(f"[*] Total Batches per Epoch: {len(dataloader)}") | |
| print(f"[*] Total Training Steps: {total_steps} (Linear Warmup {warmup_steps} steps + Cosine Decay)") | |
| print("-" * 70) | |
| model.train() | |
| start_time = time.time() | |
| accum = max(1, grad_accum) | |
| for epoch in range(start_epoch, epochs + 1): | |
| if is_ddp and sampler is not None and hasattr(sampler, "set_epoch"): | |
| sampler.set_epoch(epoch) | |
| epoch_loss = 0.0 | |
| for batch_idx, batch in enumerate(dataloader): | |
| batch_tokens, batch_mels, batch_durs, text_lens, mel_lens, batch_pitch, batch_energy, batch_audios, batch_ref_mels = [ | |
| b.to(device, non_blocking=True) if isinstance(b, torch.Tensor) else b for b in batch | |
| ] | |
| is_accum_step = ((batch_idx + 1) % accum != 0) | |
| ctx = model.no_sync() if (is_ddp and is_accum_step) else torch.enable_grad() | |
| with ctx: | |
| with torch.amp.autocast("cuda" if device.type == "cuda" else "cpu", | |
| dtype=amp_dtype, | |
| enabled=(device.type == "cuda" and prec_name != "fp32")): | |
| losses = raw_model.forward_train( | |
| text_tokens=batch_tokens, | |
| mel_target=batch_mels, | |
| durations=batch_durs, | |
| pitch_target=batch_pitch, | |
| energy_target=batch_energy, | |
| ref_mel=batch_ref_mels, | |
| text_lengths=text_lens, | |
| mel_lengths=mel_lens, | |
| cond_drop_prob=0.15, | |
| audio_target=batch_audios, | |
| train_vocoder=train_vocoder, | |
| ) | |
| loss = losses["total_loss"] / accum | |
| scaler.scale(loss).backward() | |
| if is_accum_step: | |
| continue | |
| step += 1 | |
| scaler.unscale_(optimizer) | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) | |
| scaler.step(optimizer) | |
| scaler.update() | |
| # Update EMA weights | |
| ema.update(raw_model) | |
| optimizer.zero_grad(set_to_none=True) | |
| scheduler.step() | |
| epoch_loss += losses["total_loss"].item() | |
| if is_main and (step % log_every == 0 or step == 1): | |
| elapsed = time.time() - start_time | |
| current_lr = scheduler.get_last_lr()[0] | |
| print( | |
| f"Epoch [{epoch:02d}/{epochs:02d}] Step {step:05d} | " | |
| f"Total Loss: {losses['total_loss'].item():.4f} | " | |
| f"Flow Loss: {losses['flow_loss'].item():.4f} | " | |
| f"Prosody Loss: {losses['prosody_loss'].item():.4f} | " | |
| f"LR: {current_lr:.2e} | " | |
| f"Time: {elapsed:.1f}s" | |
| ) | |
| # Optional step-based milestone save | |
| if is_main and save_every_steps and save_every_steps > 0 and step % save_every_steps == 0: | |
| step_ckpt = os.path.join(checkpoint_dir, f"viuai_tts_step_{step:06d}.pt") | |
| latest_ckpt = os.path.join(checkpoint_dir, "viuai_tts_latest.pt") | |
| step_payload = { | |
| "epoch": epoch, | |
| "step": step, | |
| "model_state_dict": raw_model.state_dict(), | |
| "ema_state_dict": ema.state_dict(), | |
| "optimizer_state_dict": optimizer.state_dict(), | |
| "scheduler_state_dict": scheduler.state_dict(), | |
| "loss": losses["total_loss"].item(), | |
| } | |
| torch.save(step_payload, step_ckpt) | |
| torch.save(step_payload, latest_ckpt) | |
| print(f"\n [🎯 MILESTONE SAVED: Step {step}] -> {step_ckpt} (updated {latest_ckpt})") | |
| # Benchmark with EMA weights | |
| b_dir = os.path.join(checkpoint_dir, "eval_benchmarks", f"step_{step:06d}") | |
| try: | |
| eval_model = ViuAITTS200M().to(device) | |
| eval_model.load_state_dict(raw_model.state_dict(), strict=False) | |
| ema.copy_to(eval_model) | |
| run_golden_benchmark(eval_model, chosen_dir, f"step_{step:06d}", os.path.join(checkpoint_dir, "eval_benchmarks")) | |
| del eval_model | |
| except Exception as eb: | |
| print(f" [!] Benchmark notice: {eb}") | |
| if is_main and push_to_hub: | |
| sync_checkpoint_to_hub( | |
| checkpoint_path=step_ckpt, | |
| repo_id=hf_repo, | |
| benchmark_dir=b_dir if os.path.exists(b_dir) else None, | |
| ) | |
| if max_steps and max_steps > 0 and step >= max_steps: | |
| break | |
| if is_ddp: | |
| dist.barrier() | |
| if is_main: | |
| avg_loss = epoch_loss / max(1, steps_per_epoch) | |
| print(f"\n---> Epoch {epoch:02d}/{epochs:02d} Completed | Avg Loss: {avg_loss:.4f}") | |
| # ALWAYS save latest_ckpt at the end of every epoch so training can seamlessly resume anytime | |
| latest_ckpt = os.path.join(checkpoint_dir, "viuai_tts_latest.pt") | |
| ckpt_payload = { | |
| "epoch": epoch, | |
| "step": step, | |
| "model_state_dict": raw_model.state_dict(), | |
| "ema_state_dict": ema.state_dict(), | |
| "optimizer_state_dict": optimizer.state_dict(), | |
| "scheduler_state_dict": scheduler.state_dict(), | |
| "loss": avg_loss, | |
| } | |
| torch.save(ckpt_payload, latest_ckpt) | |
| # Save milestone epoch checkpoint (every `save_every` epochs or last epoch) | |
| if epoch % save_every == 0 or epoch == epochs or (max_steps and step >= max_steps): | |
| ckpt_path = os.path.join(checkpoint_dir, f"viuai_tts_epoch_{epoch:02d}.pt") | |
| torch.save(ckpt_payload, ckpt_path) | |
| print(f" [CHECKPOINT SAVED] -> {ckpt_path} (updated {latest_ckpt})") | |
| # Benchmark evaluation using smoothed EMA weights | |
| b_dir = os.path.join(checkpoint_dir, "eval_benchmarks", f"epoch_{epoch:02d}") | |
| try: | |
| eval_model = ViuAITTS200M().to(device) | |
| eval_model.load_state_dict(raw_model.state_dict(), strict=False) | |
| ema.copy_to(eval_model) | |
| run_golden_benchmark(eval_model, chosen_dir, f"epoch_{epoch:02d}", os.path.join(checkpoint_dir, "eval_benchmarks")) | |
| del eval_model | |
| except Exception as eb: | |
| print(f" [!] Benchmark notice: {eb}") | |
| # Cloud sync to Hugging Face if enabled | |
| if is_main and push_to_hub: | |
| sync_checkpoint_to_hub( | |
| checkpoint_path=ckpt_path, | |
| repo_id=hf_repo, | |
| benchmark_dir=b_dir if os.path.exists(b_dir) else None, | |
| ) | |
| if device.type == "cuda": | |
| torch.cuda.empty_cache() | |
| if max_steps and max_steps > 0 and step >= max_steps: | |
| break | |
| if is_ddp: | |
| dist.barrier() | |
| dist.destroy_process_group() | |
| if is_main: | |
| print("=" * 70) | |
| print("🎉 Full Training Completed Successfully across all epochs!") | |
| print("=" * 70) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="ViuAI_TTS_200M Master Training (SOTA Mode)") | |
| parser.add_argument("--data_dir", type=str, default=None, help="Path to dataset dir with metadata.csv") | |
| parser.add_argument("--epochs", type=int, default=200, help="Number of full training epochs (default: 200)") | |
| parser.add_argument("--batch_size", type=int, default=0, help="Per-GPU batch size (0/auto = GPU profile default)") | |
| parser.add_argument("--lr", type=float, default=1e-4, help="Learning rate") | |
| parser.add_argument("--seed", type=int, default=42, help="Random seed") | |
| parser.add_argument("--precision", type=str, default="auto", choices=["auto", "fp16", "bf16", "fp32"], | |
| help="AMP precision (auto picks bf16 on Ampere+ / Hopper / Blackwell, fp16 on T4)") | |
| parser.add_argument("--grad_accum", type=int, default=1, help="Gradient accumulation steps") | |
| parser.add_argument("--num_workers", type=int, default=-1, help="Dataloader workers (-1/auto = GPU profile default)") | |
| parser.add_argument("--max_steps", type=int, default=0, help="Stop after N optimizer steps (0 = full training across all epochs)") | |
| parser.add_argument("--warmup_steps", type=int, default=1000, help="Linear warmup steps for LR scheduler (default: 1000)") | |
| parser.add_argument("--resume", type=str, default="auto", help="Resume mode: 'auto' (checks local and HF for latest checkpoint), or path/to/ckpt.pt") | |
| parser.add_argument("--push_to_hub", action="store_true", help="Auto-sync checkpoints and benchmarks to Hugging Face to prevent rental GPU data loss") | |
| parser.add_argument("--hf_repo", type=str, default="ViuAI/ViuAI_TTS_200M", help="Hugging Face repository ID for cloud backup") | |
| parser.add_argument("--save_every", type=int, default=5, help="Save checkpoint every N epochs (default: 5)") | |
| parser.add_argument("--save_every_steps", type=int, default=0, help="Save checkpoint every N optimizer steps (0 = off)") | |
| parser.add_argument("--log_every", type=int, default=10, help="Log every N optimizer steps") | |
| parser.add_argument("--train_vocoder", action="store_true", help="Enable Phase-2 neural vocoder fine-tuning alongside acoustic DiT") | |
| args = parser.parse_args() | |
| train( | |
| dataset_dir=args.data_dir, | |
| epochs=args.epochs, | |
| batch_size=args.batch_size, | |
| lr=args.lr, | |
| seed=args.seed, | |
| precision=args.precision, | |
| grad_accum=args.grad_accum, | |
| num_workers=args.num_workers, | |
| max_steps=args.max_steps, | |
| warmup_steps=args.warmup_steps, | |
| resume=args.resume, | |
| push_to_hub=args.push_to_hub, | |
| hf_repo=args.hf_repo, | |
| save_every=args.save_every, | |
| save_every_steps=args.save_every_steps, | |
| log_every=args.log_every, | |
| train_vocoder=args.train_vocoder, | |
| ) | |