backend/tests/test_ass_song_styles.py
Rama master · réplica de lectura del repositorio original.
r"""Song and karaoke styles must not be handed to a translator as dialogue.
`SIGNS_PATTERNS` anchored `^op$` and `^ed$`, which misses the naming fansubs
actually use — `OP Romaji`, `OP English`, `ED Romaji`, `ED English`. Measured on
a real library on 2026-08-24: 92 of 403 "dialogue" lines in one episode were
song lines, and the German output shipped with the *romaji* — transliterated
Japanese — machine-translated (`OP Romaji ... Wind, oh Wind`).
A style only escapes the sign/song bucket today if its karaoke lines happen to
carry `\pos`/`\move` tags, which is a property of the typesetter, not a rule.
"""
import pysubs2
from ass_utils import classify_styles
def _subs(*styles: str) -> pysubs2.SSAFile:
subs = pysubs2.SSAFile()
for name in styles:
subs.styles[name] = pysubs2.SSAStyle()
subs.events = [pysubs2.SSAEvent(text=f"line for {name}", style=name) for name in styles]
return subs
def test_prefixed_opening_and_ending_styles_are_songs():
dialog, signs = classify_styles(_subs("OP Romaji", "OP English", "ED Romaji", "ED English"))
assert dialog == set()
assert signs == {"OP Romaji", "OP English", "ED Romaji", "ED English"}
def test_bare_op_and_ed_stay_songs():
_dialog, signs = classify_styles(_subs("OP", "ED"))
assert signs == {"OP", "ED"}
def test_romaji_is_never_dialogue_whatever_it_is_called():
"""Romaji is transliterated Japanese — never something to translate."""
_dialog, signs = classify_styles(_subs("Insert Song Romaji", "romaji", "Karaoke-Romaji"))
assert signs == {"Insert Song Romaji", "romaji", "Karaoke-Romaji"}
def test_ordinary_dialogue_styles_are_untouched():
dialog, signs = classify_styles(_subs("Default", "Main", "Italics", "Top Default", "Alt."))
assert signs == set()
assert "Default" in dialog and "Top Default" in dialog
def test_words_merely_containing_ed_are_not_songs():
"""A guard against a sloppy substring match — 'Edited', 'Red', 'Shed'."""
dialog, signs = classify_styles(_subs("Edited", "Red Text", "Shed"))
assert signs == set()
assert dialog == {"Edited", "Red Text", "Shed"}
def test_a_character_named_ed_keeps_their_dialogue():
"""The reason this pattern must not be a bare word match.
``classify_styles`` also feeds ``cleanup_signs`` and
``purge_signs_after_extract``, which DELETE what they consider signs. A
fansub style named after a character — 'Ed', 'Ed Smith', 'Op Officer' —
landing in the sign bucket would strand that character's lines
untranslated and expose them to a purge.
"""
dialog, signs = classify_styles(_subs("Ed Smith", "Ed (thoughts)", "Op Officer"))
assert signs == set()
assert dialog == {"Ed Smith", "Ed (thoughts)", "Op Officer"}
def test_a_style_named_subtitle_is_dialogue():
"""Prod 2026-11-08: "title" matched inside "Subtitle", so a file whose 271
dialogue lines used style "Subtitle" counted as signs only and its
dialogue was never translated."""
dialog, signs = classify_styles(_subs("Subtitle", "Subtitles", "Title", "Episode Title"))
assert {"Subtitle", "Subtitles"} <= dialog
assert {"Title", "Episode Title"} <= signs
def test_shortened_romaji_and_english_song_styles_are_songs():
_dialog, signs = classify_styles(_subs("OP - Rom", "OP - Eng", "ED-Rom"))
assert signs == {"OP - Rom", "OP - Eng", "ED-Rom"}