mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-27 18:36:28 +08:00
Merge remote-tracking branch 'origin/main' into codex/repair-pr-475
This commit is contained in:
@@ -7,7 +7,11 @@ import pytest
|
||||
|
||||
from backlot import state as state_mod
|
||||
from backlot.state import load_board_state
|
||||
from lib.checkpoint import CheckpointValidationError, write_checkpoint
|
||||
from lib.checkpoint import (
|
||||
CANONICAL_STAGE_ARTIFACTS,
|
||||
CheckpointValidationError,
|
||||
write_checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def _script_artifact() -> dict:
|
||||
@@ -23,6 +27,22 @@ def _manifest_artifact() -> dict:
|
||||
return {"version": "1.0", "assets": [], "total_cost_usd": 0.0}
|
||||
|
||||
|
||||
def _approve_predecessors(tmp_path, project_id, pipeline_type, *stages) -> None:
|
||||
from tests.contracts.test_phase0_contracts import sample_artifact
|
||||
|
||||
for stage in stages:
|
||||
artifact_name = CANONICAL_STAGE_ARTIFACTS[stage]
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
project_id,
|
||||
stage,
|
||||
"completed",
|
||||
{artifact_name: sample_artifact(artifact_name)},
|
||||
pipeline_type=pipeline_type,
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
def _write(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data), encoding="utf-8")
|
||||
@@ -73,6 +93,15 @@ def test_handwritten_completed_checkpoint_surfaces_gate_skip(tmp_path, monkeypat
|
||||
|
||||
|
||||
def test_awaiting_then_approved_archives_history_without_gate_skip(tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path,
|
||||
"film",
|
||||
"cinematic",
|
||||
"research",
|
||||
"proposal",
|
||||
"script",
|
||||
"scene_plan",
|
||||
)
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
"film",
|
||||
|
||||
@@ -10,7 +10,8 @@ import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from lib.checkpoint import init_project, write_checkpoint
|
||||
from lib.checkpoint import CANONICAL_STAGE_ARTIFACTS, init_project, write_checkpoint
|
||||
from lib.pipeline_loader import get_stage_order, load_pipeline
|
||||
from scripts import backlot_screenshot_stage
|
||||
from tests.contracts.test_phase0_contracts import sample_artifact
|
||||
|
||||
@@ -32,6 +33,28 @@ APPROVAL_CASES = [
|
||||
]
|
||||
|
||||
|
||||
def _complete_predecessors(root, project_id: str, pipeline_type: str, stage: str) -> None:
|
||||
order = get_stage_order(load_pipeline(pipeline_type))
|
||||
for predecessor in order[: order.index(stage)]:
|
||||
artifact_name = CANONICAL_STAGE_ARTIFACTS.get(predecessor)
|
||||
if artifact_name:
|
||||
artifact = sample_artifact(artifact_name)
|
||||
if artifact_name == "edit_decisions":
|
||||
artifact["render_runtime"] = "ffmpeg"
|
||||
artifacts = {artifact_name: artifact}
|
||||
else:
|
||||
artifacts = {}
|
||||
write_checkpoint(
|
||||
root,
|
||||
project_id,
|
||||
predecessor,
|
||||
"completed",
|
||||
artifacts,
|
||||
pipeline_type=pipeline_type,
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
def _build_approval_projects() -> None:
|
||||
root = backlot_screenshot_stage.STAGE_DIR
|
||||
for project_id, pipeline_type, stage, artifact_name, _visible_text in APPROVAL_CASES:
|
||||
@@ -55,6 +78,7 @@ def _build_approval_projects() -> None:
|
||||
pipeline_type=pipeline_type,
|
||||
pipeline_dir=root,
|
||||
)
|
||||
_complete_predecessors(root, project_id, pipeline_type, stage)
|
||||
write_checkpoint(
|
||||
root,
|
||||
project_id,
|
||||
@@ -80,6 +104,12 @@ def _build_approval_projects() -> None:
|
||||
pipeline_type="character-animation",
|
||||
pipeline_dir=root,
|
||||
)
|
||||
_complete_predecessors(
|
||||
root,
|
||||
"gate-character-design",
|
||||
"character-animation",
|
||||
"character_design",
|
||||
)
|
||||
write_checkpoint(
|
||||
root,
|
||||
"gate-character-design",
|
||||
|
||||
31
tests/contracts/test_agent_instruction_integrity.py
Normal file
31
tests/contracts/test_agent_instruction_integrity.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(relative_path: str) -> str:
|
||||
return (REPO_ROOT / relative_path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_music_plans_discover_all_music_capabilities() -> None:
|
||||
instruction_files = [
|
||||
"AGENT_GUIDE.md",
|
||||
"skills/pipelines/cinematic/idea-director.md",
|
||||
"skills/pipelines/cinematic/proposal-director.md",
|
||||
"skills/pipelines/documentary-montage/idea-director.md",
|
||||
"skills/pipelines/explainer/proposal-director.md",
|
||||
]
|
||||
|
||||
for relative_path in instruction_files:
|
||||
text = _read(relative_path)
|
||||
for capability in ("music_library", "music_search", "music_generation"):
|
||||
assert f'get_by_capability("{capability}")' in text, (
|
||||
f"{relative_path} omits the {capability!r} music source"
|
||||
)
|
||||
|
||||
|
||||
def test_explainer_directors_do_not_reference_fictitious_submit_functions() -> None:
|
||||
for stage in ("idea", "script", "scene"):
|
||||
text = _read(f"skills/pipelines/explainer/{stage}-director.md")
|
||||
assert "handle_explainer_" not in text
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import pytest
|
||||
|
||||
from lib.checkpoint import (
|
||||
CANONICAL_STAGE_ARTIFACTS,
|
||||
CheckpointValidationError,
|
||||
HISTORY_DIRNAME,
|
||||
PROJECT_MARKER_FILENAME,
|
||||
@@ -27,6 +28,22 @@ def _minimal_script() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _approve_predecessors(tmp_path, project_id, pipeline_type, *stages) -> None:
|
||||
from tests.contracts.test_phase0_contracts import sample_artifact
|
||||
|
||||
for stage in stages:
|
||||
artifact_name = CANONICAL_STAGE_ARTIFACTS[stage]
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
project_id,
|
||||
stage,
|
||||
"completed",
|
||||
artifacts={artifact_name: sample_artifact(artifact_name)},
|
||||
pipeline_type=pipeline_type,
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
class TestGateEnforcement:
|
||||
"""GI-4: gated stages cannot be completed without approval evidence."""
|
||||
|
||||
@@ -39,6 +56,9 @@ class TestGateEnforcement:
|
||||
)
|
||||
|
||||
def test_awaiting_human_is_the_correct_gate_state(self, tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path, "proj", "animated-explainer", "research", "proposal"
|
||||
)
|
||||
path = write_checkpoint(
|
||||
tmp_path, "proj", "script", "awaiting_human",
|
||||
artifacts={"script": _minimal_script()},
|
||||
@@ -51,6 +71,9 @@ class TestGateEnforcement:
|
||||
assert cp["human_approval_required"] is True
|
||||
|
||||
def test_completed_with_approval_passes(self, tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path, "proj", "animated-explainer", "research", "proposal"
|
||||
)
|
||||
path = write_checkpoint(
|
||||
tmp_path, "proj", "script", "completed",
|
||||
artifacts={"script": _minimal_script()},
|
||||
@@ -84,6 +107,9 @@ class TestCheckpointHistory:
|
||||
"""Superseded checkpoints are archived, not destroyed."""
|
||||
|
||||
def test_overwrite_archives_previous(self, tmp_path):
|
||||
_approve_predecessors(
|
||||
tmp_path, "proj", "animated-explainer", "research", "proposal"
|
||||
)
|
||||
write_checkpoint(
|
||||
tmp_path, "proj", "script", "awaiting_human",
|
||||
artifacts={"script": _minimal_script()},
|
||||
|
||||
19
tests/contracts/test_env_example.py
Normal file
19
tests/contracts/test_env_example.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_env_example_does_not_turn_comments_into_credentials() -> None:
|
||||
"""A fresh copy of .env.example must leave every documented key unset."""
|
||||
|
||||
values = dotenv_values(REPO_ROOT / ".env.example")
|
||||
false_credentials = {
|
||||
key: value
|
||||
for key, value in values.items()
|
||||
if isinstance(value, str) and value.lstrip().startswith("#")
|
||||
}
|
||||
|
||||
assert false_credentials == {}
|
||||
403
tests/contracts/test_jimeng_video.py
Normal file
403
tests/contracts/test_jimeng_video.py
Normal file
@@ -0,0 +1,403 @@
|
||||
"""Contract tests for the Volcengine Jimeng video provider tool.
|
||||
|
||||
These tests verify that the tool satisfies the BaseTool contract without
|
||||
requiring real Volcengine AK/SK credentials or making any API calls.
|
||||
|
||||
Run: pytest tests/contracts/test_jimeng_video.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.base_tool import (
|
||||
BaseTool,
|
||||
Determinism,
|
||||
ExecutionMode,
|
||||
ToolRuntime,
|
||||
ToolStability,
|
||||
ToolStatus,
|
||||
ToolTier,
|
||||
)
|
||||
from tools.video.jimeng_video import JimengVideo
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Contract compliance
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestContract:
|
||||
|
||||
def test_inherits_base_tool(self):
|
||||
assert issubclass(JimengVideo, BaseTool)
|
||||
|
||||
def test_has_required_identity(self):
|
||||
tool = JimengVideo()
|
||||
assert tool.name == "jimeng_video"
|
||||
assert tool.version
|
||||
assert tool.provider == "volcengine"
|
||||
assert tool.capability == "video_generation"
|
||||
assert tool.tier == ToolTier.GENERATE
|
||||
assert tool.stability == ToolStability.EXPERIMENTAL
|
||||
assert tool.runtime == ToolRuntime.API
|
||||
|
||||
def test_execution_mode_is_async(self):
|
||||
assert JimengVideo().execution_mode == ExecutionMode.ASYNC
|
||||
|
||||
def test_has_input_schema(self):
|
||||
schema = JimengVideo().input_schema
|
||||
assert schema.get("type") == "object"
|
||||
props = schema.get("properties", {})
|
||||
required = schema.get("required", [])
|
||||
assert required == ["prompt"]
|
||||
for field in required:
|
||||
assert field in props
|
||||
|
||||
def test_has_capabilities(self):
|
||||
tool = JimengVideo()
|
||||
assert "text_to_video" in tool.capabilities
|
||||
assert "image_to_video" in tool.capabilities
|
||||
|
||||
def test_has_agent_skills(self):
|
||||
assert "ai-video-gen" in JimengVideo().agent_skills
|
||||
|
||||
def test_has_fallbacks(self):
|
||||
tool = JimengVideo()
|
||||
assert "minimax_video" in tool.fallback_tools
|
||||
assert "kling_video" in tool.fallback_tools
|
||||
|
||||
def test_has_install_instructions(self):
|
||||
tool = JimengVideo()
|
||||
assert "VOLC_ACCESSKEY" in tool.install_instructions
|
||||
assert "VOLC_SECRETKEY" in tool.install_instructions
|
||||
|
||||
def test_get_info_returns_dict(self):
|
||||
info = JimengVideo().get_info()
|
||||
assert isinstance(info, dict)
|
||||
assert info["name"] == "jimeng_video"
|
||||
assert info["provider"] == "volcengine"
|
||||
assert info["runtime"] == "api"
|
||||
|
||||
def test_status_unavailable_without_keys(self, monkeypatch):
|
||||
monkeypatch.delenv("VOLC_ACCESSKEY", raising=False)
|
||||
monkeypatch.delenv("VOLC_SECRETKEY", raising=False)
|
||||
assert JimengVideo().get_status() == ToolStatus.UNAVAILABLE
|
||||
|
||||
def test_status_available_with_keys(self, monkeypatch):
|
||||
monkeypatch.setenv("VOLC_ACCESSKEY", "fake-ak")
|
||||
monkeypatch.setenv("VOLC_SECRETKEY", "fake-sk")
|
||||
assert JimengVideo().get_status() == ToolStatus.AVAILABLE
|
||||
|
||||
def test_status_unavailable_with_only_ak(self, monkeypatch):
|
||||
monkeypatch.setenv("VOLC_ACCESSKEY", "fake-ak")
|
||||
monkeypatch.delenv("VOLC_SECRETKEY", raising=False)
|
||||
assert JimengVideo().get_status() == ToolStatus.UNAVAILABLE
|
||||
|
||||
def test_has_resource_profile(self):
|
||||
rp = JimengVideo().resource_profile
|
||||
assert rp.network_required is True
|
||||
assert rp.vram_mb == 0
|
||||
|
||||
def test_has_retry_policy(self):
|
||||
assert JimengVideo().retry_policy.max_retries >= 0
|
||||
|
||||
def test_has_side_effects(self):
|
||||
side = JimengVideo().side_effects
|
||||
assert len(side) > 0
|
||||
assert any("API" in s for s in side)
|
||||
|
||||
def test_has_user_visible_verification(self):
|
||||
assert len(JimengVideo().user_visible_verification) > 0
|
||||
|
||||
def test_lazy_imports_requests(self, monkeypatch):
|
||||
import importlib
|
||||
import sys
|
||||
mod_name = "tools.video.jimeng_video"
|
||||
if "requests" in sys.modules:
|
||||
monkeypatch.delitem(sys.modules, "requests")
|
||||
importlib.reload(sys.modules[mod_name])
|
||||
|
||||
def test_estimate_cost_returns_float(self):
|
||||
cost = JimengVideo().estimate_cost({"prompt": "x", "frames": 121})
|
||||
assert isinstance(cost, float)
|
||||
assert cost > 0.0
|
||||
|
||||
def test_dry_run_returns_dict(self):
|
||||
result = JimengVideo().dry_run({"prompt": "test"})
|
||||
assert isinstance(result, dict)
|
||||
assert result["tool"] == "jimeng_video"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Idempotency keys
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestIdempotencyKeys:
|
||||
|
||||
def test_includes_all_output_affecting_fields(self):
|
||||
fields = JimengVideo().idempotency_key_fields
|
||||
for field in ("prompt", "operation", "image_url", "frames", "aspect_ratio", "seed"):
|
||||
assert field in fields, f"missing idempotency field: {field}"
|
||||
|
||||
def test_excludes_execution_only_fields(self):
|
||||
fields = JimengVideo().idempotency_key_fields
|
||||
for field in ("output_path", "poll_interval_seconds", "timeout_seconds"):
|
||||
assert field not in fields
|
||||
|
||||
def test_differs_on_frames(self):
|
||||
tool = JimengVideo()
|
||||
base = {"prompt": "x"}
|
||||
assert tool.idempotency_key(base) != tool.idempotency_key({**base, "frames": 241})
|
||||
|
||||
def test_differs_on_aspect_ratio(self):
|
||||
tool = JimengVideo()
|
||||
base = {"prompt": "x"}
|
||||
assert tool.idempotency_key({**base, "aspect_ratio": "16:9"}) != tool.idempotency_key(
|
||||
{**base, "aspect_ratio": "9:16"}
|
||||
)
|
||||
|
||||
def test_differs_on_seed(self):
|
||||
tool = JimengVideo()
|
||||
base = {"prompt": "x"}
|
||||
assert tool.idempotency_key({**base, "seed": -1}) != tool.idempotency_key(
|
||||
{**base, "seed": 42}
|
||||
)
|
||||
|
||||
def test_differs_on_image_url(self):
|
||||
tool = JimengVideo()
|
||||
base = {"prompt": "x", "operation": "image_to_video"}
|
||||
assert tool.idempotency_key(base) != tool.idempotency_key(
|
||||
{**base, "image_url": "https://example.com/img.png"}
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool-specific behavior
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestToolSpecific:
|
||||
|
||||
def test_default_frames_is_121(self):
|
||||
tool = JimengVideo()
|
||||
assert tool.input_schema["properties"]["frames"]["default"] == 121
|
||||
|
||||
def test_default_aspect_ratio_is_16_9(self):
|
||||
tool = JimengVideo()
|
||||
assert tool.input_schema["properties"]["aspect_ratio"]["default"] == "16:9"
|
||||
|
||||
def test_default_seed_is_negative_one(self):
|
||||
tool = JimengVideo()
|
||||
assert tool.input_schema["properties"]["seed"]["default"] == -1
|
||||
|
||||
def test_cost_scales_with_frames(self):
|
||||
tool = JimengVideo()
|
||||
cost_5s = tool.estimate_cost({"prompt": "x", "frames": 121})
|
||||
cost_10s = tool.estimate_cost({"prompt": "x", "frames": 241})
|
||||
assert cost_10s > cost_5s
|
||||
|
||||
def test_build_payload_t2v(self):
|
||||
tool = JimengVideo()
|
||||
payload = tool._build_payload({"prompt": "a cat"})
|
||||
assert payload["req_key"] == "jimeng_ti2v_v30_pro"
|
||||
assert payload["prompt"] == "a cat"
|
||||
assert payload["frames"] == 121
|
||||
assert payload["aspect_ratio"] == "16:9"
|
||||
assert payload["seed"] == -1
|
||||
assert "image_urls" not in payload
|
||||
|
||||
def test_build_payload_i2v_includes_image(self):
|
||||
tool = JimengVideo()
|
||||
payload = tool._build_payload({
|
||||
"prompt": "motion",
|
||||
"operation": "image_to_video",
|
||||
"image_url": "https://example.com/img.png",
|
||||
})
|
||||
assert payload["image_urls"] == ["https://example.com/img.png"]
|
||||
|
||||
def test_build_payload_t2v_omits_image(self):
|
||||
tool = JimengVideo()
|
||||
payload = tool._build_payload({"prompt": "a cat", "operation": "text_to_video"})
|
||||
assert "image_urls" not in payload
|
||||
|
||||
def test_i2v_without_image_fails(self, monkeypatch):
|
||||
monkeypatch.setenv("VOLC_ACCESSKEY", "fake-ak")
|
||||
monkeypatch.setenv("VOLC_SECRETKEY", "fake-sk")
|
||||
result = JimengVideo().execute({"prompt": "test", "operation": "image_to_video"})
|
||||
assert result.success is False
|
||||
assert "image_url" in result.error
|
||||
|
||||
def test_no_keys_returns_error(self, monkeypatch):
|
||||
monkeypatch.delenv("VOLC_ACCESSKEY", raising=False)
|
||||
monkeypatch.delenv("VOLC_SECRETKEY", raising=False)
|
||||
result = JimengVideo().execute({"prompt": "test"})
|
||||
assert result.success is False
|
||||
assert "VOLC_ACCESSKEY" in result.error
|
||||
assert "VOLC_SECRETKEY" in result.error
|
||||
|
||||
def test_safe_error_redacts_keys(self, monkeypatch):
|
||||
monkeypatch.setenv("VOLC_ACCESSKEY", "my-ak-secret")
|
||||
monkeypatch.setenv("VOLC_SECRETKEY", "my-sk-secret")
|
||||
redacted = JimengVideo._safe_error(
|
||||
Exception("failed with ak=my-ak-secret sk=my-sk-secret")
|
||||
)
|
||||
assert "my-ak-secret" not in redacted
|
||||
assert "my-sk-secret" not in redacted
|
||||
assert "[redacted]" in redacted
|
||||
|
||||
def test_safe_error_no_empty_string_bug(self, monkeypatch):
|
||||
"""Regression: when no keys are set, _safe_error must not mangle."""
|
||||
monkeypatch.delenv("VOLC_ACCESSKEY", raising=False)
|
||||
monkeypatch.delenv("VOLC_SECRETKEY", raising=False)
|
||||
msg = JimengVideo._safe_error(Exception("abc"))
|
||||
assert msg == "abc"
|
||||
|
||||
def test_sign_returns_authorization_header(self):
|
||||
headers = JimengVideo._sign(
|
||||
"POST", "/",
|
||||
{"Action": "CVSync2AsyncSubmitTask", "Version": "2022-08-31"},
|
||||
{}, b'{"prompt":"test"}',
|
||||
"fake-ak", "fake-sk",
|
||||
)
|
||||
assert "Authorization" in headers
|
||||
assert "HMAC-SHA256" in headers["Authorization"]
|
||||
assert "fake-ak" in headers["Authorization"]
|
||||
assert "Host" in headers
|
||||
assert "X-Date" in headers
|
||||
assert "X-Content-Sha256" in headers
|
||||
|
||||
def test_sign_includes_content_type(self):
|
||||
headers = JimengVideo._sign("POST", "/", {}, {}, b"{}", "ak", "sk")
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
|
||||
def test_json_or_raise_returns_dict(self):
|
||||
class FakeResp:
|
||||
status_code = 200
|
||||
def json(self):
|
||||
return {"code": 10000, "data": {"task_id": "123"}}
|
||||
assert JimengVideo._json_or_raise(FakeResp()) == {"code": 10000, "data": {"task_id": "123"}}
|
||||
|
||||
def test_json_or_raise_raises_on_non_json(self):
|
||||
class FakeResp:
|
||||
status_code = 500
|
||||
def json(self):
|
||||
raise ValueError("not JSON")
|
||||
with pytest.raises(RuntimeError, match="Non-JSON"):
|
||||
JimengVideo._json_or_raise(FakeResp())
|
||||
|
||||
def test_check_code_passes_on_success(self):
|
||||
JimengVideo._check_code(200, {"code": 10000, "message": "Success"})
|
||||
|
||||
def test_check_code_raises_on_api_error(self):
|
||||
with pytest.raises(RuntimeError, match="code=10008"):
|
||||
JimengVideo._check_code(200, {"code": 10008, "message": "Insufficient balance"})
|
||||
|
||||
def test_check_code_raises_on_http_error(self):
|
||||
with pytest.raises(RuntimeError, match="HTTP 401"):
|
||||
JimengVideo._check_code(401, {"code": 10004, "message": "Auth failed"})
|
||||
|
||||
def test_check_code_defaults_to_success_when_code_missing(self):
|
||||
"""If code field is absent on HTTP 2xx, default to 10000 (success)."""
|
||||
JimengVideo._check_code(200, {"data": {"task_id": "123"}})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Registry discovery
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestRegistryDiscovery:
|
||||
|
||||
def test_discoverable(self):
|
||||
from tools.tool_registry import ToolRegistry
|
||||
registry = ToolRegistry()
|
||||
registry.discover()
|
||||
names = {t.name for t in registry._tools.values()}
|
||||
assert "jimeng_video" in names
|
||||
|
||||
def test_distinct_from_other_minimax_tools(self):
|
||||
from tools.tool_registry import ToolRegistry
|
||||
registry = ToolRegistry()
|
||||
registry.discover()
|
||||
jimeng = [t for t in registry._tools.values() if t.name == "jimeng_video"]
|
||||
assert len(jimeng) == 1
|
||||
assert jimeng[0].provider == "volcengine"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schema validation — reject invalid inputs before paid API call
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestSchemaValidation:
|
||||
|
||||
def test_frames_accepts_121(self):
|
||||
schema = JimengVideo().input_schema
|
||||
valid = schema["properties"]["frames"]
|
||||
assert valid["enum"] == [121, 241]
|
||||
|
||||
def test_frames_rejects_non_enum(self):
|
||||
import jsonschema
|
||||
schema = JimengVideo().input_schema
|
||||
for invalid in [1, 100, 200, 500, 0, -1]:
|
||||
instance = {"prompt": "test", "frames": invalid}
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(instance, schema)
|
||||
|
||||
def test_prompt_max_length_800(self):
|
||||
schema = JimengVideo().input_schema
|
||||
assert schema["properties"]["prompt"]["maxLength"] == 800
|
||||
|
||||
def test_prompt_rejects_over_800_chars(self):
|
||||
import jsonschema
|
||||
schema = JimengVideo().input_schema
|
||||
instance = {"prompt": "x" * 801}
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(instance, schema)
|
||||
|
||||
def test_prompt_accepts_800_chars(self):
|
||||
import jsonschema
|
||||
schema = JimengVideo().input_schema
|
||||
instance = {"prompt": "x" * 800}
|
||||
jsonschema.validate(instance, schema)
|
||||
|
||||
def test_seed_minimum_is_negative_one(self):
|
||||
schema = JimengVideo().input_schema
|
||||
assert schema["properties"]["seed"]["minimum"] == -1
|
||||
|
||||
def test_seed_rejects_below_negative_one(self):
|
||||
import jsonschema
|
||||
schema = JimengVideo().input_schema
|
||||
for invalid in [-2, -10, -100]:
|
||||
instance = {"prompt": "test", "seed": invalid}
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
jsonschema.validate(instance, schema)
|
||||
|
||||
def test_seed_accepts_negative_one(self):
|
||||
import jsonschema
|
||||
schema = JimengVideo().input_schema
|
||||
jsonschema.validate({"prompt": "test", "seed": -1}, schema)
|
||||
|
||||
def test_seed_accepts_zero_and_positive(self):
|
||||
import jsonschema
|
||||
schema = JimengVideo().input_schema
|
||||
for valid in [0, 1, 42, 999999]:
|
||||
jsonschema.validate({"prompt": "test", "seed": valid}, schema)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Selector duration → frames mapping
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestSelectorDurationMapping:
|
||||
|
||||
def test_duration_5_maps_to_121_frames(self):
|
||||
payload = JimengVideo._build_payload({"prompt": "x", "duration": 5})
|
||||
assert payload["frames"] == 121
|
||||
|
||||
def test_duration_10_maps_to_241_frames(self):
|
||||
payload = JimengVideo._build_payload({"prompt": "x", "duration": 10})
|
||||
assert payload["frames"] == 241
|
||||
|
||||
def test_duration_defaults_to_5_when_absent(self):
|
||||
payload = JimengVideo._build_payload({"prompt": "x"})
|
||||
assert payload["frames"] == 121
|
||||
|
||||
def test_frames_takes_priority_over_duration(self):
|
||||
payload = JimengVideo._build_payload({"prompt": "x", "frames": 241, "duration": 5})
|
||||
assert payload["frames"] == 241
|
||||
@@ -174,10 +174,14 @@ class TestPhase2ErrorHandling:
|
||||
# Either succeeds (provider available) or fails gracefully
|
||||
assert isinstance(r, ToolResult)
|
||||
|
||||
def test_diagram_gen_empty_boxes(self):
|
||||
def test_diagram_gen_empty_boxes(self, tmp_path):
|
||||
tool = DiagramGen()
|
||||
if tool.get_status() == ToolStatus.AVAILABLE:
|
||||
r = tool.execute({"diagram_type": "boxes", "boxes": []})
|
||||
r = tool.execute({
|
||||
"diagram_type": "boxes",
|
||||
"boxes": [],
|
||||
"output_path": str(tmp_path / "empty-boxes.png"),
|
||||
})
|
||||
assert isinstance(r, ToolResult)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import builtins
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
from types import SimpleNamespace
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -571,16 +572,26 @@ class TestVeoVideo:
|
||||
called_kwargs = mock_client.models.generate_videos.call_args[1]
|
||||
assert called_kwargs["image"] is not None
|
||||
|
||||
def test_vertex_ai_mode_rejection(self):
|
||||
def test_vertex_ai_mode_requires_inline_video_bytes(self):
|
||||
tool = VeoVideo()
|
||||
mock_client = MagicMock()
|
||||
mock_client.vertexai = True
|
||||
if hasattr(mock_client, "_api_client"):
|
||||
delattr(mock_client, "_api_client")
|
||||
mock_client.models.generate_videos.return_value = SimpleNamespace(
|
||||
done=True,
|
||||
error=None,
|
||||
response=SimpleNamespace(
|
||||
generated_videos=[
|
||||
SimpleNamespace(video=SimpleNamespace(video_bytes=None))
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {"GEMINI_API_KEY": "test_key"}),
|
||||
patch("google.genai.Client", return_value=mock_client),
|
||||
patch(
|
||||
"tools.google_credentials.get_genai_client",
|
||||
return_value=mock_client,
|
||||
),
|
||||
):
|
||||
inputs = {
|
||||
"prompt": "cinematic shot",
|
||||
@@ -589,7 +600,7 @@ class TestVeoVideo:
|
||||
res = tool.execute(inputs)
|
||||
assert res.success is False
|
||||
assert res.error is not None
|
||||
assert "only supported using the Gemini Developer API" in res.error
|
||||
assert "without inline bytes" in res.error
|
||||
|
||||
def test_missing_local_image_paths(self):
|
||||
tool = VeoVideo()
|
||||
|
||||
9
tests/contracts/test_pipeline_manifest_categories.py
Normal file
9
tests/contracts/test_pipeline_manifest_categories.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Contracts for the category vocabulary used by shipped pipelines."""
|
||||
|
||||
from lib.pipeline_loader import load_pipeline
|
||||
|
||||
|
||||
def test_documentary_pipeline_uses_a_schema_supported_category() -> None:
|
||||
manifest = load_pipeline("documentary-montage")
|
||||
|
||||
assert manifest["category"] == "documentary"
|
||||
28
tests/contracts/test_remotion_video_transition_contract.py
Normal file
28
tests/contracts/test_remotion_video_transition_contract.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_video_scene_honors_hard_cut_tokens_and_backing_color() -> None:
|
||||
source = (REPO_ROOT / "remotion-composer/src/Explainer.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert '["cut", "none"].includes((transitionIn || "").toLowerCase())' in source
|
||||
assert '["cut", "none"].includes((transitionOut || "").toLowerCase())' in source
|
||||
assert "transitionIn={cut.transition_in}" in source
|
||||
assert "transitionOut={cut.transition_out}" in source
|
||||
assert "sceneDurationSeconds={cut.out_seconds - cut.in_seconds}" in source
|
||||
assert "Math.round(sceneDurationSeconds * fps)" in source
|
||||
assert "durationInFrames - transitionFrames" in source
|
||||
assert "backgroundColor={cut.backgroundColor}" in source
|
||||
|
||||
|
||||
def test_cinematic_fades_are_bounded_by_each_scene_duration() -> None:
|
||||
source = (REPO_ROOT / "remotion-composer/src/CinematicRenderer.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "Math.round(scene.durationSeconds * fps)" in source
|
||||
assert "durationInFrames - fadeOutFrames" in source
|
||||
30
tests/contracts/test_utf8_file_io.py
Normal file
30
tests/contracts/test_utf8_file_io.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
RUNTIME_FILES = [
|
||||
"lib/checkpoint.py",
|
||||
"lib/pipeline_loader.py",
|
||||
"schemas/artifacts/__init__.py",
|
||||
"styles/playbook_loader.py",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("relative_path", RUNTIME_FILES)
|
||||
def test_pipeline_contract_files_use_explicit_utf8(relative_path: str) -> None:
|
||||
path = REPO_ROOT / relative_path
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
|
||||
bare_opens = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if not isinstance(node.func, ast.Name) or node.func.id != "open":
|
||||
continue
|
||||
if not any(keyword.arg == "encoding" for keyword in node.keywords):
|
||||
bare_opens.append(node.lineno)
|
||||
|
||||
assert bare_opens == [], f"bare open() calls at lines {bare_opens}"
|
||||
152
tests/lib/test_checkpoint_prerequisites.py
Normal file
152
tests/lib/test_checkpoint_prerequisites.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from tests.contracts.test_phase0_contracts import sample_artifact
|
||||
|
||||
from lib.checkpoint import (
|
||||
CheckpointValidationError,
|
||||
init_project,
|
||||
write_checkpoint,
|
||||
)
|
||||
|
||||
|
||||
def _script_artifact() -> dict:
|
||||
return {
|
||||
"version": "1.0",
|
||||
"title": "Smoke",
|
||||
"total_duration_seconds": 1,
|
||||
"sections": [
|
||||
{
|
||||
"id": "s1",
|
||||
"text": "One second.",
|
||||
"start_seconds": 0,
|
||||
"end_seconds": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_later_stage_cannot_skip_a_missing_predecessor(tmp_path) -> None:
|
||||
init_project(
|
||||
"run",
|
||||
title="Run",
|
||||
pipeline_type="framework-smoke",
|
||||
pipeline_dir=tmp_path,
|
||||
)
|
||||
|
||||
with pytest.raises(CheckpointValidationError, match="PREREQUISITE VIOLATION"):
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
"run",
|
||||
"script",
|
||||
"completed",
|
||||
{"script": _script_artifact()},
|
||||
pipeline_type="framework-smoke",
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
def test_later_stage_rejects_unapproved_gated_predecessor(tmp_path) -> None:
|
||||
project_dir = init_project(
|
||||
"run",
|
||||
title="Run",
|
||||
pipeline_type="framework-smoke",
|
||||
pipeline_dir=tmp_path,
|
||||
)
|
||||
predecessor_path = write_checkpoint(
|
||||
tmp_path,
|
||||
"run",
|
||||
"research",
|
||||
"awaiting_human",
|
||||
{"research_brief": sample_artifact("research_brief")},
|
||||
pipeline_type="framework-smoke",
|
||||
)
|
||||
predecessor = json.loads(predecessor_path.read_text(encoding="utf-8"))
|
||||
predecessor["status"] = "completed"
|
||||
predecessor["human_approved"] = False
|
||||
predecessor_path.write_text(json.dumps(predecessor), encoding="utf-8")
|
||||
|
||||
with pytest.raises(CheckpointValidationError, match="completed without required approval"):
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
"run",
|
||||
"script",
|
||||
"completed",
|
||||
{"script": _script_artifact()},
|
||||
pipeline_type="framework-smoke",
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
def test_malformed_predecessor_cannot_forge_completion(tmp_path) -> None:
|
||||
project_dir = init_project(
|
||||
"run",
|
||||
title="Run",
|
||||
pipeline_type="framework-smoke",
|
||||
pipeline_dir=tmp_path,
|
||||
)
|
||||
(project_dir / "checkpoint_research.json").write_text(
|
||||
json.dumps({"status": "completed", "human_approved": True}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(CheckpointValidationError, match="incomplete or missing"):
|
||||
write_checkpoint(
|
||||
tmp_path,
|
||||
"run",
|
||||
"script",
|
||||
"completed",
|
||||
{"script": _script_artifact()},
|
||||
pipeline_type="framework-smoke",
|
||||
human_approved=True,
|
||||
)
|
||||
|
||||
|
||||
def test_in_progress_heartbeat_is_not_blocked_by_prerequisites(tmp_path) -> None:
|
||||
init_project(
|
||||
"run",
|
||||
title="Run",
|
||||
pipeline_type="framework-smoke",
|
||||
pipeline_dir=tmp_path,
|
||||
)
|
||||
|
||||
path = write_checkpoint(
|
||||
tmp_path,
|
||||
"run",
|
||||
"script",
|
||||
"in_progress",
|
||||
{},
|
||||
pipeline_type="framework-smoke",
|
||||
)
|
||||
|
||||
assert path.exists()
|
||||
|
||||
|
||||
def test_unknown_style_playbook_fails_before_project_creation(tmp_path) -> None:
|
||||
with pytest.raises(CheckpointValidationError, match="style_playbook"):
|
||||
init_project(
|
||||
"run",
|
||||
title="Run",
|
||||
pipeline_type="framework-smoke",
|
||||
pipeline_dir=tmp_path,
|
||||
style_playbook="does-not-exist",
|
||||
)
|
||||
|
||||
assert not (tmp_path / "run").exists()
|
||||
|
||||
|
||||
def test_marker_derived_unknown_playbook_blocks_later_writes(tmp_path) -> None:
|
||||
project_dir = tmp_path / "run"
|
||||
project_dir.mkdir()
|
||||
(project_dir / "project.json").write_text(
|
||||
json.dumps({
|
||||
"version": "1.0",
|
||||
"project_id": "run",
|
||||
"pipeline_type": "framework-smoke",
|
||||
"style_playbook": "does-not-exist",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(CheckpointValidationError, match="style_playbook"):
|
||||
write_checkpoint(tmp_path, "run", "research", "in_progress", {})
|
||||
26
tests/lib/test_clip_embedder_compat.py
Normal file
26
tests/lib/test_clip_embedder_compat.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from lib.clip_embedder import _as_feature_tensor
|
||||
|
||||
|
||||
class _Tensor:
|
||||
pass
|
||||
|
||||
|
||||
class _ModelOutput:
|
||||
def __init__(self, pooler_output):
|
||||
self.pooler_output = pooler_output
|
||||
self.last_hidden_state = object()
|
||||
|
||||
|
||||
def test_transformers_4_tensor_passes_through() -> None:
|
||||
tensor = _Tensor()
|
||||
assert _as_feature_tensor(tensor) is tensor
|
||||
|
||||
|
||||
def test_transformers_5_output_unwraps_projected_pooler_output() -> None:
|
||||
tensor = _Tensor()
|
||||
assert _as_feature_tensor(_ModelOutput(tensor)) is tensor
|
||||
|
||||
|
||||
def test_missing_pooler_output_does_not_replace_features_with_none() -> None:
|
||||
output = _ModelOutput(None)
|
||||
assert _as_feature_tensor(output) is output
|
||||
158
tests/tools/test_3d_asset_generation.py
Normal file
158
tests/tools/test_3d_asset_generation.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Contracts for cloud mesh generation and Blender world rendering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import jsonschema
|
||||
|
||||
from tools.base_tool import ToolStatus
|
||||
from tools.graphics import atlas_3d, fal_3d
|
||||
from tools.graphics.atlas_3d import Atlas3D
|
||||
from tools.graphics import blender_world
|
||||
from tools.graphics.blender_world import BlenderWorld, first_missing_frame
|
||||
from tools.graphics.fal_3d import Fal3D
|
||||
from tools.tool_registry import ToolRegistry
|
||||
|
||||
|
||||
class _Response:
|
||||
def __init__(self, payload=None, content=b""):
|
||||
self._payload = payload
|
||||
self.content = content
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
|
||||
def test_registry_discovers_separate_3d_capabilities():
|
||||
registry = ToolRegistry()
|
||||
registry.discover("tools")
|
||||
assert {tool.name for tool in registry.get_by_capability("3d_asset_generation")} >= {
|
||||
"atlas_3d", "fal_3d"
|
||||
}
|
||||
assert {tool.name for tool in registry.get_by_capability("3d_world_rendering")} >= {
|
||||
"blender_world"
|
||||
}
|
||||
|
||||
|
||||
def test_atlas_cost_matrix_and_missing_key(monkeypatch, tmp_path):
|
||||
for key in ("ATLASCLOUD_API_KEY", "ATLAS_CLOUD_API_KEY", "ATLAS_API_KEY"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
tool = Atlas3D()
|
||||
assert tool.get_status() == ToolStatus.UNAVAILABLE
|
||||
assert tool.estimate_cost({"texture": False}) == 0.22
|
||||
assert tool.estimate_cost({"texture": True, "texture_quality": "standard"}) == 0.33
|
||||
assert tool.estimate_cost({"texture": True, "texture_quality": "detailed", "geometry_quality": "detailed"}) == 0.66
|
||||
result = tool.execute({"prompt": "a cottage", "output_path": str(tmp_path / "cottage.glb")})
|
||||
assert not result.success
|
||||
assert "key" in (result.error or "").lower()
|
||||
|
||||
|
||||
def test_fal_cost_matrix_and_input_validation(monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("FAL_KEY", raising=False)
|
||||
monkeypatch.delenv("FAL_AI_API_KEY", raising=False)
|
||||
tool = Fal3D()
|
||||
assert tool.estimate_cost({"operation": "reconstruct_objects"}) == 0.02
|
||||
assert tool.estimate_cost({"operation": "image_to_3d", "enable_pbr": False}) == 0.225
|
||||
assert tool.estimate_cost({"operation": "image_to_3d", "enable_pbr": True}) == 0.375
|
||||
result = tool.execute({"operation": "text_to_3d", "output_path": str(tmp_path / "asset.glb")})
|
||||
assert not result.success
|
||||
|
||||
|
||||
def test_blender_doctor_reports_detected_runtime(monkeypatch, tmp_path):
|
||||
executable = tmp_path / "blender"
|
||||
executable.write_bytes(b"")
|
||||
monkeypatch.setattr(blender_world, "find_blender", lambda: executable)
|
||||
monkeypatch.setattr(blender_world.subprocess, "run", lambda *args, **kwargs: subprocess.CompletedProcess(
|
||||
args=args[0], returncode=0, stdout="OPENMONTAGE_BLENDER=4.5.10 LTS\n", stderr="",
|
||||
))
|
||||
result = BlenderWorld().execute({"operation": "doctor"})
|
||||
assert result.success, result.error
|
||||
assert result.data["version_line"].startswith("OPENMONTAGE_BLENDER=4.5.10")
|
||||
|
||||
|
||||
def test_blender_doctor_explains_missing_optional_runtime(monkeypatch):
|
||||
monkeypatch.setattr(blender_world, "find_blender", lambda: None)
|
||||
result = BlenderWorld().execute({"operation": "doctor"})
|
||||
assert not result.success
|
||||
assert "Blender not found" in (result.error or "")
|
||||
|
||||
|
||||
def test_blender_resume_finds_first_missing_contiguous_frame(tmp_path):
|
||||
prefix = tmp_path / "frame-"
|
||||
for frame in (1, 2, 4):
|
||||
(tmp_path / f"frame-{frame:04d}.png").write_bytes(b"png")
|
||||
assert first_missing_frame(prefix, 1, 5) == 3
|
||||
(tmp_path / "frame-0003.png").write_bytes(b"png")
|
||||
assert first_missing_frame(prefix, 1, 4) is None
|
||||
|
||||
|
||||
def test_asset_manifest_accepts_generated_mesh_type():
|
||||
schema = json.loads(Path("schemas/artifacts/asset_manifest.schema.json").read_text(encoding="utf-8"))
|
||||
jsonschema.validate({
|
||||
"version": "1.0",
|
||||
"assets": [{
|
||||
"id": "hero-cottage",
|
||||
"type": "3d_asset",
|
||||
"path": "assets/3d/hero-cottage.glb",
|
||||
"source_tool": "atlas_3d",
|
||||
"scene_id": "village",
|
||||
}],
|
||||
}, schema)
|
||||
|
||||
|
||||
def test_atlas_success_downloads_glb_and_provenance(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("ATLASCLOUD_API_KEY", "test-key")
|
||||
monkeypatch.setattr(atlas_3d.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(atlas_3d.requests, "post", lambda *args, **kwargs: _Response({"data": {"id": "pred-1"}}))
|
||||
|
||||
def fake_get(url, **_kwargs):
|
||||
if "prediction/pred-1" in url:
|
||||
return _Response({"data": {"status": "completed", "files": [{
|
||||
"type": "glb", "url": "https://cdn.example/asset.glb",
|
||||
}]}})
|
||||
return _Response(content=b"glb-bytes")
|
||||
|
||||
monkeypatch.setattr(atlas_3d.requests, "get", fake_get)
|
||||
output = tmp_path / "asset.glb"
|
||||
result = Atlas3D().execute({"prompt": "a weathered cottage", "output_path": str(output)})
|
||||
assert result.success, result.error
|
||||
assert output.read_bytes() == b"glb-bytes"
|
||||
provenance = json.loads(output.with_suffix(".provenance.json").read_text(encoding="utf-8"))
|
||||
assert provenance["prediction_id"] == "pred-1"
|
||||
assert provenance["model"] == "tripo-h3.1/text-to-3d"
|
||||
|
||||
|
||||
def test_fal_success_downloads_glb_and_provenance(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("FAL_KEY", "test-key")
|
||||
monkeypatch.setattr(fal_3d.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(fal_3d.requests, "post", lambda *args, **kwargs: _Response({
|
||||
"request_id": "req-1",
|
||||
"status_url": "https://queue.example/status",
|
||||
"response_url": "https://queue.example/result",
|
||||
}))
|
||||
|
||||
def fake_get(url, **_kwargs):
|
||||
if url.endswith("/status"):
|
||||
return _Response({"status": "COMPLETED"})
|
||||
if url.endswith("/result"):
|
||||
return _Response({"model_urls": {"glb": {
|
||||
"url": "https://cdn.example/asset.glb", "content_type": "model/gltf-binary",
|
||||
}}})
|
||||
return _Response(content=b"fal-glb")
|
||||
|
||||
monkeypatch.setattr(fal_3d.requests, "get", fake_get)
|
||||
output = tmp_path / "fal-asset.glb"
|
||||
result = Fal3D().execute({
|
||||
"operation": "text_to_3d", "prompt": "a stone bridge", "output_path": str(output),
|
||||
})
|
||||
assert result.success, result.error
|
||||
assert output.read_bytes() == b"fal-glb"
|
||||
provenance = json.loads(output.with_suffix(".provenance.json").read_text(encoding="utf-8"))
|
||||
assert provenance["request_id"] == "req-1"
|
||||
assert provenance["provider"] == "fal"
|
||||
94
tests/tools/test_audio_mixer_target_duration.py
Normal file
94
tests/tools/test_audio_mixer_target_duration.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
import math
|
||||
import struct
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.audio.audio_mixer import AudioMixer
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None,
|
||||
reason="ffmpeg and ffprobe are required",
|
||||
)
|
||||
|
||||
|
||||
def _tone(path: Path, frequency: int, duration: float) -> None:
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y", "-f", "lavfi", "-i",
|
||||
f"sine=frequency={frequency}:duration={duration}", str(path),
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def _duration(path: Path) -> float:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0", str(path),
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
return float(result.stdout.strip())
|
||||
|
||||
|
||||
def _tail_rms(path: Path, start_seconds: float, end_seconds: float) -> float:
|
||||
with wave.open(str(path), "rb") as handle:
|
||||
sample_width = handle.getsampwidth()
|
||||
assert sample_width == 2
|
||||
frame_rate = handle.getframerate()
|
||||
handle.setpos(int(start_seconds * frame_rate))
|
||||
raw = handle.readframes(int((end_seconds - start_seconds) * frame_rate))
|
||||
samples = struct.unpack(f"<{len(raw) // 2}h", raw)
|
||||
return math.sqrt(sum(sample * sample for sample in samples) / len(samples))
|
||||
|
||||
|
||||
def test_full_mix_can_pin_the_composition_length(tmp_path) -> None:
|
||||
speech = tmp_path / "speech.wav"
|
||||
music = tmp_path / "music.wav"
|
||||
output = tmp_path / "mix.wav"
|
||||
_tone(speech, 440, 1)
|
||||
_tone(music, 220, 3)
|
||||
|
||||
result = AudioMixer().execute({
|
||||
"operation": "full_mix",
|
||||
"tracks": [
|
||||
{"path": str(speech), "role": "speech"},
|
||||
{"path": str(music), "role": "music"},
|
||||
],
|
||||
"ducking": {"enabled": True},
|
||||
"normalize": False,
|
||||
"target_duration": 3,
|
||||
"output_path": str(output),
|
||||
})
|
||||
|
||||
assert result.success, result.error
|
||||
assert result.data["target_duration"] == 3
|
||||
assert _duration(output) == pytest.approx(3.0, abs=0.15)
|
||||
assert _tail_rms(output, 2.0, 2.5) > 100
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target", [0, -1, "not-a-number"])
|
||||
def test_full_mix_rejects_invalid_target_duration(tmp_path, target) -> None:
|
||||
tone = tmp_path / "tone.wav"
|
||||
_tone(tone, 440, 0.25)
|
||||
|
||||
result = AudioMixer().execute({
|
||||
"operation": "full_mix",
|
||||
"tracks": [{"path": str(tone), "role": "speech"}],
|
||||
"target_duration": target,
|
||||
"output_path": str(tmp_path / "mix.wav"),
|
||||
})
|
||||
|
||||
assert result.success is False
|
||||
assert "target_duration" in result.error
|
||||
28
tests/tools/test_bg_remove_api.py
Normal file
28
tests/tools/test_bg_remove_api.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def test_bg_remove_selects_model_through_rembg_session(monkeypatch, tmp_path) -> None:
|
||||
fake_rembg = MagicMock()
|
||||
fake_rembg.new_session.return_value = "selected-session"
|
||||
fake_rembg.remove.side_effect = lambda image, **kwargs: image.convert("RGBA")
|
||||
monkeypatch.setitem(sys.modules, "rembg", fake_rembg)
|
||||
|
||||
input_path = tmp_path / "input.png"
|
||||
Image.new("RGB", (8, 8), (10, 20, 30)).save(input_path)
|
||||
|
||||
from tools.enhancement.bg_remove import BgRemove
|
||||
|
||||
result = BgRemove().execute({
|
||||
"input_path": str(input_path),
|
||||
"output_path": str(tmp_path / "output.png"),
|
||||
"model": "isnet-general-use",
|
||||
})
|
||||
|
||||
assert result.success, result.error
|
||||
fake_rembg.new_session.assert_called_once_with("isnet-general-use")
|
||||
kwargs = fake_rembg.remove.call_args.kwargs
|
||||
assert kwargs["session"] == "selected-session"
|
||||
assert "model_name" not in kwargs
|
||||
177
tests/tools/test_cinematic_remotion_adapter.py
Normal file
177
tests/tools/test_cinematic_remotion_adapter.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.video.video_compose import VideoCompose
|
||||
|
||||
|
||||
def test_cinematic_cut_adapter_builds_a_sequential_timeline() -> None:
|
||||
scenes = VideoCompose._cuts_to_cinematic_scenes([
|
||||
{
|
||||
"id": "v1",
|
||||
"source": "clip.mp4",
|
||||
"in_seconds": 2,
|
||||
"out_seconds": 6,
|
||||
"transition_in": "cut",
|
||||
"transition_out": "none",
|
||||
},
|
||||
{
|
||||
"id": "title",
|
||||
"source": "",
|
||||
"type": "hero_title",
|
||||
"text": "The signal arrives",
|
||||
"in_seconds": 0,
|
||||
"out_seconds": 3,
|
||||
},
|
||||
])
|
||||
|
||||
assert scenes[0] == {
|
||||
"id": "v1",
|
||||
"startSeconds": 0.0,
|
||||
"durationSeconds": 4.0,
|
||||
"kind": "video",
|
||||
"src": "clip.mp4",
|
||||
"trimBeforeSeconds": 2.0,
|
||||
"trimAfterSeconds": 6.0,
|
||||
"playbackRate": 1.0,
|
||||
"fadeInFrames": 0,
|
||||
"fadeOutFrames": 0,
|
||||
}
|
||||
assert scenes[1]["kind"] == "title"
|
||||
assert scenes[1]["startSeconds"] == 4.0
|
||||
assert scenes[1]["text"] == "The signal arrives"
|
||||
|
||||
|
||||
def test_cinematic_cut_adapter_preserves_playback_speed() -> None:
|
||||
scenes = VideoCompose._cuts_to_cinematic_scenes([
|
||||
{
|
||||
"id": "fast",
|
||||
"source": "clip.mp4",
|
||||
"in_seconds": 2,
|
||||
"out_seconds": 6,
|
||||
"speed": 2,
|
||||
}
|
||||
])
|
||||
|
||||
assert scenes[0]["durationSeconds"] == 2
|
||||
assert scenes[0]["playbackRate"] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri_style", ["standard", "legacy_windows"])
|
||||
def test_remotion_media_staging_decodes_file_uris(tmp_path, uri_style) -> None:
|
||||
source = tmp_path / "clip with space.mp4"
|
||||
source.write_bytes(b"video")
|
||||
public_dir = tmp_path / "public"
|
||||
uri = source.as_uri()
|
||||
if uri_style == "legacy_windows" and len(source.drive) == 2:
|
||||
uri = f"file://{source.drive}{source.as_posix()[2:]}".replace(" ", "%20")
|
||||
props = {"scenes": [{"src": uri}]}
|
||||
|
||||
staged_count = VideoCompose._stage_remotion_media(props, public_dir)
|
||||
|
||||
assert staged_count == 1
|
||||
assert props["scenes"][0]["src"] != uri
|
||||
assert (public_dir / props["scenes"][0]["src"]).read_bytes() == b"video"
|
||||
|
||||
|
||||
def test_remotion_render_adapts_cuts_and_stages_local_video(monkeypatch, tmp_path) -> None:
|
||||
source = tmp_path / "source.mp4"
|
||||
source.write_bytes(b"not-a-real-video")
|
||||
output = tmp_path / "render.mp4"
|
||||
captured = {}
|
||||
|
||||
def fake_run_command(self, command, **kwargs):
|
||||
captured["command"] = command
|
||||
captured["timeout"] = kwargs["timeout"]
|
||||
props_arg = next(arg for arg in command if arg.startswith("--props="))
|
||||
captured["props"] = json.loads(Path(props_arg.split("=", 1)[1]).read_text())
|
||||
public_arg = next(arg for arg in command if arg.startswith("--public-dir="))
|
||||
public_dir = Path(public_arg.split("=", 1)[1])
|
||||
captured["staged_exists_during_render"] = (
|
||||
public_dir / captured["props"]["scenes"][0]["src"]
|
||||
).exists()
|
||||
output.write_bytes(b"rendered")
|
||||
|
||||
monkeypatch.setattr(VideoCompose, "run_command", fake_run_command)
|
||||
|
||||
result = VideoCompose()._remotion_render({
|
||||
"edit_decisions": {
|
||||
"renderer_family": "cinematic-trailer",
|
||||
"cuts": [
|
||||
{
|
||||
"id": "v1",
|
||||
"source": str(source),
|
||||
"in_seconds": 0,
|
||||
"out_seconds": 2,
|
||||
}
|
||||
],
|
||||
},
|
||||
"output_path": str(output),
|
||||
})
|
||||
|
||||
assert result.success, result.error
|
||||
assert "cuts" not in captured["props"]
|
||||
assert captured["props"]["scenes"][0]["kind"] == "video"
|
||||
assert captured["staged_exists_during_render"] is True
|
||||
assert result.data["staged_media_count"] == 1
|
||||
|
||||
|
||||
def test_remotion_timeout_scales_with_scene_count(monkeypatch, tmp_path) -> None:
|
||||
output = tmp_path / "render.mp4"
|
||||
captured = {}
|
||||
|
||||
def fake_run_command(self, command, **kwargs):
|
||||
captured["timeout"] = kwargs["timeout"]
|
||||
output.write_bytes(b"rendered")
|
||||
|
||||
monkeypatch.setattr(VideoCompose, "run_command", fake_run_command)
|
||||
cuts = [
|
||||
{
|
||||
"id": f"title-{index}",
|
||||
"source": "",
|
||||
"type": "hero_title",
|
||||
"text": str(index),
|
||||
"in_seconds": 0,
|
||||
"out_seconds": 1,
|
||||
}
|
||||
for index in range(50)
|
||||
]
|
||||
|
||||
result = VideoCompose()._remotion_render({
|
||||
"edit_decisions": {"renderer_family": "cinematic-trailer", "cuts": cuts},
|
||||
"output_path": str(output),
|
||||
})
|
||||
|
||||
assert result.success, result.error
|
||||
assert captured["timeout"] == 750
|
||||
|
||||
|
||||
def test_remotion_render_preserves_direct_cinematic_scenes(monkeypatch, tmp_path) -> None:
|
||||
output = tmp_path / "render.mp4"
|
||||
captured = {}
|
||||
|
||||
def fake_run_command(self, command, **kwargs):
|
||||
props_arg = next(arg for arg in command if arg.startswith("--props="))
|
||||
captured["props"] = json.loads(Path(props_arg.split("=", 1)[1]).read_text())
|
||||
output.write_bytes(b"rendered")
|
||||
|
||||
monkeypatch.setattr(VideoCompose, "run_command", fake_run_command)
|
||||
scene = {
|
||||
"id": "authored",
|
||||
"kind": "title",
|
||||
"text": "Keep me",
|
||||
"startSeconds": 0,
|
||||
"durationSeconds": 1,
|
||||
}
|
||||
|
||||
result = VideoCompose()._remotion_render({
|
||||
"composition_data": {
|
||||
"renderer_family": "cinematic-trailer",
|
||||
"scenes": [scene],
|
||||
},
|
||||
"output_path": str(output),
|
||||
})
|
||||
|
||||
assert result.success, result.error
|
||||
assert captured["props"]["scenes"] == [scene]
|
||||
59
tests/tools/test_corpus_builder_total_failure.py
Normal file
59
tests/tools/test_corpus_builder_total_failure.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
import tools.video.stock_sources as stock_sources
|
||||
from tools.video.corpus_builder import CorpusBuilder
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Candidate:
|
||||
clip_id: str
|
||||
|
||||
|
||||
class _Source:
|
||||
name = "fake"
|
||||
|
||||
def __init__(self, count: int) -> None:
|
||||
self.count = count
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def search(self, query, filters):
|
||||
return [_Candidate(f"clip-{index}") for index in range(self.count)]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_builder(monkeypatch, tmp_path):
|
||||
def run(count: int, processor):
|
||||
monkeypatch.setattr(stock_sources, "available_sources", lambda: [_Source(count)])
|
||||
monkeypatch.setattr(stock_sources, "source_summary", lambda: {})
|
||||
monkeypatch.setattr(CorpusBuilder, "_process_candidate", processor)
|
||||
return CorpusBuilder().execute({
|
||||
"corpus_dir": str(tmp_path / f"corpus-{count}"),
|
||||
"queries": [{"query": "city at night"}],
|
||||
"max_new_clips": 50,
|
||||
})
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def test_all_candidate_failures_fail_closed_with_diagnostics(run_builder) -> None:
|
||||
def broken_clip_stack(*args, **kwargs):
|
||||
raise AttributeError("BaseModelOutput has no attribute norm")
|
||||
|
||||
result = run_builder(4, broken_clip_stack)
|
||||
|
||||
assert result.success is False
|
||||
assert result.data["candidates_seen"] == 4
|
||||
assert result.data["clips_failed"] == 4
|
||||
assert "corpus index is empty" in result.error
|
||||
assert "BaseModelOutput" in result.error
|
||||
|
||||
|
||||
def test_no_candidates_is_a_valid_empty_search(run_builder) -> None:
|
||||
result = run_builder(0, lambda *args, **kwargs: None)
|
||||
|
||||
assert result.success is True
|
||||
assert result.data["candidates_seen"] == 0
|
||||
97
tests/tools/test_google_vertex_backends.py
Normal file
97
tests/tools/test_google_vertex_backends.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_blank_google_location_uses_documented_default(monkeypatch) -> None:
|
||||
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "")
|
||||
from tools.google_credentials import resolve_google_location
|
||||
|
||||
assert resolve_google_location() == "us-central1"
|
||||
|
||||
|
||||
def test_google_music_requests_the_global_vertex_location(monkeypatch, tmp_path) -> None:
|
||||
monkeypatch.setenv("GOOGLE_API_KEY", "test-key")
|
||||
import tools.google_credentials as credentials
|
||||
|
||||
captured = {}
|
||||
|
||||
def stop_before_network(http_options=None, location=None):
|
||||
captured["location"] = location
|
||||
raise RuntimeError("stop before network")
|
||||
|
||||
monkeypatch.setattr(credentials, "get_genai_client", stop_before_network)
|
||||
|
||||
from tools.audio.google_music import GoogleMusic
|
||||
|
||||
result = GoogleMusic().execute({
|
||||
"prompt": "solo piano",
|
||||
"output_path": str(tmp_path / "music.mp3"),
|
||||
})
|
||||
|
||||
assert result.success is False
|
||||
assert captured["location"] == "global"
|
||||
|
||||
|
||||
class _VideoAsset:
|
||||
uri = None
|
||||
|
||||
def __init__(self, video_bytes):
|
||||
self.video_bytes = video_bytes
|
||||
|
||||
def save(self, path):
|
||||
Path(path).write_bytes(self.video_bytes or b"")
|
||||
|
||||
|
||||
class _Models:
|
||||
def __init__(self, video_bytes):
|
||||
self.video_bytes = video_bytes
|
||||
|
||||
def generate_videos(self, **kwargs):
|
||||
asset = _VideoAsset(self.video_bytes)
|
||||
generated = SimpleNamespace(video=asset)
|
||||
response = SimpleNamespace(generated_videos=[generated])
|
||||
return SimpleNamespace(done=True, error=None, response=response)
|
||||
|
||||
|
||||
class _VertexClient:
|
||||
vertexai = True
|
||||
|
||||
def __init__(self, video_bytes):
|
||||
self.models = _Models(video_bytes)
|
||||
self.files = SimpleNamespace(
|
||||
download=lambda **kwargs: pytest.fail("Vertex must not call files.download")
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("video_bytes", "expected_success"),
|
||||
[(b"VIDEO_BYTES", True), (None, False)],
|
||||
)
|
||||
def test_veo_accepts_vertex_and_requires_inline_bytes(
|
||||
monkeypatch, tmp_path, video_bytes, expected_success
|
||||
) -> None:
|
||||
monkeypatch.setenv("GOOGLE_API_KEY", "test-key")
|
||||
import tools.google_credentials as credentials
|
||||
|
||||
monkeypatch.setattr(
|
||||
credentials,
|
||||
"get_genai_client",
|
||||
lambda http_options=None, location=None: _VertexClient(video_bytes),
|
||||
)
|
||||
|
||||
from tools.video.veo_video import VeoVideo
|
||||
|
||||
output = tmp_path / "video.mp4"
|
||||
result = VeoVideo().execute({
|
||||
"backend": "google",
|
||||
"prompt": "wind moving across grassland",
|
||||
"output_path": str(output),
|
||||
})
|
||||
|
||||
assert result.success is expected_success
|
||||
if expected_success:
|
||||
assert output.read_bytes() == b"VIDEO_BYTES"
|
||||
else:
|
||||
assert "without inline bytes" in result.error
|
||||
@@ -250,6 +250,11 @@ def test_runtime_check_succeeds_when_npm_resolves(monkeypatch):
|
||||
"_resolve_npm_package",
|
||||
classmethod(lambda cls: {"version": "0.4.5"}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
HyperFramesCompose,
|
||||
"_probe_cli",
|
||||
classmethod(lambda cls: {"status": "ok"}),
|
||||
)
|
||||
rc = HyperFramesCompose()._runtime_check()
|
||||
# Local binaries must still pass for this to go green.
|
||||
if rc["node_major"] is None or not rc["ffmpeg_available"] or not rc["npx_available"]:
|
||||
@@ -259,6 +264,31 @@ def test_runtime_check_succeeds_when_npm_resolves(monkeypatch):
|
||||
assert rc["reasons"] == []
|
||||
|
||||
|
||||
def test_runtime_check_fails_when_published_cli_crashes(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
HyperFramesCompose,
|
||||
"_resolve_npm_package",
|
||||
classmethod(lambda cls: {"version": "0.7.89"}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
HyperFramesCompose,
|
||||
"_probe_cli",
|
||||
classmethod(
|
||||
lambda cls: {
|
||||
"error": 'doctor failed: The "file" argument must be of type string'
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
rc = HyperFramesCompose()._runtime_check()
|
||||
|
||||
if rc["node_major"] is None or not rc["ffmpeg_available"] or not rc["npx_available"]:
|
||||
pytest.skip("Local runtime floor not met on this machine")
|
||||
assert rc["runtime_available"] is False
|
||||
assert rc["cli_probe_error"] is not None
|
||||
assert any("not executable" in reason for reason in rc["reasons"])
|
||||
|
||||
|
||||
def test_video_compose_render_engines_follow_hyperframes_runtime_check(monkeypatch):
|
||||
"""Regression: `video_compose.get_info()['render_engines']['hyperframes']`
|
||||
must track the true availability, not just the local-binary floor.
|
||||
|
||||
@@ -213,8 +213,9 @@ def test_upscale_build_upsampler_uses_signature_guard(monkeypatch):
|
||||
|
||||
# Build a fake RealESRGANer whose __init__ DOES accept device=
|
||||
class FakeRealESRGANer:
|
||||
def __init__(self, *, scale, model_path, model, dni_weight, half, device=None):
|
||||
def __init__(self, *, scale, model_path, model, dni_weight, half, tile=0, tile_pad=10, device=None):
|
||||
self.called_with_device = device
|
||||
self.called_with_tile = tile
|
||||
fake_realesrganer_cls = FakeRealESRGANer
|
||||
|
||||
monkeypatch.setitem(sys.modules, "torch", fake_torch)
|
||||
@@ -239,6 +240,7 @@ def test_upscale_build_upsampler_uses_signature_guard(monkeypatch):
|
||||
tool = upscale.Upscale()
|
||||
result = tool._build_upsampler(scale=4, model_name="RealESRGAN_x4plus", denoise_strength=0.5, face_enhance=False)
|
||||
assert result.called_with_device == "device(mps)"
|
||||
assert result.called_with_tile == 256
|
||||
|
||||
|
||||
def test_upscale_build_upsampler_skips_device_when_unsupported(monkeypatch):
|
||||
@@ -252,8 +254,9 @@ def test_upscale_build_upsampler_skips_device_when_unsupported(monkeypatch):
|
||||
|
||||
# Build a fake RealESRGANer whose __init__ does NOT accept device=
|
||||
class FakeRealESRGANerNoDevice:
|
||||
def __init__(self, *, scale, model_path, model, dni_weight, half):
|
||||
def __init__(self, *, scale, model_path, model, dni_weight, half, tile=0, tile_pad=10):
|
||||
self.called_with_device = None # no device param
|
||||
self.called_with_tile = tile
|
||||
fake_realesrganer_cls = FakeRealESRGANerNoDevice
|
||||
|
||||
monkeypatch.setitem(sys.modules, "torch", fake_torch)
|
||||
@@ -279,6 +282,7 @@ def test_upscale_build_upsampler_skips_device_when_unsupported(monkeypatch):
|
||||
# Should NOT raise TypeError about unexpected keyword argument 'device'
|
||||
result = tool._build_upsampler(scale=4, model_name="RealESRGAN_x4plus", denoise_strength=0.5, face_enhance=False)
|
||||
assert result.called_with_device is None
|
||||
assert result.called_with_tile == 256
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
59
tests/tools/test_remotion_audio_mux.py
Normal file
59
tests/tools/test_remotion_audio_mux.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.video.video_compose import VideoCompose
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None,
|
||||
reason="ffmpeg and ffprobe are required",
|
||||
)
|
||||
|
||||
|
||||
def _run(command: list[str]) -> None:
|
||||
subprocess.run(command, capture_output=True, check=True, timeout=30)
|
||||
|
||||
|
||||
def test_external_audio_mux_adds_audible_stream_without_changing_video_length(tmp_path) -> None:
|
||||
video = tmp_path / "video.mp4"
|
||||
audio = tmp_path / "audio.wav"
|
||||
_run([
|
||||
"ffmpeg", "-y", "-f", "lavfi", "-i",
|
||||
"color=c=red:s=320x180:d=2:r=30",
|
||||
"-c:v", "libx264", "-pix_fmt", "yuv420p", str(video),
|
||||
])
|
||||
_run([
|
||||
"ffmpeg", "-y", "-f", "lavfi", "-i",
|
||||
"sine=frequency=440:duration=1", str(audio),
|
||||
])
|
||||
|
||||
result = VideoCompose()._mux_external_audio(video, audio)
|
||||
|
||||
assert result.success, result.error
|
||||
streams = subprocess.run(
|
||||
[
|
||||
"ffprobe", "-v", "error", "-show_entries",
|
||||
"stream=codec_type", "-of", "csv=p=0", str(video),
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
).stdout.splitlines()
|
||||
duration = float(subprocess.run(
|
||||
[
|
||||
"ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "csv=p=0", str(video),
|
||||
],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
).stdout.strip())
|
||||
|
||||
assert "video" in streams
|
||||
assert "audio" in streams
|
||||
assert duration == pytest.approx(2.0, abs=0.15)
|
||||
45
tests/tools/test_threejs_asset_catalog.py
Normal file
45
tests/tools/test_threejs_asset_catalog.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from tools.graphics.threejs_asset_catalog import CATALOGS, ThreeJSAssetCatalog
|
||||
|
||||
|
||||
def test_catalog_list_is_rights_explicit():
|
||||
result = ThreeJSAssetCatalog().execute({"operation": "list"})
|
||||
assert result.success
|
||||
assert result.data["catalogs"]
|
||||
assert all(item["license"] == "CC0-1.0" for item in result.data["catalogs"].values())
|
||||
|
||||
|
||||
def test_catalog_install_inventories_gltf(tmp_path, monkeypatch):
|
||||
source_zip = tmp_path / "fixture.zip"
|
||||
with zipfile.ZipFile(source_zip, "w") as package:
|
||||
package.writestr("Models/GLTF format/Tree.gltf", json.dumps({"asset": {"version": "2.0"}}))
|
||||
package.writestr("Models/GLTF format/Tree.bin", b"mesh")
|
||||
package.writestr("Textures/tree.png", b"texture")
|
||||
|
||||
fixture_id = "fixture-catalog"
|
||||
monkeypatch.setitem(CATALOGS, fixture_id, {
|
||||
"title": "Fixture",
|
||||
"source_url": "https://example.test/source",
|
||||
"download_url": "https://example.test/catalog.zip",
|
||||
"license": "CC0-1.0",
|
||||
"license_url": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||
"tags": ["fixture"],
|
||||
})
|
||||
|
||||
def fake_download(_url: str, destination: Path) -> None:
|
||||
destination.write_bytes(source_zip.read_bytes())
|
||||
|
||||
monkeypatch.setattr("tools.graphics.threejs_asset_catalog._download", fake_download)
|
||||
output = tmp_path / "installed"
|
||||
result = ThreeJSAssetCatalog().execute({
|
||||
"operation": "install",
|
||||
"catalog_id": fixture_id,
|
||||
"output_path": str(output),
|
||||
})
|
||||
assert result.success, result.error
|
||||
assert result.data["model_count"] == 1
|
||||
assert result.data["texture_count"] == 1
|
||||
assert (output / "catalog-manifest.json").exists()
|
||||
243
tests/tools/test_threejs_world.py
Normal file
243
tests/tools/test_threejs_world.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""Contracts for semantic Three.js world generation and atelier rendering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from tools.base_tool import ToolResult
|
||||
from tools.graphics.threejs_world import ThreeJSWorld
|
||||
from tools.tool_registry import ToolRegistry
|
||||
from tools.video.hyperframes_compose import HyperFramesCompose
|
||||
from tools.video.video_compose import VideoCompose
|
||||
|
||||
|
||||
def _world_spec(duration: float = 12.0) -> dict:
|
||||
return {
|
||||
"version": "1.0",
|
||||
"title": "The Luminous Divide",
|
||||
"seed": 260805248,
|
||||
"explicit_constraints": ["one continuous explorable world"],
|
||||
"inferred_details": ["cyan emissive accents provide visual continuity"],
|
||||
"world": {
|
||||
"size": 96,
|
||||
"resolution": 72,
|
||||
"elevation_scale": 12,
|
||||
"water_level": -1.5,
|
||||
},
|
||||
"regions": [
|
||||
{
|
||||
"id": "wetlands",
|
||||
"center": [-0.45, 0.15],
|
||||
"radius": 0.9,
|
||||
"landform": "basin",
|
||||
"color": "#204a43",
|
||||
"accent_color": "#71f7c4",
|
||||
"scatter": {"tree": 18, "rock": 8, "crystal": 6},
|
||||
},
|
||||
{
|
||||
"id": "rift",
|
||||
"center": [0.5, -0.1],
|
||||
"radius": 0.9,
|
||||
"landform": "canyon",
|
||||
"color": "#503040",
|
||||
"accent_color": "#ff765f",
|
||||
"scatter": {"tree": 0, "rock": 18, "crystal": 9},
|
||||
},
|
||||
],
|
||||
"landmarks": [
|
||||
{
|
||||
"id": "threshold-ring",
|
||||
"type": "ring",
|
||||
"region_id": "wetlands",
|
||||
"position": [-22, 0, 8],
|
||||
"scale": 3.5,
|
||||
}
|
||||
],
|
||||
"camera_path": [
|
||||
{"time": 0, "position": [-42, 23, 38], "target": [-18, 0, 4]},
|
||||
{"time": duration / 2, "position": [0, 16, 24], "target": [10, 0, -4]},
|
||||
{"time": duration, "position": [42, 25, -34], "target": [20, 0, -5]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_threejs_world_contract_and_registry_discovery():
|
||||
tool = ThreeJSWorld()
|
||||
assert tool.capability == "3d_world_generation"
|
||||
assert tool.provider == "threejs"
|
||||
assert "threejs-world-generation" in tool.agent_skills
|
||||
assert {"cinematic", "semantic", "wireframe"} == set(
|
||||
tool.input_schema["properties"]["render_mode"]["enum"]
|
||||
)
|
||||
|
||||
registry = ToolRegistry()
|
||||
registry.discover("tools")
|
||||
assert "threejs_world" in {
|
||||
discovered.name
|
||||
for discovered in registry.get_by_capability("3d_world_generation")
|
||||
}
|
||||
|
||||
|
||||
def test_threejs_world_validate_emits_worldclaw_diagnostics():
|
||||
result = ThreeJSWorld().execute(
|
||||
{"operation": "validate", "world_spec": _world_spec(), "duration_seconds": 12}
|
||||
)
|
||||
assert result.success, result.error
|
||||
report = result.data["report"]
|
||||
assert report["valid"] is True
|
||||
assert report["stats"]["region_count"] == 2
|
||||
assert report["stats"]["terrain_triangles"] > 0
|
||||
assert set(report["diagnostic_passes"]) == {"cinematic", "semantic", "wireframe"}
|
||||
assert report["review_views"] == [
|
||||
"global",
|
||||
"regional",
|
||||
"walk",
|
||||
"semantic",
|
||||
"wireframe",
|
||||
]
|
||||
|
||||
|
||||
def test_threejs_world_build_is_deterministic_and_editable(tmp_path):
|
||||
workspaces = [tmp_path / "first", tmp_path / "second"]
|
||||
hashes = []
|
||||
for workspace in workspaces:
|
||||
result = ThreeJSWorld().execute(
|
||||
{
|
||||
"operation": "build",
|
||||
"world_spec": _world_spec(),
|
||||
"output_path": str(workspace),
|
||||
"duration_seconds": 12,
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"render_mode": "semantic",
|
||||
}
|
||||
)
|
||||
assert result.success, result.error
|
||||
for filename in (
|
||||
"index.html",
|
||||
"world.css",
|
||||
"world-runtime.js",
|
||||
"world.json",
|
||||
"world-spec.js",
|
||||
"world-report.json",
|
||||
"hyperframes.json",
|
||||
):
|
||||
assert (workspace / filename).is_file()
|
||||
index = (workspace / "index.html").read_text(encoding="utf-8")
|
||||
assert "--world-width: 1280px" in index
|
||||
assert 'data-render-mode="semantic"' in index
|
||||
hashes.append(
|
||||
hashlib.sha256((workspace / "world-spec.js").read_bytes()).hexdigest()
|
||||
)
|
||||
assert hashes[0] == hashes[1]
|
||||
assert json.loads((workspaces[0] / "world.json").read_text(encoding="utf-8"))[
|
||||
"seed"
|
||||
] == 260805248
|
||||
|
||||
|
||||
def test_threejs_world_rejects_incomplete_camera_path():
|
||||
spec = _world_spec()
|
||||
spec["camera_path"][-1]["time"] = 11
|
||||
result = ThreeJSWorld().execute(
|
||||
{"operation": "validate", "world_spec": spec, "duration_seconds": 12}
|
||||
)
|
||||
assert not result.success
|
||||
assert "Last camera key" in (result.error or "")
|
||||
|
||||
|
||||
def test_production_tier_rejects_primitive_only_spec():
|
||||
result = ThreeJSWorld().execute({
|
||||
"operation": "validate",
|
||||
"world_spec": _world_spec(),
|
||||
"duration_seconds": 12,
|
||||
"quality_tier": "production",
|
||||
"asset_catalog_paths": [],
|
||||
})
|
||||
assert not result.success
|
||||
assert "asset catalog" in (result.error or "").lower()
|
||||
assert "asset-palette" in (result.error or "").lower()
|
||||
assert "terrain material" in (result.error or "").lower()
|
||||
|
||||
|
||||
def test_blockout_tier_is_labeled_as_nonproduction():
|
||||
result = ThreeJSWorld().execute({
|
||||
"operation": "validate",
|
||||
"world_spec": _world_spec(),
|
||||
"duration_seconds": 12,
|
||||
"quality_tier": "blockout",
|
||||
})
|
||||
assert result.success
|
||||
assert result.data["report"]["quality_tier"] == "blockout"
|
||||
assert any("do not present" in warning.lower() for warning in result.data["report"]["warnings"])
|
||||
|
||||
|
||||
def test_hyperframes_render_existing_preserves_authored_entry(tmp_path, monkeypatch):
|
||||
workspace = tmp_path / "world"
|
||||
workspace.mkdir()
|
||||
entry = workspace / "index.html"
|
||||
entry.write_text("<main data-composition-id='world'></main>", encoding="utf-8")
|
||||
tool = HyperFramesCompose()
|
||||
monkeypatch.setattr(tool, "_runtime_check", lambda: {"runtime_available": True})
|
||||
monkeypatch.setattr(tool, "_check", lambda inputs: ToolResult(success=True, data={"ok": True}))
|
||||
|
||||
def fake_run(args, *, cwd, timeout, check):
|
||||
output = Path(args[args.index("--output") + 1])
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_bytes(b"rendered")
|
||||
return subprocess.CompletedProcess(args, 0, "", "")
|
||||
|
||||
monkeypatch.setattr(tool, "_run_hf", fake_run)
|
||||
output = tmp_path / "renders" / "final.mp4"
|
||||
result = tool.execute(
|
||||
{
|
||||
"operation": "render_existing",
|
||||
"workspace_path": str(workspace),
|
||||
"output_path": str(output),
|
||||
"quality": "draft",
|
||||
}
|
||||
)
|
||||
assert result.success, result.error
|
||||
assert result.data["authored_entry_preserved"] is True
|
||||
assert entry.read_text(encoding="utf-8") == "<main data-composition-id='world'></main>"
|
||||
assert output.is_file()
|
||||
|
||||
|
||||
def test_video_compose_routes_empty_cut_atelier_to_existing_workspace(tmp_path, monkeypatch):
|
||||
captured = {}
|
||||
output = tmp_path / "final.mp4"
|
||||
|
||||
def fake_hyperframes_execute(self, inputs):
|
||||
captured.update(inputs)
|
||||
Path(inputs["output_path"]).write_bytes(b"fake mp4")
|
||||
return ToolResult(success=True, data={"output": inputs["output_path"]})
|
||||
|
||||
monkeypatch.setattr(VideoCompose, "_hyperframes_available", lambda self: True)
|
||||
monkeypatch.setattr(HyperFramesCompose, "execute", fake_hyperframes_execute)
|
||||
monkeypatch.setattr(
|
||||
VideoCompose,
|
||||
"_run_final_review",
|
||||
lambda self, *args, **kwargs: {"status": "pass", "issues_found": []},
|
||||
)
|
||||
|
||||
result = VideoCompose().execute(
|
||||
{
|
||||
"operation": "render",
|
||||
"workspace_path": str(tmp_path / "world"),
|
||||
"output_path": str(output),
|
||||
"edit_decisions": {
|
||||
"version": "1.0",
|
||||
"cuts": [],
|
||||
"render_runtime": "hyperframes",
|
||||
"renderer_family": "bespoke",
|
||||
"composition_mode": "atelier",
|
||||
"bespoke": {"entry": "index.html"},
|
||||
},
|
||||
}
|
||||
)
|
||||
assert result.success, result.error
|
||||
assert captured["operation"] == "render_existing"
|
||||
assert captured["asset_manifest"] == {"version": "1.0", "assets": []}
|
||||
assert captured["edit_decisions"]["cuts"] == []
|
||||
83
tests/tools/test_transcriber_device_selection.py
Normal file
83
tests/tools/test_transcriber_device_selection.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tools.analysis.transcriber import Transcriber
|
||||
|
||||
|
||||
class _Info:
|
||||
language = "en"
|
||||
duration = 1.0
|
||||
|
||||
|
||||
def test_transcriber_uses_ctranslate2_cuda_without_torch(monkeypatch, tmp_path) -> None:
|
||||
devices = []
|
||||
|
||||
class FakeWhisperModel:
|
||||
def __init__(self, model_size, *, device, compute_type):
|
||||
devices.append((device, compute_type))
|
||||
|
||||
def transcribe(self, *args, **kwargs):
|
||||
return iter(()), _Info()
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"faster_whisper",
|
||||
SimpleNamespace(WhisperModel=FakeWhisperModel),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"ctranslate2",
|
||||
SimpleNamespace(
|
||||
get_cuda_device_count=lambda: 1,
|
||||
get_supported_compute_types=lambda device: {"float16", "float32"},
|
||||
),
|
||||
)
|
||||
input_path = tmp_path / "audio.wav"
|
||||
input_path.write_bytes(b"fake")
|
||||
|
||||
result = Transcriber().execute({"input_path": str(input_path), "output_dir": str(tmp_path)})
|
||||
|
||||
assert result.success, result.error
|
||||
assert devices == [("cuda", "float16")]
|
||||
assert result.data["device"] == "cuda"
|
||||
|
||||
|
||||
def test_transcriber_falls_back_when_cuda_fails_during_iteration(monkeypatch, tmp_path) -> None:
|
||||
devices = []
|
||||
|
||||
class FakeWhisperModel:
|
||||
def __init__(self, model_size, *, device, compute_type):
|
||||
self.device = device
|
||||
devices.append((device, compute_type))
|
||||
|
||||
def transcribe(self, *args, **kwargs):
|
||||
if self.device == "cuda":
|
||||
def broken_iterator():
|
||||
raise RuntimeError("cublas64_12.dll not found")
|
||||
yield
|
||||
|
||||
return broken_iterator(), _Info()
|
||||
return iter(()), _Info()
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"faster_whisper",
|
||||
SimpleNamespace(WhisperModel=FakeWhisperModel),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"ctranslate2",
|
||||
SimpleNamespace(
|
||||
get_cuda_device_count=lambda: 1,
|
||||
get_supported_compute_types=lambda device: {"float16"},
|
||||
),
|
||||
)
|
||||
input_path = tmp_path / "audio.wav"
|
||||
input_path.write_bytes(b"fake")
|
||||
|
||||
result = Transcriber().execute({"input_path": str(input_path), "output_dir": str(tmp_path)})
|
||||
|
||||
assert result.success, result.error
|
||||
assert devices == [("cuda", "float16"), ("cpu", "int8")]
|
||||
assert result.data["device"] == "cpu"
|
||||
assert "cublas64_12.dll" in result.data["gpu_fallback_reason"]
|
||||
Reference in New Issue
Block a user