From ffac4a4da9a091e56ed5c01fb215eef6091dd48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 20 Aug 2026 23:27:49 +0300 Subject: [PATCH] fix(subtitles): enable "N words" and karaoke modes in PyQt Three related fixes: - make_subtitle_writer: accept word-count modes ("N words") in create_subtitle_writer. SubtitleMode("5 words") raised ValueError, which make_subtitle_writer swallowed and returned None, so PyQt never created or wrote the subtitle file when an "N words" mode was selected. Unknown modes now fall back to SubtitleMode.SENTENCE (writers only branch on SENTENCE_HIGHLIGHT). Regression test added. - gui.py: refresh subtitle combo item availability after initial voice/profile selection. update_subtitle_options_availability() ran during initUI with selected_lang=None for a profile, taking the non-English branch and disabling Highlighting/N-words items; the fix-up never re-ran because setCurrentIndex on an already-current index emits no signal. Now called once more after the profile/voice language is resolved. - AssWriter: stop discarding per-word karaoke timing. _add_karaoke_tags unconditionally replaced entry text with uniform {\\k100} tags, destroying the real per-word {\\kf} timings from _process_karaoke_highlighting. Only synthesize simplified tags when the text has no karaoke tags. Regression test added. --- abogen/infrastructure/subtitle_writer.py | 21 ++++++++++++++++++--- abogen/pyqt/gui.py | 1 + tests/test_subtitle_writer.py | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/abogen/infrastructure/subtitle_writer.py b/abogen/infrastructure/subtitle_writer.py index d283e38..e8f8d63 100644 --- a/abogen/infrastructure/subtitle_writer.py +++ b/abogen/infrastructure/subtitle_writer.py @@ -220,8 +220,10 @@ class AssWriter(SubtitleWriter): style = "Default" if self.config.mode == SubtitleMode.SENTENCE_HIGHLIGHT: - # Add karaoke tags for highlighting - text = self._add_karaoke_tags(text) + # Entries from process_subtitle_tokens already carry per-word + # {\kf...} timing; only synthesize simplified tags when absent. + if "{\\k" not in text: + text = self._add_karaoke_tags(text) style = "Highlight" alignment_tag = r"{\an5}" if self._is_centered else "" @@ -248,6 +250,19 @@ class AssWriter(SubtitleWriter): return f"{hours}:{minutes:02d}:{secs:05.2f}" +def _coerce_mode(mode: str) -> SubtitleMode: + """Parse a subtitle mode, tolerating word-count strings like "5 words". + + Word-count modes are grouped upstream (subtitle_generation) and the writer + only branches on SubtitleMode.SENTENCE_HIGHLIGHT, so any non-highlight + fallback is behaviorally equivalent for the writers. + """ + try: + return SubtitleMode(mode) + except ValueError: + return SubtitleMode.SENTENCE + + def create_subtitle_writer( path: Path, format: str, @@ -257,7 +272,7 @@ def create_subtitle_writer( ) -> SubtitleWriter: """Factory function to create subtitle writer.""" fmt = SubtitleFormat(format.lower()) - mode = SubtitleMode(mode) + mode = _coerce_mode(mode) align = SubtitleAlignment(alignment.lower()) config = SubtitleConfig( diff --git a/abogen/pyqt/gui.py b/abogen/pyqt/gui.py index 3e7ecee..46b0d93 100644 --- a/abogen/pyqt/gui.py +++ b/abogen/pyqt/gui.py @@ -1019,6 +1019,7 @@ class abogen(QWidget): self.selected_lang = ( language_for_voice_id(entry[0]) if entry and entry[0] else Language.EN_US ) + self.update_subtitle_options_availability() if self.save_option == "Choose output folder" and self.selected_output_folder: self.save_path_label.setText(self.selected_output_folder) self.save_path_row_widget.show() diff --git a/tests/test_subtitle_writer.py b/tests/test_subtitle_writer.py index 82a0a28..78f8bda 100644 --- a/tests/test_subtitle_writer.py +++ b/tests/test_subtitle_writer.py @@ -177,6 +177,19 @@ class TestAssWriter: assert "Highlight" in content assert r"{\k100}" in content + def test_highlight_mode_preserves_existing_karaoke_tags(self, tmp_path): + path = tmp_path / "test.ass" + config = SubtitleConfig( + format=SubtitleFormat.ASS, + mode=SubtitleMode.SENTENCE_HIGHLIGHT, + ) + writer = AssWriter(path, config) + writer.write_entry(start=0.0, end=1.0, text=r"{\kf20}Hello {\kf20}world.") + writer.close() + content = path.read_text() + assert r"{\kf20}Hello {\kf20}world." in content + assert r"{\k100}" not in content + def test_centered_alignment(self, tmp_path): path = tmp_path / "test.ass" config = SubtitleConfig( @@ -243,6 +256,12 @@ class TestCreateSubtitleWriter: with pytest.raises(ValueError): create_subtitle_writer(path, "xyz", "Line") + def test_word_count_mode(self, tmp_path): + path = tmp_path / "test.srt" + writer = create_subtitle_writer(path, "srt", "5 words", max_words=5) + assert isinstance(writer, SrtWriter) + writer.close() + # =================================================================== # Context manager