Marca cada título gerado (dynamic/plain) em metadata para que regenerar substitua a saída anterior em vez de empilhar, e usa os spans de ênfase revisados (não os segmentos brutos do Whisper) como janela da composição dinâmica, evitando que ela invada o trecho de legenda comum seguinte. suppress_plain_under_dynamic corta qualquer sobra visível como rede de segurança. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
73 lines
3.2 KiB
Python
73 lines
3.2 KiB
Python
"""Exercise the real subtitle handler across emphasis gaps and regeneration."""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from fcpxml.models import DynamicSubtitleConfig
|
|
from fcpxml.writer import FCPXMLModifier
|
|
from server_tools import subtitles
|
|
|
|
|
|
@pytest.fixture
|
|
def caption_job(tmp_path, monkeypatch):
|
|
modifier = FCPXMLModifier(Path(__file__).parents[1] / "examples/sample.fcpxml")
|
|
parent = modifier._require_clip("Interview_A")
|
|
parent.set("start", "0s")
|
|
parent.set("duration", "10s")
|
|
media = tmp_path / "speech.mov"
|
|
media.touch()
|
|
modifier.resources[parent.get("ref")]["src"] = media.as_uri()
|
|
monkeypatch.setattr(modifier, "_iter_spine_clips", lambda: iter([(0, parent)]))
|
|
monkeypatch.setattr(subtitles, "_setup_modifier", lambda *args: (
|
|
"input.fcpxml", str(tmp_path / "output.fcpxml"), modifier,
|
|
))
|
|
monkeypatch.setattr(subtitles, "get_active_dynamic_subtitle_layouts", lambda: [
|
|
{"role": "titles.dinamicas"},
|
|
])
|
|
monkeypatch.setattr(subtitles, "_build_dynamic_subtitle_config", lambda *args: DynamicSubtitleConfig())
|
|
monkeypatch.setattr(subtitles, "load_plain_subtitle_config", lambda: {
|
|
"role": "titles.convencionais", "font": "Helvetica Neue", "font_size": 48,
|
|
"font_color": "1 1 1 1", "max_words": 1, "position_y": -100,
|
|
"uppercase": False, "keep_punctuation": True,
|
|
})
|
|
monkeypatch.setattr(subtitles, "_load_plain_exclude_spans", lambda _: [])
|
|
monkeypatch.setattr(subtitles, "_load_emphasis_spans", lambda _: [
|
|
{"start": 0, "end": 1}, {"start": 4, "end": 5},
|
|
])
|
|
monkeypatch.setattr(subtitles, "_load_or_transcribe", lambda *args: ({
|
|
"words": [
|
|
{"word": "corpo", "start": 0, "end": 1},
|
|
{"word": "muda", "start": 2, "end": 3},
|
|
{"word": "também", "start": 4, "end": 5},
|
|
],
|
|
"segments": [{"start": 0, "end": 1}, {"start": 4, "end": 5}],
|
|
}, None))
|
|
return modifier, parent
|
|
|
|
|
|
def test_dynamic_composition_clears_before_plain_words_in_emphasis_gap(caption_job):
|
|
modifier, parent = caption_job
|
|
asyncio.run(subtitles.handle_generate_subtitles_by_emphasis({}))
|
|
dynamic = parent.findall("ref-clip")
|
|
plain = parent.findall("title")
|
|
assert dynamic and plain
|
|
for composition in dynamic:
|
|
start = modifier._parse_time(composition.get("offset"))
|
|
end = start + modifier._parse_time(composition.get("duration"))
|
|
for title in plain:
|
|
plain_start = modifier._parse_time(title.get("offset"))
|
|
plain_end = plain_start + modifier._parse_time(title.get("duration"))
|
|
assert not (start < plain_end and plain_start < end), "dynamic and plain overlap"
|
|
|
|
|
|
def test_regeneration_replaces_generated_subtitles_and_preserves_manual_titles(caption_job):
|
|
modifier, parent = caption_job
|
|
manual = modifier.add_text_title(parent, "Nome da médica", role="titles.manual")
|
|
asyncio.run(subtitles.handle_generate_subtitles_by_emphasis({}))
|
|
initial = len(parent.findall("ref-clip")) + len(parent.findall("title"))
|
|
asyncio.run(subtitles.handle_generate_subtitles_by_emphasis({}))
|
|
assert len(parent.findall("ref-clip")) + len(parent.findall("title")) == initial
|
|
assert manual in list(parent)
|