mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Changed audio processing pipeline back to converting temporary WAV files instead of directly creating OPUS and M4B files
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
# v1.0.9 (pre-release)
|
||||||
|
- Changed audio processing pipeline back to converting temporary WAV files instead of directly creating OPUS and M4B files. This resolves memory outage issues with long books that caused crashes, unresponsive behavior, or corrupted audio output. **This approach will be slower, but it is more stable and reliable.**
|
||||||
|
|
||||||
# v1.0.8
|
# v1.0.8
|
||||||
- Added support for AMD GPUs in Linux (Special thanks to @hg000125 for his contribution in #23)
|
- Added support for AMD GPUs in Linux (Special thanks to @hg000125 for his contribution in #23)
|
||||||
- Added voice preview caching system that stores generated previews in the cache folder, mentioned by @jborza in #22
|
- Added voice preview caching system that stores generated previews in the cache folder, mentioned by @jborza in #22
|
||||||
|
|||||||
+102
-44
@@ -519,31 +519,38 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
if separate_format == "opus":
|
if separate_format == "opus":
|
||||||
static_ffmpeg.add_paths()
|
static_ffmpeg.add_paths()
|
||||||
|
# Write to temporary WAV file first instead of using pipe
|
||||||
|
temp_chapter_wav = f"{chapter_out_path}_temp.wav"
|
||||||
|
try:
|
||||||
|
# Write chapter audio to temp file
|
||||||
|
sf.write(temp_chapter_wav, chapter_audio, 24000, format="wav")
|
||||||
|
|
||||||
|
self.log_updated.emit(
|
||||||
|
f"\nGenerating chapter in OPUS format..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use file input instead of pipe
|
||||||
proc = create_process(
|
proc = create_process(
|
||||||
[
|
[
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-y",
|
"-y",
|
||||||
"-thread_queue_size", "32768", # Increased thread_queue_size for chapter opus
|
"-i", temp_chapter_wav,
|
||||||
"-f",
|
|
||||||
"f32le",
|
|
||||||
"-ar",
|
|
||||||
"24000",
|
|
||||||
"-ac",
|
|
||||||
"1",
|
|
||||||
"-i",
|
|
||||||
"pipe:0",
|
|
||||||
"-c:a",
|
"-c:a",
|
||||||
"libopus",
|
"libopus",
|
||||||
"-b:a",
|
"-b:a",
|
||||||
"24000",
|
"24000",
|
||||||
chapter_out_path,
|
chapter_out_path,
|
||||||
],
|
]
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
text=False # Ensure binary stdin for audio data
|
|
||||||
)
|
)
|
||||||
proc.stdin.write(chapter_audio.astype("float32").tobytes())
|
|
||||||
proc.stdin.close()
|
|
||||||
proc.wait()
|
proc.wait()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up temp file
|
||||||
|
if os.path.exists(temp_chapter_wav):
|
||||||
|
try:
|
||||||
|
os.remove(temp_chapter_wav)
|
||||||
|
except Exception as e:
|
||||||
|
self.log_updated.emit((f"Warning: Could not delete temporary chapter file {temp_chapter_wav}: {e}", "orange"))
|
||||||
else:
|
else:
|
||||||
sf.write(
|
sf.write(
|
||||||
chapter_out_path,
|
chapter_out_path,
|
||||||
@@ -607,17 +614,23 @@ class ConversionThread(QThread):
|
|||||||
elif intended_output_format == "opus":
|
elif intended_output_format == "opus":
|
||||||
static_ffmpeg.add_paths()
|
static_ffmpeg.add_paths()
|
||||||
opus_out_path = f"{base_filepath_no_ext}.opus"
|
opus_out_path = f"{base_filepath_no_ext}.opus"
|
||||||
|
temp_wav_path = f"{base_filepath_no_ext}_temp.wav"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# First, write audio data to a temporary WAV file
|
||||||
|
self.log_updated.emit("Writing temporary audio file...\n")
|
||||||
|
sf.write(temp_wav_path, audio_data_np, 24000, format="wav")
|
||||||
|
|
||||||
|
# Use the temp WAV file as input instead of pipe
|
||||||
ffmpeg_cmd_opus = [
|
ffmpeg_cmd_opus = [
|
||||||
"ffmpeg", "-y",
|
"ffmpeg", "-y",
|
||||||
"-thread_queue_size", "32768", # Increased thread_queue_size
|
"-i", temp_wav_path,
|
||||||
"-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
|
"-c:a", "libopus",
|
||||||
"-c:a", "libopus", "-b:a", "24000", # Original bitrate
|
"-b:a", "24000", # Original bitrate
|
||||||
opus_out_path,
|
opus_out_path,
|
||||||
]
|
]
|
||||||
try:
|
|
||||||
process = create_process(ffmpeg_cmd_opus, stdin=subprocess.PIPE, text=False) # Ensure binary stdin
|
process = create_process(ffmpeg_cmd_opus)
|
||||||
process.stdin.write(audio_data_np.astype("float32").tobytes())
|
|
||||||
process.stdin.close()
|
|
||||||
if process.wait() == 0:
|
if process.wait() == 0:
|
||||||
final_out_path = opus_out_path
|
final_out_path = opus_out_path
|
||||||
else:
|
else:
|
||||||
@@ -626,6 +639,13 @@ class ConversionThread(QThread):
|
|||||||
except Exception as e_opus:
|
except Exception as e_opus:
|
||||||
self.log_updated.emit((f"Error during Opus conversion: {str(e_opus)}", "red"))
|
self.log_updated.emit((f"Error during Opus conversion: {str(e_opus)}", "red"))
|
||||||
final_out_path = None
|
final_out_path = None
|
||||||
|
finally:
|
||||||
|
# Clean up the temporary file
|
||||||
|
if os.path.exists(temp_wav_path):
|
||||||
|
try:
|
||||||
|
os.remove(temp_wav_path)
|
||||||
|
except Exception as e_clean:
|
||||||
|
self.log_updated.emit((f"Warning: Could not delete temporary file {temp_wav_path}: {e_clean}", "orange"))
|
||||||
else: # For other formats like wav
|
else: # For other formats like wav
|
||||||
standard_out_path = f"{base_filepath_no_ext}.{intended_output_format}"
|
standard_out_path = f"{base_filepath_no_ext}.{intended_output_format}"
|
||||||
try:
|
try:
|
||||||
@@ -682,7 +702,11 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
def _generate_m4b_with_chapters(self, audio_data_np, chapters_time, base_filepath_no_ext):
|
def _generate_m4b_with_chapters(self, audio_data_np, chapters_time, base_filepath_no_ext):
|
||||||
final_wav_path = f"{base_filepath_no_ext}.wav"
|
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"
|
||||||
|
temp_wav_path = f"{base_filepath_no_ext}_temp.wav"
|
||||||
|
|
||||||
|
# Early check for single/no chapter case
|
||||||
if not chapters_time or len(chapters_time) <= 1:
|
if not chapters_time or len(chapters_time) <= 1:
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
(
|
(
|
||||||
@@ -697,24 +721,32 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit((f"Failed to save single/no chapter audio as WAV: {str(e_wav_single)}", "red"))
|
self.log_updated.emit((f"Failed to save single/no chapter audio as WAV: {str(e_wav_single)}", "red"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
output_m4b_path = f"{base_filepath_no_ext}.m4b"
|
|
||||||
chapters_info_path = f"{base_filepath_no_ext}_chapters.txt"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# First, write audio data to a temporary WAV file
|
||||||
|
self.log_updated.emit("Writing temporary audio file...\n")
|
||||||
|
sf.write(temp_wav_path, audio_data_np, 24000, format="wav")
|
||||||
|
|
||||||
|
# Write chapter metadata
|
||||||
with open(chapters_info_path, "w", encoding="utf-8") as f:
|
with open(chapters_info_path, "w", encoding="utf-8") as f:
|
||||||
f.write(";FFMETADATA1\n")
|
f.write(";FFMETADATA1\n")
|
||||||
for chapter in chapters_time:
|
for chapter in chapters_time:
|
||||||
|
# Handle potential special characters in chapter titles
|
||||||
|
chapter_title = chapter['chapter'].replace('=', '\\=')
|
||||||
f.write(f"[CHAPTER]\n")
|
f.write(f"[CHAPTER]\n")
|
||||||
f.write(f"TIMEBASE=1/1000\n") # Using milliseconds for precision
|
f.write(f"TIMEBASE=1/1000\n") # Using milliseconds for precision
|
||||||
f.write(f"START={int(chapter['start']*1000)}\n")
|
f.write(f"START={int(chapter['start']*1000)}\n")
|
||||||
f.write(f"END={int(chapter['end']*1000)}\n")
|
f.write(f"END={int(chapter['end']*1000)}\n")
|
||||||
f.write(f"title={chapter['chapter']}\n\n")
|
f.write(f"title={chapter_title}\n\n")
|
||||||
|
|
||||||
static_ffmpeg.add_paths()
|
static_ffmpeg.add_paths()
|
||||||
|
|
||||||
|
# Extract metadata tags first to handle any errors separately
|
||||||
|
metadata_options = self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
||||||
|
|
||||||
|
# Build the FFmpeg command
|
||||||
ffmpeg_cmd = [
|
ffmpeg_cmd = [
|
||||||
"ffmpeg", "-y",
|
"ffmpeg", "-y",
|
||||||
"-thread_queue_size", "32768", # Increased thread_queue_size
|
"-i", temp_wav_path, # Use file input
|
||||||
"-f", "f32le", "-ar", "24000", "-ac", "1", "-i", "pipe:0",
|
|
||||||
"-i", chapters_info_path,
|
"-i", chapters_info_path,
|
||||||
"-map", "0:a",
|
"-map", "0:a",
|
||||||
"-map_metadata", "1",
|
"-map_metadata", "1",
|
||||||
@@ -722,44 +754,71 @@ class ConversionThread(QThread):
|
|||||||
# Add metadata tags
|
# Add metadata tags
|
||||||
"-metadata", "composer=Narrator",
|
"-metadata", "composer=Narrator",
|
||||||
"-metadata", "genre=Audiobook",
|
"-metadata", "genre=Audiobook",
|
||||||
# Extract metadata from text markers if available
|
# Add additional extracted metadata tags
|
||||||
*self._extract_and_add_metadata_tags_to_ffmpeg_cmd(),
|
*metadata_options,
|
||||||
"-c:a", "aac",
|
"-c:a", "aac",
|
||||||
"-q:a", "2", # Quality-based VBR for better quality control (lower = better, range 0.1-2)
|
"-movflags", "+faststart+use_metadata_tags",
|
||||||
"-movflags", "+faststart+use_metadata_tags", # Added for better compatibility
|
"-f", "mp4", # Explicitly specify format
|
||||||
output_m4b_path,
|
output_m4b_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.log_updated.emit(f"Generating audio with chapters...\n")
|
self.log_updated.emit(f"Generating M4B with chapters...\n")
|
||||||
|
|
||||||
process = create_process(ffmpeg_cmd, stdin=subprocess.PIPE, text=False) # Ensure binary stdin
|
process = create_process(ffmpeg_cmd)
|
||||||
process.stdin.write(audio_data_np.astype("float32").tobytes())
|
|
||||||
process.stdin.close()
|
|
||||||
return_code = process.wait()
|
return_code = process.wait()
|
||||||
|
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
return output_m4b_path
|
return output_m4b_path
|
||||||
else:
|
else:
|
||||||
self.log_updated.emit(
|
error_message = f"FFmpeg failed to create M4B (return code {return_code}).\n"
|
||||||
(f"FFmpeg failed to create M4B (return code {return_code}).\n\nFalling back to WAV.\n", "red")
|
# Try to get more detailed error info if available
|
||||||
)
|
if hasattr(process, 'stderr') and process.stderr:
|
||||||
sf.write(final_wav_path, audio_data_np, 24000, format="wav")
|
stderr_output = process.stderr.read()
|
||||||
|
if stderr_output:
|
||||||
|
error_message += f"Error details: {stderr_output.decode('utf-8', errors='replace')}\n"
|
||||||
|
|
||||||
|
self.log_updated.emit((f"{error_message}\nFalling back to WAV.\n", "red"))
|
||||||
|
# Simply rename the temp WAV file instead of creating a new one
|
||||||
|
try:
|
||||||
|
# os is already imported at the top of the function
|
||||||
|
os.rename(temp_wav_path, final_wav_path)
|
||||||
|
self.log_updated.emit("Using temporary WAV file as final output.\n")
|
||||||
|
# Set temp_wav_path to None so it's not deleted in the finally block
|
||||||
|
temp_wav_path = None
|
||||||
return final_wav_path
|
return final_wav_path
|
||||||
|
except Exception as e_rename:
|
||||||
|
self.log_updated.emit((f"Failed to rename temporary WAV file: {str(e_rename)}", "red"))
|
||||||
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_updated.emit((f"Error during M4B generation: {str(e)}.\n\nFalling back to WAV.\n", "red"))
|
self.log_updated.emit((f"Error during M4B generation: {str(e)}.\n\nFalling back to WAV.\n", "red"))
|
||||||
|
# Check if the temp file exists and can be used
|
||||||
|
if os.path.exists(temp_wav_path):
|
||||||
|
try:
|
||||||
|
os.rename(temp_wav_path, final_wav_path)
|
||||||
|
self.log_updated.emit("Using temporary WAV file as final output.\n")
|
||||||
|
# Set temp_wav_path to None so it's not deleted in the finally block
|
||||||
|
temp_wav_path = None
|
||||||
|
return final_wav_path
|
||||||
|
except Exception as e_rename:
|
||||||
|
self.log_updated.emit((f"Failed to rename temporary WAV file: {str(e_rename)}", "red"))
|
||||||
|
|
||||||
|
# If the temp file doesn't exist or renaming failed, create a new WAV file
|
||||||
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
|
||||||
except Exception as e_wav_fallback:
|
except Exception as e_wav_fallback:
|
||||||
self.log_updated.emit((f"Critical error: Failed to save fallback WAV: {str(e_wav_fallback)}\n", "red"))
|
self.log_updated.emit((f"Critical error: Failed to save fallback WAV: {str(e_wav_fallback)}\n", "red"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(chapters_info_path):
|
# Clean up temporary files, but only delete temp_wav_path if it wasn't renamed
|
||||||
|
for temp_file in [chapters_info_path] + ([temp_wav_path] if temp_wav_path else []):
|
||||||
|
if os.path.exists(temp_file):
|
||||||
try:
|
try:
|
||||||
os.remove(chapters_info_path)
|
os.remove(temp_file)
|
||||||
except Exception as e_clean:
|
except Exception as e_clean:
|
||||||
self.log_updated.emit((f"Warning: Could not delete temporary chapter file {chapters_info_path}: {e_clean}", "orange"))
|
self.log_updated.emit((f"Warning: Could not delete temporary file {temp_file}: {e_clean}", "orange"))
|
||||||
|
|
||||||
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"""
|
||||||
@@ -788,9 +847,8 @@ class ConversionThread(QThread):
|
|||||||
# Use display path or filename as fallback for title
|
# Use display path or filename as fallback for title
|
||||||
filename = os.path.splitext(os.path.basename(self.display_path if self.display_path else self.file_name))[0]
|
filename = os.path.splitext(os.path.basename(self.display_path if self.display_path else self.file_name))[0]
|
||||||
|
|
||||||
# Add title metadata
|
|
||||||
if title_match:
|
if title_match:
|
||||||
metadata_options.extend(["-metadata", f"title=Dinginliğin Gücü"])
|
metadata_options.extend(["-metadata", f"title={title_match.group(1)}"])
|
||||||
else:
|
else:
|
||||||
metadata_options.extend(["-metadata", f"title={filename}"])
|
metadata_options.extend(["-metadata", f"title={filename}"])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user