mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-24 17:10:27 +08:00
Merge remote-tracking branch 'origin/main' into codex/repair-pr-482
# Conflicts: # skills/pipelines/animation/asset-director.md
This commit is contained in:
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"
|
||||
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"] == []
|
||||
Reference in New Issue
Block a user