Added a download tracker that displays informative messages

This commit is contained in:
Deniz Şafak
2025-05-22 06:03:56 +03:00
parent a0e59564af
commit 3278447d6a
4 changed files with 92 additions and 13 deletions
+1
View File
@@ -3,6 +3,7 @@
- Added voice preview caching system that stores generated previews in the cache folder, mentioned by @jborza in #22 - 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 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 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. - 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. - 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. - Fixed voice preview player keeps playing silently at the background after preview ends.
+34 -5
View File
@@ -12,6 +12,7 @@ import soundfile as sf
from utils import clean_text, create_process from utils import clean_text, create_process
from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS
from voice_formulas import get_new_voice from voice_formulas import get_new_voice
import hf_tracker
import static_ffmpeg import static_ffmpeg
import threading # for efficient waiting import threading # for efficient waiting
import subprocess 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" 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: try:
hf_tracker.set_log_callback(lambda msg: self.log_updated.emit(msg))
# Show configuration # Show configuration
self.log_updated.emit("Configuration:") self.log_updated.emit("Configuration:")
# Use display_path for logs if available, otherwise use the actual file name # 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): def __init__(self, wav_path, parent=None):
super().__init__(parent) super().__init__(parent)
self.wav_path = wav_path self.wav_path = wav_path
self.is_canceled = False
def run(self): def run(self):
try: try:
@@ -1012,11 +1015,37 @@ class PlayAudioThread(QThread):
pygame.mixer.init() pygame.mixer.init()
pygame.mixer.music.load(self.wav_path) pygame.mixer.music.load(self.wav_path)
pygame.mixer.music.play() pygame.mixer.music.play()
# Wait until playback is finished # Wait until playback is finished or canceled
while pygame.mixer.music.get_busy(): while pygame.mixer.music.get_busy() and not self.is_canceled:
_time.sleep(0.2) _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() self.finished.emit()
except Exception as e: 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
+21 -8
View File
@@ -4,6 +4,7 @@ import tempfile
import platform import platform
import base64 import base64
import re import re
import hf_tracker
import hashlib # Added for cache path generation import hashlib # Added for cache path generation
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
@@ -80,6 +81,13 @@ if platform.system() == "Windows":
import ctypes 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): class ThreadSafeLogSignal(QObject):
log_signal = pyqtSignal(object) log_signal = pyqtSignal(object)
@@ -533,6 +541,11 @@ class abogen(QWidget):
self.log_signal = ThreadSafeLogSignal() self.log_signal = ThreadSafeLogSignal()
self.log_signal.log_signal.connect(self._update_log_main_thread) 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 # Set application icon
icon_path = get_resource_path("abogen.assets", "icon.ico") icon_path = get_resource_path("abogen.assets", "icon.ico")
if icon_path: if icon_path:
@@ -584,6 +597,9 @@ class abogen(QWidget):
if self.check_updates: if self.check_updates:
QTimer.singleShot(1000, self.check_for_updates_startup) QTimer.singleShot(1000, self.check_for_updates_startup)
# Set hf_tracker callbacks
hf_tracker.set_log_callback(self.update_log)
def initUI(self): def initUI(self):
self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}") self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}")
screen = QApplication.primaryScreen().geometry() screen = QApplication.primaryScreen().geometry()
@@ -1571,14 +1587,8 @@ class abogen(QWidget):
if self.preview_playing: if self.preview_playing:
try: try:
if self.play_audio_thread and self.play_audio_thread.isRunning(): if self.play_audio_thread and self.play_audio_thread.isRunning():
# Attempt to stop pygame mixer if it's in use by PlayAudioThread # Call the stop method on PlayAudioThread to safely handle stopping
# This is a bit indirect, ideally PlayAudioThread would have a stop method self.play_audio_thread.stop()
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
self.play_audio_thread.wait(500) # Wait a bit self.play_audio_thread.wait(500) # Wait a bit
except Exception as e: except Exception as e:
print(f"Error stopping preview audio: {e}") print(f"Error stopping preview audio: {e}")
@@ -2428,3 +2438,6 @@ Categories=AudioVideo;Audio;Utility;
self.config["separate_chapters_format"] = fmt self.config["separate_chapters_format"] = fmt
save_config(self.config) save_config(self.config)
def show_model_download_warning(self, title, message):
QMessageBox.information(self, title, message)
+36
View File
@@ -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", "<unknown repo>")
filename = kwargs.get("filename", "<unknown file>")
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