7 Commits
6 changed files with 110 additions and 93 deletions
+11
View File
@@ -1,3 +1,14 @@
# 1.1.4
- Fixed extra metadata information not being saved to M4B files, ensuring that all metadata is correctly written to the output file.
- Reformatted the code using Black for better readability and consistency.
# 1.1.3
- `M4B (with chapters)` generation is faster now, as it directly generates `m4b` files instead of converting from `wav`, which significantly reduces processing time, fixes the issue mentioned by @Milor123 in #39
- Better sleep state handling for Linux.
- The app window now tries to fit the screen if its height would exceed the available display area.
- Fixed issue where the app would not restart properly on Windows.
- Fixed last sentence/subtitle entry timing in generated subtitles, the end time of the final subtitle entry now correctly matches the end of the audio chunk, preventing zero or invalid timings at the end.
# v1.1.2
- Now you can play the audio files while they are processing.
- Audio and subtitle files are now written directly to disk during generation, which significantly reduces memory usage.
+1 -1
View File
@@ -1 +1 @@
1.1.2
1.1.4
+59 -77
View File
@@ -488,13 +488,33 @@ class ConversionThread(QThread):
)
ffmpeg_proc = None
elif self.output_format == "m4b":
temp_wav_path = os.path.join(
out_dir, f"temp_{base_name}{suffix}.wav"
)
merged_out_file = sf.SoundFile(
temp_wav_path, "w", samplerate=24000, channels=1, format="wav"
)
# Real-time M4B generation using FFmpeg pipe
static_ffmpeg.add_paths()
merged_out_file = None
ffmpeg_proc = None
# Prepare ffmpeg command for m4b output
cmd = [
"ffmpeg",
"-y",
"-thread_queue_size",
"32768",
"-f",
"f32le",
"-ar",
"24000",
"-ac",
"1",
"-i",
"pipe:0",
"-c:a",
"aac",
"-q:a",
"2",
"-movflags",
"+faststart+use_metadata_tags",
]
cmd.append(merged_out_path)
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
elif self.output_format == "opus":
static_ffmpeg.add_paths()
cmd = [
@@ -793,6 +813,7 @@ class ConversionThread(QThread):
tokens_with_timestamps,
new_entries,
self.max_subtitle_words,
fallback_end_time=chunk_start + chunk_dur,
)
if merged_subtitle_file:
subtitle_format = getattr(
@@ -819,6 +840,7 @@ class ConversionThread(QThread):
chapter_tokens_with_timestamps,
new_chapter_entries,
self.max_subtitle_words,
fallback_end_time=chapter_current_time + chunk_dur,
)
if chapter_subtitle_file:
subtitle_format = getattr(
@@ -910,8 +932,9 @@ class ConversionThread(QThread):
if self.output_format in ["wav", "mp3", "flac"]:
merged_out_file.close()
elif self.output_format == "m4b":
merged_out_file.close()
# Only generate chapters info if there are chapters
ffmpeg_proc.stdin.close()
ffmpeg_proc.wait()
# Add chapters via fast post-processing
if total_chapters > 1:
chapters_info_path = f"{base_filepath_no_ext}_chapters.txt"
with open(chapters_info_path, "w", encoding="utf-8") as f:
@@ -923,15 +946,19 @@ class ConversionThread(QThread):
f.write(f"START={int(chapter['start']*1000)}\n")
f.write(f"END={int(chapter['end']*1000)}\n")
f.write(f"title={chapter_title}\n\n")
# Fast mux chapters into m4b (write to temp file, then replace original)
static_ffmpeg.add_paths()
orig_path = merged_out_path
root, ext = os.path.splitext(orig_path)
tmp_path = root + ".tmp" + ext
metadata_options = (
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
)
static_ffmpeg.add_paths()
cmd = [
"ffmpeg",
"-y",
"-i",
temp_wav_path,
orig_path,
"-i",
chapters_info_path,
"-map",
@@ -940,46 +967,15 @@ class ConversionThread(QThread):
"1",
"-map_chapters",
"1",
*metadata_options,
"-c:a",
"aac",
"-q:a",
"2",
"-movflags",
"+faststart+use_metadata_tags",
merged_out_path,
"copy",
]
cmd += metadata_options
cmd.append(tmp_path)
proc = create_process(cmd)
proc.wait()
self.log_updated.emit(
("\nCleaning up temporary files...", "grey")
)
if os.path.exists(chapters_info_path):
os.replace(tmp_path, orig_path)
os.remove(chapters_info_path)
else:
# No chapters: skip chapters info and related ffmpeg args
metadata_options = (
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
)
static_ffmpeg.add_paths()
cmd = [
"ffmpeg",
"-y",
"-i",
temp_wav_path,
*metadata_options,
"-c:a",
"aac",
"-q:a",
"2",
"-movflags",
"+faststart+use_metadata_tags",
merged_out_path,
]
proc = create_process(cmd)
proc.wait()
if os.path.exists(temp_wav_path):
os.remove(temp_wav_path)
elif self.output_format in ["opus"]:
ffmpeg_proc.stdin.close()
ffmpeg_proc.wait()
@@ -1146,7 +1142,11 @@ class ConversionThread(QThread):
return f"{h}:{m:02}:{s:02}.{cs:02}"
def _process_subtitle_tokens(
self, tokens_with_timestamps, subtitle_entries, max_subtitle_words
self,
tokens_with_timestamps,
subtitle_entries,
max_subtitle_words,
fallback_end_time=None,
):
"""Helper function to process subtitle tokens according to the subtitle mode"""
if not tokens_with_timestamps:
@@ -1191,9 +1191,15 @@ class ConversionThread(QThread):
sentence_text = ""
for t in current_sentence:
sentence_text += t["text"] + (t.get("whitespace", "") or "")
subtitle_entries.append((start_time, end_time, sentence_text.strip()))
# Fallback for last entry
if subtitle_entries and fallback_end_time is not None:
last_entry = subtitle_entries[-1]
start, end, text = last_entry
if end is None or end <= start or end <= 0:
subtitle_entries[-1] = (start, fallback_end_time, text)
else:
# Word count-based grouping
try:
@@ -1230,36 +1236,12 @@ class ConversionThread(QThread):
subtitle_entries.append(
(group[0]["start"], group[-1]["end"], text.strip())
)
def _write_ass_subtitle(
self, file_path, subtitle_entries, is_centered=False, is_narrow=False
):
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
# Minimal ASS header
f.write("[Script Info]\n")
f.write("Title: Generated by Abogen\n")
f.write("ScriptType: v4.00+\n\n")
# Only events section, use override tags for positioning
f.write("[Events]\n")
f.write(
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
)
# Set margin based on is_narrow parameter
margin = "90" if is_narrow else ""
alignment_tag = ""
if is_centered:
alignment = 5
alignment_tag = f"{{\\an{alignment}}}"
# Write each subtitle with override tag for alignment and margins
for i, (start, end, text) in enumerate(subtitle_entries, 1):
start_time = self._ass_time(start)
end_time = self._ass_time(end)
f.write(
f"Dialogue: 0,{start_time},{end_time},Default,,{margin},{margin},0,,{alignment_tag}{text}\n"
)
# Fallback for last entry
if subtitle_entries and fallback_end_time is not None:
last_entry = subtitle_entries[-1]
start, end, text = last_entry
if end is None or end <= start or end <= 0:
subtitle_entries[-1] = (start, fallback_end_time, text)
def cancel(self):
self.cancel_requested = True
+25 -11
View File
@@ -763,7 +763,11 @@ class abogen(QWidget):
self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}")
screen = QApplication.primaryScreen().geometry()
width, height = 500, 800
x, y = (screen.width() - width) // 2, (screen.height() - height) // 2
x = (screen.width() - width) // 2
# If desired height is larger than screen, fit to screen height
if height > screen.height() - 65:
height = screen.height() - 100 # Leave a margin for window borders
y = max((screen.height() - height) // 2, 0)
self.setGeometry(x, y, width, height)
outer_layout = QVBoxLayout()
outer_layout.setContentsMargins(15, 15, 15, 15)
@@ -2815,6 +2819,24 @@ class abogen(QWidget):
self.config["replace_single_newlines"] = enabled
save_config(self.config)
def restart_app(self):
from PyQt5.QtCore import QProcess
import sys
exe = sys.executable
args = sys.argv
# On Windows, use .exe if available
if platform.system() == "Windows":
script_path = args[0]
if not script_path.lower().endswith(".exe"):
exe_path = os.path.splitext(script_path)[0] + ".exe"
if os.path.exists(exe_path):
args[0] = exe_path
QProcess.startDetached(exe, args)
QApplication.quit()
def toggle_kokoro_internet_access(self, disabled):
if disabled:
message = (
@@ -2838,11 +2860,7 @@ class abogen(QWidget):
self.config["disable_kokoro_internet"] = disabled
save_config(self.config)
try:
from PyQt5.QtCore import QProcess
import sys
QProcess.startDetached(sys.executable, sys.argv)
QApplication.quit()
self.restart_app()
except Exception as e:
QMessageBox.critical(
self, "Restart Failed", f"Failed to restart the application:\n{e}"
@@ -2858,16 +2876,12 @@ class abogen(QWidget):
)
if reply == QMessageBox.Yes:
from abogen.utils import get_user_config_path
import sys
config_path = get_user_config_path()
try:
if os.path.exists(config_path):
os.remove(config_path)
from PyQt5.QtCore import QProcess
QProcess.startDetached(sys.executable, sys.argv)
QApplication.quit()
self.restart_app()
except Exception as e:
QMessageBox.critical(
self, "Reset Error", f"Could not reset settings:\n{e}"
+5 -1
View File
@@ -2,6 +2,7 @@ from json import load
import os
import sys
import platform
import atexit
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
@@ -9,7 +10,7 @@ from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
# Add the directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
from abogen.utils import get_resource_path, load_config
from abogen.utils import get_resource_path, load_config, prevent_sleep_end
# Set Hugging Face Hub environment variables
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" # Disable Hugging Face telemetry
@@ -27,6 +28,9 @@ from abogen.constants import PROGRAM_NAME, VERSION
os.environ["MIOPEN_FIND_MODE"] = "FAST"
os.environ["MIOPEN_CONV_PRECISE_ROCM_TUNING"] = "0"
# Reset sleep states
atexit.register(prevent_sleep_end)
# Ensure sys.stdout and sys.stderr are valid in GUI mode
if sys.stdout is None:
sys.stdout = open(os.devnull, "w")
+8 -2
View File
@@ -244,6 +244,8 @@ def get_gpu_acceleration(enabled):
def prevent_sleep_start():
from abogen.constants import PROGRAM_NAME
system = platform.system()
if system == "Windows":
import ctypes
@@ -254,11 +256,15 @@ def prevent_sleep_start():
elif system == "Darwin":
_sleep_procs["Darwin"] = create_process(["caffeinate"])
elif system == "Linux":
# use a sleep that never exits so inhibition stays active
# Add program name and reason for inhibition
program_name = PROGRAM_NAME
reason = "Prevent sleep during abogen process"
_sleep_procs["Linux"] = create_process(
[
"systemd-inhibit",
"--what=handle-lid-switch:sleep",
f"--who={program_name}",
f"--why={reason}",
"--what=sleep",
"--mode=block",
"sleep",
"infinity",