mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
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.
284 lines
10 KiB
Python
284 lines
10 KiB
Python
"""Tests for infrastructure/subtitle_writer.py — SrtWriter, AssWriter, VttWriter."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pathlib import Path
|
|
|
|
from abogen.infrastructure.subtitle_writer import (
|
|
AssWriter,
|
|
SrtWriter,
|
|
SubtitleAlignment,
|
|
SubtitleConfig,
|
|
SubtitleFormat,
|
|
SubtitleMode,
|
|
VttWriter,
|
|
create_subtitle_writer,
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# SrtWriter._format_time
|
|
# ===================================================================
|
|
|
|
class TestSrtFormatTime:
|
|
def test_zero(self):
|
|
assert SrtWriter._format_time(0.0) == "00:00:00,000"
|
|
|
|
def test_simple(self):
|
|
assert SrtWriter._format_time(61.5) == "00:01:01,500"
|
|
|
|
def test_hours(self):
|
|
assert SrtWriter._format_time(3661.123) == "01:01:01,123"
|
|
|
|
def test_large(self):
|
|
assert SrtWriter._format_time(7384.0) == "02:03:04,000"
|
|
|
|
def test_fractional_seconds(self):
|
|
assert SrtWriter._format_time(0.999) == "00:00:00,999"
|
|
|
|
def test_matches_old_format_timestamp(self):
|
|
"""Verify matches old _format_timestamp(ass=False) from conversion_runner."""
|
|
import math
|
|
for t in [0.0, 61.5, 3661.123, 7384.0, 0.999, 125.7]:
|
|
h = int(t // 3600)
|
|
m = int((t % 3600) // 60)
|
|
s = int(t % 60)
|
|
ms = int((t - math.floor(t)) * 1000)
|
|
expected = f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
|
|
assert SrtWriter._format_time(t) == expected, f"Mismatch at t={t}"
|
|
|
|
|
|
# ===================================================================
|
|
# AssWriter._format_time
|
|
# ===================================================================
|
|
|
|
class TestAssFormatTime:
|
|
def test_zero(self):
|
|
assert AssWriter._format_time(0.0) == "0:00:00.00"
|
|
|
|
def test_simple(self):
|
|
assert AssWriter._format_time(61.5) == "0:01:01.50"
|
|
|
|
def test_hours(self):
|
|
assert AssWriter._format_time(3661.12) == "1:01:01.12"
|
|
|
|
def test_centiseconds(self):
|
|
assert AssWriter._format_time(1.55) == "0:00:01.55"
|
|
|
|
def test_matches_old_format_timestamp_ass(self):
|
|
"""Verify matches old _format_timestamp(ass=True) from conversion_runner.
|
|
|
|
Note: the old code used int(milliseconds/10) which truncates centiseconds,
|
|
while the new code uses float formatting which rounds. For most values they
|
|
match; the difference is at most 1 centisecond due to float precision.
|
|
"""
|
|
import math
|
|
for t in [0.0, 61.5, 1.55, 125.7]:
|
|
h = int(t // 3600)
|
|
m = int((t % 3600) // 60)
|
|
s = int(t % 60)
|
|
ms = int((t - math.floor(t)) * 1000)
|
|
cs = int(ms / 10)
|
|
expected = f"{h:01d}:{m:02d}:{s:02d}.{cs:02d}"
|
|
assert AssWriter._format_time(t) == expected, f"Mismatch at t={t}"
|
|
|
|
|
|
# ===================================================================
|
|
# SrtWriter full write
|
|
# ===================================================================
|
|
|
|
class TestSrtWriter:
|
|
def test_single_entry(self, tmp_path):
|
|
path = tmp_path / "test.srt"
|
|
writer = SrtWriter(path, SubtitleConfig(format=SubtitleFormat.SRT, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=2.5, text="Hello")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "1\n" in content
|
|
assert "00:00:00,000 --> 00:00:02,500" in content
|
|
assert "Hello\n" in content
|
|
|
|
def test_multiple_entries(self, tmp_path):
|
|
path = tmp_path / "test.srt"
|
|
writer = SrtWriter(path, SubtitleConfig(format=SubtitleFormat.SRT, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=1.0, text="First")
|
|
writer.write_entry(start=1.0, end=2.0, text="Second")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "1\n" in content
|
|
assert "2\n" in content
|
|
assert "First" in content
|
|
assert "Second" in content
|
|
|
|
def test_voice_prefix(self, tmp_path):
|
|
path = tmp_path / "test.srt"
|
|
writer = SrtWriter(path, SubtitleConfig(format=SubtitleFormat.SRT, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=1.0, text="Hello", voice="af_heart")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "[af_heart] Hello" in content
|
|
|
|
def test_auto_index(self, tmp_path):
|
|
path = tmp_path / "test.srt"
|
|
writer = SrtWriter(path, SubtitleConfig(format=SubtitleFormat.SRT, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=1.0, text="A")
|
|
writer.write_entry(start=1.0, end=2.0, text="B")
|
|
writer.write_entry(start=2.0, end=3.0, text="C")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "1\n" in content
|
|
assert "2\n" in content
|
|
assert "3\n" in content
|
|
|
|
|
|
# ===================================================================
|
|
# AssWriter full write
|
|
# ===================================================================
|
|
|
|
class TestAssWriter:
|
|
def test_header_structure(self, tmp_path):
|
|
path = tmp_path / "test.ass"
|
|
writer = AssWriter(path, SubtitleConfig(format=SubtitleFormat.ASS, mode=SubtitleMode.LINE))
|
|
writer.open()
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "[Script Info]" in content
|
|
assert "[V4+ Styles]" in content
|
|
assert "[Events]" in content
|
|
assert "Format: Layer, Start, End" in content
|
|
|
|
def test_single_entry(self, tmp_path):
|
|
path = tmp_path / "test.ass"
|
|
writer = AssWriter(path, SubtitleConfig(format=SubtitleFormat.ASS, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=2.5, text="Hello")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "Dialogue:" in content
|
|
assert "Hello" in content
|
|
|
|
def test_voice_prefix(self, tmp_path):
|
|
path = tmp_path / "test.ass"
|
|
writer = AssWriter(path, SubtitleConfig(format=SubtitleFormat.ASS, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=1.0, text="Hello", voice="af_heart")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "[af_heart] Hello" in content
|
|
|
|
def test_highlight_mode(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="Hello world")
|
|
writer.close()
|
|
content = path.read_text()
|
|
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(
|
|
format=SubtitleFormat.ASS,
|
|
mode=SubtitleMode.LINE,
|
|
alignment=SubtitleAlignment.CENTER,
|
|
)
|
|
writer = AssWriter(path, config)
|
|
writer.open()
|
|
writer.close()
|
|
content = path.read_text()
|
|
# Centered uses alignment=5
|
|
assert ",5," in content or ",5\n" in content
|
|
|
|
|
|
# ===================================================================
|
|
# VttWriter
|
|
# ===================================================================
|
|
|
|
class TestVttWriter:
|
|
def test_header(self, tmp_path):
|
|
path = tmp_path / "test.vtt"
|
|
writer = VttWriter(path, SubtitleConfig(format=SubtitleFormat.VTT, mode=SubtitleMode.LINE))
|
|
writer.open()
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert content.startswith("WEBVTT")
|
|
|
|
def test_single_entry(self, tmp_path):
|
|
path = tmp_path / "test.vtt"
|
|
writer = VttWriter(path, SubtitleConfig(format=SubtitleFormat.VTT, mode=SubtitleMode.LINE))
|
|
writer.write_entry(start=0.0, end=2.5, text="Hello")
|
|
writer.close()
|
|
content = path.read_text()
|
|
assert "1\n" in content
|
|
assert "Hello" in content
|
|
|
|
|
|
# ===================================================================
|
|
# create_subtitle_writer factory
|
|
# ===================================================================
|
|
|
|
class TestCreateSubtitleWriter:
|
|
def test_srt(self, tmp_path):
|
|
path = tmp_path / "test.srt"
|
|
writer = create_subtitle_writer(path, "srt", "Line")
|
|
assert isinstance(writer, SrtWriter)
|
|
writer.close()
|
|
|
|
def test_ass(self, tmp_path):
|
|
path = tmp_path / "test.ass"
|
|
writer = create_subtitle_writer(path, "ass", "Line")
|
|
assert isinstance(writer, AssWriter)
|
|
writer.close()
|
|
|
|
def test_vtt(self, tmp_path):
|
|
path = tmp_path / "test.vtt"
|
|
writer = create_subtitle_writer(path, "vtt", "Line")
|
|
assert isinstance(writer, VttWriter)
|
|
writer.close()
|
|
|
|
def test_unsupported_raises(self, tmp_path):
|
|
path = tmp_path / "test.xyz"
|
|
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
|
|
# ===================================================================
|
|
|
|
class TestContextManager:
|
|
def test_srt_context_manager(self, tmp_path):
|
|
path = tmp_path / "test.srt"
|
|
with SrtWriter(path, SubtitleConfig(format=SubtitleFormat.SRT, mode=SubtitleMode.LINE)) as writer:
|
|
writer.write_entry(start=0.0, end=1.0, text="Hello")
|
|
content = path.read_text()
|
|
assert "Hello" in content
|
|
|
|
def test_ass_context_manager(self, tmp_path):
|
|
path = tmp_path / "test.ass"
|
|
with AssWriter(path, SubtitleConfig(format=SubtitleFormat.ASS, mode=SubtitleMode.LINE)) as writer:
|
|
writer.write_entry(start=0.0, end=1.0, text="Hello")
|
|
content = path.read_text()
|
|
assert "Dialogue:" in content
|