From d67cfce896e639d49c24854933d31a30b7cd71dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Sun, 27 Apr 2025 05:09:52 +0300 Subject: [PATCH] Corrections to code logic --- abogen/__init__.py | 9 ---- abogen/book_handler.py | 2 +- abogen/constants.py | 117 +++++++++++++++++++++++++++++++++++++++++ abogen/conversion.py | 5 +- abogen/gui.py | 20 ++++--- abogen/main.py | 15 ++++-- abogen/utils.py | 114 +-------------------------------------- demo/convert.py | 71 +++++++++++++++---------- 8 files changed, 190 insertions(+), 163 deletions(-) delete mode 100644 abogen/__init__.py create mode 100644 abogen/constants.py diff --git a/abogen/__init__.py b/abogen/__init__.py deleted file mode 100644 index ab4795e..0000000 --- a/abogen/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from abogen.utils import get_version - -# Program Information -PROGRAM_NAME = "abogen" -PROGRAM_DESCRIPTION = ( - "Generate audiobooks from EPUBs, PDFs and text with synchronized captions." -) -GITHUB_URL = "https://github.com/denizsafak/abogen" -VERSION = get_version() diff --git a/abogen/book_handler.py b/abogen/book_handler.py index 869e53b..ffd531d 100644 --- a/abogen/book_handler.py +++ b/abogen/book_handler.py @@ -21,7 +21,7 @@ from PyQt5.QtWidgets import ( QMenu, ) from PyQt5.QtCore import Qt -from abogen.utils import clean_text, calculate_text_length +from utils import clean_text, calculate_text_length import os diff --git a/abogen/constants.py b/abogen/constants.py new file mode 100644 index 0000000..e657063 --- /dev/null +++ b/abogen/constants.py @@ -0,0 +1,117 @@ +from utils import get_version, get_user_config_path + +# Program Information +PROGRAM_NAME = "abogen" +PROGRAM_DESCRIPTION = ( + "Generate audiobooks from EPUBs, PDFs and text with synchronized captions." +) +GITHUB_URL = "https://github.com/denizsafak/abogen" +VERSION = get_version() + +# Language description mapping +LANGUAGE_DESCRIPTIONS = { + "a": "American English", + "b": "British English", + "e": "Spanish", + "f": "French", + "h": "Hindi", + "i": "Italian", + "j": "Japanese", + "p": "Brazilian Portuguese", + "z": "Mandarin Chinese", +} + +# Supported languages for subtitle generation +# Currently, only 'a (American English)' and 'b (British English)' are supported for subtitle generation. +# This is because tokens that contain timestamps are not generated for other languages in the Kokoro pipeline. +# Please refer to: https://github.com/hexgrad/kokoro/blob/6d87f4ae7abc2d14dbc4b3ef2e5f19852e861ac2/kokoro/pipeline.py +# 383 English processing (unchanged) +# 384 if self.lang_code in 'ab': +SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION = [ + "a", + "b", +] + +# Voice and sample text constants +VOICES_INTERNAL = [ + "af_alloy", + "af_aoede", + "af_bella", + "af_heart", + "af_jessica", + "af_kore", + "af_nicole", + "af_nova", + "af_river", + "af_sarah", + "af_sky", + "am_adam", + "am_echo", + "am_eric", + "am_fenrir", + "am_liam", + "am_michael", + "am_onyx", + "am_puck", + "am_santa", + "bf_alice", + "bf_emma", + "bf_isabella", + "bf_lily", + "bm_daniel", + "bm_fable", + "bm_george", + "bm_lewis", + "ef_dora", + "em_alex", + "em_santa", + "ff_siwis", + "hf_alpha", + "hf_beta", + "hm_omega", + "hm_psi", + "if_sara", + "im_nicola", + "jf_alpha", + "jf_gongitsune", + "jf_nezumi", + "jf_tebukuro", + "jm_kumo", + "pf_dora", + "pm_alex", + "pm_santa", + "zf_xiaobei", + "zf_xiaoni", + "zf_xiaoxiao", + "zf_xiaoyi", + "zm_yunjian", + "zm_yunxi", + "zm_yunxia", + "zm_yunyang", +] + +# Voice and sample text mapping +SAMPLE_VOICE_TEXTS = { + "a": "This is a sample of the selected voice.", + "b": "This is a sample of the selected voice.", + "e": "Este es una muestra de la voz seleccionada.", + "f": "Ceci est un exemple de la voix sélectionnée.", + "h": "यह चयनित आवाज़ का एक नमूना है।", + "i": "Questo è un esempio della voce selezionata.", + "j": "これは選択した声のサンプルです。", + "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/conversion.py b/abogen/conversion.py index c454334..81888b4 100644 --- a/abogen/conversion.py +++ b/abogen/conversion.py @@ -5,8 +5,8 @@ import time from PyQt5.QtCore import QThread, pyqtSignal, Qt from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox import soundfile as sf -from abogen.utils import clean_text, SAMPLE_VOICE_TEXTS, LANGUAGE_DESCRIPTIONS -from abogen import PROGRAM_NAME +from utils import clean_text +from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS def get_sample_voice_text(lang_code): @@ -688,6 +688,7 @@ class PlayAudioThread(QThread): try: import pygame import time as _time + pygame.mixer.init() pygame.mixer.music.load(self.wav_path) pygame.mixer.music.play() diff --git a/abogen/gui.py b/abogen/gui.py index 35e5b5b..a27d2bc 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -46,7 +46,7 @@ from PyQt5.QtGui import ( QColor, QMovie, ) -from abogen.utils import ( +from utils import ( load_config, save_config, get_gpu_acceleration, @@ -56,14 +56,19 @@ from abogen.utils import ( calculate_text_length, get_resource_path, LoadPipelineThread, +) +from conversion import ConversionThread, VoicePreviewThread, PlayAudioThread +from book_handler import HandlerDialog +from constants import ( + PROGRAM_NAME, + VERSION, + GITHUB_URL, + PROGRAM_DESCRIPTION, + LANGUAGE_DESCRIPTIONS, FLAGS, VOICES_INTERNAL, SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION, - LANGUAGE_DESCRIPTIONS, ) -from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread -from abogen.book_handler import HandlerDialog -from abogen import PROGRAM_NAME, VERSION, GITHUB_URL, PROGRAM_DESCRIPTION from threading import Thread # Import ctypes for Windows-specific taskbar icon @@ -1271,6 +1276,7 @@ class abogen(QWidget): if self.preview_playing: try: import pygame + pygame.mixer.music.stop() except Exception: pass @@ -1470,7 +1476,7 @@ class abogen(QWidget): def show_chapter_options_dialog(self, chapter_count): """Show dialog to ask user about chapter processing options when chapters are detected in a .txt file""" - from abogen.conversion import ChapterOptionsDialog + from conversion import ChapterOptionsDialog dialog = ChapterOptionsDialog(chapter_count, parent=self) dialog.setWindowModality(Qt.ApplicationModal) @@ -1539,7 +1545,7 @@ class abogen(QWidget): def reveal_config_in_explorer(self): """Open the configuration file location in file explorer.""" - from abogen.utils import get_user_config_path + from utils import get_user_config_path try: config_path = get_user_config_path() diff --git a/abogen/main.py b/abogen/main.py index 2a994e6..b88f2c0 100644 --- a/abogen/main.py +++ b/abogen/main.py @@ -3,9 +3,12 @@ import sys import platform from PyQt5.QtWidgets import QApplication from PyQt5.QtGui import QIcon -from abogen.gui import abogen -from abogen.utils import get_resource_path -from abogen import PROGRAM_NAME, VERSION + +# Add the directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) +from gui import abogen +from utils import get_resource_path +from constants import PROGRAM_NAME, VERSION # Ensure sys.stdout and sys.stderr are valid in GUI mode if sys.stdout is None: @@ -28,7 +31,11 @@ if platform.system() == "Windows": if platform.system() == "Linux": xdg_session = os.environ.get("XDG_SESSION_TYPE", "").lower() desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower() - if "gnome" in desktop and xdg_session == "wayland" and "QT_QPA_PLATFORM" not in os.environ: + if ( + "gnome" in desktop + and xdg_session == "wayland" + and "QT_QPA_PLATFORM" not in os.environ + ): os.environ["QT_QPA_PLATFORM"] = "wayland" diff --git a/abogen/utils.py b/abogen/utils.py index 9420dce..93f3393 100644 --- a/abogen/utils.py +++ b/abogen/utils.py @@ -10,114 +10,6 @@ from threading import Thread os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" warnings.filterwarnings("ignore") -# Language description mapping -LANGUAGE_DESCRIPTIONS = { - "a": "American English", - "b": "British English", - "e": "Spanish", - "f": "French", - "h": "Hindi", - "i": "Italian", - "j": "Japanese", - "p": "Brazilian Portuguese", - "z": "Mandarin Chinese", -} - -# Supported languages for subtitle generation -# Currently, only 'a (American English)' and 'b (British English)' are supported for subtitle generation. -# This is because tokens that contain timestamps are not generated for other languages in the Kokoro pipeline. -# Please refer to: https://github.com/hexgrad/kokoro/blob/6d87f4ae7abc2d14dbc4b3ef2e5f19852e861ac2/kokoro/pipeline.py -# 383 English processing (unchanged) -# 384 if self.lang_code in 'ab': -SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION = [ - "a", - "b", -] - -# Voice and sample text constants -VOICES_INTERNAL = [ - "af_alloy", - "af_aoede", - "af_bella", - "af_heart", - "af_jessica", - "af_kore", - "af_nicole", - "af_nova", - "af_river", - "af_sarah", - "af_sky", - "am_adam", - "am_echo", - "am_eric", - "am_fenrir", - "am_liam", - "am_michael", - "am_onyx", - "am_puck", - "am_santa", - "bf_alice", - "bf_emma", - "bf_isabella", - "bf_lily", - "bm_daniel", - "bm_fable", - "bm_george", - "bm_lewis", - "ef_dora", - "em_alex", - "em_santa", - "ff_siwis", - "hf_alpha", - "hf_beta", - "hm_omega", - "hm_psi", - "if_sara", - "im_nicola", - "jf_alpha", - "jf_gongitsune", - "jf_nezumi", - "jf_tebukuro", - "jm_kumo", - "pf_dora", - "pm_alex", - "pm_santa", - "zf_xiaobei", - "zf_xiaoni", - "zf_xiaoxiao", - "zf_xiaoyi", - "zm_yunjian", - "zm_yunxi", - "zm_yunxia", - "zm_yunyang", -] - -# Voice and sample text mapping -SAMPLE_VOICE_TEXTS = { - "a": "This is a sample of the selected voice.", - "b": "This is a sample of the selected voice.", - "e": "Este es una muestra de la voz seleccionada.", - "f": "Ceci est un exemple de la voix sélectionnée.", - "h": "यह चयनित आवाज़ का एक नमूना है।", - "i": "Questo è un esempio della voce selezionata.", - "j": "これは選択した声のサンプルです。", - "p": "Este é um exemplo da voz selecionada.", - "z": "这是所选语音的示例。", -} - -# flags mapping for voice display -FLAGS = { - "a": "🇺🇸", - "b": "🇬🇧", - "e": "🇪🇸", - "f": "🇫🇷", - "h": "🇮🇳", - "i": "🇮🇹", - "j": "🇯🇵", - "p": "🇧🇷", - "z": "🇨🇳", -} - def get_resource_path(package, resource): """ @@ -174,8 +66,6 @@ def get_user_config_path(): return os.path.join(config_dir, "config.json") -CONFIG_PATH = get_user_config_path() - _sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes @@ -193,7 +83,7 @@ def clean_text(text): def load_config(): try: - with open(CONFIG_PATH, "r", encoding="utf-8") as f: + with open(get_user_config_path(), "r", encoding="utf-8") as f: return json.load(f) except Exception: return {} @@ -201,7 +91,7 @@ def load_config(): def save_config(config): try: - with open(CONFIG_PATH, "w", encoding="utf-8") as f: + with open(get_user_config_path(), "w", encoding="utf-8") as f: json.dump(config, f, indent=2) except Exception: pass diff --git a/demo/convert.py b/demo/convert.py index 93e4d26..6c5238b 100644 --- a/demo/convert.py +++ b/demo/convert.py @@ -2,7 +2,7 @@ # # For WebM (smaller filesize): # ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm -# +# # For MP4 (higher quality): # ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libx264 -preset slow -crf 18 -movflags +faststart -c:a copy -shortest demo.mp4 # @@ -13,23 +13,27 @@ import re import sys from datetime import timedelta + def parse_time(s): # For ASS format - if ':' in s and '.' in s: - h, m, s = s.split(':') - sec, cs = s.split('.') - return timedelta(hours=int(h), minutes=int(m), seconds=int(sec), milliseconds=int(cs) * 10) + if ":" in s and "." in s: + h, m, s = s.split(":") + sec, cs = s.split(".") + return timedelta( + hours=int(h), minutes=int(m), seconds=int(sec), milliseconds=int(cs) * 10 + ) # For SRT format (00:00:00,000) - elif ':' in s and ',' in s: - parts = s.split(':') + elif ":" in s and "," in s: + parts = s.split(":") h = int(parts[0]) m = int(parts[1]) - sec_parts = parts[2].split(',') + sec_parts = parts[2].split(",") sec = int(sec_parts[0]) ms = int(sec_parts[1]) return timedelta(hours=h, minutes=m, seconds=sec, milliseconds=ms) return None + def format_time(t): total_seconds = int(t.total_seconds()) cs = int((t.total_seconds() - total_seconds) * 100) @@ -38,6 +42,7 @@ def format_time(t): s = total_seconds % 60 return f"{h}:{m:02}:{s:02}.{cs:02}" + # Desired script info and style DESIRED_SCRIPT_INFO = """[Script Info] Title: Centered Subs @@ -51,8 +56,9 @@ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, Style: Default,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,2,0,5,10,10,10,1 """ + def process_subtitle_file(input_file): - output_file = input_file.replace('.ass', '_demo.ass').replace('.srt', '_demo.ass') + output_file = input_file.replace(".ass", "_demo.ass").replace(".srt", "_demo.ass") if output_file == input_file: output_file = f"{input_file}_demo.ass" @@ -67,11 +73,11 @@ def process_subtitle_file(input_file): sys.exit(1) # Check if it's an SRT file - is_srt = input_file.lower().endswith('.srt') - + is_srt = input_file.lower().endswith(".srt") + if is_srt: return convert_srt_to_ass(input_file, output_file, lines) - + # Process ASS file header = [] events = [] @@ -124,9 +130,12 @@ def process_subtitle_file(input_file): if end > next_start: end = next_start # cut current subtitle to stop at next one's start - fixed_line = re.sub(r"Dialogue: \d,[^,]+,[^,]+,", - f"Dialogue: 0,{format_time(start)},{format_time(end)},", - line, count=1) + fixed_line = re.sub( + r"Dialogue: \d,[^,]+,[^,]+,", + f"Dialogue: 0,{format_time(start)},{format_time(end)},", + line, + count=1, + ) fixed_lines.append(fixed_line) try: @@ -138,12 +147,13 @@ def process_subtitle_file(input_file): print(f"❌ Error writing output file: {e}") sys.exit(1) + def convert_srt_to_ass(input_file, output_file, lines): """Convert SRT format to ASS format.""" events = [] subtitle_blocks = [] current_block = [] - + # Parse SRT file for line in lines: line = line.strip() @@ -153,41 +163,45 @@ def convert_srt_to_ass(input_file, output_file, lines): current_block = [] else: current_block.append(line) - + # Don't forget the last block if current_block: subtitle_blocks.append(current_block) - + # Process subtitle blocks for block in subtitle_blocks: if len(block) < 3: continue # Invalid block - + # Skip the subtitle number timing_line = block[1] - + # Parse timing information - timing_match = re.match(r'(\d+:\d+:\d+,\d+)\s+-->\s+(\d+:\d+:\d+,\d+)', timing_line) + timing_match = re.match( + r"(\d+:\d+:\d+,\d+)\s+-->\s+(\d+:\d+:\d+,\d+)", timing_line + ) if not timing_match: continue - + start_time = parse_time(timing_match.group(1)) end_time = parse_time(timing_match.group(2)) - + # Combine text lines text = "\\N".join(block[2:]) - + # Create ASS Dialogue line dialogue_line = f"Dialogue: 0,{format_time(start_time)},{format_time(end_time)},Default,,0,0,0,,{text}\n" events.append(dialogue_line) - + # Create ASS file try: with open(output_file, "w", encoding="utf-8") as f: f.write(DESIRED_SCRIPT_INFO) f.write(DESIRED_STYLE) f.write("[Events]\n") - f.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n") + f.write( + "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n" + ) f.writelines(events) print(f"✅ Successfully converted SRT to ASS. Output file: {output_file}") return True @@ -195,11 +209,12 @@ def convert_srt_to_ass(input_file, output_file, lines): print(f"❌ Error writing output file: {e}") sys.exit(1) + if __name__ == "__main__": if len(sys.argv) < 2: print("❌ Error: No input file specified") print(f"Usage: {sys.argv[0]} ") sys.exit(1) - + input_file = sys.argv[1] - process_subtitle_file(input_file) \ No newline at end of file + process_subtitle_file(input_file)