From 44e555c83e73c25446e320a73388106ff10e9b14 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Mon, 28 Apr 2025 09:19:48 +0200 Subject: [PATCH] added voice formulas for mixed text --- abogen/conversion.py | 6 +++++- abogen/gui.py | 14 +++++++++++-- abogen/voice_formulas.py | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 abogen/voice_formulas.py diff --git a/abogen/conversion.py b/abogen/conversion.py index c2eadb3..66386dd 100644 --- a/abogen/conversion.py +++ b/abogen/conversion.py @@ -9,6 +9,7 @@ from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButt import soundfile as sf from utils import clean_text from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS +from voice_formulas import get_new_voice def get_sample_voice_text(lang_code): @@ -346,9 +347,12 @@ class ConversionThread(QThread): # Set split_pattern to \n+ which will split on one or more newlines split_pattern = r"\n+" + + loaded_voice = get_new_voice(tts, self.voice, self.use_gpu) + for result in tts( chapter_text, - voice=self.voice, + voice=loaded_voice, speed=self.speed, split_pattern=split_pattern, ): diff --git a/abogen/gui.py b/abogen/gui.py index c8e30ed..269e12e 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -567,6 +567,13 @@ class abogen(QWidget): '"a" => American English\n"b" => British English\n"e" => Spanish\n"f" => French\n"h" => Hindi\n"i" => Italian\n"j" => Japanese\n"p" => Brazilian Portuguese\n"z" => Mandarin Chinese\nThe second character represents the gender:\n"m" => Male\n"f" => Female' ) voice_row.addWidget(self.voice_combo) + + # voice formula - text box for now, add a dialog later + self.voice_formula = QTextEdit(self) + self.voice_formula.setAcceptRichText(False) + self.voice_formula.setPlaceholderText("Enter voice formula here...") + self.voice_formula.setText('0.242 * am_michael + 0.758 * bf_isabella') + voice_row.addWidget(self.voice_formula) # Play/Stop icons def make_icon(color, shape): @@ -1037,12 +1044,15 @@ class abogen(QWidget): if not self.subtitle_combo.isEnabled() else self.subtitle_mode ) - + + voice_formula = self.voice_formula.toPlainText() + self.conversion_thread = ConversionThread( self.selected_file, self.selected_lang, speed, - self.selected_voice, + #self.selected_voice, + voice_formula, self.save_option, self.selected_output_folder, subtitle_mode=actual_subtitle_mode, diff --git a/abogen/voice_formulas.py b/abogen/voice_formulas.py new file mode 100644 index 0000000..29272e5 --- /dev/null +++ b/abogen/voice_formulas.py @@ -0,0 +1,43 @@ +from constants import VOICES_INTERNAL + +# Calls parsing and loads the voice to gpu or cpu +def get_new_voice(pipeline, formula, use_gpu): + try: + weighted_voice = parse_voice_formula(pipeline, formula) + device = "cuda" if use_gpu else "cpu" + return weighted_voice.to(device) + except Exception as e: + raise ValueError(f"Failed to create voice: {str(e)}") + +# Parse the formula and get the combined voice tensor +def parse_voice_formula(pipeline, formula): + """Parse the voice formula string and return the combined voice tensor.""" + if not formula.strip(): + raise ValueError("Empty voice formula") + + # Initialize the weighted sum + weighted_sum = None + + # Split the formula into terms + terms = formula.split('+') + + for term in terms: + # Parse each term (format: "0.333 * voice_name") + weight, voice_name = term.strip().split('*') + weight = float(weight.strip()) + voice_name = voice_name.strip() + + # Get the voice tensor + # use VOICES_INTERNAL + if voice_name not in VOICES_INTERNAL: + raise ValueError(f"Unknown voice: {voice_name}") + + voice_tensor = pipeline.load_single_voice(voice_name) + + # Add to weighted sum + if weighted_sum is None: + weighted_sum = weight * voice_tensor + else: + weighted_sum += weight * voice_tensor + + return weighted_sum \ No newline at end of file