diff --git a/README.md b/README.md index 570aa96..218fd0c 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ Feel free to explore the code and make any changes you like. - Abogen uses [Kokoro](https://github.com/hexgrad/kokoro) for its high-quality, natural-sounding text-to-speech synthesis. Huge thanks to the Kokoro team for making this possible. - Thanks to [@wojiushixiaobai](https://github.com/wojiushixiaobai) for [Embedded Python](https://github.com/wojiushixiaobai/Python-Embed-Win64) packages. These modified packages include pip pre-installed, enabling Abogen to function as a standalone application without requiring users to separately install Python in Windows. - Special thanks to the [PyQt](https://www.riverbankcomputing.com/software/pyqt/) team for providing the cross-platform GUI toolkit that powers Abogen's interface. +- [Female](https://icons8.com/icon/uI49hxbpxTkp/female), [Male](https://icons8.com/icon/12351/male) and [Mix](https://icons8.com/icon/21700/adjust) icons by [Icons8](https://icons8.com/). ## `License` This project is available under the MIT License - see the [LICENSE](https://github.com/denizsafak/abogen/blob/main/LICENSE) file for details. diff --git a/abogen/assets/female.png b/abogen/assets/female.png new file mode 100644 index 0000000..250dca3 Binary files /dev/null and b/abogen/assets/female.png differ diff --git a/abogen/assets/male.png b/abogen/assets/male.png new file mode 100644 index 0000000..3455b92 Binary files /dev/null and b/abogen/assets/male.png differ diff --git a/abogen/assets/voice_mixer.png b/abogen/assets/voice_mixer.png new file mode 100644 index 0000000..31a71d7 Binary files /dev/null and b/abogen/assets/voice_mixer.png differ diff --git a/abogen/gui.py b/abogen/gui.py index bf04f19..1f52886 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -574,7 +574,8 @@ class abogen(QWidget): # Voice formula button self.btn_voice_formula_mixer = QPushButton(self) - self.btn_voice_formula_mixer.setText("🛠") # TODO add voice formula icon + mixer_icon_path = get_resource_path("abogen.assets", "voice_mixer.png") + self.btn_voice_formula_mixer.setIcon(QIcon(mixer_icon_path)) self.btn_voice_formula_mixer.setToolTip("Mix and match voices") self.btn_voice_formula_mixer.setFixedSize(40, 36) self.btn_voice_formula_mixer.setStyleSheet("QPushButton { padding: 6px 12px; }") @@ -1318,6 +1319,7 @@ class abogen(QWidget): self.btn_preview.setEnabled(False) self.btn_preview.setToolTip("Loading...") self.voice_combo.setEnabled(False) + self.btn_voice_formula_mixer.setEnabled(False) # Disable mixer button self.btn_start.setEnabled(False) # Disable start button during preview # start loading animation self.loading_movie.start() @@ -1339,6 +1341,7 @@ class abogen(QWidget): self.btn_preview.setEnabled(True) self.btn_preview.setToolTip("Preview selected voice") self.voice_combo.setEnabled(True) + self.btn_voice_formula_mixer.setEnabled(True) # Re-enable mixer button self.btn_start.setEnabled(True) # Re-enable start button on error return @@ -1365,6 +1368,7 @@ class abogen(QWidget): self.btn_preview.setEnabled(True) self.btn_preview.setToolTip("Preview selected voice") self.voice_combo.setEnabled(True) + self.btn_voice_formula_mixer.setEnabled(True) # Re-enable mixer button self.btn_start.setEnabled(True) return # stop loading animation, switch to stop icon @@ -1414,6 +1418,7 @@ class abogen(QWidget): self.btn_preview.setToolTip("Preview selected voice") self.btn_preview.setEnabled(True) self.voice_combo.setEnabled(True) + self.btn_voice_formula_mixer.setEnabled(True) # Re-enable mixer button self.btn_start.setEnabled(True) def _preview_error(self, msg): diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py index acde5ac..395dcac 100644 --- a/abogen/voice_formula_gui.py +++ b/abogen/voice_formula_gui.py @@ -16,10 +16,12 @@ from PyQt5.QtWidgets import ( QStyle, ) from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize +from PyQt5.QtGui import QPixmap from constants import VOICES_INTERNAL, FLAGS +from utils import get_resource_path # Constants -VOICE_MIXER_WIDTH = 160 +VOICE_MIXER_WIDTH = 120 SLIDER_WIDTH = 32 MIN_WINDOW_WIDTH = 600 MIN_WINDOW_HEIGHT = 400 @@ -119,6 +121,7 @@ class VoiceMixer(QWidget): ): super().__init__() self.voice_name = voice_name + #self.setFixedWidth(VOICE_MIXER_WIDTH) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # TODO Set CSS for rounded corners @@ -133,17 +136,18 @@ class VoiceMixer(QWidget): self.checkbox.stateChanged.connect(self.toggle_inputs) layout.addWidget(self.checkbox, alignment=Qt.AlignCenter) - # Voice name label - voice_gender = ( - FEMALE - if self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f" - else MALE - ) + # Voice name label with gender icon + is_female = self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f" + gender_icon_path = get_resource_path("abogen.assets", "female.png" if is_female else "male.png") name = voice_name[3:].capitalize() name_layout = QHBoxLayout() - name_layout.addWidget( - QLabel(f"{language_icon} {voice_gender} {name}"), alignment=Qt.AlignCenter - ) + # Gender icon + pixmap = QPixmap(gender_icon_path) + if not pixmap.isNull(): + gender_label = QLabel() + gender_label.setPixmap(pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + name_layout.addWidget(gender_label) + name_layout.addWidget(QLabel(f"{language_icon} {name}"), alignment=Qt.AlignCenter) layout.addLayout(name_layout) # Spinbox and slider @@ -211,26 +215,30 @@ class HoverLabel(QLabel): self.delete_button.setStyleSheet( """ QPushButton { - background-color: red; + background-color: #ff5555; color: white; - border-radius: 8px; + border-radius: 7px; font-weight: bold; font-size: 12px; border: none; padding: 0px; - text-align: center; + margin: 0px; } QPushButton:hover { - background-color: #ff5555; + background-color: red; } """ ) + # Make sure the entire button is clickable, not just the text + self.delete_button.setFocusPolicy(Qt.NoFocus) + self.delete_button.setAttribute(Qt.WA_TransparentForMouseEvents, False) self.delete_button.setCursor(Qt.PointingHandCursor) self.delete_button.hide() def resizeEvent(self, event): super().resizeEvent(event) - self.delete_button.move(self.width() - 16, 0) + # Position the button in the top-right corner with a small margin + self.delete_button.move(self.width() - 16, + 0) def enterEvent(self, event): self.delete_button.show() @@ -398,7 +406,8 @@ class VoiceFormulaDialog(QDialog): # Add voice labels for name, weight in selected: percentage = weight / total * 100 - voice_label = HoverLabel(f"{name}: {percentage:.1f}%", name) + # Make the voice name bold and include percentage + voice_label = HoverLabel(f"{name}: {percentage:.1f}%", name) voice_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) voice_label.delete_button.clicked.connect( lambda _, vn=name: self.disable_voice_by_name(vn)