diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7ab61..85645c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,4 @@ -- Added voice mixing, allowing multiple voices to be combined into a single “Mixed Voice”, a feature mentioned by @PulsarFTW in #1. Special thanks to @jborza for making this possible through his contributions in #5. -- Added profile system to voice mixer, allowing users to create and manage multiple voice profiles. -- Improvements in the voice mixer, mostly for organizing controls and enhancing user experience. -- Added icons for flags and genders in the GUI, making it easier to identify different options. -- Improved the content and chapter extraction process for EPUB files, ensuring better handling of various structures. -- Switched to platformdirs for determining the correct desktop path, instead of using old methods. -- Fixed preview voices was not using GPU acceleration, which was causing performance issues. -- Improvements in code and documentation. \ No newline at end of file +- Fixed the issue when a voice is selected, the voice mixer tries to pre-select that voice and ignores existing profiles. +- Fixed the error while renaming the default "New profile" in the voice mixer. +- Prevented using special characters in the profile name to avoid conflicts. +- Improved invalid profile handling in the voice mixer. \ No newline at end of file diff --git a/abogen/gui.py b/abogen/gui.py index 3f7ceec..8587a63 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1820,25 +1820,36 @@ class abogen(QWidget): save_config(self.config) def show_voice_formula_dialog(self): - # If a profile is selected in combo, open mixer with that profile + from voice_profiles import load_profiles + profiles = load_profiles() initial_state = None selected_profile = self.selected_profile_name if selected_profile: - entry = load_profiles().get(selected_profile, {}) - # support new dict format + entry = profiles.get(selected_profile, {}) if isinstance(entry, dict): initial_state = entry.get("voices", []) else: initial_state = entry elif self.mixed_voice_state is not None: initial_state = self.mixed_voice_state + elif self.selected_voice: + # If a single voice is selected, default to first profile if available + if profiles: + first_profile = next(iter(profiles)) + entry = profiles[first_profile] + selected_profile = first_profile + if isinstance(entry, dict): + initial_state = entry.get("voices", []) + else: + initial_state = entry + else: + initial_state = [] else: - initial_state = [(self.selected_voice, 1.0)] if self.selected_voice else [] + initial_state = [] dialog = VoiceFormulaDialog( self, initial_state=initial_state, selected_profile=selected_profile ) if dialog.exec_() == QDialog.Accepted: - # After OK, select the profile in combo and update config if dialog.current_profile: self.selected_profile_name = dialog.current_profile self.config["selected_profile_name"] = dialog.current_profile diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py index dc92b5d..75aeebf 100644 --- a/abogen/voice_formula_gui.py +++ b/abogen/voice_formula_gui.py @@ -846,10 +846,10 @@ class VoiceFormulaDialog(QDialog): self.update_weighted_sums() def _handle_zero_weight_profiles(self): - if self.profile_list.count() <= 1: + profiles = load_profiles() + if len(profiles) < 1: return False zero = [] - profiles = load_profiles() for i in range(self.profile_list.count()): item = self.profile_list.item(i) name = item.text().lstrip("*") @@ -868,7 +868,6 @@ class VoiceFormulaDialog(QDialog): if not zero: return False msg = f"{len(zero)} invalid profile(s) with no voices selected or their total weights are 0. They will be ignored and deleted. Do you want to delete?" - # Use Delete instead of Ignore reply = QMessageBox.question( self, "Invalid Profiles", @@ -963,8 +962,17 @@ class VoiceFormulaDialog(QDialog): self.update_profile_list_colors() def new_profile(self): - name, ok = QInputDialog.getText(self, "New Profile", "Enter profile name:") - if ok and name: + import re + while True: + name, ok = QInputDialog.getText(self, "New Profile", "Enter profile name:") + if not ok or not name: + break + name = name.strip() # Remove leading/trailing spaces + if not name: + continue + if not re.match(r'^[\w\- ]+$', name): + QMessageBox.warning(self, "Invalid Name", "Profile name can only contain letters, numbers, spaces, underscores, and hyphens.") + continue profiles = load_profiles() # Remove 'New profile' placeholder if not persisted in JSON if ( @@ -977,7 +985,7 @@ class VoiceFormulaDialog(QDialog): self._profile_dirty.pop("New profile", None) if name in profiles: QMessageBox.warning(self, "Duplicate Name", "Profile already exists.") - return + continue profiles[name] = { "voices": [], "language": self.language_combo.currentData(), @@ -996,6 +1004,7 @@ class VoiceFormulaDialog(QDialog): parent = self.parent() if hasattr(parent, "populate_profiles_in_voice_combo"): parent.populate_profiles_in_voice_combo() + break self.update_profile_save_buttons() self.update_profile_list_colors() self.update_weighted_sums() @@ -1182,21 +1191,38 @@ class VoiceFormulaDialog(QDialog): ) def rename_profile(self, item): + name = item.text().lstrip("*") + # block if profile has unsaved changes + if self._profile_dirty.get(name, False): + QMessageBox.warning( + self, "Unsaved Changes", "Please save the profile before renaming." + ) + return old = item.text().lstrip("*") - new, ok = QInputDialog.getText( - self, "Rename Profile", f"Profile name:", text=old - ) - if ok and new and new != old: + import re + while True: + new, ok = QInputDialog.getText( + self, "Rename Profile", f"Profile name:", text=old + ) + if not ok or not new or new == old: + break + new = new.strip() # Remove leading/trailing spaces + if not new: + continue + if not re.match(r'^[\w\- ]+$', new): + QMessageBox.warning(self, "Invalid Name", "Profile name can only contain letters, numbers, spaces, underscores, and hyphens.") + continue profiles = load_profiles() if new in profiles: QMessageBox.warning(self, "Duplicate Name", "Profile already exists.") - return + continue profiles[new] = profiles.pop(old) save_profiles(profiles) item.setText(new) parent = self.parent() if hasattr(parent, "populate_profiles_in_voice_combo"): parent.populate_profiles_in_voice_combo() + break self.update_profile_save_buttons() self.update_profile_list_colors() @@ -1227,6 +1253,13 @@ class VoiceFormulaDialog(QDialog): self.update_profile_list_colors() def duplicate_profile(self, item): + name = item.text().lstrip("*") + # block duplicating if profile has unsaved changes + if self._profile_dirty.get(name, False): + QMessageBox.warning( + self, "Unsaved Changes", "Please save the profile before duplicating." + ) + return src = item.text().lstrip("*") profiles = load_profiles() base = f"{src}_duplicate"