Files
gart/code/tests/test_voice_actions_tool.py

268 lines
10 KiB
Python

"""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