From 4acf70954e4524df02451155866596b53571cd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Wed, 30 Apr 2025 18:45:02 +0300 Subject: [PATCH] Added icons for flags, improvements in voice mixer --- CHANGELOG.md | 1 + README.md | 2 +- abogen/assets/flags/a.png | Bin 0 -> 484 bytes abogen/assets/flags/b.png | Bin 0 -> 531 bytes abogen/assets/flags/e.png | Bin 0 -> 331 bytes abogen/assets/flags/f.png | Bin 0 -> 306 bytes abogen/assets/flags/h.png | Bin 0 -> 373 bytes abogen/assets/flags/i.png | Bin 0 -> 306 bytes abogen/assets/flags/j.png | Bin 0 -> 314 bytes abogen/assets/flags/p.png | Bin 0 -> 464 bytes abogen/assets/flags/z.png | Bin 0 -> 317 bytes abogen/constants.py | 13 -------- abogen/gui.py | 15 +++++++-- abogen/utils.py | 6 ++++ abogen/voice_formula_gui.py | 64 +++++++++++++++++++++++------------- 15 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 abogen/assets/flags/a.png create mode 100644 abogen/assets/flags/b.png create mode 100644 abogen/assets/flags/e.png create mode 100644 abogen/assets/flags/f.png create mode 100644 abogen/assets/flags/h.png create mode 100644 abogen/assets/flags/i.png create mode 100644 abogen/assets/flags/j.png create mode 100644 abogen/assets/flags/p.png create mode 100644 abogen/assets/flags/z.png 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 0000000000000000000000000000000000000000..a110f86bb8e2e112796442afbc7f6a7a5d7d7a1f GIT binary patch literal 484 zcmV;kt|eR4C_AgIwL zbT@x-JB4if|2aDF*vH=X&Npw~@Zr=vVfK|LDMg3> zmo}dIKXLZe{~ePr{?D&H{y(Gq=>PXi790cPX%m*bb1~+}Fd#bn!20s$Q~wKUPyCNa z-~T_Yd$6)wFO=%~FpCi)u{Yxx7^gkkF-~Zs0z5gRK_WcL(<8lxF zcM02$aKVxV$KEYoaO`7!g&N!dhoEiMAph2No&BF&c<4XO{~&%=#nJ!CMTb#b0Cq)r zZZ_P2%!;GyN}CY=Mh=JmnODGGKz0ELe^|K~;f>m^Q$3wi&mY5w(OvKXP))yy_@yt&)*kQ=3hI6%Yfvf!}p(EymjPtdU7v{=8w~+sXw(gfBtd$ltcgj z{Q3L#-S@L^-v2lNGvL|Fug8v_c((stPxrp3R%S2Wb+qpP|Np-#+<^c8|L6X`bNk@u zRV$ADdT{^Ye+YoP;Q#;syMEodeFG%_`^n=c|NsAg`Tzg_Y`6hW!J!RNOguK3k8Hqv zk`1shormm&od5s--~Rjg^QX_NS04X<^2Fu;|Nno4DgOKS|4ooT{(k@d?aP|g$G;!g ze-fkz*#MOQVQYGe82RZ7y)n-?A*c=PthA-HYNzZ_e)?f#*^fB%2}dH(#hr;c`4 zUq?o2pv1(BK)=ba_Z&P>+jZtNu4oKR+57V0lg}sr|NXlVBOP41@-qfp^8#2?qrLxn0Z?+qnv?^!{(m>BlHUV4J2x=q;006py VwROOx%%cDR002ovPDHLkV1nOE8|VN4 literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/e.png b/abogen/assets/flags/e.png new file mode 100644 index 0000000000000000000000000000000000000000..f6474407b271343c652508af1fa784d7dab3f657 GIT binary patch literal 331 zcmV-R0kr;!P)im`5-koHQ&);IFGIo18Xy!#cIggYUZ}rSxm=>!63CLn*Z{m=h{?Ei_v2iX7qKgf;n&O zZw}@Ae|$mS{}-EN2)Kf628IF0YApWudT9M$mKp#5Sd%?T29$yX_sp~WzbaYd|K>#f z|9j?I6EdKfvOwI=u0XwL1OaF&JV$myLNa6l)}*Y>@aL{J!#tdtkpU><+|gzzMaoR2 dL}ey&00463Z^aQxU_1Z-002ovPDHLkV1j-wlTH8t literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/f.png b/abogen/assets/flags/f.png new file mode 100644 index 0000000000000000000000000000000000000000..f66e9cc92734c6670c4ff7b00360575c9e18d3a9 GIT binary patch literal 306 zcmV-20nPr2P)kl8wU%&e>4~#+VCr{J~YP9!XFJKqA zW%}aPH;%n||M5RE{=B&7*nb;_>Hl3A__1m>_SwW`AGr0Hec;yri&kI9FyPCg&SU>= z8UFvbVL0{Qo`DC$0K34g^Ff*k8UVrbP&C^FZBeod-13`f18f-n{I_RNLD(L!wFst} zNEd(%sX_!^z}6XL7_fj+1Ljla1zUzn>IEXj(a6BSz+e%&iqk%D>ro;S(#ij>4BQw7 zfPhQrZhpK;*_L4%&ZLYCz?td&r!r_}nhRzVlbOf?0Mx_6KP)RCt_YV4wx~KZ`;8{|ttj|1%gCg7I{Q>i^Rjvxp zf0wBo`@2MmIJ`{d*pCa7)!+tv`SPXW>({UUk@1Tc@BeSvas!NCzWjh92f|;zd?|+; z@b&B01?UR4ZM*S*!GiPu7cM*xW~0l0{rYttvH|n48359Z$AGzbz3}qo2jac3?!r2? z;JM+)g64)1g+t~>99@2L9U>4xz`WOHiAA5yf1+@&^&$pzU^dZ=&%D?A2oVMx2C2m` z00i{9UAe(7>9zTXX)8#~dND{1R?W!3yx&gKqSw08yw_#{2!q&`J(d~-HIf4Wfkl8wU%&e>4~#+VCr{J~YRsQ+FOV_M zeEOC%i;ul||M5RE{=B&7*nb;_>Hl3A__1nEn`z9IG0*&1#(azaTh1)PFyPCg&SU>= z8UFvbVL0{Qo`DC$fQ)$-^Ff*k8UVrbP&8-GHBm~RZ~mKT18f-n{I_RNK{$V&c@a!A zkuCrkQiX8-eDfJ(7_fj+1Ljla1zUzn>IEXj(a6BSz)-l#f-`--`B5Sg(#ij>4BQw7 zfI!+zV}86z*_L4%&ZLYCz?td&r!r_}nhRzVlbOf?02U>|?~c{G;{X5v07*qoM6N<$ Eg6r&sPyhe` literal 0 HcmV?d00001 diff --git a/abogen/assets/flags/j.png b/abogen/assets/flags/j.png new file mode 100644 index 0000000000000000000000000000000000000000..4bcf2f98f7134250fa09b717bf476bf61d233fc2 GIT binary patch literal 314 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkQ1FeX zi(^Q|oUOs$T!#z<-g1_tNBbx2om2Ywy-uA&?)$|`v9H3?Jk)MpKhZJY#8kwQnK|)L z_2loLgjoJZbnNPHTBeY3^k}uqToa~Nf!rxt?2h46PTlzWBDLejrzXcs2U)LmsJykG zv>>EX_($xPm6>d>Zxt{WOD$Q(p&ab8(Q^6Az2Os+tDBnAzOk^$G%pCcQ*1MH4@bk? zZG4rRyaP&{rY9r{>u9qC#;AAf);sveo6~tei_6>{YDS#*k0d8?E~tKIoEC5M_?L6d z#!`OI0WbcEP)@~7iNbMhP&gIS|R6@}Uw9j;9K@b!a z)UHgZY!NY2l-iZtB3<%$f83oH^6f^dDjD7_Ee} zAmJ=p$)J}rXT!#xt$}7D=%&tDSr-g!$yu3j7Sj@%qi(Y$q>93+_03^Y<6rjA%cditfitGre~L- zcQ6U<{c#9|7oc2p+0}41@dLnF6j|R2Zh*%&xXO;f?!he_9-ELpeSpQt1)OI_WbNbL zRZ&ADR)E#1BQHFgUjQf@nK3(jaR|?4;U*W5H6%eY!r6*q!M{f#neUczQFaw1G&wKN z&>q@UEOC$6F5c8sh=!$c;}0xNbCrbT=b(&?J0$PPC-XFFk*#5rud1?<>=9Bu5sV+7Nigzx{?)nxAN^R&@j=m9 P00000NkvXXu0mjfze<4p literal 0 HcmV?d00001 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)