backend/tests/test_drawing_lines_not_translated.py
Rama master · réplica de lectura del repositorio original.
"""ASS drawing commands are vector data, not language — never translate them.
Prod, 2026-09-17: ``subtitle_automation`` spent 22:57–23:09 translating and
quality-retrying seven consecutive events whose text read
``m 78.43 15.3 b 77.04 15.3 75.83 16.15 …`` — the coordinates of a ``{\\p1}``
drawing. Each scored 1, was retranslated, scored 1 again. Over 72 hours
59 of 384 quality retries (15 %) were spent this way.
Two things have to line up for a drawing to reach the model:
- ``extract_tags`` splits out every ``{…}`` override tag, including the
``{\\p1}`` that is the only thing marking the rest as a drawing, and hands
back the bare coordinates as "clean text";
- the style filter only separates dialog styles from signs/songs styles, so a
drawing authored under a dialog style passes it.
So the guard has to look at the event text itself, before the tags are gone.
"""
from __future__ import annotations
import pytest
@pytest.mark.parametrize(
"text, expected",
[
# A closed drawing span contributes no text at all.
(r"{\p1}m 78.43 15.3 b 77.04 15.3 75.83 16.15{\p0}", ""),
# The real prod shape: the opener is never closed, the line just ends.
(r"{\p4}m 0 0 l 100 0 100 100 0 100", ""),
# A drawing that carries no override tag beyond the opener.
(r"{\p1}m 1 2 l 3 4", ""),
# Ordinary dialogue is untouched.
("Guten Morgen.", "Guten Morgen."),
# Ordinary dialogue with positioning/styling tags stays translatable.
(r"{\an8}{\i1}Guten Morgen.{\i0}", "Guten Morgen."),
# \p0 is not a drawing opener — it turns drawing mode off.
(r"{\p0}Guten Morgen.", "Guten Morgen."),
# Mixed: real text after the span closes must survive.
(r"{\p1}m 1 2{\p0}Guten Morgen.", "Guten Morgen."),
# Drawing mode re-opened after real text still yields that text.
(r"Hallo{\p1}m 1 2", "Hallo"),
# Empty stays empty.
("", ""),
],
)
def test_text_outside_drawing(text, expected):
from ass_utils import text_outside_drawing
assert text_outside_drawing(text) == expected
@pytest.mark.parametrize(
"text, expected",
[
# A space before the number: libass skips whitespace, so this ends
# drawing mode and the dialogue after it is visible.
(r"{\p1}m 0 0 l 10 10{\p 0}Good morning.", "Good morning."),
# A negative argument is <= 0, so drawing is off.
(r"{\p1}m 0 0 l 10 10{\p-1}Good morning.", "Good morning."),
# No argument at all parses as 0 — also off.
(r"{\p1}m 0 0 l 10 10{\p}Good morning.", "Good morning."),
# Leading zero still names mode 1: this IS a drawing.
(r"{\p01}m 0 0 l 10 10", ""),
# Uppercase is not the drawing tag at all; libass ignores it and the
# line renders as ordinary dialogue.
(r"{\P1}Good morning.", "Good morning."),
# \pbo is the baseline-offset tag and must not be read as \p.
(r"{\pbo3}Good morning.", "Good morning."),
(r"{\p1}m 0 0{\pbo3}l 10 10", ""),
# Mode numbers above 9 exist; two digits must not be truncated.
(r"{\p12}m 0 0 l 10 10", ""),
],
)
def test_text_outside_drawing_follows_libass(text, expected):
"""The predicate has to agree with the renderer, not with a guess.
Sandbox VM, 1.14.4-rc.9: seven pixel comparisons against the ffmpeg/libass
shipped in the release image showed ``\\P1`` rendering identically to plain
dialogue, ``\\p01`` identically to ``\\p1``, and ``{\\p 0}`` / ``{\\p-1}`` /
``{\\p}`` identically to ``{\\p0}``. The first regex pair
(``\\p[1-9]`` / ``\\p0``, both IGNORECASE) disagreed with all of those —
wrongly skipping whole events with visible dialogue in three of them, and
wrongly translating coordinates in the fourth.
"""
from ass_utils import text_outside_drawing
assert text_outside_drawing(text) == expected
@pytest.mark.parametrize(
"text, expected_body",
[
# Drawing first, dialogue after — the VM's reproduction.
(r"{\p1}m 0 0 l 10 10{\p0}Good morning.", "Good morning."),
# Dialogue first, drawing after.
(r"Good morning.{\p1}m 0 0 l 10 10", "Good morning."),
# Inline styling around the dialogue rides along in prefix/suffix; the
# body is the sentence itself, which is all the model needs.
(r"{\p1}m 0 0{\p0}{\i1}Good morning.{\i0}", "Good morning."),
# Styling *inside* the sentence splits it into three runs. The body has
# to span all of them, tags included, or an ordinary sign caption with
# one italic word would be excluded from translation entirely.
(
r"{\p1}m 0 0{\p0}Good {\i1}morning{\i0} to you.",
r"Good {\i1}morning{\i0} to you.",
),
],
)
def test_mixed_events_offer_only_their_dialogue(text, expected_body):
"""Only the dialogue half may reach the model — never the coordinates.
Sandbox VM, 1.14.4-rc.9 (F3): the helper was used as a yes/no filter and
``extract_tags`` then got the *whole* original, so the coordinates went to
the model anyway. With them in front, restoring the tags proportionally
shifted them into the German sentence and the sanitizer then removed part
of it: ``{\\p1}m 0 0 l 10 10{\\p0}Good morning.`` was saved as
``" Morgen."``. Present in rc.8 too, and the drawing filter did not close
it.
"""
from ass_utils import split_around_drawings
split = split_around_drawings(text)
assert split is not None, "a mixed event must be splittable"
prefix, body, suffix = split
assert body == expected_body
assert prefix + body + suffix == text, "the split must lose nothing"
@pytest.mark.parametrize(
"text, expected_body",
[
(r"\N{\p1}m 0 0 l 50 0{\p0}Good morning.", "Good morning."),
(r"Good morning.{\p1}m 0 0 l 50 0{\p0}\N", "Good morning."),
(r"\h{\p1}m 0 0 l 50 0{\p0}Good morning.", "Good morning."),
],
)
def test_a_line_break_is_not_a_second_piece_of_dialogue(text, expected_body):
"""``\\N`` and ``\\h`` are layout, not language.
Sandbox VM, 1.14.4-rc.10 (F3b): Python does not see the raw two-character
sequences as whitespace, so a leading ``\\N`` counted as its own stretch of
dialogue. The drawing then looked as if it sat *between* two sentences and
the whole caption was skipped — the English line was saved and the flow
still reported success.
"""
from ass_utils import split_around_drawings
split = split_around_drawings(text)
assert split is not None, "a layout escape was mistaken for dialogue"
assert split[1] == expected_body
def test_a_line_break_inside_the_sentence_stays_in_the_body():
from ass_utils import split_around_drawings
split = split_around_drawings(r"{\p1}m 0 0{\p0}Good\Nmorning.")
assert split is not None
assert split[1] == r"Good\Nmorning."
def test_drawing_between_two_pieces_of_dialogue_is_not_split():
"""Two text halves around a drawing cannot be put back together.
One translated string cannot be mapped onto two separate runs, so the
event is left alone rather than reassembled by guesswork.
"""
from ass_utils import split_around_drawings
assert split_around_drawings(r"Good{\p1}m 0 0{\p0}morning.") is None
def test_mixed_event_saves_the_whole_translated_sentence(tmp_path, app_ctx, monkeypatch):
"""End to end: the sentence arrives whole, not cut short.
The saved file no longer carries the drawing — ``subtitle_sanitizer``
removes complete ``{\\pN}…{\\p0}`` spans on purpose, because a drawing-mode
overlay can cover the screen. That is not what was broken. What was broken
is that the coordinates travelled *through* the model as part of the text,
so ``restore_tags`` placed ``{\\p0}`` proportionally inside the German
sentence and the sanitizer then removed everything up to it, saving
``" Morgen."`` (sandbox VM, rc.9). The sentence must survive whole.
"""
pytest.importorskip("pysubs2")
import pysubs2
import translator.core as _core
from translator.ass_flow import _translate_external_ass
src = tmp_path / "source.ass"
subs = pysubs2.SSAFile()
subs.styles["Default"] = pysubs2.SSAStyle()
subs.events.append(
pysubs2.SSAEvent(
start=0, end=1000, style="Default", text=r"{\p1}m 0 0 l 10 10{\p0}Good morning."
)
)
subs.save(str(src))
seen = {}
class _Result:
backend_name = "stub"
success = True
def _fake_translate(lines, **kwargs):
seen["lines"] = list(lines)
return ["Guten Morgen."], _Result()
monkeypatch.setattr(_core._pkg(), "_translate_with_manager", _fake_translate)
monkeypatch.setattr(_core._pkg(), "_get_quality_config", lambda: (False, 35, 1))
result = _translate_external_ass(
str(tmp_path / "video.mkv"), str(src), target_language="de", source_language="en"
)
assert seen["lines"] == ["Good morning."], "coordinates reached the model"
out = pysubs2.load(result["output_path"])
assert out.events[0].text == r"{\p1}{\p0}Guten Morgen.", (
"the sanitizer took part of the sentence with the drawing"
)
def test_drawing_events_are_not_offered_for_translation(tmp_path, app_ctx):
"""End to end through the flow's own selection, not just the helper.
A file whose dialog style holds one real line and one unclosed drawing must
offer exactly the real line to the translator.
"""
pytest.importorskip("pysubs2")
import pysubs2
from translator.ass_flow import _collect_translatable_events
subs = pysubs2.SSAFile()
subs.styles["Default"] = pysubs2.SSAStyle()
subs.events.append(pysubs2.SSAEvent(start=0, end=1000, style="Default", text="Guten Morgen."))
subs.events.append(
pysubs2.SSAEvent(
start=1000,
end=2000,
style="Default",
text=r"{\p4}m 78.43 15.3 b 77.04 15.3 75.83 16.15 75.33 17.41 l 14.03 4",
)
)
indices, texts, *_rest = _collect_translatable_events(subs, {"Default"})
assert texts == ["Guten Morgen."]
assert indices == [0]