From 9fa9b8259b69b9fbd03a5f6a9f26c18c645a27aa Mon Sep 17 00:00:00 2001 From: Alan Date: Fri, 4 Sep 2026 02:32:27 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat(video=5Fcompose):=20allow=20a=20j?= =?UTF-8?q?ob-scoped=20browser=20executable=20for=20Remotion=20renders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remotion renders default to locating a browser automatically (bundled Chromium or system default). On locked-down or heterogeneous machines (e.g. air-gapped Windows setups, pinned enterprise browser binaries, or multi-job workflows), passing an explicit browser binary is necessary. - Add remotion_browser_executable to VideoCompose.input_schema. - Add _append_remotion_browser_executable() helper validating file existence and appending --browser-executable=. - Wire into both standard _remotion_render and atelier render paths. - Forward from execute(operation="render") high-level entrypoint. Tests: test flag pass-through, fail-fast on nonexistent file, high-level forwarding, omitted flag case, and atelier mode pass-through. Closes #619 --- tests/tools/test_remotion_diagnostics.py | 119 +++++++++++++++++++++++ tools/video/video_compose.py | 37 +++++++ 2 files changed, 156 insertions(+) diff --git a/tests/tools/test_remotion_diagnostics.py b/tests/tools/test_remotion_diagnostics.py index 79db4633ca..1aa3eecb07 100644 --- a/tests/tools/test_remotion_diagnostics.py +++ b/tests/tools/test_remotion_diagnostics.py @@ -126,3 +126,122 @@ def fake_run_command(cmd, *a, **k): assert not any(str(c).startswith("--timeout") for c in seen["cmd"]) assert seen["timeout"] == 600 + + +def test_remotion_browser_executable_is_passed_through(tool, tmp_path, monkeypatch): + seen = {} + browser = tmp_path / "custom_chrome.exe" + browser.write_text("dummy binary") + + def fake_run_command(cmd, *a, **k): + seen["cmd"] = cmd + return None + + monkeypatch.setattr(tool, "run_command", fake_run_command) + tool._remotion_render( + { + "composition_data": {"cuts": []}, + "output_path": str(tmp_path / "out.mp4"), + "remotion_browser_executable": str(browser), + } + ) + + assert f"--browser-executable={browser.resolve()}" in seen["cmd"] + + +def test_remotion_browser_executable_fails_on_missing_file(tool, tmp_path): + missing = tmp_path / "nonexistent_chrome.exe" + result = tool._remotion_render( + { + "composition_data": {"cuts": []}, + "output_path": str(tmp_path / "out.mp4"), + "remotion_browser_executable": str(missing), + } + ) + + assert result.success is False + assert "remotion_browser_executable does not exist or is not a file" in result.error + + +def test_high_level_render_forwards_browser_executable(tool, tmp_path, monkeypatch): + captured = {} + monkeypatch.setattr(tool, "_pre_compose_validation", lambda *a, **k: None) + monkeypatch.setattr(tool, "_needs_remotion", lambda *a, **k: True) + + def fake_remotion_render(inputs): + captured.update(inputs) + from tools.base_tool import ToolResult + + return ToolResult(success=True, data={}, artifacts=[]) + + monkeypatch.setattr(tool, "_remotion_render", fake_remotion_render) + monkeypatch.setattr(tool, "_run_final_review", lambda *a, **k: {}) + + tool._render( + { + "edit_decisions": { + "render_runtime": "remotion", + "renderer_family": "explainer-data", + "cuts": [{"id": "c1", "source": "a1", "in_seconds": 0, "out_seconds": 2}], + }, + "asset_manifest": {"assets": [{"id": "a1", "path": "/tmp/a1.mp4"}]}, + "output_path": str(tmp_path / "out.mp4"), + "remotion_browser_executable": "/custom/browser", + } + ) + + assert captured.get("remotion_browser_executable") == "/custom/browser" + + +def test_no_browser_executable_flag_when_not_requested(tool, tmp_path, monkeypatch): + seen = {} + + def fake_run_command(cmd, *a, **k): + seen["cmd"] = cmd + return None + + monkeypatch.setattr(tool, "run_command", fake_run_command) + tool._remotion_render( + {"composition_data": {"cuts": []}, "output_path": str(tmp_path / "out.mp4")} + ) + + assert not any(str(c).startswith("--browser-executable") for c in seen["cmd"]) + + +def test_atelier_render_appends_browser_executable(tool, tmp_path, monkeypatch): + seen = {} + browser = tmp_path / "browser_atelier.exe" + browser.write_text("dummy binary") + + entry = tmp_path / "entry.tsx" + entry.write_text("// dummy") + + def fake_run_command(cmd, *a, **k): + seen["cmd"] = cmd + return None + + orig_exists = Path.exists + + def fake_exists(self): + if "remotion-composer" in str(self): + return True + return orig_exists(self) + + monkeypatch.setattr(tool, "run_command", fake_run_command) + monkeypatch.setattr(Path, "exists", fake_exists) + + tool._render_via_atelier( + inputs={ + "output_path": str(tmp_path / "out.mp4"), + "remotion_browser_executable": str(browser), + }, + edit_decisions={ + "bespoke": { + "entry": str(entry), + "composition_id": "CompId", + } + }, + ) + + assert f"--browser-executable={browser.resolve()}" in seen["cmd"] + diff --git a/tools/video/video_compose.py b/tools/video/video_compose.py index 7c2600e304..416642dd2c 100644 --- a/tools/video/video_compose.py +++ b/tools/video/video_compose.py @@ -203,6 +203,15 @@ class VideoCompose(BaseTool): "networks). The subprocess timeout is widened to match." ), }, + "remotion_browser_executable": { + "type": "string", + "description": ( + "Optional absolute path to a browser executable for Remotion " + "renders, passed through as `--browser-executable=`. " + "Use when running on machines without bundled Chromium or " + "when a pinned enterprise browser binary is required." + ), + }, }, } @@ -930,6 +939,23 @@ def visit(node: Any, parent_key: str | None = None) -> Any: visit(value) return len(staged_by_source) + @staticmethod + def _append_remotion_browser_executable( + cmd: list[str], inputs: dict[str, Any] + ) -> str | None: + """Validate and append a job-scoped Remotion browser executable.""" + requested = inputs.get("remotion_browser_executable") + if requested is None: + return None + browser = Path(str(requested)).expanduser().resolve() + if not browser.is_file(): + raise ValueError( + "remotion_browser_executable does not exist or is not a file: " + f"{browser}" + ) + cmd.append(f"--browser-executable={browser}") + return str(browser) + def _render_via_atelier( self, inputs: dict[str, Any], @@ -1021,6 +1047,10 @@ def _render_via_atelier( output_path.parent.mkdir(parents=True, exist_ok=True) cmd = ["npx", "remotion", "render", str(effective_entry), str(comp_id), str(output_path)] + try: + self._append_remotion_browser_executable(cmd, inputs) + except ValueError as e: + return ToolResult(success=False, error=str(e)) props_path = bespoke.get("props_path") if props_path: @@ -1668,6 +1698,8 @@ def _render(self, inputs: dict[str, Any]) -> ToolResult: # would only take effect on a direct _remotion_render() call. if inputs.get("remotion_timeout_ms") is not None: remotion_inputs["remotion_timeout_ms"] = inputs["remotion_timeout_ms"] + if inputs.get("remotion_browser_executable") is not None: + remotion_inputs["remotion_browser_executable"] = inputs["remotion_browser_executable"] if inputs.get("public_dir") is not None: remotion_inputs["public_dir"] = inputs["public_dir"] render_result = self._remotion_render(remotion_inputs) @@ -2062,6 +2094,11 @@ def _remotion_render(self, inputs: dict[str, Any]) -> ToolResult: if public_dir is not None: cmd.append(f"--public-dir={public_dir}") + try: + self._append_remotion_browser_executable(cmd, inputs) + except ValueError as e: + return ToolResult(success=False, error=str(e)) + # Apply media profile dimensions if profile_name: try: