mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Improved process handling for subpprocess calls
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
- Improve chaptered audio generation by outputting directly as `m4b` instead of converting from `wav`.
|
- Improve chaptered audio generation by outputting directly as `m4b` instead of converting from `wav`.
|
||||||
- Ignore chapter markers and single newlines when calculating text length, improving the accuracy of the text length calculation.
|
- Ignore chapter markers and single newlines when calculating text length, improving the accuracy of the text length calculation.
|
||||||
- Prevent cancellation if process is at 99%, ensuring the process is not interrupted at the last moment.
|
- Prevent cancellation if process is at 99%, ensuring the process is not interrupted at the last moment.
|
||||||
|
- Improved process handling for subpprocess calls, ensuring better management of subprocesses.
|
||||||
- Added `.opus` as output format for generated audio files.
|
- Added `.opus` as output format for generated audio files.
|
||||||
- Added "Playing..." indicator for "Preview" button in the voice mixer.
|
- Added "Playing..." indicator for "Preview" button in the voice mixer.
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ from platformdirs import user_desktop_dir
|
|||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt
|
from PyQt5.QtCore import QThread, pyqtSignal, Qt
|
||||||
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
from utils import clean_text
|
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 static_ffmpeg
|
import static_ffmpeg
|
||||||
import threading # for efficient waiting
|
import threading # for efficient waiting
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import platform
|
|
||||||
|
|
||||||
|
|
||||||
def get_sample_voice_text(lang_code):
|
def get_sample_voice_text(lang_code):
|
||||||
@@ -509,7 +507,7 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
if self.output_format == "opus":
|
if self.output_format == "opus":
|
||||||
static_ffmpeg.add_paths()
|
static_ffmpeg.add_paths()
|
||||||
proc = subprocess.Popen(
|
proc = create_process(
|
||||||
[
|
[
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-y",
|
"-y",
|
||||||
@@ -529,6 +527,7 @@ class ConversionThread(QThread):
|
|||||||
chapter_out_path,
|
chapter_out_path,
|
||||||
],
|
],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
|
text=False # Ensure binary stdin for audio data
|
||||||
)
|
)
|
||||||
proc.stdin.write(chapter_audio.astype("float32").tobytes())
|
proc.stdin.write(chapter_audio.astype("float32").tobytes())
|
||||||
proc.stdin.close()
|
proc.stdin.close()
|
||||||
@@ -605,7 +604,7 @@ class ConversionThread(QThread):
|
|||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
print(f"Executing FFMPEG for Opus: {' '.join(ffmpeg_cmd_opus)}")
|
print(f"Executing FFMPEG for Opus: {' '.join(ffmpeg_cmd_opus)}")
|
||||||
process = subprocess.Popen(ffmpeg_cmd_opus, stdin=subprocess.PIPE)
|
process = create_process(ffmpeg_cmd_opus, stdin=subprocess.PIPE, text=False) # Ensure binary stdin
|
||||||
process.stdin.write(audio_data_np.astype("float32").tobytes())
|
process.stdin.write(audio_data_np.astype("float32").tobytes())
|
||||||
process.stdin.close()
|
process.stdin.close()
|
||||||
if process.wait() == 0:
|
if process.wait() == 0:
|
||||||
@@ -716,7 +715,7 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit(f"Generating audio with chapters...\n")
|
self.log_updated.emit(f"Generating audio with chapters...\n")
|
||||||
print(f"Executing FFMPEG for M4B: {' '.join(ffmpeg_cmd)}")
|
print(f"Executing FFMPEG for M4B: {' '.join(ffmpeg_cmd)}")
|
||||||
|
|
||||||
process = subprocess.Popen(ffmpeg_cmd, stdin=subprocess.PIPE)
|
process = create_process(ffmpeg_cmd, stdin=subprocess.PIPE, text=False) # Ensure binary stdin
|
||||||
process.stdin.write(audio_data_np.astype("float32").tobytes())
|
process.stdin.write(audio_data_np.astype("float32").tobytes())
|
||||||
process.stdin.close()
|
process.stdin.close()
|
||||||
return_code = process.wait()
|
return_code = process.wait()
|
||||||
@@ -725,13 +724,13 @@ class ConversionThread(QThread):
|
|||||||
return output_m4b_path
|
return output_m4b_path
|
||||||
else:
|
else:
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
(f"FFmpeg failed to create M4B (return code {return_code}). Falling back to WAV.\n", "red")
|
(f"FFmpeg failed to create M4B (return code {return_code}).\n\nFalling back to WAV.\n", "red")
|
||||||
)
|
)
|
||||||
sf.write(final_wav_path, audio_data_np, 24000, format="wav")
|
sf.write(final_wav_path, audio_data_np, 24000, format="wav")
|
||||||
return final_wav_path
|
return final_wav_path
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_updated.emit((f"Error during M4B generation: {str(e)}. Falling back to WAV.\n", "red"))
|
self.log_updated.emit((f"Error during M4B generation: {str(e)}.\n\nFalling back to WAV.\n", "red"))
|
||||||
try:
|
try:
|
||||||
sf.write(final_wav_path, audio_data_np, 24000, format="wav")
|
sf.write(final_wav_path, audio_data_np, 24000, format="wav")
|
||||||
return final_wav_path
|
return final_wav_path
|
||||||
|
|||||||
+18
-27
@@ -695,7 +695,7 @@ class abogen(QWidget):
|
|||||||
("wav", "wav"),
|
("wav", "wav"),
|
||||||
("flac", "flac"),
|
("flac", "flac"),
|
||||||
("mp3", "mp3"),
|
("mp3", "mp3"),
|
||||||
("opus", "opus (best)"),
|
("opus", "opus (best compression)"),
|
||||||
("m4b", "m4b (with chapters)"),
|
("m4b", "m4b (with chapters)"),
|
||||||
]:
|
]:
|
||||||
self.format_combo.addItem(label, key)
|
self.format_combo.addItem(label, key)
|
||||||
@@ -1814,8 +1814,9 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def add_shortcut_to_desktop(self):
|
def add_shortcut_to_desktop(self):
|
||||||
"""Create a desktop shortcut to this program using PowerShell."""
|
"""Create a desktop shortcut to this program using PowerShell."""
|
||||||
import sys, subprocess
|
import sys
|
||||||
from platformdirs import user_desktop_dir
|
from platformdirs import user_desktop_dir
|
||||||
|
from utils import create_process
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# where to put the .lnk
|
# where to put the .lnk
|
||||||
@@ -1834,47 +1835,37 @@ class abogen(QWidget):
|
|||||||
# icon (fallback to exe if missing)
|
# icon (fallback to exe if missing)
|
||||||
icon = get_resource_path("abogen.assets", "icon.ico")
|
icon = get_resource_path("abogen.assets", "icon.ico")
|
||||||
if not icon or not os.path.exists(icon):
|
if not icon or not os.path.exists(icon):
|
||||||
icon = target
|
icon = target # Create a more direct PowerShell command
|
||||||
|
shortcut_ps = shortcut_path.replace("'", "''").replace("\\", "\\\\")
|
||||||
|
target_ps = target.replace("'", "''").replace("\\", "\\\\")
|
||||||
|
workdir_ps = os.path.dirname(target).replace("'", "''").replace("\\", "\\\\")
|
||||||
|
icon_ps = icon.replace("'", "''").replace("\\", "\\\\")
|
||||||
|
# Create PowerShell script as a single line with no line breaks (more reliable)
|
||||||
|
ps_cmd = f"$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('{shortcut_ps}'); $lnk.TargetPath='{target_ps}'; $lnk.WorkingDirectory='{workdir_ps}'; $lnk.IconLocation='{icon_ps}'; $lnk.Save()"
|
||||||
|
|
||||||
# PowerShell command to make the shortcut
|
# Run PowerShell with the command directly
|
||||||
ps_cmd = r"""
|
proc = create_process("powershell -NoProfile -ExecutionPolicy Bypass -Command \"" + ps_cmd + "\"")
|
||||||
$s = New-Object -ComObject WScript.Shell
|
proc.wait()
|
||||||
$lnk = $s.CreateShortcut('{lnk}')
|
|
||||||
$lnk.TargetPath = '{tgt}'
|
|
||||||
$lnk.WorkingDirectory = '{cwd}'
|
|
||||||
$lnk.IconLocation = '{ico}'
|
|
||||||
$lnk.Save()
|
|
||||||
""".format(
|
|
||||||
lnk=shortcut_path.replace("'", "''"),
|
|
||||||
tgt=target.replace("'", "''"),
|
|
||||||
cwd=os.path.dirname(target).replace("'", "''"),
|
|
||||||
ico=icon.replace("'", "''"),
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
subprocess.run(
|
|
||||||
["powershell", "-NoProfile", "-Command", ps_cmd],
|
|
||||||
check=True,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if proc.returncode == 0:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"Shortcut Created",
|
"Shortcut Created",
|
||||||
f"Shortcut created on desktop:\n{shortcut_path}",
|
f"Shortcut created on desktop:\n{shortcut_path}",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self,
|
self,
|
||||||
"Shortcut Error",
|
"Shortcut Error",
|
||||||
f"PowerShell failed:\n{e.stderr.decode(errors='ignore')}",
|
f"PowerShell failed with exit code: {proc.returncode}"
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Shortcut Error", f"Could not create shortcut:\n{e}"
|
self, "Shortcut Error", f"Could not create shortcut:\n{e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def toggle_check_updates(self, checked):
|
def toggle_check_updates(self, checked):
|
||||||
self.config["check_updates"] = checked
|
self.config["check_updates"] = checked
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|||||||
+85
-10
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import json
|
import json
|
||||||
import warnings
|
import warnings
|
||||||
import platform
|
import platform
|
||||||
@@ -102,6 +103,86 @@ def clean_text(text, *args, **kwargs):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
default_encoding = sys.getfilesystemencoding()
|
||||||
|
|
||||||
|
def create_process(cmd, stdin=None, text=True):
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Configure root logger to output to console if not already configured
|
||||||
|
root = logging.getLogger()
|
||||||
|
if not root.handlers:
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
formatter = logging.Formatter('%(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
root.addHandler(handler)
|
||||||
|
root.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
"shell": True,
|
||||||
|
"stdout": subprocess.PIPE,
|
||||||
|
"stderr": subprocess.STDOUT,
|
||||||
|
"bufsize": 1, # Line buffered
|
||||||
|
}
|
||||||
|
|
||||||
|
if text:
|
||||||
|
# Configure for text I/O
|
||||||
|
kwargs["text"] = True
|
||||||
|
kwargs["encoding"] = default_encoding
|
||||||
|
kwargs["errors"] = "replace"
|
||||||
|
else:
|
||||||
|
# Configure for binary I/O
|
||||||
|
kwargs["text"] = False
|
||||||
|
# For binary mode, 'encoding' and 'errors' arguments must not be passed to Popen
|
||||||
|
|
||||||
|
if stdin is not None:
|
||||||
|
kwargs["stdin"] = stdin
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||||
|
kwargs.update(
|
||||||
|
{"startupinfo": startupinfo, "creationflags": subprocess.CREATE_NO_WINDOW}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print the command being executed
|
||||||
|
print(f"Executing: {cmd if isinstance(cmd, str) else ' '.join(cmd)}")
|
||||||
|
|
||||||
|
proc = subprocess.Popen(cmd, **kwargs)
|
||||||
|
|
||||||
|
# Stream output to console in real-time
|
||||||
|
if proc.stdout:
|
||||||
|
def _stream_output(stream):
|
||||||
|
if text:
|
||||||
|
# For text mode, read character by character for real-time output
|
||||||
|
while True:
|
||||||
|
char = stream.read(1)
|
||||||
|
if not char:
|
||||||
|
break
|
||||||
|
# Direct write to stdout for immediate feedback
|
||||||
|
sys.stdout.write(char)
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
# For binary mode, read small chunks
|
||||||
|
while True:
|
||||||
|
chunk = stream.read(1) # Read byte by byte for real-time output
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
# Try to decode binary data for display
|
||||||
|
sys.stdout.write(chunk.decode(default_encoding, errors='replace'))
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
# Start a daemon thread to handle output streaming
|
||||||
|
Thread(target=_stream_output, args=(proc.stdout,), daemon=True).start()
|
||||||
|
|
||||||
|
return proc
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
try:
|
try:
|
||||||
with open(get_user_config_path(), "r", encoding="utf-8") as f:
|
with open(get_user_config_path(), "r", encoding="utf-8") as f:
|
||||||
@@ -148,21 +229,15 @@ def prevent_sleep_start():
|
|||||||
0x80000000 | 0x00000001 | 0x00000040
|
0x80000000 | 0x00000001 | 0x00000040
|
||||||
) # ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED
|
) # ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED
|
||||||
elif system == "Darwin":
|
elif system == "Darwin":
|
||||||
_sleep_procs["Darwin"] = subprocess.Popen(["caffeinate"])
|
_sleep_procs["Darwin"] = create_process("caffeinate")
|
||||||
elif system == "Linux":
|
elif system == "Linux":
|
||||||
try:
|
try:
|
||||||
_sleep_procs["Linux"] = subprocess.Popen(
|
_sleep_procs["Linux"] = create_process(
|
||||||
[
|
"systemd-inhibit --what=sleep --why=TextToAudiobook conversion sleep 999999"
|
||||||
"systemd-inhibit",
|
|
||||||
"--what=sleep",
|
|
||||||
"--why=TextToAudiobook conversion",
|
|
||||||
"sleep",
|
|
||||||
"999999",
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(["xdg-screensaver", "reset"])
|
create_process("xdg-screensaver reset")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user