diff --git a/abogen/domain/split_pattern.py b/abogen/domain/split_pattern.py new file mode 100644 index 0000000..0282780 --- /dev/null +++ b/abogen/domain/split_pattern.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +"""Unified split pattern logic extracted from 3 copies.""" +import re + + +PUNCTUATION_SENTENCE = r".!?。!?" +PUNCTUATION_SENTENCE_COMMA = r".!?,。!?、," + + +def get_split_pattern(language: str, subtitle_mode: str) -> str: + """Get the appropriate split pattern based on language and subtitle mode. + + Args: + language: Language code (a, b, e, f, etc.) + subtitle_mode: Subtitle mode ("Sentence", "Sentence + Comma", "Line", etc.) + + Returns: + Split pattern string + """ + # For English, always use newline splitting only + if language in ("a", "b"): + return "\n" + + # Determine spacing pattern based on language + spacing = r"\s*" if language in ("z", "j") else r"\s+" + + # For CJK languages, when subtitle mode is Disabled or Line, prefer + # punctuation-based splitting instead of plain newline splitting. + if subtitle_mode in ("Disabled", "Line") and language in ("z", "j"): + return rf"(?<=[{PUNCTUATION_SENTENCE}]){spacing}|\n+" + + if subtitle_mode == "Line": + return "\n" + elif subtitle_mode == "Sentence": + return rf"(?<=[{PUNCTUATION_SENTENCE}]){spacing}|\n+" + elif subtitle_mode == "Sentence + Comma": + return rf"(?<=[{PUNCTUATION_SENTENCE_COMMA}]){spacing}|\n+" + else: + return r"\n+" diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index 811d56a..3a33e32 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -14,7 +14,6 @@ from abogen.utils import ( ) from abogen.constants import ( LANGUAGE_DESCRIPTIONS, - SAMPLE_VOICE_TEXTS, COLORS, CHAPTER_OPTIONS_COUNTDOWN, SUBTITLE_FORMATS, @@ -22,11 +21,11 @@ from abogen.constants import ( SUPPORTED_SUBTITLE_FORMATS, ) from abogen.voice_formulas import get_new_voice +from abogen.domain.split_pattern import get_split_pattern import abogen.hf_tracker as hf_tracker import static_ffmpeg import threading # for efficient waiting import subprocess -import platform # Configuration constants _USER_RESPONSE_TIMEOUT = ( @@ -43,10 +42,7 @@ from abogen.subtitle_utils import ( get_sample_voice_text, sanitize_name_for_os, _CHAPTER_MARKER_SEARCH_PATTERN, - _VOICE_MARKER_PATTERN, - _VOICE_MARKER_SEARCH_PATTERN, - split_text_by_voice_markers, - validate_voice_name, + split_text_by_voice_markers ) class CountdownDialog(QDialog): @@ -216,40 +212,6 @@ class ConversionThread(QThread): PUNCTUATION_SENTENCE_COMMA = ".!?,।。!?、," PUNCTUATION_COMMAS = ",,、" - def _get_split_pattern(self, lang_code, subtitle_mode): - """ - Get the appropriate split pattern based on language and subtitle mode. - - Args: - lang_code: Language code (a, b, e, f, etc.) - subtitle_mode: Subtitle mode ("Sentence", "Sentence + Comma", "Line", etc.) - - Returns: - Split pattern string - """ - # For English, always use newline splitting only - if lang_code in ["a", "b"]: - return "\n" - - # Determine spacing pattern based on language - spacing_pattern = r"\s*" if lang_code in ["z", "j"] else r"\s+" - - # For Chinese/Japanese, when subtitle mode is Disabled or Line, prefer - # punctuation-based splitting instead of plain newline splitting. - if subtitle_mode in ("Disabled", "Line") and lang_code in ["z", "j"]: - return r"(?<=[{}]){}|\n+".format(self.PUNCTUATION_SENTENCE, spacing_pattern) - - if subtitle_mode == "Line": - return "\n" - elif subtitle_mode == "Sentence": - return r"(?<=[{}]){}|\n+".format(self.PUNCTUATION_SENTENCE, spacing_pattern) - elif subtitle_mode == "Sentence + Comma": - return r"(?<=[{}]){}|\n+".format( - self.PUNCTUATION_SENTENCE_COMMA, spacing_pattern - ) - else: - return r"\n+" # Default to line breaks - def __init__( self, file_name, @@ -298,7 +260,7 @@ class ConversionThread(QThread): self.silence_duration = 2.0 # Default value, will be overridden from GUI self.use_spacy_segmentation = True # Default, will be overridden from GUI # Set split pattern based on language and subtitle mode - self.split_pattern = self._get_split_pattern(lang_code, subtitle_mode) + self.split_pattern = get_split_pattern(lang_code, subtitle_mode) self.voice_cache = {} # Cache for loaded voices def load_voice_cached(self, voice_name, tts): diff --git a/tests/test_split_pattern.py b/tests/test_split_pattern.py new file mode 100644 index 0000000..152179c --- /dev/null +++ b/tests/test_split_pattern.py @@ -0,0 +1,101 @@ +"""Tests for split pattern logic (3 identical copies in codebase).""" +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +import pytest + +from abogen.domain.split_pattern import get_split_pattern + + +# --- English always returns \n --- + +class TestEnglish: + def test_english_sentence(self): + assert get_split_pattern("a", "Sentence") == "\n" + + def test_english_sentence_comma(self): + assert get_split_pattern("a", "Sentence + Comma") == "\n" + + def test_english_line(self): + assert get_split_pattern("a", "Line") == "\n" + + def test_english_disabled(self): + assert get_split_pattern("a", "Disabled") == "\n" + + def test_english_b(self): + assert get_split_pattern("b", "Sentence") == "\n" + + +# --- CJK languages --- + +class TestCJK: + def test_chinese_disabled(self): + pattern = get_split_pattern("z", "Disabled") + assert pattern != "\n" + assert r"\n+" in pattern + + def test_chinese_line(self): + pattern = get_split_pattern("z", "Line") + assert pattern != "\n" + assert r"\n+" in pattern + + def test_chinese_sentence(self): + pattern = get_split_pattern("z", "Sentence") + assert r"\n+" in pattern + + def test_chinese_sentence_comma(self): + pattern = get_split_pattern("z", "Sentence + Comma") + assert r"\n+" in pattern + + def test_japanese_disabled(self): + pattern = get_split_pattern("j", "Disabled") + assert pattern != "\n" + assert r"\n+" in pattern + + def test_japanese_sentence(self): + pattern = get_split_pattern("j", "Sentence") + assert r"\n+" in pattern + + +# --- Other languages --- + +class TestOtherLanguages: + def test_spanish_sentence(self): + pattern = get_split_pattern("e", "Sentence") + assert r"\n+" in pattern + + def test_spanish_line(self): + assert get_split_pattern("e", "Line") == "\n" + + def test_spanish_disabled(self): + # canonical: \n+ for non-CJK Disabled + assert get_split_pattern("e", "Disabled") == r"\n+" + + def test_french_sentence_comma(self): + pattern = get_split_pattern("f", "Sentence + Comma") + assert r"\n+" in pattern + + def test_unknown_lang(self): + pattern = get_split_pattern("x", "Sentence") + assert r"\n+" in pattern + + +# --- Pattern structure --- + +class TestPatternStructure: + def test_sentence_has_lookbehind(self): + pattern = get_split_pattern("e", "Sentence") + assert r"(?<=" in pattern + + def test_sentence_comma_has_comma_chars(self): + pattern = get_split_pattern("e", "Sentence + Comma") + assert "," in pattern + + def test_cjk_spacing_uses_star(self): + pattern = get_split_pattern("z", "Sentence") + assert r"\s*" in pattern + + def test_non_cjk_spacing_uses_plus(self): + pattern = get_split_pattern("e", "Sentence") + assert r"\s+" in pattern