Merge pull request #8 from denizsafak/beta

Imrpovements for voice mixer
This commit is contained in:
Deniz Şafak
2025-05-02 20:12:47 +03:00
committed by GitHub
3 changed files with 64 additions and 24 deletions
+4 -8
View File
@@ -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. - Fixed the issue when a voice is selected, the voice mixer tries to pre-select that voice and ignores existing profiles.
- Added profile system to voice mixer, allowing users to create and manage multiple voice profiles. - Fixed the error while renaming the default "New profile" in the voice mixer.
- Improvements in the voice mixer, mostly for organizing controls and enhancing user experience. - Prevented using special characters in the profile name to avoid conflicts.
- Added icons for flags and genders in the GUI, making it easier to identify different options. - Improved invalid profile handling in the voice mixer.
- 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.
+16 -5
View File
@@ -1820,25 +1820,36 @@ class abogen(QWidget):
save_config(self.config) save_config(self.config)
def show_voice_formula_dialog(self): 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 initial_state = None
selected_profile = self.selected_profile_name selected_profile = self.selected_profile_name
if selected_profile: if selected_profile:
entry = load_profiles().get(selected_profile, {}) entry = profiles.get(selected_profile, {})
# support new dict format
if isinstance(entry, dict): if isinstance(entry, dict):
initial_state = entry.get("voices", []) initial_state = entry.get("voices", [])
else: else:
initial_state = entry initial_state = entry
elif self.mixed_voice_state is not None: elif self.mixed_voice_state is not None:
initial_state = self.mixed_voice_state 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: else:
initial_state = [(self.selected_voice, 1.0)] if self.selected_voice else [] initial_state = []
dialog = VoiceFormulaDialog( dialog = VoiceFormulaDialog(
self, initial_state=initial_state, selected_profile=selected_profile self, initial_state=initial_state, selected_profile=selected_profile
) )
if dialog.exec_() == QDialog.Accepted: if dialog.exec_() == QDialog.Accepted:
# After OK, select the profile in combo and update config
if dialog.current_profile: if dialog.current_profile:
self.selected_profile_name = dialog.current_profile self.selected_profile_name = dialog.current_profile
self.config["selected_profile_name"] = dialog.current_profile self.config["selected_profile_name"] = dialog.current_profile
+44 -11
View File
@@ -846,10 +846,10 @@ class VoiceFormulaDialog(QDialog):
self.update_weighted_sums() self.update_weighted_sums()
def _handle_zero_weight_profiles(self): def _handle_zero_weight_profiles(self):
if self.profile_list.count() <= 1: profiles = load_profiles()
if len(profiles) < 1:
return False return False
zero = [] zero = []
profiles = load_profiles()
for i in range(self.profile_list.count()): for i in range(self.profile_list.count()):
item = self.profile_list.item(i) item = self.profile_list.item(i)
name = item.text().lstrip("*") name = item.text().lstrip("*")
@@ -868,7 +868,6 @@ class VoiceFormulaDialog(QDialog):
if not zero: if not zero:
return False 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?" 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( reply = QMessageBox.question(
self, self,
"Invalid Profiles", "Invalid Profiles",
@@ -963,8 +962,17 @@ class VoiceFormulaDialog(QDialog):
self.update_profile_list_colors() self.update_profile_list_colors()
def new_profile(self): def new_profile(self):
name, ok = QInputDialog.getText(self, "New Profile", "Enter profile name:") import re
if ok and name: 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() profiles = load_profiles()
# Remove 'New profile' placeholder if not persisted in JSON # Remove 'New profile' placeholder if not persisted in JSON
if ( if (
@@ -977,7 +985,7 @@ class VoiceFormulaDialog(QDialog):
self._profile_dirty.pop("New profile", None) self._profile_dirty.pop("New profile", None)
if name in profiles: if name in profiles:
QMessageBox.warning(self, "Duplicate Name", "Profile already exists.") QMessageBox.warning(self, "Duplicate Name", "Profile already exists.")
return continue
profiles[name] = { profiles[name] = {
"voices": [], "voices": [],
"language": self.language_combo.currentData(), "language": self.language_combo.currentData(),
@@ -996,6 +1004,7 @@ class VoiceFormulaDialog(QDialog):
parent = self.parent() parent = self.parent()
if hasattr(parent, "populate_profiles_in_voice_combo"): if hasattr(parent, "populate_profiles_in_voice_combo"):
parent.populate_profiles_in_voice_combo() parent.populate_profiles_in_voice_combo()
break
self.update_profile_save_buttons() self.update_profile_save_buttons()
self.update_profile_list_colors() self.update_profile_list_colors()
self.update_weighted_sums() self.update_weighted_sums()
@@ -1182,21 +1191,38 @@ class VoiceFormulaDialog(QDialog):
) )
def rename_profile(self, item): 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("*") old = item.text().lstrip("*")
new, ok = QInputDialog.getText( import re
self, "Rename Profile", f"Profile name:", text=old while True:
) new, ok = QInputDialog.getText(
if ok and new and new != old: 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() profiles = load_profiles()
if new in profiles: if new in profiles:
QMessageBox.warning(self, "Duplicate Name", "Profile already exists.") QMessageBox.warning(self, "Duplicate Name", "Profile already exists.")
return continue
profiles[new] = profiles.pop(old) profiles[new] = profiles.pop(old)
save_profiles(profiles) save_profiles(profiles)
item.setText(new) item.setText(new)
parent = self.parent() parent = self.parent()
if hasattr(parent, "populate_profiles_in_voice_combo"): if hasattr(parent, "populate_profiles_in_voice_combo"):
parent.populate_profiles_in_voice_combo() parent.populate_profiles_in_voice_combo()
break
self.update_profile_save_buttons() self.update_profile_save_buttons()
self.update_profile_list_colors() self.update_profile_list_colors()
@@ -1227,6 +1253,13 @@ class VoiceFormulaDialog(QDialog):
self.update_profile_list_colors() self.update_profile_list_colors()
def duplicate_profile(self, item): 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("*") src = item.text().lstrip("*")
profiles = load_profiles() profiles = load_profiles()
base = f"{src}_duplicate" base = f"{src}_duplicate"