diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c2fa7..84cc88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ - Added voice mixing functionality that enables combining multiple voices into a single "Mixed voice", as suggested by @PulsarFTW in #1. (Special thanks to @jborza for making this feature possible with his contributions in #5) +- Added icons for flags and genders in the GUI, making it easier to identify different options. - Switched to platformdirs for determining the correct desktop path, instead of using old methods. \ No newline at end of file diff --git a/README.md b/README.md index 218fd0c..ab5d746 100644 --- a/README.md +++ b/README.md @@ -163,7 +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/). +- [US](https://icons8.com/icon/aRiu1GGi6Aoe/usa), [Great Britain](https://icons8.com/icon/t3NE3BsOAQwq/great-britain), [Spain](https://icons8.com/icon/ly7tzANRt33n/spain), [France](https://icons8.com/icon/3muzEmi4dpD5/france), [India](https://icons8.com/icon/esGVrxg9VCJ1/india), [Italy](https://icons8.com/icon/PW8KZnP7qXzO/italy), [Japan](https://icons8.com/icon/McQbrq9qaQye/japan), [Brazil](https://icons8.com/icon/zHmH8HpOmM90/brazil), [China](https://icons8.com/icon/Ej50Oe3crXwF/china), [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/flags/a.png b/abogen/assets/flags/a.png new file mode 100644 index 0000000..a110f86 Binary files /dev/null and b/abogen/assets/flags/a.png differ diff --git a/abogen/assets/flags/b.png b/abogen/assets/flags/b.png new file mode 100644 index 0000000..859e950 Binary files /dev/null and b/abogen/assets/flags/b.png differ diff --git a/abogen/assets/flags/e.png b/abogen/assets/flags/e.png new file mode 100644 index 0000000..f647440 Binary files /dev/null and b/abogen/assets/flags/e.png differ diff --git a/abogen/assets/flags/f.png b/abogen/assets/flags/f.png new file mode 100644 index 0000000..f66e9cc Binary files /dev/null and b/abogen/assets/flags/f.png differ diff --git a/abogen/assets/flags/h.png b/abogen/assets/flags/h.png new file mode 100644 index 0000000..a3a4209 Binary files /dev/null and b/abogen/assets/flags/h.png differ diff --git a/abogen/assets/flags/i.png b/abogen/assets/flags/i.png new file mode 100644 index 0000000..d602e54 Binary files /dev/null and b/abogen/assets/flags/i.png differ diff --git a/abogen/assets/flags/j.png b/abogen/assets/flags/j.png new file mode 100644 index 0000000..4bcf2f9 Binary files /dev/null and b/abogen/assets/flags/j.png differ diff --git a/abogen/assets/flags/p.png b/abogen/assets/flags/p.png new file mode 100644 index 0000000..9913de8 Binary files /dev/null and b/abogen/assets/flags/p.png differ diff --git a/abogen/assets/flags/z.png b/abogen/assets/flags/z.png new file mode 100644 index 0000000..104dcbd Binary files /dev/null and b/abogen/assets/flags/z.png differ diff --git a/abogen/constants.py b/abogen/constants.py index e407ae6..a8fdfef 100644 --- a/abogen/constants.py +++ b/abogen/constants.py @@ -102,16 +102,3 @@ SAMPLE_VOICE_TEXTS = { "p": "Este é um exemplo da voz selecionada.", "z": "这是所选语音的示例。", } - -# flags mapping for voice display -FLAGS = { - "a": "🇺🇸", - "b": "🇬🇧", - "e": "🇪🇸", - "f": "🇫🇷", - "h": "🇮🇳", - "i": "🇮🇹", - "j": "🇯🇵", - "p": "🇧🇷", - "z": "🇨🇳", -} diff --git a/abogen/gui.py b/abogen/gui.py index 2cc5f4d..0dc7f2a 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -66,7 +66,6 @@ from constants import ( GITHUB_URL, PROGRAM_DESCRIPTION, LANGUAGE_DESCRIPTIONS, - FLAGS, VOICES_INTERNAL, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, ) @@ -560,9 +559,19 @@ class abogen(QWidget): voice_layout.addWidget(QLabel("Select Voice:", self)) voice_row = QHBoxLayout() self.voice_combo = QComboBox(self) + for v in VOICES_INTERNAL: - flag = FLAGS.get(v[0], "") - self.voice_combo.addItem(f"{flag} {v}", v) + # Get flag icon for this voice + lang_code = v[0] + flag_path = get_resource_path("abogen.assets.flags", f"{lang_code}.png") + + icon = QIcon() + if flag_path and os.path.exists(flag_path): + icon = QIcon(flag_path) + + # Add item with flag icon and voice name + self.voice_combo.addItem(icon, f"{v}", v) + self.voice_combo.setStyleSheet( "QComboBox { min-height: 20px; padding: 6px 12px; }" ) diff --git a/abogen/utils.py b/abogen/utils.py index 56d90dc..300faa8 100644 --- a/abogen/utils.py +++ b/abogen/utils.py @@ -32,6 +32,12 @@ def get_resource_path(package, resource): except (ImportError, FileNotFoundError): pass + # Always try to resolve as a relative path from this file + parts = package.split('.') + rel_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), *parts[1:], resource) + if os.path.exists(rel_path): + return rel_path + # Fallback to local file system try: # Extract the subdirectory from package name (e.g., 'assets' from 'abogen.assets') diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py index 395dcac..4329d7b 100644 --- a/abogen/voice_formula_gui.py +++ b/abogen/voice_formula_gui.py @@ -17,11 +17,11 @@ from PyQt5.QtWidgets import ( ) from PyQt5.QtCore import Qt, QTimer, QPoint, QRect, QSize from PyQt5.QtGui import QPixmap -from constants import VOICES_INTERNAL, FLAGS +from constants import VOICES_INTERNAL from utils import get_resource_path # Constants -VOICE_MIXER_WIDTH = 120 +VOICE_MIXER_WIDTH = 100 SLIDER_WIDTH = 32 MIN_WINDOW_WIDTH = 600 MIN_WINDOW_HEIGHT = 400 @@ -117,11 +117,11 @@ class FlowLayout(QLayout): class VoiceMixer(QWidget): def __init__( - self, voice_name, language_icon, initial_status=False, initial_weight=0.0 + self, voice_name, language_code, initial_status=False, initial_weight=0.0 ): super().__init__() self.voice_name = voice_name - #self.setFixedWidth(VOICE_MIXER_WIDTH) + self.setFixedWidth(VOICE_MIXER_WIDTH) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # TODO Set CSS for rounded corners @@ -130,26 +130,39 @@ class VoiceMixer(QWidget): layout = QVBoxLayout() - # Checkbox + # Name label at the top + name = voice_name + layout.addWidget(QLabel(name), alignment=Qt.AlignCenter) + + # Voice name label with gender icon + is_female = self.voice_name in VOICES_INTERNAL and self.voice_name[1] == "f" + + # Icons layout (flag and gender) + icons_layout = QHBoxLayout() + icons_layout.setSpacing(3) + icons_layout.setAlignment(Qt.AlignCenter) # Center the icons horizontally + + # Flag icon + flag_icon_path = get_resource_path("abogen.assets.flags", f"{language_code}.png") + gender_icon_path = get_resource_path("abogen.assets", "female.png" if is_female else "male.png") + flag_label = QLabel() + gender_label = QLabel() + flag_pixmap = QPixmap(flag_icon_path) + flag_label.setPixmap(flag_pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + gender_pixmap = QPixmap(gender_icon_path) + gender_label.setPixmap(gender_pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation)) + icons_layout.addWidget(flag_label) + icons_layout.addWidget(gender_label) + + # Add icons layout + layout.addLayout(icons_layout) + + # Checkbox (now below icons) self.checkbox = QCheckBox() self.checkbox.setChecked(initial_status) self.checkbox.stateChanged.connect(self.toggle_inputs) layout.addWidget(self.checkbox, alignment=Qt.AlignCenter) - # 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() - # 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 self.spin_box = QDoubleSpinBox() self.spin_box.setRange(0, 1) @@ -212,6 +225,8 @@ class HoverLabel(QLabel): # Create delete button self.delete_button = QPushButton("×", self) self.delete_button.setFixedSize(16, 16) + self.delete_button = QPushButton("×", self) + self.delete_button.setFixedSize(16, 16) self.delete_button.setStyleSheet( """ QPushButton { @@ -251,6 +266,9 @@ class VoiceFormulaDialog(QDialog): def __init__(self, parent=None, initial_state=None): super().__init__(parent) self.setWindowTitle("Voice Mixer") + self.setWindowFlags( + Qt.Window | Qt.WindowCloseButtonHint | Qt.WindowMaximizeButtonHint + ) self.setMinimumSize(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT) self.resize(INITIAL_WINDOW_WIDTH, INITIAL_WINDOW_HEIGHT) self.voice_mixers = [] @@ -334,13 +352,13 @@ class VoiceFormulaDialog(QDialog): def add_voices(self, initial_state): first_enabled_voice = None for voice in VOICES_INTERNAL: - flag = FLAGS.get(voice[0], "") + language_code = voice[0] # First character is the language code matching_voice = next( (item for item in initial_state if item[0] == voice), None ) initial_status = matching_voice is not None initial_weight = matching_voice[1] if matching_voice else 1.0 - voice_mixer = self.add_voice(voice, flag, initial_status, initial_weight) + voice_mixer = self.add_voice(voice, language_code, initial_status, initial_weight) if initial_status and first_enabled_voice is None: first_enabled_voice = voice_mixer @@ -350,10 +368,10 @@ class VoiceFormulaDialog(QDialog): ) def add_voice( - self, voice_name, language_icon, initial_status=False, initial_weight=1.0 + self, voice_name, language_code, initial_status=False, initial_weight=1.0 ): voice_mixer = VoiceMixer( - voice_name, language_icon, initial_status, initial_weight + voice_name, language_code, initial_status, initial_weight ) self.voice_mixers.append(voice_mixer) self.voice_list_layout.addWidget(voice_mixer)