mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Directly write to audio file instead of RAM, improvements
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
# v1.1.2 (pre-release)
|
# 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.
|
||||||
- Potential fix for #37 and #38, where the program was becoming slow while processing large files.
|
- Potential fix for #37 and #38, where the program was becoming slow while processing large files.
|
||||||
- Fixed `Open folder` and `Open file` buttons in the queue manager GUI.
|
- Fixed `Open folder` and `Open file` buttons in the queue manager GUI.
|
||||||
|
- Improvements in code structure.
|
||||||
|
|
||||||
# v1.1.1
|
# v1.1.1
|
||||||
- Fixed adding wrong file in queue for EPUB and PDF files, ensuring the correct file is added to the queue.
|
- Fixed adding wrong file in queue for EPUB and PDF files, ensuring the correct file is added to the queue.
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ VERSION = get_version()
|
|||||||
|
|
||||||
# Settings
|
# Settings
|
||||||
CHAPTER_OPTIONS_COUNTDOWN = 30 # Countdown seconds for chapter options
|
CHAPTER_OPTIONS_COUNTDOWN = 30 # Countdown seconds for chapter options
|
||||||
|
SUBTITLE_FORMATS = [
|
||||||
|
("srt", "SRT (standard)"),
|
||||||
|
("ass_wide", "ASS (wide)"),
|
||||||
|
("ass_narrow", "ASS (narrow)"),
|
||||||
|
("ass_centered_wide", "ASS (centered wide)"),
|
||||||
|
("ass_centered_narrow", "ASS (centered narrow)"),
|
||||||
|
]
|
||||||
|
|
||||||
# Language description mapping
|
# Language description mapping
|
||||||
LANGUAGE_DESCRIPTIONS = {
|
LANGUAGE_DESCRIPTIONS = {
|
||||||
|
|||||||
+230
-424
@@ -16,6 +16,7 @@ from abogen.constants import (
|
|||||||
SAMPLE_VOICE_TEXTS,
|
SAMPLE_VOICE_TEXTS,
|
||||||
COLORS,
|
COLORS,
|
||||||
CHAPTER_OPTIONS_COUNTDOWN,
|
CHAPTER_OPTIONS_COUNTDOWN,
|
||||||
|
SUBTITLE_FORMATS,
|
||||||
)
|
)
|
||||||
from abogen.voice_formulas import get_new_voice
|
from abogen.voice_formulas import get_new_voice
|
||||||
import abogen.hf_tracker as hf_tracker
|
import abogen.hf_tracker as hf_tracker
|
||||||
@@ -244,137 +245,6 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
return samples_processed
|
return samples_processed
|
||||||
|
|
||||||
def _process_audio_segments(
|
|
||||||
self,
|
|
||||||
audio_segments,
|
|
||||||
output_path,
|
|
||||||
output_format,
|
|
||||||
use_ffmpeg=False,
|
|
||||||
ffmpeg_args=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Process audio segments to a target format with memory-efficient handling
|
|
||||||
|
|
||||||
Args:
|
|
||||||
audio_segments: List of audio segments to process
|
|
||||||
output_path: Path for output file
|
|
||||||
output_format: Format of output (wav, mp3, flac, opus, m4b)
|
|
||||||
use_ffmpeg: Whether to use FFmpeg instead of soundfile
|
|
||||||
ffmpeg_args: Optional additional FFmpeg arguments when use_ffmpeg=True
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (success, output_path)
|
|
||||||
"""
|
|
||||||
self.log_updated.emit(f"\nProcessing audio data to {output_format.upper()}...")
|
|
||||||
segments_count = len(audio_segments)
|
|
||||||
|
|
||||||
# Handle direct streaming for WAV (the only format supporting append mode)
|
|
||||||
if output_format == "wav" and not use_ffmpeg:
|
|
||||||
try:
|
|
||||||
# Write first segment
|
|
||||||
sf.write(output_path, audio_segments[0], 24000, format="wav")
|
|
||||||
|
|
||||||
# Append remaining segments
|
|
||||||
for i, segment in enumerate(audio_segments[1:], 1):
|
|
||||||
with sf.SoundFile(output_path, mode="r+") as f:
|
|
||||||
f.seek(0, sf.SEEK_END)
|
|
||||||
f.write(segment)
|
|
||||||
return True, output_path
|
|
||||||
except Exception as e:
|
|
||||||
self.log_updated.emit((f"Error writing WAV file: {str(e)}", "red"))
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
# For formats requiring FFmpeg (opus, m4b) or when explicitly requested
|
|
||||||
if use_ffmpeg or output_format in ["opus", "m4b"]:
|
|
||||||
static_ffmpeg.add_paths()
|
|
||||||
|
|
||||||
# Basic FFmpeg command
|
|
||||||
cmd = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-y",
|
|
||||||
"-thread_queue_size",
|
|
||||||
"32768",
|
|
||||||
"-f",
|
|
||||||
"f32le",
|
|
||||||
"-ar",
|
|
||||||
"24000",
|
|
||||||
"-ac",
|
|
||||||
"1",
|
|
||||||
"-i",
|
|
||||||
"pipe:0",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add custom FFmpeg arguments if provided
|
|
||||||
if ffmpeg_args:
|
|
||||||
cmd.extend(ffmpeg_args)
|
|
||||||
else:
|
|
||||||
# Default codec settings based on format
|
|
||||||
if output_format == "opus":
|
|
||||||
cmd.extend(["-c:a", "libopus", "-b:a", "24000"])
|
|
||||||
elif output_format == "mp3":
|
|
||||||
cmd.extend(["-c:a", "libmp3lame", "-q:a", "2"])
|
|
||||||
elif output_format == "flac":
|
|
||||||
cmd.extend(["-c:a", "flac", "-compression_level", "8"])
|
|
||||||
else:
|
|
||||||
cmd.extend(["-c:a", "aac", "-q:a", "2"])
|
|
||||||
|
|
||||||
# Add output path
|
|
||||||
cmd.append(output_path)
|
|
||||||
|
|
||||||
# Create process
|
|
||||||
proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
|
|
||||||
|
|
||||||
# Process segments
|
|
||||||
try:
|
|
||||||
# Use the unified streaming function
|
|
||||||
def process_chunk(chunk_bytes, is_last):
|
|
||||||
proc.stdin.write(chunk_bytes)
|
|
||||||
|
|
||||||
self._stream_audio_in_chunks(
|
|
||||||
audio_segments,
|
|
||||||
process_chunk,
|
|
||||||
progress_prefix=f"Processing {output_format.upper()}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Close stdin and wait for process to complete
|
|
||||||
proc.stdin.close()
|
|
||||||
if proc.wait() != 0:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"{output_format.upper()} conversion failed.", "red")
|
|
||||||
)
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
return True, output_path
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(
|
|
||||||
f"Error during {output_format.upper()} conversion: {str(e)}",
|
|
||||||
"red",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
proc.stdin.close()
|
|
||||||
try:
|
|
||||||
proc.terminate()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
# For formats supported by soundfile (mp3, flac)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
with sf.SoundFile(
|
|
||||||
output_path, "w", samplerate=24000, channels=1, format=output_format
|
|
||||||
) as f:
|
|
||||||
for i, segment in enumerate(audio_segments):
|
|
||||||
f.write(segment)
|
|
||||||
return True, output_path
|
|
||||||
except Exception as e:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"Error processing {output_format.upper()} file: {str(e)}", "red")
|
|
||||||
)
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
print(
|
print(
|
||||||
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"
|
||||||
@@ -408,7 +278,7 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}")
|
self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}")
|
||||||
self.log_updated.emit(f"- Output format: {self.output_format}")
|
self.log_updated.emit(f"- Output format: {self.output_format}")
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
f"- Subtitle format: {getattr(self, 'subtitle_format', 'srt')}"
|
f"- Subtitle format: {next((label for value, label in SUBTITLE_FORMATS if value == getattr(self, 'subtitle_format', 'srt')), getattr(self, 'subtitle_format', 'srt'))}"
|
||||||
)
|
)
|
||||||
self.log_updated.emit(f"- Save option: {self.save_option}")
|
self.log_updated.emit(f"- Save option: {self.save_option}")
|
||||||
if self.replace_single_newlines:
|
if self.replace_single_newlines:
|
||||||
@@ -523,6 +393,12 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# If save_chapters_separately is enabled, find a unique suffix ONCE and use for both folder and merged file
|
# If save_chapters_separately is enabled, find a unique suffix ONCE and use for both folder and merged file
|
||||||
save_chapters_separately = getattr(self, "save_chapters_separately", False)
|
save_chapters_separately = getattr(self, "save_chapters_separately", False)
|
||||||
|
merge_chapters_at_end = getattr(self, "merge_chapters_at_end", True)
|
||||||
|
|
||||||
|
# Ensure merge_chapters_at_end is True if not saving chapters separately
|
||||||
|
if not save_chapters_separately:
|
||||||
|
merge_chapters_at_end = True
|
||||||
|
|
||||||
chapters_out_dir = None
|
chapters_out_dir = None
|
||||||
suffix = ""
|
suffix = ""
|
||||||
|
|
||||||
@@ -562,58 +438,90 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not os.path.exists(chapters_out_dir_candidate)
|
not os.path.exists(chapters_out_dir_candidate)
|
||||||
and not os.path.exists(merged_file_candidate)
|
and (not merge_chapters_at_end or not os.path.exists(merged_file_candidate))
|
||||||
and (
|
and (
|
||||||
self.subtitle_mode == "Disabled"
|
self.subtitle_mode == "Disabled"
|
||||||
|
or not merge_chapters_at_end
|
||||||
or not os.path.exists(merged_srt_candidate)
|
or not os.path.exists(merged_srt_candidate)
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
counter += 1
|
counter += 1
|
||||||
if save_chapters_separately and total_chapters > 1:
|
if save_chapters_separately and total_chapters > 1:
|
||||||
|
separate_chapters_format = getattr(self, "separate_chapters_format", "wav")
|
||||||
chapters_out_dir = chapters_out_dir_candidate
|
chapters_out_dir = chapters_out_dir_candidate
|
||||||
os.makedirs(chapters_out_dir, exist_ok=True)
|
os.makedirs(chapters_out_dir, exist_ok=True)
|
||||||
self.log_updated.emit(f"\nChapters output folder: {chapters_out_dir}")
|
self.log_updated.emit((f"\nChapters output folder: {chapters_out_dir}", "grey"))
|
||||||
|
|
||||||
audio_segments = []
|
# Prepare merged output file for incremental writing ONLY if merge_chapters_at_end is True
|
||||||
|
if merge_chapters_at_end:
|
||||||
|
out_dir = parent_dir
|
||||||
|
base_filepath_no_ext = os.path.join(out_dir, f"{base_name}{suffix}")
|
||||||
|
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
|
||||||
subtitle_entries = []
|
subtitle_entries = []
|
||||||
current_time = 0.0
|
current_time = 0.0
|
||||||
rate = 24000
|
rate = 24000
|
||||||
subtitle_mode = self.subtitle_mode
|
subtitle_mode = self.subtitle_mode
|
||||||
raw_tts_results = [] # Collect all raw tts Result objects
|
|
||||||
|
|
||||||
# ETR timing starts here, after model loading but before processing
|
|
||||||
self.etr_start_time = time.time()
|
self.etr_start_time = time.time()
|
||||||
self.processed_char_count = 0 # Initialize processed character count
|
self.processed_char_count = 0
|
||||||
|
current_segment = 0
|
||||||
# Initialize current segment counter
|
chapters_time = [
|
||||||
|
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
||||||
|
for chapter in chapters
|
||||||
|
]
|
||||||
|
# Prepare output file/ffmpeg process for merged output
|
||||||
|
if self.output_format in ["wav", "mp3", "flac"]:
|
||||||
|
merged_out_file = sf.SoundFile(merged_out_path, "w", samplerate=24000, channels=1, format=self.output_format)
|
||||||
|
ffmpeg_proc = None
|
||||||
|
elif self.output_format == "m4b":
|
||||||
|
temp_wav_path = f"{base_filepath_no_ext}_temp.wav"
|
||||||
|
merged_out_file = sf.SoundFile(temp_wav_path, "w", samplerate=24000, channels=1, format="wav")
|
||||||
|
ffmpeg_proc = None
|
||||||
|
elif self.output_format == "opus":
|
||||||
|
static_ffmpeg.add_paths()
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y", "-thread_queue_size", "32768", "-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0"
|
||||||
|
]
|
||||||
|
cmd.extend(["-c:a", "libopus", "-b:a", "24000"])
|
||||||
|
cmd.append(merged_out_path)
|
||||||
|
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
|
||||||
|
merged_out_file = None
|
||||||
|
else:
|
||||||
|
self.log_updated.emit((f"Unsupported output format: {self.output_format}", "red"))
|
||||||
|
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# If not merging, set merged_out_file and related variables to None
|
||||||
|
merged_out_file = None
|
||||||
|
ffmpeg_proc = None
|
||||||
|
merged_out_path = None
|
||||||
|
subtitle_entries = []
|
||||||
|
current_time = 0.0
|
||||||
|
rate = 24000
|
||||||
|
subtitle_mode = self.subtitle_mode
|
||||||
|
self.etr_start_time = time.time()
|
||||||
|
self.processed_char_count = 0
|
||||||
current_segment = 0
|
current_segment = 0
|
||||||
|
|
||||||
# Initialize chapter times
|
|
||||||
chapters_time = [
|
chapters_time = [
|
||||||
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
||||||
for chapter in chapters
|
for chapter in chapters
|
||||||
]
|
]
|
||||||
|
|
||||||
# Instead of processing the whole text, process by chapter
|
# Instead of processing the whole text, process by chapter
|
||||||
for chapter_idx, (chapter_name, chapter_text) in enumerate(chapters, 1):
|
for chapter_idx, (chapter_name, chapter_text) in enumerate(chapters, 1):
|
||||||
|
chapter_out_path = None
|
||||||
if total_chapters > 1:
|
if total_chapters > 1:
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
(
|
(
|
||||||
f"\nChapter {chapter_idx}/{total_chapters}: {chapter_name}",
|
f"\nChapter {chapter_idx}/{total_chapters}: {chapter_name}",
|
||||||
"green",
|
"blue",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Variables for per-chapter processing when save_chapters_separately is enabled
|
|
||||||
chapter_audio_segments = []
|
|
||||||
chapter_subtitle_entries = []
|
chapter_subtitle_entries = []
|
||||||
chapter_current_time = 0.0
|
chapter_current_time = 0.0
|
||||||
|
# Set chapter start time before processing
|
||||||
# chapter start time
|
|
||||||
chapter_time = chapters_time[chapter_idx - 1]
|
chapter_time = chapters_time[chapter_idx - 1]
|
||||||
|
if merge_chapters_at_end:
|
||||||
chapter_time["start"] = current_time
|
chapter_time["start"] = current_time
|
||||||
|
|
||||||
# Set split_pattern to \n+ which will split on one or more newlines
|
# Set split_pattern to \n+ which will split on one or more newlines
|
||||||
split_pattern = r"\n+"
|
split_pattern = r"\n+"
|
||||||
|
|
||||||
@@ -622,7 +530,37 @@ class ConversionThread(QThread):
|
|||||||
loaded_voice = get_new_voice(tts, self.voice, self.use_gpu)
|
loaded_voice = get_new_voice(tts, self.voice, self.use_gpu)
|
||||||
else:
|
else:
|
||||||
loaded_voice = self.voice
|
loaded_voice = self.voice
|
||||||
|
# Prepare per-chapter output file if needed
|
||||||
|
if save_chapters_separately and total_chapters > 1:
|
||||||
|
sanitized = re.sub(r"[^\w\s\-]", "", chapter_name)
|
||||||
|
sanitized = re.sub(r"[\s\-]+", "_", sanitized).strip("_")
|
||||||
|
MAX_LEN = 80
|
||||||
|
if len(sanitized) > MAX_LEN:
|
||||||
|
pos = sanitized[:MAX_LEN].rfind("_")
|
||||||
|
sanitized = sanitized[: pos if pos > 0 else MAX_LEN].rstrip("_")
|
||||||
|
chapter_filename = f"{chapter_idx:02d}_{sanitized}"
|
||||||
|
chapter_out_path = os.path.join(
|
||||||
|
chapters_out_dir, f"{chapter_filename}.{separate_chapters_format}"
|
||||||
|
)
|
||||||
|
if separate_chapters_format in ["wav", "mp3", "flac"]:
|
||||||
|
chapter_out_file = sf.SoundFile(chapter_out_path, "w", samplerate=24000, channels=1, format=separate_chapters_format)
|
||||||
|
chapter_ffmpeg_proc = None
|
||||||
|
elif separate_chapters_format == "opus":
|
||||||
|
static_ffmpeg.add_paths()
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y", "-thread_queue_size", "32768", "-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0"
|
||||||
|
]
|
||||||
|
cmd.extend(["-c:a", "libopus", "-b:a", "24000"])
|
||||||
|
cmd.append(chapter_out_path)
|
||||||
|
chapter_ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
|
||||||
|
chapter_out_file = None
|
||||||
|
else:
|
||||||
|
self.log_updated.emit((f"Unsupported chapter format: {separate_chapters_format}", "red"))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
chapter_out_file = None
|
||||||
|
chapter_out_path = None
|
||||||
|
chapter_ffmpeg_proc = None
|
||||||
for result in tts(
|
for result in tts(
|
||||||
chapter_text,
|
chapter_text,
|
||||||
voice=loaded_voice,
|
voice=loaded_voice,
|
||||||
@@ -632,6 +570,10 @@ class ConversionThread(QThread):
|
|||||||
# Print the result for debugging
|
# Print the result for debugging
|
||||||
# print(f"Result: {result}")
|
# print(f"Result: {result}")
|
||||||
if self.cancel_requested:
|
if self.cancel_requested:
|
||||||
|
if chapter_out_file:
|
||||||
|
chapter_out_file.close()
|
||||||
|
if merged_out_file:
|
||||||
|
merged_out_file.close()
|
||||||
self.conversion_finished.emit("Cancelled", None)
|
self.conversion_finished.emit("Cancelled", None)
|
||||||
return
|
return
|
||||||
current_segment += 1
|
current_segment += 1
|
||||||
@@ -641,18 +583,27 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
f"\n{self.processed_char_count:,}/{self.total_char_count:,}: {result.graphemes}"
|
f"\n{self.processed_char_count:,}/{self.total_char_count:,}: {result.graphemes}"
|
||||||
)
|
)
|
||||||
raw_tts_results.append(result)
|
|
||||||
|
|
||||||
chunk_dur = len(result.audio) / rate
|
chunk_dur = len(result.audio) / rate
|
||||||
chunk_start = current_time
|
chunk_start = current_time
|
||||||
audio_segments.append(result.audio)
|
# Write audio directly to merged file ONLY if merging
|
||||||
|
if merge_chapters_at_end and merged_out_file:
|
||||||
# For per-chapter output
|
merged_out_file.write(result.audio)
|
||||||
if save_chapters_separately and total_chapters > 1:
|
elif merge_chapters_at_end and ffmpeg_proc:
|
||||||
chapter_audio_segments.append(result.audio)
|
if hasattr(result.audio, "numpy"):
|
||||||
chapter_chunk_start = chapter_current_time
|
audio_bytes = result.audio.numpy().astype("float32").tobytes()
|
||||||
|
else:
|
||||||
# Process token timestamps for subtitle generation
|
audio_bytes = result.audio.astype("float32").tobytes()
|
||||||
|
ffmpeg_proc.stdin.write(audio_bytes)
|
||||||
|
if chapter_out_file:
|
||||||
|
chapter_out_file.write(result.audio)
|
||||||
|
elif chapter_ffmpeg_proc:
|
||||||
|
if hasattr(result.audio, "numpy"):
|
||||||
|
audio_bytes = result.audio.numpy().astype("float32").tobytes()
|
||||||
|
else:
|
||||||
|
audio_bytes = result.audio.astype("float32").tobytes()
|
||||||
|
chapter_ffmpeg_proc.stdin.write(audio_bytes)
|
||||||
|
# Subtitle logic
|
||||||
if self.subtitle_mode != "Disabled":
|
if self.subtitle_mode != "Disabled":
|
||||||
tokens_list = getattr(result, "tokens", [])
|
tokens_list = getattr(result, "tokens", [])
|
||||||
tokens_with_timestamps = []
|
tokens_with_timestamps = []
|
||||||
@@ -668,46 +619,44 @@ class ConversionThread(QThread):
|
|||||||
"whitespace": tok.whitespace,
|
"whitespace": tok.whitespace,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if save_chapters_separately and total_chapters > 1:
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
chapter_tokens_with_timestamps.append(
|
chapter_tokens_with_timestamps.append(
|
||||||
{
|
{
|
||||||
"start": chapter_chunk_start
|
"start": chapter_current_time + (tok.start_ts or 0),
|
||||||
+ (tok.start_ts or 0),
|
"end": chapter_current_time + (tok.end_ts or 0),
|
||||||
"end": chapter_chunk_start + (tok.end_ts or 0),
|
|
||||||
"text": tok.text,
|
"text": tok.text,
|
||||||
"whitespace": tok.whitespace,
|
"whitespace": tok.whitespace,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process tokens according to subtitle mode
|
# Process tokens according to subtitle mode
|
||||||
# Global subtitle processing
|
# Global subtitle processing ONLY if merging
|
||||||
|
if merge_chapters_at_end:
|
||||||
self._process_subtitle_tokens(
|
self._process_subtitle_tokens(
|
||||||
tokens_with_timestamps,
|
tokens_with_timestamps,
|
||||||
subtitle_entries,
|
subtitle_entries,
|
||||||
self.max_subtitle_words,
|
self.max_subtitle_words,
|
||||||
)
|
)
|
||||||
|
# Per-chapter subtitle processing for both file and ffmpeg_proc
|
||||||
# Per-chapter subtitle processing if enabled
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
if save_chapters_separately and total_chapters > 1:
|
|
||||||
self._process_subtitle_tokens(
|
self._process_subtitle_tokens(
|
||||||
chapter_tokens_with_timestamps,
|
chapter_tokens_with_timestamps,
|
||||||
chapter_subtitle_entries,
|
chapter_subtitle_entries,
|
||||||
self.max_subtitle_words,
|
self.max_subtitle_words,
|
||||||
)
|
)
|
||||||
|
if merge_chapters_at_end:
|
||||||
current_time += chunk_dur
|
current_time += chunk_dur
|
||||||
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
# Update chapter_current_time for per-chapter output
|
chapter_current_time += chunk_dur
|
||||||
if save_chapters_separately and total_chapters > 1:
|
else:
|
||||||
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
chapter_current_time += chunk_dur
|
chapter_current_time += chunk_dur
|
||||||
|
|
||||||
# Calculate percentage based on characters processed
|
# Calculate percentage based on characters processed
|
||||||
percent = min(
|
percent = min(
|
||||||
int(self.processed_char_count / self.total_char_count * 100), 99
|
int(self.processed_char_count / self.total_char_count * 100), 99
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate ETR based on characters processed
|
# Calculate ETR based on characters processed
|
||||||
etr_str = "Estimating..."
|
etr_str = "Processing..."
|
||||||
chars_done = self.processed_char_count
|
chars_done = self.processed_char_count
|
||||||
elapsed = time.time() - self.etr_start_time
|
elapsed = time.time() - self.etr_start_time
|
||||||
|
|
||||||
@@ -727,61 +676,25 @@ class ConversionThread(QThread):
|
|||||||
# Update progress more frequently (after each result)
|
# Update progress more frequently (after each result)
|
||||||
self.progress_updated.emit(percent, etr_str)
|
self.progress_updated.emit(percent, etr_str)
|
||||||
|
|
||||||
# Update chapter end time
|
# Set chapter end time after processing
|
||||||
|
if merge_chapters_at_end:
|
||||||
chapter_time["end"] = current_time
|
chapter_time["end"] = current_time
|
||||||
|
# Finalize chapter file for ffmpeg formats
|
||||||
# Save the individual chapter output if save_chapters_separately is enabled
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
if (
|
self.log_updated.emit(("\nProcessing chapter audio...", "grey"))
|
||||||
save_chapters_separately
|
if chapter_ffmpeg_proc:
|
||||||
and total_chapters > 1
|
chapter_ffmpeg_proc.stdin.close()
|
||||||
and chapters_out_dir
|
chapter_ffmpeg_proc.wait()
|
||||||
and chapter_audio_segments
|
if chapter_out_file:
|
||||||
):
|
chapter_out_file.close()
|
||||||
|
# Subtitle file for chapter
|
||||||
# strip any non‐alphanumeric or space/dash
|
if save_chapters_separately and total_chapters > 1 and self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
||||||
sanitized = re.sub(r"[^\w\s\-]", "", chapter_name)
|
|
||||||
# collapse spaces or dashes to single underscore
|
|
||||||
sanitized = re.sub(r"[\s\-]+", "_", sanitized).strip("_")
|
|
||||||
# enforce max length
|
|
||||||
MAX_LEN = 80
|
|
||||||
if len(sanitized) > MAX_LEN:
|
|
||||||
# Find last word boundary before limit
|
|
||||||
pos = sanitized[:MAX_LEN].rfind("_")
|
|
||||||
# Use word boundary if found, otherwise use hard limit
|
|
||||||
sanitized = sanitized[: pos if pos > 0 else MAX_LEN].rstrip("_")
|
|
||||||
chapter_filename = f"{chapter_idx:02d}_{sanitized}"
|
|
||||||
|
|
||||||
# Use separate_chapters_format
|
|
||||||
separate_format = getattr(self, "separate_chapters_format", "wav")
|
|
||||||
|
|
||||||
chapter_out_path = os.path.join(
|
|
||||||
chapters_out_dir, f"{chapter_filename}.{separate_format}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process audio segments using the unified function
|
|
||||||
success, chapter_out_path = self._process_audio_segments(
|
|
||||||
chapter_audio_segments,
|
|
||||||
chapter_out_path,
|
|
||||||
separate_format,
|
|
||||||
use_ffmpeg=(separate_format in ["opus", "m4b"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"Failed to write {separate_format.upper()} file.", "red")
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Generate subtitle file for chapter if not Disabled
|
|
||||||
if self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
|
||||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||||
chapter_subtitle_path = os.path.join(
|
chapter_subtitle_path = os.path.join(
|
||||||
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if "ass" in subtitle_format:
|
if "ass" in subtitle_format:
|
||||||
# Generate ASS subtitle
|
|
||||||
is_centered = subtitle_format in (
|
is_centered = subtitle_format in (
|
||||||
"ass_centered_wide",
|
"ass_centered_wide",
|
||||||
"ass_centered_narrow",
|
"ass_centered_narrow",
|
||||||
@@ -797,7 +710,6 @@ class ConversionThread(QThread):
|
|||||||
is_narrow,
|
is_narrow,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Generate SRT subtitle (default)
|
|
||||||
with open(
|
with open(
|
||||||
chapter_subtitle_path,
|
chapter_subtitle_path,
|
||||||
"w",
|
"w",
|
||||||
@@ -810,14 +722,13 @@ class ConversionThread(QThread):
|
|||||||
srt_file.write(
|
srt_file.write(
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
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\nSubtitle saved to: {chapter_subtitle_path}",
|
f"\nChapter {chapter_idx} saved to: {chapter_out_path}\n\nChapter subtitle saved to: {chapter_subtitle_path}",
|
||||||
"green",
|
"green",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
elif chapter_out_path:
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
(
|
(
|
||||||
f"\nChapter {chapter_idx} saved to: {chapter_out_path}",
|
f"\nChapter {chapter_idx} saved to: {chapter_out_path}",
|
||||||
@@ -825,62 +736,67 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set progress to 100% when processing is complete
|
# Finalize merged output file ONLY if merging
|
||||||
|
if merge_chapters_at_end:
|
||||||
|
self.log_updated.emit(("\nFinalizing audio...", "grey"))
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
f.write(";FFMETADATA1\n")
|
||||||
|
for chapter in chapters_time:
|
||||||
|
chapter_title = chapter["chapter"].replace("=", "\\=")
|
||||||
|
f.write(f"[CHAPTER]\n")
|
||||||
|
f.write(f"TIMEBASE=1/1000\n")
|
||||||
|
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")
|
||||||
|
metadata_options = self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
||||||
|
static_ffmpeg.add_paths()
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y", "-i", temp_wav_path, "-i", chapters_info_path,
|
||||||
|
"-map", "0:a", "-map_metadata", "1", "-map_chapters", "1",
|
||||||
|
*metadata_options,
|
||||||
|
"-c:a", "aac", "-q:a", "2", "-movflags", "+faststart+use_metadata_tags",
|
||||||
|
merged_out_path
|
||||||
|
]
|
||||||
|
proc = create_process(cmd)
|
||||||
|
proc.wait()
|
||||||
|
self.log_updated.emit(("\nCleaning up temporary files...", "grey"))
|
||||||
|
if os.path.exists(chapters_info_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()
|
||||||
self.progress_updated.emit(100, "00:00:00")
|
self.progress_updated.emit(100, "00:00:00")
|
||||||
|
|
||||||
# Only generate the merged output file if merge_chapters_at_end is True or save_chapters_separately is False
|
|
||||||
merge_chapters = (
|
|
||||||
not hasattr(self, "save_chapters_separately")
|
|
||||||
or not self.save_chapters_separately
|
|
||||||
or getattr(self, "merge_chapters_at_end", True)
|
|
||||||
)
|
|
||||||
|
|
||||||
intended_output_format = self.output_format # Store the original choice
|
|
||||||
|
|
||||||
if audio_segments and merge_chapters:
|
|
||||||
self.log_updated.emit("\nFinalizing audio file...")
|
|
||||||
|
|
||||||
out_dir = parent_dir
|
|
||||||
base_filepath_no_ext = os.path.join(out_dir, f"{base_name}{suffix}")
|
|
||||||
|
|
||||||
final_out_path = None
|
|
||||||
|
|
||||||
# Use dedicated chapter processing for M4B when we have chapters
|
|
||||||
if intended_output_format == "m4b":
|
|
||||||
self.log_updated.emit("\nGenerating audio with chapters...")
|
|
||||||
final_out_path = self._generate_m4b_with_chapters(
|
|
||||||
audio_segments, chapters_time, base_filepath_no_ext
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Process audio segments using the unified function
|
|
||||||
success, final_out_path = self._process_audio_segments(
|
|
||||||
audio_segments,
|
|
||||||
f"{base_filepath_no_ext}.{intended_output_format}",
|
|
||||||
intended_output_format,
|
|
||||||
use_ffmpeg=(intended_output_format in ["opus", "m4b"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
final_out_path = None
|
|
||||||
|
|
||||||
if not final_out_path:
|
|
||||||
self.log_updated.emit(("Audio generation failed.", "red"))
|
|
||||||
self.conversion_finished.emit(
|
|
||||||
("Audio generation failed.", "red"), None
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Subtitle and final message logic
|
# Subtitle and final message logic
|
||||||
if final_out_path:
|
if merge_chapters_at_end:
|
||||||
if self.subtitle_mode != "Disabled":
|
if self.subtitle_mode != "Disabled":
|
||||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||||
subtitle_path = (
|
subtitle_path = (
|
||||||
os.path.splitext(final_out_path)[0] + f".{file_extension}"
|
os.path.splitext(merged_out_path)[0] + f".{file_extension}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if "ass" in subtitle_format:
|
if "ass" in subtitle_format:
|
||||||
# Generate ASS subtitle
|
|
||||||
is_centered = subtitle_format in (
|
is_centered = subtitle_format in (
|
||||||
"ass_centered_wide",
|
"ass_centered_wide",
|
||||||
"ass_centered_narrow",
|
"ass_centered_narrow",
|
||||||
@@ -893,7 +809,6 @@ class ConversionThread(QThread):
|
|||||||
subtitle_path, subtitle_entries, is_centered, is_narrow
|
subtitle_path, subtitle_entries, is_centered, is_narrow
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Generate SRT subtitle (default)
|
|
||||||
with open(
|
with open(
|
||||||
subtitle_path, "w", encoding="utf-8", errors="replace"
|
subtitle_path, "w", encoding="utf-8", errors="replace"
|
||||||
) as srt_file:
|
) as srt_file:
|
||||||
@@ -905,35 +820,39 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
self.conversion_finished.emit(
|
self.conversion_finished.emit(
|
||||||
(
|
(
|
||||||
f"\nAudiobook saved to: {final_out_path}\n\nSubtitle saved to: {subtitle_path}",
|
f"\nAudiobook saved to: {merged_out_path}\n\nSubtitle saved to: {subtitle_path}",
|
||||||
"green",
|
"green",
|
||||||
),
|
),
|
||||||
final_out_path,
|
merged_out_path,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.conversion_finished.emit(
|
self.conversion_finished.emit(
|
||||||
(f"\nAudiobook saved to: {final_out_path}", "green"),
|
(f"\nAudiobook saved to: {merged_out_path}", "green"),
|
||||||
final_out_path,
|
merged_out_path,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.log_updated.emit(
|
# If not merging, just finish after chapters
|
||||||
("Audio generation failed (final_out_path was not set).", "red")
|
self.progress_updated.emit(100, "00:00:00")
|
||||||
)
|
|
||||||
self.conversion_finished.emit(
|
self.conversion_finished.emit(
|
||||||
("Audio generation failed.", "red"), None
|
("\nAll chapters saved.", "green"),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
elif audio_segments and not merge_chapters:
|
|
||||||
self.conversion_finished.emit(
|
|
||||||
(
|
|
||||||
f"\nAll chapters processed successfully and saved to: {chapters_out_dir}",
|
|
||||||
"green",
|
|
||||||
),
|
|
||||||
chapters_out_dir,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.log_updated.emit(("No audio segments were generated.", "red"))
|
|
||||||
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Cleanup ffmpeg subprocesses on error
|
||||||
|
try:
|
||||||
|
if 'ffmpeg_proc' in locals() and ffmpeg_proc:
|
||||||
|
ffmpeg_proc.stdin.close()
|
||||||
|
ffmpeg_proc.terminate()
|
||||||
|
ffmpeg_proc.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if 'chapter_ffmpeg_proc' in locals() and chapter_ffmpeg_proc:
|
||||||
|
chapter_ffmpeg_proc.stdin.close()
|
||||||
|
chapter_ffmpeg_proc.terminate()
|
||||||
|
chapter_ffmpeg_proc.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.log_updated.emit((f"Error occurred: {str(e)}", "red"))
|
self.log_updated.emit((f"Error occurred: {str(e)}", "red"))
|
||||||
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
||||||
|
|
||||||
@@ -944,132 +863,6 @@ class ConversionThread(QThread):
|
|||||||
self.waiting_for_user_input = False
|
self.waiting_for_user_input = False
|
||||||
self._chapter_options_event.set()
|
self._chapter_options_event.set()
|
||||||
|
|
||||||
def _generate_m4b_with_chapters(
|
|
||||||
self, audio_segments, chapters_time, base_filepath_no_ext
|
|
||||||
):
|
|
||||||
"""Generate M4B file with chapters from audio segments"""
|
|
||||||
final_wav_path = f"{base_filepath_no_ext}.wav"
|
|
||||||
output_m4b_path = f"{base_filepath_no_ext}.m4b"
|
|
||||||
chapters_info_path = f"{base_filepath_no_ext}_chapters.txt"
|
|
||||||
|
|
||||||
# Early check for single/no chapter case
|
|
||||||
if not chapters_time or len(chapters_time) <= 1:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(
|
|
||||||
"\nFile contains only one chapter or no chapters were detected. Audio will be saved as a standard .wav file instead.",
|
|
||||||
"red",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Use the established unified method to create a WAV file
|
|
||||||
success, wav_path = self._process_audio_segments(
|
|
||||||
audio_segments, final_wav_path, "wav", use_ffmpeg=False
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
return wav_path
|
|
||||||
else:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"\nFailed to save single/no chapter audio as WAV", "red")
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Write chapter metadata file
|
|
||||||
with open(chapters_info_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(";FFMETADATA1\n")
|
|
||||||
for chapter in chapters_time:
|
|
||||||
chapter_title = chapter["chapter"].replace("=", "\\=")
|
|
||||||
f.write(f"[CHAPTER]\n")
|
|
||||||
f.write(f"TIMEBASE=1/1000\n")
|
|
||||||
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")
|
|
||||||
|
|
||||||
# For M4B with chapters, we need to use input file for chapter information
|
|
||||||
static_ffmpeg.add_paths()
|
|
||||||
metadata_options = self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
|
||||||
|
|
||||||
# Use pipe-based approach for audio input with special args for M4B chapters
|
|
||||||
ffmpeg_args = [
|
|
||||||
"-i",
|
|
||||||
chapters_info_path,
|
|
||||||
"-map",
|
|
||||||
"0:a",
|
|
||||||
"-map_metadata",
|
|
||||||
"1",
|
|
||||||
"-map_chapters",
|
|
||||||
"1",
|
|
||||||
*metadata_options,
|
|
||||||
"-c:a",
|
|
||||||
"aac",
|
|
||||||
"-q:a",
|
|
||||||
"2", # Quality-based VBR for better quality control
|
|
||||||
"-movflags",
|
|
||||||
"+faststart+use_metadata_tags", # Added for better compatibility
|
|
||||||
]
|
|
||||||
|
|
||||||
# Use the established unified method with M4B-specific args
|
|
||||||
success, out_path = self._process_audio_segments(
|
|
||||||
audio_segments,
|
|
||||||
output_m4b_path,
|
|
||||||
"m4b",
|
|
||||||
use_ffmpeg=True,
|
|
||||||
ffmpeg_args=ffmpeg_args,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clean up the temporary chapter metadata file
|
|
||||||
if os.path.exists(chapters_info_path):
|
|
||||||
try:
|
|
||||||
os.remove(chapters_info_path)
|
|
||||||
except Exception as e:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"Warning: Could not delete chapters file: {e}", "orange")
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
return out_path
|
|
||||||
|
|
||||||
# If M4B generation failed, fallback to WAV
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"M4B conversion failed. Falling back to WAV.\n", "red")
|
|
||||||
)
|
|
||||||
success, wav_path = self._process_audio_segments(
|
|
||||||
audio_segments, final_wav_path, "wav", use_ffmpeg=False
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
return wav_path
|
|
||||||
else:
|
|
||||||
self.log_updated.emit((f"Failed to write WAV file as fallback.", "red"))
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# General error during M4B generation - create WAV file directly as final output
|
|
||||||
self.log_updated.emit(
|
|
||||||
(
|
|
||||||
f"Error during M4B generation: {str(e)}.\n\nFalling back to WAV.\n",
|
|
||||||
"red",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use the established unified method to create a WAV file as fallback
|
|
||||||
success, wav_path = self._process_audio_segments(
|
|
||||||
audio_segments, final_wav_path, "wav", use_ffmpeg=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clean up temp files
|
|
||||||
if os.path.exists(chapters_info_path):
|
|
||||||
try:
|
|
||||||
os.remove(chapters_info_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if success:
|
|
||||||
return wav_path
|
|
||||||
else:
|
|
||||||
self.log_updated.emit(
|
|
||||||
(f"Critical error: Failed to save WAV fallback", "red")
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _extract_and_add_metadata_tags_to_ffmpeg_cmd(self):
|
def _extract_and_add_metadata_tags_to_ffmpeg_cmd(self):
|
||||||
"""Extract metadata tags from text content and add them to ffmpeg command"""
|
"""Extract metadata tags from text content and add them to ffmpeg command"""
|
||||||
metadata_options = []
|
metadata_options = []
|
||||||
@@ -1198,9 +991,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 (
|
if (re.search(separator, token["text"])) or word_count >= max_subtitle_words:
|
||||||
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"]
|
||||||
@@ -1306,6 +1097,21 @@ class ConversionThread(QThread):
|
|||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Terminate ffmpeg subprocesses if running
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'ffmpeg_proc') and self.ffmpeg_proc:
|
||||||
|
self.ffmpeg_proc.stdin.close()
|
||||||
|
self.ffmpeg_proc.terminate()
|
||||||
|
self.ffmpeg_proc.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(self, 'chapter_ffmpeg_proc') and self.chapter_ffmpeg_proc:
|
||||||
|
self.chapter_ffmpeg_proc.stdin.close()
|
||||||
|
self.chapter_ffmpeg_proc.terminate()
|
||||||
|
self.chapter_ffmpeg_proc.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class VoicePreviewThread(QThread):
|
class VoicePreviewThread(QThread):
|
||||||
|
|||||||
+4
-11
@@ -52,8 +52,6 @@ from PyQt5.QtGui import (
|
|||||||
QPolygon,
|
QPolygon,
|
||||||
QColor,
|
QColor,
|
||||||
QMovie,
|
QMovie,
|
||||||
QFont,
|
|
||||||
QTextCharFormat,
|
|
||||||
)
|
)
|
||||||
from abogen.utils import (
|
from abogen.utils import (
|
||||||
load_config,
|
load_config,
|
||||||
@@ -77,6 +75,7 @@ from abogen.constants import (
|
|||||||
VOICES_INTERNAL,
|
VOICES_INTERNAL,
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
COLORS,
|
COLORS,
|
||||||
|
SUBTITLE_FORMATS,
|
||||||
)
|
)
|
||||||
import threading
|
import threading
|
||||||
from abogen.voice_formula_gui import VoiceFormulaDialog
|
from abogen.voice_formula_gui import VoiceFormulaDialog
|
||||||
@@ -132,6 +131,7 @@ LOG_COLOR_MAP = {
|
|||||||
"green": COLORS["GREEN"],
|
"green": COLORS["GREEN"],
|
||||||
"orange": COLORS["ORANGE"],
|
"orange": COLORS["ORANGE"],
|
||||||
"blue": COLORS["BLUE"],
|
"blue": COLORS["BLUE"],
|
||||||
|
"grey": COLORS["LIGHT_DISABLED"],
|
||||||
None: COLORS["LIGHT_DISABLED"],
|
None: COLORS["LIGHT_DISABLED"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,14 +953,7 @@ class abogen(QWidget):
|
|||||||
self.subtitle_format_combo.setSizePolicy(
|
self.subtitle_format_combo.setSizePolicy(
|
||||||
QSizePolicy.Expanding, QSizePolicy.Fixed
|
QSizePolicy.Expanding, QSizePolicy.Fixed
|
||||||
)
|
)
|
||||||
subtitle_formats = [
|
for value, text in SUBTITLE_FORMATS:
|
||||||
("srt", "SRT (standard)"),
|
|
||||||
("ass_wide", "ASS (wide)"),
|
|
||||||
("ass_narrow", "ASS (narrow)"),
|
|
||||||
("ass_centered_wide", "ASS (centered wide)"),
|
|
||||||
("ass_centered_narrow", "ASS (centered narrow)"),
|
|
||||||
]
|
|
||||||
for value, text in subtitle_formats:
|
|
||||||
self.subtitle_format_combo.addItem(text, value)
|
self.subtitle_format_combo.addItem(text, value)
|
||||||
subtitle_format = self.config.get("subtitle_format", "ass_centered_narrow")
|
subtitle_format = self.config.get("subtitle_format", "ass_centered_narrow")
|
||||||
idx = self.subtitle_format_combo.findData(subtitle_format)
|
idx = self.subtitle_format_combo.findData(subtitle_format)
|
||||||
@@ -1892,7 +1885,7 @@ class abogen(QWidget):
|
|||||||
f"<span style='color:{COLORS['GREEN']}; font-weight:bold;'>{idx}) {os.path.basename(item.file_name)}</span><br>"
|
f"<span style='color:{COLORS['GREEN']}; font-weight:bold;'>{idx}) {os.path.basename(item.file_name)}</span><br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Language:</span> {item.lang_code}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Language:</span> {item.lang_code}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Voice:</span> {item.voice}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Voice:</span> {item.voice}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Speed:</span> {item.speed * 100:.1f}%<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Speed:</span> {item.speed}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Characters:</span> {item.total_char_count}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Characters:</span> {item.total_char_count}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Input:</span> {item.file_name}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Input:</span> {item.file_name}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Output:</span> {output}</span>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Output:</span> {output}</span>"
|
||||||
|
|||||||
Reference in New Issue
Block a user