From 50d75eb2fcf5d698000c86fecbdf897b15feeff8 Mon Sep 17 00:00:00 2001 From: Artem Akymenko Date: Wed, 15 Jul 2026 13:28:21 +0000 Subject: [PATCH] standardize m4b encoding to VBR -q:a 2; replace remaining ffmpeg blocks in desktop GUI with domain modules --- abogen/domain/audio_helpers.py | 2 +- abogen/pyqt/conversion.py | 172 +++++++++++---------------------- tests/test_audio_helpers.py | 1 + 3 files changed, 58 insertions(+), 117 deletions(-) diff --git a/abogen/domain/audio_helpers.py b/abogen/domain/audio_helpers.py index 9cef77f..ef9d179 100644 --- a/abogen/domain/audio_helpers.py +++ b/abogen/domain/audio_helpers.py @@ -35,7 +35,7 @@ def build_ffmpeg_command(path: Path, fmt: str, metadata: Optional[Dict[str, str] elif fmt == "opus": base += ["-c:a", "libopus", "-b:a", "24000"] elif fmt == "m4b": - base += ["-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart+use_metadata_tags"] + base += ["-c:a", "aac", "-q:a", "2", "-movflags", "+faststart+use_metadata_tags"] else: base += ["-c:a", "copy"] diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index 65a5d4c..3ae5654 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -23,6 +23,12 @@ from abogen.constants import ( from abogen.voice_formulas import get_new_voice from abogen.infrastructure.subtitle_writer import _format_timestamp from abogen.domain.split_pattern import get_split_pattern +from abogen.domain.output_paths import ( + resolve_output_directory, + build_output_path, + sanitize_output_stem, +) +from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32 import abogen.hf_tracker as hf_tracker import static_ffmpeg import threading # for efficient waiting @@ -639,15 +645,17 @@ class ConversionThread(QThread): base_path = self.display_path if self.display_path else self.file_name base_name = os.path.splitext(os.path.basename(base_path))[0] - # Sanitize base_name for folder/file creation based on OS sanitized_base_name = sanitize_name_for_os(base_name, is_folder=True) - if self.save_option == "Save to Desktop": - parent_dir = user_desktop_dir() - elif self.save_option == "Save next to input file": - parent_dir = os.path.dirname(base_path) - else: - parent_dir = self.output_folder or os.getcwd() + parent_dir = resolve_output_directory( + save_mode=self.save_option, + stored_path=Path(base_path), + output_folder=getattr(self, "output_folder", None), + desktop_dir=Path(user_desktop_dir()), + user_output_path=None, + user_cache_outputs=Path(os.getcwd()), + ) + parent_dir = str(parent_dir) # Ensure the output folder exists, error if it doesn't if not os.path.exists(parent_dir): self.log_updated.emit( @@ -717,77 +725,40 @@ class ConversionThread(QThread): format=self.output_format, ) ffmpeg_proc = None - elif self.output_format == "m4b": - # Real-time M4B generation using FFmpeg pipe + elif self.output_format in ("m4b", "opus"): + # Real-time generation using FFmpeg pipe static_ffmpeg.add_paths() merged_out_file = None ffmpeg_proc = None metadata_options, cover_path = ( self._extract_and_add_metadata_tags_to_ffmpeg_cmd() + if self.output_format == "m4b" + else ([], None) ) - # Prepare ffmpeg command for m4b output - cmd = [ - "ffmpeg", - "-y", - "-thread_queue_size", - "32768", - "-f", - "f32le", - "-ar", - "24000", - "-ac", - "1", - "-i", - "pipe:0", - ] - if cover_path and os.path.exists(cover_path): - cmd.extend( - [ - "-i", - cover_path, - "-map", - "0:a", - "-map", - "1", - "-c:v", - "copy", - "-disposition:v", - "attached_pic", - ] - ) - cmd.extend( - [ - "-c:a", - "aac", - "-q:a", - "2", - "-movflags", - "+faststart+use_metadata_tags", - ] + cmd = build_ffmpeg_command( + Path(merged_out_path), + self.output_format, ) - cmd += metadata_options - cmd.append(merged_out_path) + # Insert thread queue size after ffmpeg header + cmd.insert(2, "-thread_queue_size") + cmd.insert(3, "32768") + if self.output_format == "m4b" and cover_path and os.path.exists(cover_path): + # Insert cover image input before the output path + output_path = cmd.pop() + cmd.extend([ + "-i", cover_path, + "-map", "0:a", + "-map", "1", + "-c:v", "copy", + "-disposition:v", "attached_pic", + ]) + cmd.extend(metadata_options) + cmd.append(output_path) + elif self.output_format == "m4b": + output_path = cmd.pop() + cmd.extend(metadata_options) + cmd.append(output_path) ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False) - 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") @@ -1560,58 +1531,27 @@ class ConversionThread(QThread): ) else: static_ffmpeg.add_paths() - cmd = [ - "ffmpeg", - "-y", - "-thread_queue_size", - "32768", - "-f", - "f32le", - "-ar", - str(rate), - "-ac", - "1", - "-i", - "pipe:0", - ] + cmd = build_ffmpeg_command( + Path(merged_out_path), + self.output_format, + ) + cmd.insert(2, "-thread_queue_size") + cmd.insert(3, "32768") if self.output_format == "m4b": metadata_options, cover_path = ( self._extract_and_add_metadata_tags_to_ffmpeg_cmd() ) if cover_path and os.path.exists(cover_path): - cmd.extend( - [ - "-i", - cover_path, - "-map", - "0:a", - "-map", - "1", - "-c:v", - "copy", - "-disposition:v", - "attached_pic", - ] - ) - cmd.extend( - [ - "-c:a", - "aac", - "-q:a", - "2", - "-movflags", - "+faststart+use_metadata_tags", - ] - ) + output_path = cmd.pop() + cmd.extend([ + "-i", cover_path, + "-map", "0:a", + "-map", "1", + "-c:v", "copy", + "-disposition:v", "attached_pic", + ]) + cmd.append(output_path) cmd.extend(metadata_options) - elif self.output_format == "opus": - cmd.extend(["-c:a", "libopus", "-b:a", "24000"]) - else: - self.log_updated.emit( - (f"Unsupported output format: {self.output_format}", "red") - ) - return - cmd.append(merged_out_path) ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False) # Always generate subtitles for subtitle input files diff --git a/tests/test_audio_helpers.py b/tests/test_audio_helpers.py index 3ce6bfc..8ffb2eb 100644 --- a/tests/test_audio_helpers.py +++ b/tests/test_audio_helpers.py @@ -46,6 +46,7 @@ class TestBuildFfmpegCommand: cmd = build_ffmpeg_command(Path("/out.m4b"), "m4b") assert "aac" in cmd + assert "-q:a" in cmd assert "+faststart+use_metadata_tags" in cmd def test_wav_copy_codec(self):