Ctrl K
ComunidadGuías…

backend/tests/test_ass_song_styles.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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"}