File size: 7,495 Bytes
2ea6f91 a13d606 aa7891c 2ea6f91 aa7a330 2ea6f91 aa7a330 49bbecb aa7a330 49bbecb 2ea6f91 b3c0e45 a13d606 2ea6f91 aa7891c 2ea6f91 aa7891c b3c0e45 49bbecb aa7891c 2ea6f91 b3c0e45 2ea6f91 b3c0e45 aa7891c b3c0e45 aa7891c 2ea6f91 b3c0e45 aa7891c 2ea6f91 49bbecb 2ea6f91 49bbecb 2ea6f91 49bbecb 2ea6f91 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | import torch
import random
import re
import yaml
import spacy
import warnings
import noisereduce as nr
def generate_false_start(word):
"""Creates progressive phonetic false starts optimized for TTS."""
clean_word = word.strip()
if len(clean_word) <= 2:
return f"{clean_word[0]}... {word}"
stutters = []
num_stutters = random.choices([1, 2], weights=[0.7, 0.3])[0]
if num_stutters == 1:
cut = random.randint(1, max(1, len(clean_word) - 2))
stutters.append(f"{clean_word[:cut]}...")
else:
cut1 = max(1, len(clean_word) // 3)
cut2 = max(cut1 + 1, int(len(clean_word) * 0.75))
stutters.append(f"{clean_word[:cut1]}...")
if cut2 < len(clean_word):
stutters.append(f"{clean_word[:cut2]}...")
return " ".join(stutters) + f" {word}"
nlp = None
def inject_aphasia_cues(text, severity=0.3):
"""Injects fillers, repetitions, and false starts into a sentence."""
global nlp
if nlp is None:
try:
nlp = spacy.load("en_core_web_sm")
except OSError:
raise RuntimeError(
"\n" + "="*60 + "\n"
"🚨 Missing Dependency: spaCy English Language Model\n"
"To use apply_aphasia=True, you must download the NLP model first.\n"
"Please run the following command in your terminal or Jupyter Notebook:\n\n"
" python -m spacy download en_core_web_sm\n\n"
+ "="*60
)
doc = nlp(text)
fillers = ["um...", "uh,", "uh...", "um,"]
manipulated_words = []
for token in doc:
is_content_word = token.pos_ in ["NOUN", "VERB", "ADJ", "PROPN"]
if is_content_word and random.random() < severity:
stutter_type = random.choices(
["filler", "repetition", "false_start"],
weights=[0.4, 0.2, 0.4]
)[0]
if stutter_type == "false_start":
manipulated_words.append(generate_false_start(token.text_with_ws))
continue
elif stutter_type == "repetition" and token.i > 0 and doc[token.i - 1].pos_ != "PUNCT":
prev_word = doc[token.i - 1].text
manipulated_words.append(f"... {prev_word} ")
elif stutter_type == "filler":
filler = random.choice(fillers)
manipulated_words.append(f"{filler} ")
manipulated_words.append(token.text_with_ws)
final_text = "".join(manipulated_words)
final_text = re.sub(r'\s+([?.!,])', r'\1', final_text)
return re.sub(r'\s{2,}', ' ', final_text).strip()
class AphaVoicePipeline:
def __init__(self, config_path="config.yaml", model_path="aphavoice_model.ckpt", vocoder_path="vocoder"):
warnings.filterwarnings("ignore", category=FutureWarning)
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 1. Load Config
with open(config_path, "r") as f:
self.config = yaml.safe_load(f)
self.mel_mean = self.config['data']['mel_mean']
self.mel_std = self.config['data']['mel_std']
# 2. Load the Matcha Model using PyTorch Lightning's loader!
from matcha.models.matcha_tts import MatchaTTS
self.model = MatchaTTS.load_from_checkpoint(
model_path,
map_location=self.device,
strict=False,
n_vocab=self.config['model']['n_vocab'],
n_spks=self.config['model']['n_spks'],
spk_emb_dim=self.config['model']['spk_emb_dim'],
weights_only=False
).to(self.device)
self.model.mel_mean = torch.full((1, 80, 1), self.mel_mean).to(self.device)
self.model.mel_std = torch.full((1, 80, 1), self.mel_std).to(self.device)
self.model.eval()
# 3. Load the Vocoder using your exact original logic!
from matcha.hifigan.models import Generator as HiFiGAN
from matcha.hifigan.config import v1
class AttrDict(dict):
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self
h = AttrDict(v1)
self.vocoder = HiFiGAN(h).to(self.device)
state_dict = torch.load(vocoder_path, map_location=self.device, weights_only=False)
if 'generator' in state_dict:
self.vocoder.load_state_dict(state_dict['generator'])
else:
self.vocoder.load_state_dict(state_dict)
self.vocoder.eval()
self.vocoder.remove_weight_norm()
def synthesise(self, text, speaker_id=0, apply_aphasia=True, severity=0.3, length_scale=1.0):
"""
The main generation function incorporating all custom post-processing and denoising.
"""
from matcha.text import text_to_sequence
from matcha.utils.utils import intersperse
# Step 1: Apply textual dysfluency if toggled
if apply_aphasia:
text = inject_aphasia_cues(text, severity=severity)
with torch.no_grad():
# Step 2: Convert text to sequence & handle tuple return
sequence_output = text_to_sequence(text, [self.config['data']['cleaner']])
if isinstance(sequence_output, tuple):
sequence = sequence_output[0]
else:
sequence = sequence_output
# Interpserse adds blank tokens (0) between phonemes for better rhythm
sequence = intersperse(sequence, 0)
x = torch.tensor(sequence, dtype=torch.long, device=self.device).unsqueeze(0)
x_lengths = torch.tensor([x.size(-1)], dtype=torch.long, device=self.device)
# Step 3: Handle multi-speaker ID
spk_tensor = torch.tensor([speaker_id], dtype=torch.long, device=self.device)
# Step 4: Generate Mel-Spectrogram
output = self.model.synthesise(
x,
x_lengths,
n_timesteps=self.config['inference']['n_timesteps'],
temperature=self.config['inference']['temperature'],
spks=spk_tensor,
length_scale=length_scale
)
mel = output['mel']
# Step 5: 4D Tensor Fix (Prevents shape mismatch in vocoder)
if mel.ndim == 4:
if mel.shape[1] == mel.shape[2] == 80:
mel = mel[:, 0, :, :]
else:
mel = mel.squeeze(1)
# Step 6: Vocoder to Audio
audio = self.vocoder(mel).squeeze()
# Step 7: Peak Normalization
audio = audio / (torch.max(torch.abs(audio)) + 1e-6)
audio_np = audio.cpu().numpy()
# Step 8: Spectral Denoising
clean_audio_np = nr.reduce_noise(
y=audio_np,
sr=self.config['data']['sample_rate'],
prop_decrease=0.20,
n_fft=1024,
win_length=1024
)
# Return sample rate, audio array (for Gradio player), and final manipulated text
return self.config['data']['sample_rate'], clean_audio_np, text |