From a7a7473d36e5d9ad30a30aea92f8ee423c1d9135 Mon Sep 17 00:00:00 2001 From: IAnMove <216241348+IAnMove@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:23:37 +0200 Subject: [PATCH] feat: publish asset-manifest v1 from editor and scene-recording sidecars Gallery sidecars for scene recording, Video Editor screenshot/export, and comic animatic keep their legacy keys and now write the canonical document through publish_generation_sidecar, without inventing actor. --- app/_launch_runtime.py | 98 ++++++++++++------ tests/test_execution_mode.py | 115 ++++++++++++++++++++++ tests/test_video_editor_scheduler_jobs.py | 18 +++- 3 files changed, 200 insertions(+), 31 deletions(-) diff --git a/app/_launch_runtime.py b/app/_launch_runtime.py index bb8737dd..a631874a 100644 --- a/app/_launch_runtime.py +++ b/app/_launch_runtime.py @@ -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(...), @@ -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) @@ -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.""" @@ -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}", @@ -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 @@ -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, @@ -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 @@ -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", diff --git a/tests/test_execution_mode.py b/tests/test_execution_mode.py index 6fb4d27a..ca05dfa4 100644 --- a/tests/test_execution_mode.py +++ b/tests/test_execution_mode.py @@ -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", @@ -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 diff --git a/tests/test_video_editor_scheduler_jobs.py b/tests/test_video_editor_scheduler_jobs.py index abb93c48..26a3ad3f 100644 --- a/tests/test_video_editor_scheduler_jobs.py +++ b/tests/test_video_editor_scheduler_jobs.py @@ -26,6 +26,8 @@ import pytest +from services.asset_manifest import SCHEMA_NAME + ROOT = Path(__file__).parents[1] LAUNCH = ROOT / "app" / "_launch_runtime.py" @@ -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) @@ -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", @@ -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():