chore: adiciona .gitignore e commit.command
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"""Tests for admin/models_api.py — the SwiftUI JSON bridge commands.
|
||||
|
||||
Focused on the transcription-flow changes: atomic save, speaker renaming, and
|
||||
the "use the selected model" default plus model-availability guard.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import admin.models_api as api
|
||||
|
||||
|
||||
def _capture(monkeypatch):
|
||||
captured: list[dict] = []
|
||||
|
||||
def _emit(obj):
|
||||
captured.append(obj)
|
||||
|
||||
monkeypatch.setattr(api, "_emit", _emit)
|
||||
return captured
|
||||
|
||||
|
||||
def test_save_json_atomic(tmp_path):
|
||||
p = tmp_path / "t.json"
|
||||
api._save_json_atomic(p, {"a": [1, 2], "text": "olá"})
|
||||
assert p.exists()
|
||||
assert not (tmp_path / "t.json.tmp").exists()
|
||||
assert json.loads(p.read_text(encoding="utf-8"))["text"] == "olá"
|
||||
|
||||
|
||||
def test_rename_speakers(tmp_path, monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
p = tmp_path / "t.json"
|
||||
p.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"speakers": [
|
||||
{"id": "SPEAKER_00", "name": "Speaker 1"},
|
||||
{"id": "SPEAKER_01", "name": "Speaker 2"},
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert api.cmd_rename_speakers({"path": str(p), "speakers": {"SPEAKER_01": "Erika"}}) == 0
|
||||
assert captured[0]["ok"] is True
|
||||
saved = json.loads(p.read_text(encoding="utf-8"))
|
||||
assert saved["speakers"][0]["name"] == "Speaker 1"
|
||||
assert saved["speakers"][1]["name"] == "Erika"
|
||||
|
||||
|
||||
def test_rename_speakers_missing_file(monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
assert api.cmd_rename_speakers({"path": "/nonexistent/x.json"}) == 1
|
||||
assert captured[0]["type"] == "error"
|
||||
|
||||
|
||||
def test_transcribe_requires_output_dir(monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
monkeypatch.setattr(api, "load_selected_model", lambda: "small")
|
||||
monkeypatch.setattr(api, "is_model_downloaded", lambda m: True)
|
||||
assert api.cmd_transcribe({"path": "/some/project.fcpxml"}) == 1
|
||||
assert captured[0]["type"] == "error"
|
||||
assert "pasta do projeto" in captured[0]["message"]
|
||||
|
||||
|
||||
def test_transcribe_requires_installed_model(monkeypatch, tmp_path):
|
||||
captured = _capture(monkeypatch)
|
||||
monkeypatch.setattr(api, "load_selected_model", lambda: "")
|
||||
monkeypatch.setattr(api, "is_model_downloaded", lambda m: False)
|
||||
assert api.cmd_transcribe({"path": "/some/project.fcpxml", "output_dir": str(tmp_path)}) == 1
|
||||
assert captured[0]["type"] == "error"
|
||||
assert "instalado" in captured[0]["message"]
|
||||
|
||||
|
||||
def test_transcribe_defaults_to_selected_model(monkeypatch, tmp_path):
|
||||
captured = _capture(monkeypatch)
|
||||
monkeypatch.setattr(api, "load_selected_model", lambda: "small")
|
||||
monkeypatch.setattr(api, "is_model_downloaded", lambda m: m == "small")
|
||||
|
||||
class FakeTL:
|
||||
clips = []
|
||||
|
||||
class FakeProject:
|
||||
primary_timeline = None
|
||||
timelines = [FakeTL()]
|
||||
|
||||
monkeypatch.setattr(api, "parse_fcpxml", lambda p: FakeProject())
|
||||
# No media accessible -> reaches the media-path check (past model validation).
|
||||
assert api.cmd_transcribe({"path": "/some/project.fcpxml", "output_dir": str(tmp_path)}) == 1
|
||||
assert captured[0]["type"] == "error"
|
||||
assert "mídia" in captured[0]["message"]
|
||||
|
||||
|
||||
def test_set_language_persists(monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
assert api.cmd_set_language({"language": "pt"}) == 0
|
||||
assert captured[0]["ok"] is True
|
||||
assert captured[0]["language"] == "pt"
|
||||
assert api.load_transcript_language() == "pt"
|
||||
|
||||
|
||||
def test_set_language_rejects_unknown(monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
assert api.cmd_set_language({"language": "xx"}) == 1
|
||||
assert captured[0]["ok"] is False
|
||||
assert "language" in captured[0]["error"]
|
||||
|
||||
|
||||
def test_transcribe_defaults_language_to_persisted(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(api, "load_selected_model", lambda: "small")
|
||||
monkeypatch.setattr(api, "is_model_downloaded", lambda m: m == "small")
|
||||
monkeypatch.setattr(api, "load_transcript_language", lambda: "pt")
|
||||
|
||||
media = tmp_path / "clip.mov"
|
||||
media.write_bytes(b"fake")
|
||||
|
||||
class FakeClip:
|
||||
media_path = ""
|
||||
|
||||
class FakeTL:
|
||||
clips = [FakeClip()]
|
||||
|
||||
class FakeProject:
|
||||
primary_timeline = None
|
||||
timelines = [FakeTL()]
|
||||
|
||||
monkeypatch.setattr(api, "parse_fcpxml", lambda p: FakeProject())
|
||||
monkeypatch.setattr(api, "media_src_to_path", lambda mp: str(media))
|
||||
called = {}
|
||||
monkeypatch.setattr(
|
||||
api, "transcribe", lambda mp, model_size, language, **kw: called.update(lang=language)
|
||||
)
|
||||
assert api.cmd_transcribe({"path": "/some/project.fcpxml", "output_dir": str(tmp_path / "out")}) == 1
|
||||
assert called["lang"] == "pt"
|
||||
|
||||
|
||||
def test_srt_stamp_format():
|
||||
assert api.srt_stamp(0.0) == "00:00:00,000"
|
||||
assert api.srt_stamp(1.5) == "00:00:01,500"
|
||||
assert api.srt_stamp(3661.234) == "01:01:01,234"
|
||||
|
||||
|
||||
_FCPXML_SAMPLE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<fcpxml version="1.13">
|
||||
<resources>
|
||||
<asset id="r1" name="clip" uid="u1" start="0s" duration="100s"
|
||||
hasVideo="1" format="f1" hasAudio="1">
|
||||
<media-rep kind="original-media" src="file:///tmp/clip.mp4"/>
|
||||
</asset>
|
||||
<format id="f1" name="FFVideoFormat1080p25" frameDuration="1/25s" width="1920" height="1080"/>
|
||||
</resources>
|
||||
<library>
|
||||
<event name="Event">
|
||||
<project name="P">
|
||||
<sequence format="f1">
|
||||
<spine>
|
||||
<asset-clip ref="r1" offset="0s" start="10s" duration="10s" name="clip"/>
|
||||
<gap name="Espaço" offset="10s" duration="90s" start="10s"/>
|
||||
</spine>
|
||||
</sequence>
|
||||
</project>
|
||||
</event>
|
||||
</library>
|
||||
</fcpxml>
|
||||
"""
|
||||
|
||||
|
||||
def test_cmd_export_srt_maps_to_edited_timeline(tmp_path, monkeypatch):
|
||||
"""Captions must reflect the EDITED timeline, not the whole source file."""
|
||||
captured = _capture(monkeypatch)
|
||||
project = tmp_path / "proj.fcpxml"
|
||||
project.write_text(_FCPXML_SAMPLE, encoding="utf-8")
|
||||
media = tmp_path / "clip.mp4"
|
||||
media.write_bytes(b"fake")
|
||||
# Transcript covers 0..100s; the clip only USES source 10..20s -> timeline 0..10s.
|
||||
transcript = {
|
||||
"words": [],
|
||||
"segments": [
|
||||
{"start": 5.0, "end": 6.0, "text": "antes do corte"},
|
||||
{"start": 12.0, "end": 14.0, "text": "dentro do corte"},
|
||||
{"start": 50.0, "end": 51.0, "text": "depois do corte"},
|
||||
]
|
||||
}
|
||||
tj = api._transcript_json_path(media)
|
||||
tj.parent.mkdir(parents=True, exist_ok=True)
|
||||
api._save_json_atomic(tj, transcript)
|
||||
monkeypatch.setattr(api, "media_src_to_path", lambda src: str(media))
|
||||
|
||||
assert api.cmd_export_srt({"path": str(project)}) == 0
|
||||
assert captured[0]["ok"] is True
|
||||
srt = tmp_path / "clip_captions.srt"
|
||||
assert srt.exists()
|
||||
text = srt.read_text(encoding="utf-8")
|
||||
# Only the segment inside the used source window (12s) survives.
|
||||
assert "dentro do corte" in text
|
||||
assert "antes do corte" not in text
|
||||
assert "depois do corte" not in text
|
||||
# Mapped to timeline 0..10s -> the 12s source segment lands at 2s.
|
||||
assert "00:00:02,000 --> 00:00:04,000" in text
|
||||
|
||||
|
||||
def test_cmd_export_srt_no_transcript(tmp_path, monkeypatch):
|
||||
captured = _capture(monkeypatch)
|
||||
project = tmp_path / "proj.fcpxml"
|
||||
project.write_text(_FCPXML_SAMPLE, encoding="utf-8")
|
||||
media = tmp_path / "clip.mp4"
|
||||
media.write_bytes(b"fake")
|
||||
monkeypatch.setattr(api, "media_src_to_path", lambda src: str(media))
|
||||
assert api.cmd_export_srt({"path": str(project)}) == 1
|
||||
assert captured[0]["ok"] is False
|
||||
|
||||
|
||||
def test_cmd_export_srt_clamps_past_project_duration(tmp_path, monkeypatch):
|
||||
"""A segment ending after the last clip must be clamped to the project end.
|
||||
|
||||
Final Cut rejects an SRT whose final cue overruns the timeline
|
||||
("subtitle extends beyond project duration").
|
||||
"""
|
||||
captured = _capture(monkeypatch)
|
||||
project = tmp_path / "proj.fcpxml"
|
||||
project.write_text(_FCPXML_SAMPLE, encoding="utf-8")
|
||||
media = tmp_path / "clip.mp4"
|
||||
media.write_bytes(b"fake")
|
||||
# Clip uses source 10..20s -> timeline 0..10s. A segment 12..30s maps to
|
||||
# timeline 2..20s, but the project only lasts 10s: must clamp end to 10s.
|
||||
transcript = {
|
||||
"words": [],
|
||||
"segments": [
|
||||
{"start": 12.0, "end": 30.0, "text": "longa fala"},
|
||||
]
|
||||
}
|
||||
tj = api._transcript_json_path(media)
|
||||
tj.parent.mkdir(parents=True, exist_ok=True)
|
||||
api._save_json_atomic(tj, transcript)
|
||||
monkeypatch.setattr(api, "media_src_to_path", lambda src: str(media))
|
||||
|
||||
assert api.cmd_export_srt({"path": str(project)}) == 0
|
||||
assert captured[0]["ok"] is True
|
||||
srt = tmp_path / "clip_captions.srt"
|
||||
text = srt.read_text(encoding="utf-8")
|
||||
# Timeline is 10s; the cue must not end past it.
|
||||
assert "00:00:02,000 --> 00:00:10,000" in text
|
||||
assert "00:00:20,000" not in text
|
||||
Reference in New Issue
Block a user