diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d21482..5085058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added voice preview caching system that stores generated previews in the cache folder, mentioned by @jborza in #22 - Added extra metadata support for chaptered M4B files, ensuring better compatibility with audiobook players. - Added new option: `Separate chapters audio format`, allowing to choose between `wav`, `mp4`, `flac` and `opus` formats for chaptered audio files. +- Added a download tracker that displays informative messages while downloading Kokoro models or voices from HuggingFace. - Skipping PyTorch CUDA installation if GPU is not NVIDIA in WINDOWS_INSTALL.bat script, preventing unnecessary installation of PyTorch. - Removed `abogen_` prefix that was adding to converted books in temp directory. - Fixed voice preview player keeps playing silently at the background after preview ends. diff --git a/abogen/conversion.py b/abogen/conversion.py index e3708ca..cd64a23 100644 --- a/abogen/conversion.py +++ b/abogen/conversion.py @@ -12,6 +12,7 @@ import soundfile as sf from utils import clean_text, create_process from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS from voice_formulas import get_new_voice +import hf_tracker import static_ffmpeg import threading # for efficient waiting import subprocess @@ -161,6 +162,7 @@ class ConversionThread(QThread): f"\nVoice: {self.voice}\nLanguage: {self.lang_code}\nSpeed: {self.speed}\nGPU: {self.use_gpu}\nFile: {self.file_name}\nSubtitle mode: {self.subtitle_mode}\nOutput format: {self.output_format}\nSave option: {self.save_option}\n" ) try: + hf_tracker.set_log_callback(lambda msg: self.log_updated.emit(msg)) # Show configuration self.log_updated.emit("Configuration:") # Use display_path for logs if available, otherwise use the actual file name @@ -1003,6 +1005,7 @@ class PlayAudioThread(QThread): def __init__(self, wav_path, parent=None): super().__init__(parent) self.wav_path = wav_path + self.is_canceled = False def run(self): try: @@ -1012,11 +1015,37 @@ class PlayAudioThread(QThread): pygame.mixer.init() pygame.mixer.music.load(self.wav_path) pygame.mixer.music.play() - # Wait until playback is finished - while pygame.mixer.music.get_busy(): + # Wait until playback is finished or canceled + while pygame.mixer.music.get_busy() and not self.is_canceled: _time.sleep(0.2) - pygame.mixer.music.unload() - pygame.mixer.quit() # Quit the mixer + + # Make sure to clean up regardless of how we exited the loop + try: + pygame.mixer.music.stop() + pygame.mixer.music.unload() + pygame.mixer.quit() # Quit the mixer + except Exception: + # Ignore any errors during cleanup + pass + self.finished.emit() except Exception as e: - self.error.emit(f"Audio playback error: {str(e)}") + # Handle initialization errors separately to give better error messages + if "mixer not initialized" in str(e): + self.error.emit("Audio playback error: The audio system was not properly initialized") + else: + self.error.emit(f"Audio playback error: {str(e)}") + + def stop(self): + """Safely stop playback""" + self.is_canceled = True + # Try to stop pygame if it's running, but catch all exceptions + try: + import pygame + if pygame.mixer.get_init(): + if pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + pygame.mixer.music.unload() + except Exception: + # Ignore all errors when stopping since mixer might not be initialized + pass diff --git a/abogen/gui.py b/abogen/gui.py index d959b63..efc40b4 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -4,6 +4,7 @@ import tempfile import platform import base64 import re +import hf_tracker import hashlib # Added for cache path generation from PyQt5.QtWidgets import ( QApplication, @@ -80,6 +81,13 @@ if platform.system() == "Windows": import ctypes +class ShowWarningSignalEmitter(QObject): # New class to handle signal emission + show_warning_signal = pyqtSignal(str, str) + + def emit(self, title, message): + self.show_warning_signal.emit(title, message) + + class ThreadSafeLogSignal(QObject): log_signal = pyqtSignal(object) @@ -533,6 +541,11 @@ class abogen(QWidget): self.log_signal = ThreadSafeLogSignal() self.log_signal.log_signal.connect(self._update_log_main_thread) + # Create warning signal emitter + self.warning_signal_emitter = ShowWarningSignalEmitter() + self.warning_signal_emitter.show_warning_signal.connect(self.show_model_download_warning) + hf_tracker.set_show_warning_signal_emitter(self.warning_signal_emitter) + # Set application icon icon_path = get_resource_path("abogen.assets", "icon.ico") if icon_path: @@ -584,6 +597,9 @@ class abogen(QWidget): if self.check_updates: QTimer.singleShot(1000, self.check_for_updates_startup) + # Set hf_tracker callbacks + hf_tracker.set_log_callback(self.update_log) + def initUI(self): self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}") screen = QApplication.primaryScreen().geometry() @@ -1571,14 +1587,8 @@ class abogen(QWidget): if self.preview_playing: try: if self.play_audio_thread and self.play_audio_thread.isRunning(): - # Attempt to stop pygame mixer if it's in use by PlayAudioThread - # This is a bit indirect, ideally PlayAudioThread would have a stop method - import pygame - if pygame.mixer.get_init() and pygame.mixer.music.get_busy(): - pygame.mixer.music.stop() - pygame.mixer.music.unload() # Ensure it's unloaded - pygame.mixer.quit() # Quit the mixer - self.play_audio_thread.quit() # Ask thread to finish + # Call the stop method on PlayAudioThread to safely handle stopping + self.play_audio_thread.stop() self.play_audio_thread.wait(500) # Wait a bit except Exception as e: print(f"Error stopping preview audio: {e}") @@ -2428,3 +2438,6 @@ Categories=AudioVideo;Audio;Utility; self.config["separate_chapters_format"] = fmt save_config(self.config) + def show_model_download_warning(self, title, message): + QMessageBox.information(self, title, message) + diff --git a/abogen/hf_tracker.py b/abogen/hf_tracker.py new file mode 100644 index 0000000..a0f760e --- /dev/null +++ b/abogen/hf_tracker.py @@ -0,0 +1,36 @@ +log_callback = None +show_warning_signal_emitter = None # Renamed for clarity + +def set_log_callback(cb): + global log_callback + log_callback = cb + +def set_show_warning_signal_emitter(emitter): # Renamed for clarity + global show_warning_signal_emitter + show_warning_signal_emitter = emitter + +from huggingface_hub import hf_hub_download + +def tracked_hf_hub_download(*args, **kwargs): + try: + local_kwargs = dict(kwargs) + local_kwargs["local_files_only"] = True + hf_hub_download(*args, **local_kwargs) + except Exception: + repo_id = kwargs.get("repo_id", "") + filename = kwargs.get("filename", "") + if filename.endswith(".pth"): + msg = f"\nDownloading model '{filename}' from Hugging Face ({repo_id}). This may take a while. Please wait..." + if show_warning_signal_emitter: # Check if the emitter is set + show_warning_signal_emitter.emit("Downloading Model", f"Downloading model '{filename}' from Hugging Face repository '{repo_id}'. This may take a while, please wait.") + else: + msg = f"\nDownloading '{filename}' from Hugging Face ({repo_id}). Please wait..." + if log_callback: + print(msg, flush=True) + log_callback(msg) + else: + print(msg, flush=True) + return hf_hub_download(*args, **kwargs) + +import huggingface_hub +huggingface_hub.hf_hub_download = tracked_hf_hub_download \ No newline at end of file