chore: atualização geral
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""Tests for the apply_voice_actions MCP tool — decisions -> real FCPXML.
|
||||
|
||||
Uses an inline fixture rather than examples/sample.fcpxml so the source
|
||||
windows are explicit and the assertions can be exact.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
from fcpxml.safe_xml import safe_parse
|
||||
|
||||
_FIXTURE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<fcpxml version="1.13">
|
||||
<resources>
|
||||
<format id="r1" name="FFVideoFormat1080p30" frameDuration="100/3000s" width="1920" height="1080"/>
|
||||
<asset id="a1" name="entrevista" start="0s" duration="600/30s" hasVideo="1" hasAudio="1" format="r1">
|
||||
<media-rep kind="original-media" src="file:///media/entrevista.mov"/>
|
||||
</asset>
|
||||
</resources>
|
||||
<library>
|
||||
<event name="Ev">
|
||||
<project name="Proj">
|
||||
<sequence format="r1" duration="600/30s" tcStart="0s">
|
||||
<spine>
|
||||
<asset-clip name="entrevista" ref="a1" offset="0s" start="0s" duration="600/30s"/>
|
||||
</spine>
|
||||
</sequence>
|
||||
</project>
|
||||
</event>
|
||||
</library>
|
||||
</fcpxml>
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project(tmp_path):
|
||||
path = tmp_path / "proj.fcpxml"
|
||||
path.write_text(_FIXTURE)
|
||||
return path
|
||||
|
||||
|
||||
def _out(project):
|
||||
return project.with_name("proj_voice_edit.fcpxml")
|
||||
|
||||
|
||||
class TestApplyVoiceActionsHandler:
|
||||
async def test_no_actions_reports_instead_of_writing(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({"filepath": str(project)})
|
||||
assert "nothing to apply" in result[0].text.lower()
|
||||
assert not _out(project).exists()
|
||||
|
||||
async def test_all_invalid_actions_writes_nothing(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{"kind": "teleport", "start": 1.0, "end": 2.0}],
|
||||
})
|
||||
assert "No valid actions" in result[0].text
|
||||
assert "teleport" in result[0].text
|
||||
assert not _out(project).exists()
|
||||
|
||||
async def test_applies_zoom_into_the_hosting_clip(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{
|
||||
"kind": "zoom", "start": 5.0, "end": 6.0,
|
||||
"params": {"scale": 1.4}, "reason": "argumento central",
|
||||
}],
|
||||
})
|
||||
assert "argumento central" in result[0].text
|
||||
|
||||
tree = safe_parse(str(_out(project)))
|
||||
transforms = tree.getroot().findall(".//adjust-transform")
|
||||
assert len(transforms) == 1
|
||||
|
||||
async def test_applies_text_title(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{
|
||||
"kind": "text", "start": 3.0, "end": 4.0,
|
||||
"params": {"content": "SEGURANÇA"},
|
||||
}],
|
||||
})
|
||||
titles = safe_parse(str(_out(project))).getroot().findall(".//title")
|
||||
assert len(titles) == 1
|
||||
texts = [t.text for t in titles[0].iter() if t.text]
|
||||
assert any("SEGURANÇA" in t for t in texts)
|
||||
|
||||
async def test_applies_marker(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{
|
||||
"kind": "marker", "start": 2.0, "end": 2.5, "reason": "virada",
|
||||
}],
|
||||
})
|
||||
markers = safe_parse(str(_out(project))).getroot().findall(".//marker")
|
||||
assert len(markers) == 1
|
||||
|
||||
async def test_cut_shortens_the_timeline(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
before = safe_parse(str(project)).getroot().find(".//asset-clip").get("duration")
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{"kind": "cut", "start": 5.0, "end": 10.0, "reason": "digressão"}],
|
||||
})
|
||||
assert "Cuts applied" in result[0].text
|
||||
|
||||
clips = safe_parse(str(_out(project))).getroot().findall(".//asset-clip")
|
||||
total = sum(
|
||||
int(c.get("duration").split("/")[0]) / int(c.get("duration").split("/")[1].rstrip("s"))
|
||||
for c in clips
|
||||
)
|
||||
original = int(before.split("/")[0]) / int(before.split("/")[1].rstrip("s"))
|
||||
assert total < original
|
||||
|
||||
async def test_action_inside_a_cut_is_dropped_and_reported(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [
|
||||
{"kind": "cut", "start": 4.0, "end": 12.0},
|
||||
{"kind": "zoom", "start": 6.0, "end": 7.0, "reason": "some no material cortado"},
|
||||
],
|
||||
})
|
||||
text = result[0].text
|
||||
assert "Dropped" in text
|
||||
assert "some no material cortado" in text
|
||||
assert safe_parse(str(_out(project))).getroot().findall(".//adjust-transform") == []
|
||||
|
||||
async def test_action_beyond_the_media_is_reported_not_silent(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{"kind": "zoom", "start": 500.0, "end": 501.0}],
|
||||
})
|
||||
assert "Not placed" in result[0].text
|
||||
assert "outside the edited timeline" in result[0].text
|
||||
|
||||
async def test_original_file_is_untouched(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
original = project.read_text()
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{"kind": "zoom", "start": 5.0, "end": 6.0}],
|
||||
})
|
||||
assert project.read_text() == original
|
||||
|
||||
async def test_mixed_valid_and_invalid_applies_the_valid_ones(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [
|
||||
{"kind": "zoom", "start": 5.0, "end": 6.0},
|
||||
{"kind": "zoom", "start": 8.0, "end": 9.0, "params": {"scale": 99}},
|
||||
],
|
||||
})
|
||||
assert "Rejected" in result[0].text
|
||||
assert len(safe_parse(str(_out(project))).getroot().findall(".//adjust-transform")) == 1
|
||||
|
||||
async def test_respects_explicit_output_path(self, project, tmp_path):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
target = tmp_path / "custom.fcpxml"
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [{"kind": "marker", "start": 1.0, "end": 2.0}],
|
||||
"output_path": str(target),
|
||||
})
|
||||
assert target.exists()
|
||||
|
||||
async def test_output_is_valid_parseable_fcpxml(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [
|
||||
{"kind": "cut", "start": 2.0, "end": 4.0},
|
||||
{"kind": "zoom", "start": 10.0, "end": 11.0},
|
||||
{"kind": "text", "start": 12.0, "end": 13.0, "params": {"content": "OK"}},
|
||||
],
|
||||
})
|
||||
root = safe_parse(str(_out(project))).getroot()
|
||||
assert root.tag == "fcpxml"
|
||||
assert root.find(".//spine") is not None
|
||||
|
||||
|
||||
class TestSampleFixtureStillParses:
|
||||
"""The applier must not corrupt a real-world document."""
|
||||
|
||||
async def test_real_sample_survives_a_zoom(self, tmp_path):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
src = "examples/sample.fcpxml"
|
||||
target = tmp_path / "sample.fcpxml"
|
||||
shutil.copy(src, target)
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(target),
|
||||
"actions": [{"kind": "marker", "start": 1.0, "end": 2.0, "reason": "teste"}],
|
||||
})
|
||||
assert "Voice Actions Applied" in result[0].text
|
||||
|
||||
|
||||
class TestPlacementsLandOnTheRightPieceAfterCuts:
|
||||
"""Cutting splits a clip into same-named pieces. Placing before cutting
|
||||
duplicated the zoom onto every piece and lost markers outright; a
|
||||
name-based lookup afterwards would always resolve to the first piece.
|
||||
Both bugs shipped past the suite and only showed up on real footage."""
|
||||
|
||||
async def test_zoom_lands_on_exactly_one_piece(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [
|
||||
{"kind": "cut", "start": 2.0, "end": 5.0},
|
||||
{"kind": "cut", "start": 8.0, "end": 12.0},
|
||||
{"kind": "zoom", "start": 15.0, "end": 16.0, "params": {"scale": 1.2}},
|
||||
],
|
||||
})
|
||||
root = safe_parse(str(_out(project))).getroot()
|
||||
assert len(root.findall(".//spine/asset-clip")) == 3
|
||||
assert len(root.findall(".//adjust-transform")) == 1
|
||||
|
||||
async def test_zoom_lands_on_the_last_piece_not_the_first(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [
|
||||
{"kind": "cut", "start": 2.0, "end": 5.0},
|
||||
{"kind": "zoom", "start": 15.0, "end": 16.0},
|
||||
],
|
||||
})
|
||||
clips = safe_parse(str(_out(project))).getroot().findall(".//spine/asset-clip")
|
||||
# the zoom is at 15s source -> 12s after a 3s cut, i.e. the 2nd piece
|
||||
assert clips[0].find("adjust-transform") is None
|
||||
assert clips[1].find("adjust-transform") is not None
|
||||
|
||||
async def test_markers_survive_the_cut(self, project):
|
||||
from server import handle_apply_voice_actions
|
||||
|
||||
result = await handle_apply_voice_actions({
|
||||
"filepath": str(project),
|
||||
"actions": [
|
||||
{"kind": "cut", "start": 5.0, "end": 10.0},
|
||||
{"kind": "marker", "start": 4.9, "end": 5.05, "reason": "emenda"},
|
||||
{"kind": "marker", "start": 15.0, "end": 15.2, "reason": "depois"},
|
||||
],
|
||||
})
|
||||
assert "Dropped" not in result[0].text
|
||||
markers = safe_parse(str(_out(project))).getroot().findall(".//marker")
|
||||
assert len(markers) == 2
|
||||
Reference in New Issue
Block a user