mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 15:50:25 +02:00
* feat: bundle standalone taste distillation and application workflows * docs: fix imported taste skill markdown lint * docs: align Turkish agent catalog with taste skills * refactor: make ECC the canonical reusable video engine * fix: preserve video duration when applying image overlays * fix: preserve background colors in image compositing * fix: report best-effort duration targets and shortfalls * feat: ship verified Fusion presets with compatibility provenance * feat(tasteforge): preserve native edits in application bundles * feat(tasteforge): compile local preservation without hosted input * fix: update js-yaml to patched 4.3.2 * test: report bounded Stop wrapper failure diagnostics * fix(tasteforge): fail closed on unsafe output names, missing overlays and cadence - cli: default report and spec paths are derived from pack name and profile genre; require the manifest's name pattern before using either as a filename part so a traversal string cannot write outside cwd/out. - apply_local: a pack without cadence.json, or with no measured shots and no explicit mean_shot, raises instead of silently planning 1.0s shots and reporting a measured cadence. - legacy apply: a missing overlay aborts before any paid upload; forge() would have rejected it after every take was generated. - requirements-live: pin fal-client>=0.13.0, the first release whose subscribe() accepts client_timeout. Addresses the five P1 findings from the independent review of #3033. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fxHRsydPqEcYngGbqkgt1 --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
253 lines
9.5 KiB
Python
253 lines
9.5 KiB
Python
"""Regression coverage for the original standalone creative pipeline."""
|
|
|
|
import importlib.util
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
if any(
|
|
importlib.util.find_spec(name) is None for name in ("numpy", "cv2", "scenedetect")
|
|
):
|
|
raise unittest.SkipTest(
|
|
"Install taste-application/scripts/requirements.txt for the creative pipeline tests"
|
|
)
|
|
|
|
SCRIPTS = Path(__file__).resolve().parents[1] / "skills/taste-application/scripts"
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
|
|
|
|
def load(name):
|
|
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
pipeline = load("pipeline")
|
|
forge = load("forge")
|
|
apply = load("apply")
|
|
|
|
|
|
class PipelineTests(unittest.TestCase):
|
|
def run_pipeline(self, *extra):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / "look").mkdir()
|
|
(root / "look/grade.json").touch()
|
|
with (
|
|
patch.object(
|
|
sys,
|
|
"argv",
|
|
["pipeline", "--genre", "look", "--root", directory, *extra],
|
|
),
|
|
patch.object(
|
|
pipeline.subprocess,
|
|
"run",
|
|
return_value=SimpleNamespace(returncode=0),
|
|
) as run,
|
|
):
|
|
pipeline.main()
|
|
return run.call_args_list
|
|
|
|
def test_passthrough_is_offline_and_preserves_caller_paths(self):
|
|
calls = self.run_pipeline(
|
|
"--takes", "relative/take.mp4", "--out", "relative/final.mp4", "--fps", "30"
|
|
)
|
|
self.assertEqual(
|
|
[Path(c.args[0][1]).name for c in calls], ["forge.py", "verify.py"]
|
|
)
|
|
for call in calls:
|
|
self.assertTrue(Path(call.args[0][1]).is_absolute())
|
|
self.assertNotIn("cwd", call.kwargs)
|
|
self.assertIn("relative/take.mp4", calls[0].args[0])
|
|
self.assertIn("--fps", calls[0].args[0])
|
|
|
|
def test_passthrough_dry_run_executes_nothing(self):
|
|
self.assertEqual(self.run_pipeline("--takes", "take.mp4", "--dry-run"), [])
|
|
|
|
def test_passthrough_rejects_prop_before_execution(self):
|
|
with self.assertRaises(SystemExit):
|
|
self.run_pipeline("--takes", "take.mp4", "--prop", "chrome")
|
|
|
|
def test_collision_blocks_all_provider_stages(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
out = Path(directory) / "final.mp4"
|
|
out.touch()
|
|
with patch.object(pipeline, "_run") as run:
|
|
with self.assertRaises(FileExistsError):
|
|
self.run_pipeline("--out", str(out), "--prop", "chrome")
|
|
run.assert_not_called()
|
|
|
|
def test_tier_is_forwarded(self):
|
|
calls = self.run_pipeline("--tier", "value", "--no-distill", "--dry-run")
|
|
self.assertIn("--tier", calls[0].args[0])
|
|
self.assertIn("value", calls[0].args[0])
|
|
|
|
def test_invalid_fps_rejected_before_execution(self):
|
|
for fps in ["0", "-1", "nan", "inf"]:
|
|
with self.subTest(fps=fps), self.assertRaises(SystemExit):
|
|
self.run_pipeline("--takes", "take.mp4", "--fps", fps)
|
|
|
|
|
|
class ApplyTests(unittest.TestCase):
|
|
def test_tier_selected_before_provider_calls(self):
|
|
with (
|
|
tempfile.TemporaryDirectory() as directory,
|
|
patch.object(apply.falapi, "use_tier") as tier,
|
|
patch.object(
|
|
apply.pack_mod,
|
|
"load",
|
|
side_effect=RuntimeError("stop before generation"),
|
|
),
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "stop before generation"):
|
|
apply.apply(
|
|
"look",
|
|
"",
|
|
"",
|
|
1,
|
|
out=str(Path(directory) / "fresh.mp4"),
|
|
tier="value",
|
|
)
|
|
tier.assert_called_once_with("reference_to_video", "value")
|
|
|
|
def test_collision_rejected_before_pack_or_provider_access(self):
|
|
with (
|
|
tempfile.TemporaryDirectory() as directory,
|
|
patch.object(apply.pack_mod, "load") as pack,
|
|
):
|
|
out = Path(directory) / "final.mp4"
|
|
for collision in (
|
|
out,
|
|
out.with_suffix(".generation.json"),
|
|
out.parent / "final_takes",
|
|
):
|
|
collision.touch()
|
|
with self.assertRaises(FileExistsError):
|
|
apply.apply("look", "", "", 1, out=str(out))
|
|
pack.assert_not_called()
|
|
collision.unlink()
|
|
|
|
def test_invalid_fps_rejected_before_pack_or_provider_access(self):
|
|
with patch.object(apply.pack_mod, "load") as pack:
|
|
with self.assertRaises(ValueError):
|
|
apply.apply("look", "", "", 1, fps=float("nan"))
|
|
pack.assert_not_called()
|
|
|
|
|
|
class ForgeTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.take = self.root / "take.mp4"
|
|
self.take.touch()
|
|
self.work = self.root / "work"
|
|
self.out = self.root / "final.mp4"
|
|
info = SimpleNamespace(width=640, height=480, fps=24.0, duration=1.0)
|
|
cadence = SimpleNamespace(
|
|
mean_shot=1, cuts_per_min=60, rhythm_variance=0, plan_shots=lambda _: [1]
|
|
)
|
|
stats = SimpleNamespace(contrast=1, black_point=0, white_point=1)
|
|
for target, value in [
|
|
(
|
|
forge.pack_mod,
|
|
("load", SimpleNamespace(grade_path="grade", cadence_path="cadence")),
|
|
),
|
|
(forge.grade_mod, ("load_stats", stats)),
|
|
(forge.cad_mod, ("load", cadence)),
|
|
(forge.frame_mod, ("probe", info)),
|
|
]:
|
|
p = patch.object(target, value[0], return_value=value[1])
|
|
p.start()
|
|
self.addCleanup(p.stop)
|
|
|
|
def render(self, **kwargs):
|
|
def write(_src, dst, *_args, **_kwargs):
|
|
Path(dst).parent.mkdir(parents=True, exist_ok=True)
|
|
Path(dst).touch()
|
|
return Path(dst)
|
|
|
|
def cuts(_src, _shots, dst, **_kwargs):
|
|
return [write(None, Path(dst) / "shot.mp4")]
|
|
|
|
def timeline(*_args, **kwargs):
|
|
return write(None, kwargs["out_path"])
|
|
|
|
with (
|
|
patch.object(forge.asm, "normalize", side_effect=write) as normalize,
|
|
patch.object(forge.grade_mod, "grade_clip_direct", side_effect=write),
|
|
patch.object(forge.asm, "cut_take", side_effect=cuts),
|
|
patch.object(forge.asm, "concat", side_effect=write),
|
|
patch.object(forge.tl_mod, "write_timeline", side_effect=timeline),
|
|
patch.object(forge.asm, "write_manifest"),
|
|
):
|
|
result = forge.forge(
|
|
"look", [str(self.take)], str(self.out), work=str(self.work), **kwargs
|
|
)
|
|
return result, normalize.call_args
|
|
|
|
def test_keeps_previous_editable_shots_and_uses_explicit_fps(self):
|
|
self.work.mkdir()
|
|
old = self.work / "sole-editable.mp4"
|
|
old.write_bytes(b"precious")
|
|
_, call = self.render(fps=30)
|
|
self.assertEqual(old.read_bytes(), b"precious")
|
|
self.assertEqual(call.args[-1], 30)
|
|
self.assertNotEqual(call.args[1].parent, self.work)
|
|
|
|
def test_output_collision_rejected_without_writes(self):
|
|
for suffix in [".mp4", ".fcpxml", ".edl", ".json"]:
|
|
with self.subTest(suffix=suffix):
|
|
existing = self.out.with_suffix(suffix)
|
|
existing.touch()
|
|
with self.assertRaises((ValueError, FileExistsError)):
|
|
self.render()
|
|
self.assertFalse(self.work.exists())
|
|
existing.unlink()
|
|
|
|
def test_invalid_input_does_not_create_work(self):
|
|
self.take.unlink()
|
|
with self.assertRaises((ValueError, FileNotFoundError, SystemExit)):
|
|
self.render()
|
|
self.assertFalse(self.work.exists())
|
|
|
|
def test_nonfinite_fps_does_not_create_work(self):
|
|
for fps in [0, -1, float("nan"), float("inf")]:
|
|
with self.subTest(fps=fps), self.assertRaises(ValueError):
|
|
self.render(fps=fps)
|
|
self.assertFalse(self.work.exists())
|
|
|
|
def test_timeline_export_failure_propagates(self):
|
|
for failing_format in ("fcpxml", "edl"):
|
|
|
|
def export(*args, **kwargs):
|
|
if kwargs["fmt"] == failing_format:
|
|
raise RuntimeError("export broken")
|
|
path = kwargs["out_path"]
|
|
path.touch()
|
|
return path
|
|
|
|
with (
|
|
self.subTest(format=failing_format),
|
|
patch.object(forge.tl_mod, "write_timeline", side_effect=export),
|
|
patch.object(forge.asm, "normalize", return_value=self.take),
|
|
patch.object(forge.grade_mod, "grade_clip_direct"),
|
|
patch.object(forge.asm, "cut_take", return_value=[self.take]),
|
|
patch.object(forge.asm, "concat"),
|
|
patch.object(forge.asm, "write_manifest") as manifest,
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "export broken"):
|
|
forge.forge(
|
|
"look", [str(self.take)], str(self.out), work=str(self.work)
|
|
)
|
|
manifest.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|