import torch from pathlib import Path import json import os def load_config(): """Loads configuration from config.json. Returns: dict: The configuration loaded from the JSON file. Raises: FileNotFoundError: If the config.json file is not found. """ config_path = Path(__file__).parent.parent / "config" / "config.json" if not config_path.exists(): raise FileNotFoundError(f"Config file not found at {config_path}") with open(config_path) as f: return json.load(f) def get_available_voices(voices_dir): """Gets a list of available voice names without the .pt extension. Args: voices_dir (str): The path to the directory containing voice files. Returns: list: A list of voice names (strings). """ voices_dir = Path(voices_dir) if not voices_dir.exists(): return [] return [f.stem for f in voices_dir.glob("*.pt")] def validate_voice_name(voice_name, voices_dir): """Validates that a voice name exists in the voices directory. Args: voice_name (str): The name of the voice to validate. voices_dir (str): The path to the directory containing voice files. Returns: bool: True if the voice name is valid. Raises: ValueError: If the voice name is not found in the voices directory. """ available_voices = get_available_voices(voices_dir) if voice_name not in available_voices: raise ValueError( f"Voice '{voice_name}' not found. Available voices: {', '.join(available_voices)}" ) return True def load_voice(voice_name, voices_dir): """Loads a voice from the voices directory. Args: voice_name (str): The name of the voice to load. voices_dir (str): The path to the directory containing voice files. Returns: torch.Tensor: The loaded voice as a torch tensor. Raises: AssertionError: If the voices directory or voice file does not exist, or if the voice path is not a file. RuntimeError: If there is an error loading the voice file or converting it to a tensor. """ voices_dir = Path(voices_dir) assert voices_dir.exists(), f"Voices directory does not exist: {voices_dir}" assert voices_dir.is_dir(), f"Voices path is not a directory: {voices_dir}" validate_voice_name(voice_name, voices_dir) voice_path = voices_dir / f"{voice_name}.pt" assert voice_path.exists(), f"Voice file not found: {voice_path}" assert voice_path.is_file(), f"Voice path is not a file: {voice_path}" try: voice = torch.load(voice_path, weights_only=True) except Exception as e: raise RuntimeError(f"Error loading voice file {voice_path}: {str(e)}") if not isinstance(voice, torch.Tensor): try: voice = torch.tensor(voice) except Exception as e: raise RuntimeError(f"Could not convert voice to tensor: {str(e)}") return voice def quick_mix_voice(output_name, voices_dir, *voices, weights=None): """Mixes and saves voices with specified weights. Args: output_name (str): The name of the output mixed voice file (without extension). voices_dir (str): The path to the directory containing voice files. *voices (torch.Tensor): Variable number of voice tensors to mix. weights (list, optional): List of weights for each voice. Defaults to equal weights if None. Returns: torch.Tensor: The mixed voice as a torch tensor. Raises: ValueError: If no voices are provided, if the number of weights does not match the number of voices, or if the sum of weights is not positive. AssertionError: If the voices directory does not exist or is not a directory. """ voices_dir = Path(voices_dir) assert voices_dir.exists(), f"Voices directory does not exist: {voices_dir}" assert voices_dir.is_dir(), f"Voices path is not a directory: {voices_dir}" if not voices: raise ValueError("Must provide at least one voice") base_shape = voices[0].shape for i, voice in enumerate(voices): if not isinstance(voice, torch.Tensor): raise ValueError(f"Voice {i} is not a tensor") if voice.shape != base_shape: raise ValueError( f"Voice {i} has shape {voice.shape}, but expected {base_shape} (same as first voice)" ) if weights is None: weights = [1.0 / len(voices)] * len(voices) else: if len(weights) != len(voices): raise ValueError( f"Number of weights ({len(weights)}) must match number of voices ({len(voices)})" ) weights_sum = sum(weights) if weights_sum <= 0: raise ValueError("Sum of weights must be positive") weights = [w / weights_sum for w in weights] device = voices[0].device voices = [v.to(device) for v in voices] stacked = torch.stack(voices) weights = torch.tensor(weights, device=device) mixed = torch.zeros_like(voices[0]) for i, weight in enumerate(weights): mixed += stacked[i] * weight output_path = voices_dir / f"{output_name}.pt" torch.save(mixed, output_path) print(f"Created mixed voice: {output_name}.pt") return mixed def split_into_sentences(text): """Splits text into sentences using more robust rules. Args: text (str): The input text to split. Returns: list: A list of sentences (strings). """ import re text = text.strip() if not text: return [] abbreviations = { "Mr.": "Mr", "Mrs.": "Mrs", "Dr.": "Dr", "Ms.": "Ms", "Prof.": "Prof", "Sr.": "Sr", "Jr.": "Jr", "vs.": "vs", "etc.": "etc", "i.e.": "ie", "e.g.": "eg", "a.m.": "am", "p.m.": "pm", } for abbr, repl in abbreviations.items(): text = text.replace(abbr, repl) sentences = [] current = [] words = re.findall(r"\S+|\s+", text) for word in words: current.append(word) if re.search(r"[.!?]+$", word): if not re.match(r"^[A-Z][a-z]{1,2}$", word[:-1]): sentence = "".join(current).strip() if sentence: sentences.append(sentence) current = [] continue if current: sentence = "".join(current).strip() if sentence: sentences.append(sentence) for abbr, repl in abbreviations.items(): sentences = [s.replace(repl, abbr) for s in sentences] sentences = [s.strip() for s in sentences if s.strip()] final_sentences = [] for s in sentences: if len(s) > 200: parts = s.split(",") parts = [p.strip() for p in parts if p.strip()] if len(parts) > 1: final_sentences.extend(parts) else: final_sentences.append(s) else: final_sentences.append(s) return final_sentences