chore: atualização geral
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""Tests for the refine_voice_timeline MCP tool.
|
||||
|
||||
The tool exists because emphasis is *relative*: cut the loudest moment of a
|
||||
recording and every surviving score is still measured against something the
|
||||
viewer never sees. These tests pin the re-normalization actually happening,
|
||||
and the times staying in original source seconds so the result can be fed
|
||||
straight back to apply_voice_actions.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.test_voice_features_tool import _write_silent_wav
|
||||
from tests.test_voice_timeline_tool import _TRANSCRIPT, patched, wav # noqa: F401
|
||||
|
||||
_ = _write_silent_wav, _TRANSCRIPT # re-exported fixtures need the imports
|
||||
|
||||
|
||||
async def _build(wav_path):
|
||||
from server import handle_build_voice_timeline
|
||||
|
||||
await handle_build_voice_timeline({"media_path": str(wav_path)})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_candidates(monkeypatch):
|
||||
"""A transcript where BOTH lines carry a content word.
|
||||
|
||||
The shared fixture's first line is "isso e" — two function words, which
|
||||
``suggest_zoom_windows`` skips by design, so it can never produce more
|
||||
than one candidate to cap.
|
||||
"""
|
||||
import fcpxml.voice_timeline as vt
|
||||
import server_tools._shared as _shared_mod
|
||||
|
||||
transcript = {
|
||||
"language": "pt",
|
||||
"duration": 4.0,
|
||||
"text": "cirurgia rapida seguranca total",
|
||||
"segments": [
|
||||
{"text": "cirurgia rapida", "start": 0.0, "end": 1.0},
|
||||
{"text": "seguranca total", "start": 2.0, "end": 4.0},
|
||||
],
|
||||
"words": [
|
||||
{"word": "cirurgia", "start": 0.0, "end": 0.4, "confidence": 0.9},
|
||||
{"word": "rapida", "start": 0.5, "end": 0.7, "confidence": 0.9},
|
||||
{"word": "seguranca", "start": 2.0, "end": 2.9, "confidence": 0.9},
|
||||
{"word": "total", "start": 3.0, "end": 3.5, "confidence": 0.9},
|
||||
],
|
||||
}
|
||||
monkeypatch.setattr(_shared_mod, "transcribe", lambda *a, **k: transcript)
|
||||
monkeypatch.setattr(vt, "extract_pitch", lambda *a, **k: [(2.4, 260.0), (0.2, 120.0)])
|
||||
monkeypatch.setattr(vt, "extract_energy", lambda *a, **k: [(2.4, 0.95), (0.2, 0.10)])
|
||||
|
||||
|
||||
class TestRefineVoiceTimelineHandler:
|
||||
async def test_requires_an_existing_timeline(self, wav): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
result = await handle_refine_voice_timeline({"media_path": str(wav), "cuts": []})
|
||||
assert "build_voice_timeline" in result[0].text
|
||||
|
||||
async def test_rejects_disallowed_extension(self, tmp_path):
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
bad = tmp_path / "clip.txt"
|
||||
bad.write_text("not audio")
|
||||
with pytest.raises(ValueError):
|
||||
await handle_refine_voice_timeline({"media_path": str(bad), "cuts": []})
|
||||
|
||||
async def test_compares_raw_against_survivors(self, wav, patched): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
result = await handle_refine_voice_timeline(
|
||||
{"media_path": str(wav), "cuts": [{"start": 0.0, "end": 1.0}]}
|
||||
)
|
||||
text = result[0].text
|
||||
assert "Survivors only" in text
|
||||
assert "Average emphasis" in text
|
||||
|
||||
async def test_cut_words_are_excluded(self, wav, patched): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
result = await handle_refine_voice_timeline(
|
||||
{"media_path": str(wav), "cuts": [{"start": 0.0, "end": 1.0}], "save": True}
|
||||
)
|
||||
assert "_voice_timeline_refined.json" in result[0].text
|
||||
data = json.loads(
|
||||
(wav.parent / "clip_voice_timeline_refined.json").read_text(encoding="utf-8")
|
||||
)
|
||||
words = [w["text"] for s in data["segments"] for w in s["words"]]
|
||||
assert "isso" not in words
|
||||
assert "seguranca" in words
|
||||
|
||||
async def test_times_stay_in_original_source_seconds(self, wav, patched): # noqa: F811
|
||||
"""A cut at the head must NOT slide the survivors back to zero."""
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
await handle_refine_voice_timeline(
|
||||
{"media_path": str(wav), "cuts": [{"start": 0.0, "end": 1.0}], "save": True}
|
||||
)
|
||||
data = json.loads(
|
||||
(wav.parent / "clip_voice_timeline_refined.json").read_text(encoding="utf-8")
|
||||
)
|
||||
first = data["segments"][0]["words"][0]
|
||||
assert first["start"] == pytest.approx(2.0)
|
||||
|
||||
async def test_proposes_zoom_candidates(self, wav, patched): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
result = await handle_refine_voice_timeline({"media_path": str(wav), "cuts": []})
|
||||
assert "Zoom Candidates" in result[0].text
|
||||
|
||||
async def test_max_zooms_caps_the_list(self, wav, two_candidates): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
|
||||
async def zoom_rows(**extra):
|
||||
result = await handle_refine_voice_timeline(
|
||||
{"media_path": str(wav), "cuts": [], "min_gap": 0.0, **extra}
|
||||
)
|
||||
section = result[0].text.split("## Zoom Candidates", 1)[1]
|
||||
return [
|
||||
ln for ln in section.splitlines()
|
||||
if ln.startswith("| ") and ln.rstrip().endswith("|") and "Start" not in ln
|
||||
]
|
||||
|
||||
assert len(await zoom_rows()) == 2
|
||||
assert len(await zoom_rows(max_zooms=1)) == 1
|
||||
|
||||
async def test_malformed_cut_is_reported_not_raised(self, wav, patched): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
result = await handle_refine_voice_timeline(
|
||||
{"media_path": str(wav), "cuts": [{"start": 3.0, "end": 1.0}]}
|
||||
)
|
||||
text = result[0].text
|
||||
assert "Rejected cuts" in text
|
||||
assert "must be after start" in text
|
||||
|
||||
async def test_cutting_everything_says_so(self, wav, patched): # noqa: F811
|
||||
from server import handle_refine_voice_timeline
|
||||
|
||||
await _build(wav)
|
||||
result = await handle_refine_voice_timeline(
|
||||
{"media_path": str(wav), "cuts": [{"start": 0.0, "end": 60.0}]}
|
||||
)
|
||||
assert "removed every word" in result[0].text
|
||||
|
||||
|
||||
class TestRefineVoiceTimelineRegistration:
|
||||
async def test_tool_is_listed(self):
|
||||
from server import list_tools
|
||||
|
||||
assert "refine_voice_timeline" in {t.name for t in await list_tools()}
|
||||
|
||||
async def test_tool_is_dispatched(self):
|
||||
from server import TOOL_HANDLERS, handle_refine_voice_timeline
|
||||
|
||||
assert TOOL_HANDLERS["refine_voice_timeline"] is handle_refine_voice_timeline
|
||||
Reference in New Issue
Block a user