Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 68 additions & 30 deletions app/_launch_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -26632,6 +26632,17 @@ def delete_character_kit_library_item(kit_id: str, body: dict):
))


def _write_scene_recording_sidecar(output_path, sidecar, workspace_id):
from services.asset_manifest import publish_generation_sidecar

publish_generation_sidecar(
output_path,
sidecar,
workspace_id=workspace_id,
tool="scene-animator-3d",
)


@api.post("/api/v1/scenes/recordings")
async def save_scene_recording(
file: UploadFile = File(...),
Expand Down Expand Up @@ -26784,8 +26795,7 @@ async def save_scene_recording(
"output_filename": output_name,
}
try:
with open(os.path.splitext(output_path)[0] + ".meta.json", "w", encoding="utf-8") as handle:
json.dump(sidecar, handle, ensure_ascii=False, indent=2)
_write_scene_recording_sidecar(output_path, sidecar, details.get("workspace"))
except Exception as error:
try:
os.remove(output_path)
Expand Down Expand Up @@ -35527,6 +35537,17 @@ def serve_video_editor_thumbnail(source: str):
)


def _write_video_editor_screenshot_sidecar(output_path, sidecar, workspace_id):
from services.asset_manifest import publish_generation_sidecar

publish_generation_sidecar(
output_path,
sidecar,
workspace_id=workspace_id,
tool="video-editor-screenshot",
)


@api.post("/api/v1/video-editor/screenshot")
def capture_video_editor_frame(body: dict):
"""Save the current source-video frame as a reusable Maestro image output."""
Expand Down Expand Up @@ -35575,12 +35596,9 @@ def capture_video_editor_frame(body: dict):
"generation_mode": "image",
"created_at": time.time(),
}
meta_path = os.path.join(
out_dir,
os.path.splitext(output_name)[0] + ".meta.json",
_write_video_editor_screenshot_sidecar(
output_path, sidecar, body.get("workspace"),
)
with open(meta_path, "w", encoding="utf-8") as handle:
json.dump(sidecar, handle, indent=2, ensure_ascii=False)
return {
"filename": output_name,
"url": f"/api/v1/file/{output_name}",
Expand Down Expand Up @@ -35615,6 +35633,17 @@ def _video_editor_task_identity(body: dict, job_id: str) -> tuple[str, str, str
return task_id, root_task_id, supplied_parent_id or None


def _write_video_editor_export_sidecar(output_path, sidecar, workspace_id):
from services.asset_manifest import publish_generation_sidecar

publish_generation_sidecar(
output_path,
sidecar,
workspace_id=workspace_id,
tool="video-editor",
)


def _run_video_editor_export(job_id: str, body: dict, out_dir: str, output_path: str) -> None:
from services.video_editor import build_source_provenance_manifest, render_project

Expand Down Expand Up @@ -35820,9 +35849,7 @@ def report(progress: int, message: str) -> None:
"workspace": workspace,
"created_at": time.time(),
}
meta_path = os.path.join(out_dir, os.path.splitext(output_name)[0] + ".meta.json")
with open(meta_path, "w", encoding="utf-8") as handle:
json.dump(sidecar, handle, indent=2, ensure_ascii=False)
_write_video_editor_export_sidecar(output_path, sidecar, workspace)

completed = _video_editor_job_update(
job_id,
Expand Down Expand Up @@ -36047,6 +36074,17 @@ def start_video_editor_export(body: dict):
return _public_video_editor_job(snapshot)


def _write_comic_animatic_sidecar(output_path, sidecar, workspace_id):
from services.asset_manifest import publish_generation_sidecar

publish_generation_sidecar(
output_path,
sidecar,
workspace_id=workspace_id,
tool="comic-animatic",
)


def _run_comic_animatic(job_id: str, body: dict, output_path: str) -> None:
from services.video_editor import render_comic_animatic

Expand Down Expand Up @@ -36199,27 +36237,27 @@ def report(progress: int, message: str) -> None:
return

output_name = os.path.basename(output_path)
with open(os.path.splitext(output_path)[0] + ".meta.json", "w", encoding="utf-8") as handle:
json.dump({
"params": {
"source": "comic_animatic",
"comic_animatic": {
"version": 1,
"comic_id": body.get("comic_id"),
"comic_title": body.get("comic_title"),
"width": body["width"], "height": body["height"], "fps": body["fps"],
"transition": body["transition"],
"transition_duration": body["transition_duration"],
"panels": [{key: value for key, value in panel.items() if key != "resolved_path"} for panel in panels],
},
sidecar = {
"params": {
"source": "comic_animatic",
"comic_animatic": {
"version": 1,
"comic_id": body.get("comic_id"),
"comic_title": body.get("comic_title"),
"width": body["width"], "height": body["height"], "fps": body["fps"],
"transition": body["transition"],
"transition_duration": body["transition_duration"],
"panels": [{key: value for key, value in panel.items() if key != "resolved_path"} for panel in panels],
},
"generation_mode": "video",
"job_id": job_id,
"task_id": task_id,
"root_task_id": str(job["root_task_id"]),
"workspace": workspace,
"created_at": time.time(),
}, handle, indent=2, ensure_ascii=False)
},
"generation_mode": "video",
"job_id": job_id,
"task_id": task_id,
"root_task_id": str(job["root_task_id"]),
"workspace": workspace,
"created_at": time.time(),
}
_write_comic_animatic_sidecar(output_path, sidecar, workspace)
completed = _video_editor_job_update(
job_id,
status="completed",
Expand Down
115 changes: 115 additions & 0 deletions tests/test_execution_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,117 @@ def test_edit_shot_sidecar_publishes_canonical_manifest_without_invented_actor(
assert read_asset_manifest(other, workspace_id="lab")["origin"]["tool"] == tool


@pytest.mark.parametrize(
("writer_name", "tool", "filename", "sidecar"),
[
(
"_write_scene_recording_sidecar",
"scene-animator-3d",
"scene.mp4",
{
"params": {
"model_type": "scene-animator-3d",
"generation_mode": "3d-scene-compositor",
"prompt": "a 3d scene",
},
"generation_mode": "video",
"tool": "scene-animator-3d",
"generation_time": 1.5,
"created_at": 1_700_000_100,
"output_filename": "scene.mp4",
},
),
(
"_write_video_editor_screenshot_sidecar",
"video-editor-screenshot",
"frame.png",
{
"params": {
"video_editor_screenshot": {
"version": 1,
"source": "clip.mp4",
"source_name": "clip.mp4",
"time": 1.25,
},
"source": "video_editor_screenshot",
},
"generation_mode": "image",
"created_at": 1_700_000_100,
},
),
(
"_write_video_editor_export_sidecar",
"video-editor",
"edited.mp4",
{
"params": {
"video_editor": {"version": 2, "width": 1280, "height": 720, "fps": 30},
"source": "video_editor",
},
"generation_mode": "video",
"job_id": "video-edit-1",
"task_id": "task-video-editor-1",
"root_task_id": "task-root-1",
"workspace": "lab",
"created_at": 1_700_000_100,
},
),
(
"_write_comic_animatic_sidecar",
"comic-animatic",
"animatic.mp4",
{
"params": {
"source": "comic_animatic",
"comic_animatic": {"version": 1, "comic_id": "comic-1"},
},
"generation_mode": "video",
"job_id": "video-edit-2",
"task_id": "task-video-editor-2",
"root_task_id": "task-root-2",
"workspace": "lab",
"created_at": 1_700_000_100,
},
),
],
)
def test_editor_gallery_sidecar_publishes_canonical_manifest_without_invented_actor(
tmp_path, writer_name, tool, filename, sidecar,
):
artifact = tmp_path / filename
artifact.write_bytes(b"media")
write = _load_launch_function(
writer_name,
{
"os": os,
"time": time,
"publish_generation_sidecar": publish_generation_sidecar,
},
)
write(str(artifact), sidecar, "lab")
loaded = read_asset_manifest(artifact, workspace_id="lab")
raw = json.loads(artifact.with_suffix(".meta.json").read_text(encoding="utf-8"))
assert loaded is not None
assert raw["schema"] == SCHEMA_NAME
for key, value in sidecar.items():
assert raw[key] == value
assert loaded["origin"]["tool"] == tool
assert loaded["origin"]["actor"] == "unknown"
assert loaded["origin"]["workspace_id"] == "lab"
if "job_id" in sidecar:
assert loaded["execution"]["job_id"] == sidecar["job_id"]
assert loaded["execution"]["task_id"] == sidecar["task_id"]
assert loaded["execution"]["root_task_id"] == sidecar["root_task_id"]
first_id = loaded["asset"]["id"]
write(str(artifact), sidecar, "lab")
assert read_asset_manifest(artifact, workspace_id="lab")["asset"]["id"] == first_id
other = tmp_path / f"other-{filename}"
other.write_bytes(b"media-b")
write(str(other), sidecar, "lab")
assert read_asset_manifest(other, workspace_id="lab")["asset"]["id"] != first_id
assert read_asset_manifest(other, workspace_id="lab")["origin"]["tool"] == tool


def test_launch_runtime_has_one_global_policy_boundary_before_inference():
source = (Path(__file__).parents[1] / "app" / "_launch_runtime.py").read_text(
encoding="utf-8",
Expand Down Expand Up @@ -415,6 +526,10 @@ def function_source(name):
"_write_recast_shot_aware_sidecar",
"_write_repaint_shot_aware_sidecar",
"_write_outpaint_shot_aware_sidecar",
"_write_scene_recording_sidecar",
"_write_video_editor_screenshot_sidecar",
"_write_video_editor_export_sidecar",
"_write_comic_animatic_sidecar",
):
body = function_source(name)
assert "publish_generation_sidecar" in body
Expand Down
18 changes: 17 additions & 1 deletion tests/test_video_editor_scheduler_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

import pytest

from services.asset_manifest import SCHEMA_NAME


ROOT = Path(__file__).parents[1]
LAUNCH = ROOT / "app" / "_launch_runtime.py"
Expand Down Expand Up @@ -159,10 +161,17 @@ def default_render_animatic(_panels, output_path, *, progress, **_settings):
} for index, clip in enumerate(clips)],
},
)
services_module = _module("services", video_editor=video_editor_module)
import services.asset_manifest as asset_manifest_module

services_module = _module(
"services",
video_editor=video_editor_module,
asset_manifest=asset_manifest_module,
)
services_module.__path__ = []
monkeypatch.setitem(sys.modules, "services", services_module)
monkeypatch.setitem(sys.modules, "services.video_editor", video_editor_module)
monkeypatch.setitem(sys.modules, "services.asset_manifest", asset_manifest_module)

def workspace_dir(workspace=None) -> str:
workspace_calls.append(workspace)
Expand Down Expand Up @@ -209,8 +218,10 @@ def publish(job: dict, adapter: str) -> dict:
"_remove_video_editor_output_bundle",
"_finish_video_editor_cancelled",
"_video_editor_task_identity",
"_write_video_editor_export_sidecar",
"_run_video_editor_export",
"start_video_editor_export",
"_write_comic_animatic_sidecar",
"_run_comic_animatic",
"start_comic_animatic",
"cancel_video_editor_export",
Expand Down Expand Up @@ -430,13 +441,18 @@ def test_progress_and_terminal_publish_without_get_and_sidecar_keeps_task_hierar
assert output_path.exists()
assert sidecar_path.exists()
sidecar = json.loads(sidecar_path.read_text(encoding="utf-8"))
assert sidecar["schema"] == SCHEMA_NAME
assert sidecar["origin"]["actor"] == "unknown"
assert sidecar["job_id"] == response["job_id"]
assert sidecar["task_id"] == response["task_id"]
assert sidecar["root_task_id"] == response["root_task_id"]
if render_kind == "export":
assert sidecar["origin"]["tool"] == "video-editor"
editor = sidecar["params"]["video_editor"]
assert editor["version"] == 2
assert editor["source_manifest"]["clips"][0]["source"] == "clip.mp4"
else:
assert sidecar["origin"]["tool"] == "comic-animatic"


def test_canonical_adapter_exposes_real_lane_cancel_contract_and_control_route():
Expand Down