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)
|
# v1.1.2 (pre-release)
|
||||||
- Now you can play the audio files while they are processing.
|
- 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 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: `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.
|
- 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):
|
def closeEvent(self, event):
|
||||||
if self.pdf_doc is not None:
|
if self.pdf_doc is not None:
|
||||||
self.pdf_doc.close()
|
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()
|
event.accept()
|
||||||
|
|||||||
+95
-69
@@ -305,8 +305,9 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
if self.save_option == "Choose output folder":
|
if self.save_option == "Choose output folder":
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
f" - Output folder: {self.output_folder or os.getcwd()}"
|
f"- Output folder: {self.output_folder or os.getcwd()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log_updated.emit("\nInitializing TTS pipeline...")
|
self.log_updated.emit("\nInitializing TTS pipeline...")
|
||||||
|
|
||||||
# Set device based on use_gpu setting
|
# 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.log_updated.emit((f"Unsupported output format: {self.output_format}", "red"))
|
||||||
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
||||||
return
|
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:
|
else:
|
||||||
# If not merging, set merged_out_file and related variables to None
|
# If not merging, set merged_out_file and related variables to None
|
||||||
merged_out_file = None
|
merged_out_file = None
|
||||||
@@ -557,10 +582,37 @@ class ConversionThread(QThread):
|
|||||||
else:
|
else:
|
||||||
self.log_updated.emit((f"Unsupported chapter format: {separate_chapters_format}", "red"))
|
self.log_updated.emit((f"Unsupported chapter format: {separate_chapters_format}", "red"))
|
||||||
continue
|
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:
|
else:
|
||||||
chapter_out_file = None
|
chapter_out_file = None
|
||||||
chapter_out_path = None
|
chapter_out_path = None
|
||||||
chapter_ffmpeg_proc = None
|
chapter_ffmpeg_proc = None
|
||||||
|
chapter_subtitle_file = None
|
||||||
|
chapter_subtitle_path = None
|
||||||
for result in tts(
|
for result in tts(
|
||||||
chapter_text,
|
chapter_text,
|
||||||
voice=loaded_voice,
|
voice=loaded_voice,
|
||||||
@@ -631,18 +683,49 @@ class ConversionThread(QThread):
|
|||||||
# Process tokens according to subtitle mode
|
# Process tokens according to subtitle mode
|
||||||
# Global subtitle processing ONLY if merging
|
# Global subtitle processing ONLY if merging
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
|
# Incremental subtitle writing for merged output
|
||||||
|
new_entries = []
|
||||||
self._process_subtitle_tokens(
|
self._process_subtitle_tokens(
|
||||||
tokens_with_timestamps,
|
tokens_with_timestamps,
|
||||||
subtitle_entries,
|
new_entries,
|
||||||
self.max_subtitle_words,
|
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
|
# Per-chapter subtitle processing for both file and ffmpeg_proc
|
||||||
if chapter_out_file or chapter_ffmpeg_proc:
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
|
new_chapter_entries = []
|
||||||
self._process_subtitle_tokens(
|
self._process_subtitle_tokens(
|
||||||
chapter_tokens_with_timestamps,
|
chapter_tokens_with_timestamps,
|
||||||
chapter_subtitle_entries,
|
new_chapter_entries,
|
||||||
self.max_subtitle_words,
|
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:
|
if merge_chapters_at_end:
|
||||||
current_time += chunk_dur
|
current_time += chunk_dur
|
||||||
if chapter_out_file or chapter_ffmpeg_proc:
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
@@ -687,41 +770,10 @@ class ConversionThread(QThread):
|
|||||||
chapter_ffmpeg_proc.wait()
|
chapter_ffmpeg_proc.wait()
|
||||||
if chapter_out_file:
|
if chapter_out_file:
|
||||||
chapter_out_file.close()
|
chapter_out_file.close()
|
||||||
# Subtitle file for chapter
|
# Close chapter subtitle file if open
|
||||||
if save_chapters_separately and total_chapters > 1 and self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
if chapter_subtitle_file:
|
||||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
chapter_subtitle_file.close()
|
||||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
if save_chapters_separately and total_chapters > 1 and self.subtitle_mode != "Disabled" and chapter_subtitle_path:
|
||||||
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"
|
|
||||||
)
|
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
(
|
(
|
||||||
f"\nChapter {chapter_idx} saved to: {chapter_out_path}\n\nChapter subtitle saved to: {chapter_subtitle_path}",
|
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",
|
"green",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Finalize merged output file ONLY if merging
|
# Finalize merged output file ONLY if merging
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
self.log_updated.emit(("\nFinalizing audio...", "grey"))
|
self.log_updated.emit(("\nFinalizing audio...", "grey"))
|
||||||
@@ -787,40 +838,15 @@ class ConversionThread(QThread):
|
|||||||
ffmpeg_proc.stdin.close()
|
ffmpeg_proc.stdin.close()
|
||||||
ffmpeg_proc.wait()
|
ffmpeg_proc.wait()
|
||||||
self.progress_updated.emit(100, "00:00:00")
|
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
|
# Subtitle and final message logic
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
if self.subtitle_mode != "Disabled":
|
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(
|
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",
|
"green",
|
||||||
),
|
),
|
||||||
merged_out_path,
|
merged_out_path,
|
||||||
@@ -991,7 +1017,7 @@ class ConversionThread(QThread):
|
|||||||
word_count += 1
|
word_count += 1
|
||||||
|
|
||||||
# Split sentences based on separator or word count
|
# 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:
|
if current_sentence:
|
||||||
# Create subtitle entry for this sentence
|
# Create subtitle entry for this sentence
|
||||||
start_time = current_sentence[0]["start"]
|
start_time = current_sentence[0]["start"]
|
||||||
|
|||||||
+14
-2
@@ -2808,10 +2808,22 @@ class abogen(QWidget):
|
|||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|
||||||
def toggle_kokoro_internet_access(self, disabled):
|
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(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Restart Required",
|
"Restart Required",
|
||||||
"Changing Kokoro's internet access requires restarting the application.\nDo you want to continue?",
|
|
||||||
|
message,
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.No,
|
QMessageBox.No,
|
||||||
)
|
)
|
||||||
@@ -2834,7 +2846,7 @@ class abogen(QWidget):
|
|||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Reset Settings",
|
"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.Yes | QMessageBox.No,
|
||||||
QMessageBox.No,
|
QMessageBox.No,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user