diff --git a/abogen/infrastructure/subtitle_writer.py b/abogen/infrastructure/subtitle_writer.py index a8a145a..dcf9610 100644 --- a/abogen/infrastructure/subtitle_writer.py +++ b/abogen/infrastructure/subtitle_writer.py @@ -272,14 +272,14 @@ def create_subtitle_writer( fmt = SubtitleFormat(format.lower()) mode = SubtitleMode(mode) align = SubtitleAlignment(alignment.lower()) - + config = SubtitleConfig( format=fmt, mode=mode, alignment=align, max_words=max_words, ) - + if fmt == SubtitleFormat.SRT: return SrtWriter(path, config) elif fmt == SubtitleFormat.VTT: @@ -290,6 +290,71 @@ def create_subtitle_writer( raise ValueError(f"Unsupported subtitle format: {format}") +def resolve_subtitle_format( + subtitle_format: str | None, + subtitle_mode: str, +) -> tuple[str, str]: + """Resolve a subtitle_format setting string to (file_extension, alignment). + + Handles the PyQt convention where format strings encode alignment + (e.g. ``"ass_centered_narrow"`` → extension ``"ass"``, alignment + ``"center_narrow"``). + + Also enforces that ``"Sentence + Highlighting"`` mode requires ASS. + + Returns: + Tuple of (file_extension, alignment) suitable for + :func:`create_subtitle_writer`. + """ + fmt = (subtitle_format or "srt").lower() + + if subtitle_mode == "Sentence + Highlighting" and fmt == "srt": + fmt = "ass" + + if "ass" in fmt: + extension = "ass" + if "centered_narrow" in fmt: + alignment = "center_narrow" + elif "centered" in fmt: + alignment = "center" + elif "narrow" in fmt: + alignment = "narrow" + else: + alignment = "left" + else: + extension = fmt if fmt in ("srt", "vtt") else "srt" + alignment = "left" + + return extension, alignment + + +def make_subtitle_writer( + audio_path: Path, + subtitle_format: str | None, + subtitle_mode: str, + max_words: int = 50, +) -> SubtitleWriter | None: + """Convenience: resolve format and create a writer, or return None if disabled. + + Returns ``None`` when ``subtitle_mode`` is ``"Disabled"`` or the + format is unsupported. + """ + if subtitle_mode == "Disabled": + return None + + extension, alignment = resolve_subtitle_format(subtitle_format, subtitle_mode) + try: + return create_subtitle_writer( + audio_path.with_suffix(f".{extension}"), + extension, + subtitle_mode, + alignment=alignment, + max_words=max_words, + ) + except (ValueError, KeyError): + return None + + __all__ = [ "SubtitleFormat", "SubtitleMode", @@ -300,4 +365,6 @@ __all__ = [ "VttWriter", "AssWriter", "create_subtitle_writer", + "resolve_subtitle_format", + "make_subtitle_writer", ] diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index c786efd..340d0f9 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -21,7 +21,7 @@ from abogen.constants import ( SUPPORTED_SOUND_FORMATS, SUPPORTED_SUBTITLE_FORMATS, ) -from abogen.infrastructure.subtitle_writer import create_subtitle_writer +from abogen.infrastructure.subtitle_writer import make_subtitle_writer, resolve_subtitle_format from abogen.domain.split_pattern import get_split_pattern from abogen.domain.subtitle_processor import ( parse_subtitle_file, @@ -65,17 +65,6 @@ import threading # for efficient waiting import subprocess -def _subtitle_alignment_from_format(subtitle_format: str) -> str: - """Map PyQt subtitle format string to SubtitleAlignment value.""" - if subtitle_format in ("ass_centered_wide", "ass_centered_narrow"): - if subtitle_format == "ass_centered_narrow": - return "center_narrow" - return "center" - if subtitle_format in ("ass_narrow",): - return "narrow" - return "left" - - # Configuration constants _USER_RESPONSE_TIMEOUT = ( @@ -743,19 +732,18 @@ class ConversionThread(QThread): merged_subtitle_path = None if self.subtitle_mode != "Disabled": subtitle_format = getattr(self, "subtitle_format", "srt") - file_extension = "ass" if "ass" in subtitle_format else "srt" + extension, _ = resolve_subtitle_format(subtitle_format, self.subtitle_mode) merged_subtitle_path = ( - os.path.splitext(merged_out_path)[0] + f".{file_extension}" + os.path.splitext(merged_out_path)[0] + f".{extension}" ) - alignment = _subtitle_alignment_from_format(subtitle_format) - merged_subtitle_writer = create_subtitle_writer( + merged_subtitle_writer = make_subtitle_writer( Path(merged_subtitle_path), - file_extension, + subtitle_format, self.subtitle_mode, - alignment=alignment, max_words=self.max_subtitle_words, ) - merged_subtitle_writer.open() + if merged_subtitle_writer: + merged_subtitle_writer.open() else: merged_subtitle_path = None merged_subtitle_writer = None @@ -817,19 +805,18 @@ class ConversionThread(QThread): continue if self.subtitle_mode != "Disabled": subtitle_format = getattr(self, "subtitle_format", "srt") - file_extension = "ass" if "ass" in subtitle_format else "srt" + extension, _ = resolve_subtitle_format(subtitle_format, self.subtitle_mode) chapter_subtitle_path = os.path.join( - chapters_out_dir, f"{chapter_filename}.{file_extension}" + chapters_out_dir, f"{chapter_filename}.{extension}" ) - alignment = _subtitle_alignment_from_format(subtitle_format) - chapter_subtitle_writer = create_subtitle_writer( + chapter_subtitle_writer = make_subtitle_writer( Path(chapter_subtitle_path), - file_extension, + subtitle_format, self.subtitle_mode, - alignment=alignment, max_words=self.max_subtitle_words, ) - chapter_subtitle_writer.open() + if chapter_subtitle_writer: + chapter_subtitle_writer.open() else: chapter_subtitle_writer = None else: @@ -1189,17 +1176,16 @@ class ConversionThread(QThread): subtitle_writer = None subtitle_path = None subtitle_format = getattr(self, "subtitle_format", "srt") - file_extension = "ass" if "ass" in subtitle_format else "srt" - subtitle_path = f"{base_filepath_no_ext}.{file_extension}" - alignment_str = _subtitle_alignment_from_format(subtitle_format) - subtitle_writer = create_subtitle_writer( + extension, _ = resolve_subtitle_format(subtitle_format, self.subtitle_mode) + subtitle_path = f"{base_filepath_no_ext}.{extension}" + subtitle_writer = make_subtitle_writer( Path(subtitle_path), - file_extension, + subtitle_format, self.subtitle_mode, - alignment=alignment_str, max_words=self.max_subtitle_words, ) - subtitle_writer.open() + if subtitle_writer: + subtitle_writer.open() # Load voice loaded_voice = resolve_voice(self.voice, tts, self.use_gpu) diff --git a/abogen/webui/conversion_runner.py b/abogen/webui/conversion_runner.py index 6b3e2ac..a25f75c 100644 --- a/abogen/webui/conversion_runner.py +++ b/abogen/webui/conversion_runner.py @@ -33,7 +33,7 @@ from abogen.utils import ( ) from abogen.voice_profiles import load_profiles, normalize_profile_entry from abogen.llm_client import LLMClientError -from abogen.infrastructure.subtitle_writer import create_subtitle_writer +from abogen.infrastructure.subtitle_writer import make_subtitle_writer from abogen.domain.chapter_titles import ( simplify_heading_text as _simplify_heading_text, headings_equivalent as _headings_equivalent, @@ -338,7 +338,18 @@ def run_conversion_job(job: Job) -> None: cancel_check=lambda: job.cancel_requested, ) ) - subtitle_writer = _create_subtitle_writer(job, audio_path) + subtitle_writer = make_subtitle_writer( + audio_path, + job.subtitle_format, + job.subtitle_mode or "Line", + max_words=job.max_subtitle_words, + ) + if subtitle_writer is None and job.subtitle_mode != "Disabled": + fmt = (job.subtitle_format or "srt").lower() + if job.subtitle_mode == "Sentence + Highlighting" and fmt == "srt": + job.add_log("Highlighting requires ASS subtitles. Switching format.", level="warning") + else: + job.add_log(f"Unsupported subtitle format '{job.subtitle_format}'. Skipping.", level="warning") job.result.audio_path = audio_path if subtitle_writer: job.result.subtitle_paths.append(subtitle_writer.path) @@ -1063,25 +1074,6 @@ def _resolve_voice(pipeline, voice_spec: str, use_gpu: bool): return voice_spec -def _create_subtitle_writer(job: Job, audio_path: Path): - if job.subtitle_mode == "Disabled": - return None - - fmt = (job.subtitle_format or "srt").lower() - if job.subtitle_mode == "Sentence + Highlighting" and fmt == "srt": - job.add_log("Highlighting requires ASS subtitles. Switching format.", level="warning") - fmt = "ass" - - try: - return create_subtitle_writer( - audio_path.with_suffix(f".{fmt}"), - fmt, - job.subtitle_mode or "Line", - ) - except (ValueError, KeyError): - job.add_log(f"Unsupported subtitle format '{job.subtitle_format}'. Skipping.", level="warning") - return None - def _make_canceller(job: Job) -> Callable[[], None]: def _cancel() -> None: