mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Generate subtitles dynamically like sound files, improvements in code
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
# v1.1.2 (pre-release)
|
||||
- Now you can play the audio files while they are processing.
|
||||
- While generating audio files, now it directly writes to audio file instead of RAM, which significantly reduces memory usage and prevents memory outage issues.
|
||||
- Audio and subtitle files are now written directly to disk during generation, which significantly reduces memory usage.
|
||||
- Added a better logic for detecting chapters from the epub, mentioned by @jefro108 in #33
|
||||
- Added a new option: `Reset to default settings`, allowing users to reset all settings to their default values.
|
||||
- Added a new option: `Disable Kokoro's internet access`. This lets you prevent Kokoro from downloading models or voices from HuggingFace Hub, which can help avoid long waiting times if your computer is offline.
|
||||
|
||||
@@ -1908,5 +1908,13 @@ class HandlerDialog(QDialog):
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self.pdf_doc is not None:
|
||||
try:
|
||||
if hasattr(self.pdf_doc, "is_closed"):
|
||||
if not self.pdf_doc.is_closed:
|
||||
self.pdf_doc.close()
|
||||
else:
|
||||
# Fallback: try/except close
|
||||
self.pdf_doc.close()
|
||||
except Exception:
|
||||
pass
|
||||
event.accept()
|
||||
|
||||
+94
-68
@@ -307,6 +307,7 @@ class ConversionThread(QThread):
|
||||
self.log_updated.emit(
|
||||
f"- Output folder: {self.output_folder or os.getcwd()}"
|
||||
)
|
||||
|
||||
self.log_updated.emit("\nInitializing TTS pipeline...")
|
||||
|
||||
# Set device based on use_gpu setting
|
||||
@@ -490,6 +491,30 @@ class ConversionThread(QThread):
|
||||
self.log_updated.emit((f"Unsupported output format: {self.output_format}", "red"))
|
||||
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
||||
return
|
||||
# Open merged subtitle file for incremental writing if needed
|
||||
merged_subtitle_file = None
|
||||
if self.subtitle_mode != "Disabled":
|
||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||
merged_subtitle_path = os.path.splitext(merged_out_path)[0] + f".{file_extension}"
|
||||
if "ass" in subtitle_format:
|
||||
merged_subtitle_file = open(merged_subtitle_path, "w", encoding="utf-8", errors="replace")
|
||||
# Minimal ASS header
|
||||
merged_subtitle_file.write("[Script Info]\n")
|
||||
merged_subtitle_file.write("Title: Generated by Abogen\n")
|
||||
merged_subtitle_file.write("ScriptType: v4.00+\n\n")
|
||||
merged_subtitle_file.write("[Events]\n")
|
||||
merged_subtitle_file.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
|
||||
# Set margin/alignment for ASS
|
||||
is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
|
||||
is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
|
||||
merged_subtitle_margin = "90" if is_narrow else ""
|
||||
merged_subtitle_alignment_tag = f"{{\\an5}}" if is_centered else ""
|
||||
else:
|
||||
merged_subtitle_file = open(merged_subtitle_path, "w", encoding="utf-8", errors="replace")
|
||||
else:
|
||||
merged_subtitle_path = None
|
||||
merged_subtitle_file = None
|
||||
else:
|
||||
# If not merging, set merged_out_file and related variables to None
|
||||
merged_out_file = None
|
||||
@@ -557,10 +582,37 @@ class ConversionThread(QThread):
|
||||
else:
|
||||
self.log_updated.emit((f"Unsupported chapter format: {separate_chapters_format}", "red"))
|
||||
continue
|
||||
# Open chapter subtitle file for incremental writing if needed
|
||||
chapter_subtitle_file = None
|
||||
if self.subtitle_mode != "Disabled":
|
||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||
chapter_subtitle_path = os.path.join(
|
||||
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
||||
)
|
||||
if "ass" in subtitle_format:
|
||||
chapter_subtitle_file = open(chapter_subtitle_path, "w", encoding="utf-8", errors="replace")
|
||||
# Minimal ASS header
|
||||
chapter_subtitle_file.write("[Script Info]\n")
|
||||
chapter_subtitle_file.write("Title: Generated by Abogen\n")
|
||||
chapter_subtitle_file.write("ScriptType: v4.00+\n\n")
|
||||
chapter_subtitle_file.write("[Events]\n")
|
||||
chapter_subtitle_file.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
|
||||
is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
|
||||
is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
|
||||
chapter_subtitle_margin = "90" if is_narrow else ""
|
||||
chapter_subtitle_alignment_tag = f"{{\\an5}}" if is_centered else ""
|
||||
else:
|
||||
chapter_subtitle_file = open(chapter_subtitle_path, "w", encoding="utf-8", errors="replace")
|
||||
else:
|
||||
chapter_subtitle_path = None
|
||||
chapter_subtitle_file = None
|
||||
else:
|
||||
chapter_out_file = None
|
||||
chapter_out_path = None
|
||||
chapter_ffmpeg_proc = None
|
||||
chapter_subtitle_file = None
|
||||
chapter_subtitle_path = None
|
||||
for result in tts(
|
||||
chapter_text,
|
||||
voice=loaded_voice,
|
||||
@@ -631,18 +683,49 @@ class ConversionThread(QThread):
|
||||
# Process tokens according to subtitle mode
|
||||
# Global subtitle processing ONLY if merging
|
||||
if merge_chapters_at_end:
|
||||
# Incremental subtitle writing for merged output
|
||||
new_entries = []
|
||||
self._process_subtitle_tokens(
|
||||
tokens_with_timestamps,
|
||||
subtitle_entries,
|
||||
new_entries,
|
||||
self.max_subtitle_words,
|
||||
)
|
||||
if merged_subtitle_file:
|
||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||
if "ass" in subtitle_format:
|
||||
for (start, end, text) in new_entries:
|
||||
start_time = self._ass_time(start)
|
||||
end_time = self._ass_time(end)
|
||||
merged_subtitle_file.write(
|
||||
f"Dialogue: 0,{start_time},{end_time},Default,,{merged_subtitle_margin},{merged_subtitle_margin},0,,{merged_subtitle_alignment_tag}{text}\n"
|
||||
)
|
||||
else:
|
||||
for i, (start, end, text) in enumerate(new_entries, 1):
|
||||
merged_subtitle_file.write(
|
||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||
)
|
||||
# Per-chapter subtitle processing for both file and ffmpeg_proc
|
||||
if chapter_out_file or chapter_ffmpeg_proc:
|
||||
new_chapter_entries = []
|
||||
self._process_subtitle_tokens(
|
||||
chapter_tokens_with_timestamps,
|
||||
chapter_subtitle_entries,
|
||||
new_chapter_entries,
|
||||
self.max_subtitle_words,
|
||||
)
|
||||
if chapter_subtitle_file:
|
||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||
if "ass" in subtitle_format:
|
||||
for (start, end, text) in new_chapter_entries:
|
||||
start_time = self._ass_time(start)
|
||||
end_time = self._ass_time(end)
|
||||
chapter_subtitle_file.write(
|
||||
f"Dialogue: 0,{start_time},{end_time},Default,,{chapter_subtitle_margin},{chapter_subtitle_margin},0,,{chapter_subtitle_alignment_tag}{text}\n"
|
||||
)
|
||||
else:
|
||||
for i, (start, end, text) in enumerate(new_chapter_entries, 1):
|
||||
chapter_subtitle_file.write(
|
||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||
)
|
||||
if merge_chapters_at_end:
|
||||
current_time += chunk_dur
|
||||
if chapter_out_file or chapter_ffmpeg_proc:
|
||||
@@ -687,41 +770,10 @@ class ConversionThread(QThread):
|
||||
chapter_ffmpeg_proc.wait()
|
||||
if chapter_out_file:
|
||||
chapter_out_file.close()
|
||||
# Subtitle file for chapter
|
||||
if save_chapters_separately and total_chapters > 1 and self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||
chapter_subtitle_path = os.path.join(
|
||||
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
||||
)
|
||||
if "ass" in subtitle_format:
|
||||
is_centered = subtitle_format in (
|
||||
"ass_centered_wide",
|
||||
"ass_centered_narrow",
|
||||
)
|
||||
is_narrow = subtitle_format in (
|
||||
"ass_narrow",
|
||||
"ass_centered_narrow",
|
||||
)
|
||||
self._write_ass_subtitle(
|
||||
chapter_subtitle_path,
|
||||
chapter_subtitle_entries,
|
||||
is_centered,
|
||||
is_narrow,
|
||||
)
|
||||
else:
|
||||
with open(
|
||||
chapter_subtitle_path,
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
) as srt_file:
|
||||
for i, (start, end, text) in enumerate(
|
||||
chapter_subtitle_entries, 1
|
||||
):
|
||||
srt_file.write(
|
||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||
)
|
||||
# Close chapter subtitle file if open
|
||||
if chapter_subtitle_file:
|
||||
chapter_subtitle_file.close()
|
||||
if save_chapters_separately and total_chapters > 1 and self.subtitle_mode != "Disabled" and chapter_subtitle_path:
|
||||
self.log_updated.emit(
|
||||
(
|
||||
f"\nChapter {chapter_idx} saved to: {chapter_out_path}\n\nChapter subtitle saved to: {chapter_subtitle_path}",
|
||||
@@ -735,7 +787,6 @@ class ConversionThread(QThread):
|
||||
"green",
|
||||
)
|
||||
)
|
||||
|
||||
# Finalize merged output file ONLY if merging
|
||||
if merge_chapters_at_end:
|
||||
self.log_updated.emit(("\nFinalizing audio...", "grey"))
|
||||
@@ -787,40 +838,15 @@ class ConversionThread(QThread):
|
||||
ffmpeg_proc.stdin.close()
|
||||
ffmpeg_proc.wait()
|
||||
self.progress_updated.emit(100, "00:00:00")
|
||||
|
||||
# Close merged subtitle file if open
|
||||
if merged_subtitle_file:
|
||||
merged_subtitle_file.close()
|
||||
# Subtitle and final message logic
|
||||
if merge_chapters_at_end:
|
||||
if self.subtitle_mode != "Disabled":
|
||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||
subtitle_path = (
|
||||
os.path.splitext(merged_out_path)[0] + f".{file_extension}"
|
||||
)
|
||||
if "ass" in subtitle_format:
|
||||
is_centered = subtitle_format in (
|
||||
"ass_centered_wide",
|
||||
"ass_centered_narrow",
|
||||
)
|
||||
is_narrow = subtitle_format in (
|
||||
"ass_narrow",
|
||||
"ass_centered_narrow",
|
||||
)
|
||||
self._write_ass_subtitle(
|
||||
subtitle_path, subtitle_entries, is_centered, is_narrow
|
||||
)
|
||||
else:
|
||||
with open(
|
||||
subtitle_path, "w", encoding="utf-8", errors="replace"
|
||||
) as srt_file:
|
||||
for i, (start, end, text) in enumerate(
|
||||
subtitle_entries, 1
|
||||
):
|
||||
srt_file.write(
|
||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||
)
|
||||
self.conversion_finished.emit(
|
||||
(
|
||||
f"\nAudiobook saved to: {merged_out_path}\n\nSubtitle saved to: {subtitle_path}",
|
||||
f"\nAudiobook saved to: {merged_out_path}\n\nSubtitle saved to: {merged_subtitle_path}",
|
||||
"green",
|
||||
),
|
||||
merged_out_path,
|
||||
@@ -991,7 +1017,7 @@ class ConversionThread(QThread):
|
||||
word_count += 1
|
||||
|
||||
# Split sentences based on separator or word count
|
||||
if (re.search(separator, token["text"])) or word_count >= max_subtitle_words:
|
||||
if (re.search(separator, token["text"]) and token["whitespace"] == " ") or word_count >= max_subtitle_words:
|
||||
if current_sentence:
|
||||
# Create subtitle entry for this sentence
|
||||
start_time = current_sentence[0]["start"]
|
||||
|
||||
+14
-2
@@ -2808,10 +2808,22 @@ class abogen(QWidget):
|
||||
save_config(self.config)
|
||||
|
||||
def toggle_kokoro_internet_access(self, disabled):
|
||||
if disabled:
|
||||
message = (
|
||||
"Disabling Kokoro's internet access will block downloads of models and voices from Hugging Face Hub. "
|
||||
"This can make processing faster when there is no internet connection, since no requests will be made. "
|
||||
"The app needs to restart to apply this change.\n\nDo you want to continue?"
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
"Enabling Kokoro's internet access will allow it to download models and voices from Hugging Face Hub. "
|
||||
"The app needs to restart to apply this change.\n\nDo you want to continue?"
|
||||
)
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Restart Required",
|
||||
"Changing Kokoro's internet access requires restarting the application.\nDo you want to continue?",
|
||||
|
||||
message,
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
@@ -2834,7 +2846,7 @@ class abogen(QWidget):
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Reset Settings",
|
||||
"This will reset all settings to their default values and restart the application.\nDo you want to continue?",
|
||||
"This will reset all settings to their default values and restart the application.\n\nDo you want to continue?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user