From a5e99ab0c4a2f27a12540bb6beba49f7502ea969 Mon Sep 17 00:00:00 2001 From: Chris Z <535257617@qq.com> Date: Sun, 13 Sep 2026 18:09:15 +0800 Subject: [PATCH] fix(video): forward --aspect-ratio into the Gemini Veo request (#5388) * fix(video): forward --aspect-ratio into the Gemini Veo request The video-generation skill accepts --aspect-ratio and passes it into generate_video(), but the Gemini branch drops the value: _generate_video_gemini has no aspect_ratio parameter and builds the predictLongRunning body with only instances, so every Veo request runs at the default ratio regardless of the CLI flag. Forward the value as parameters.aspectRatio and cover it with a monkeypatch regression test that captures the outgoing request body. * fix(skills): avoid false credential findings in video generation --------- Co-authored-by: Willem Jiang --- backend/tests/test_skill_review_core.py | 8 ++++ skills/public/video-generation/SKILL.md | 3 ++ .../video-generation/scripts/generate.py | 31 +++++++------ tests/skills/test_video_generation.py | 45 +++++++++++++++++++ 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/backend/tests/test_skill_review_core.py b/backend/tests/test_skill_review_core.py index 179794d56..16b9c4106 100644 --- a/backend/tests/test_skill_review_core.py +++ b/backend/tests/test_skill_review_core.py @@ -16,6 +16,14 @@ from deerflow.skills.review.renderer import build_static_report, render_report_m CONTRACTS_DIR = Path(__file__).resolve().parents[2] / "contracts" / "skill_review" +def test_video_generation_runtime_credentials_pass_skill_review(): + skill_dir = Path(__file__).resolve().parents[2] / "skills" / "public" / "video-generation" + facts = analyze_skill_package(LocalDirectoryReader(skill_dir).read()) + + assert facts["summary"]["blockers"] == 0 + assert facts["summary"]["errors"] == 0, facts["findings"] + + def _write(path: Path, text: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") diff --git a/skills/public/video-generation/SKILL.md b/skills/public/video-generation/SKILL.md index 7d8d55b24..2c74d0023 100644 --- a/skills/public/video-generation/SKILL.md +++ b/skills/public/video-generation/SKILL.md @@ -140,6 +140,9 @@ After generation: ## Providers (Gemini / MiniMax) +Provider credentials are read from the runtime environment, not embedded in the +script. Do not put their values in prompt files or command-line arguments. + Auto-selected by environment variables (CLI unchanged): - `GEMINI_API_KEY` set → Gemini Veo (default, unchanged). diff --git a/skills/public/video-generation/scripts/generate.py b/skills/public/video-generation/scripts/generate.py index 94e26e34e..505706162 100644 --- a/skills/public/video-generation/scripts/generate.py +++ b/skills/public/video-generation/scripts/generate.py @@ -107,11 +107,11 @@ def _download(url: str, output_file: str) -> None: def _generate_video_minimax( prompt: str, reference_images: list[str], output_file: str ) -> str: - api_key = os.getenv("MINIMAX_API_KEY") - if not api_key: + minimax_api_key = os.getenv("MINIMAX_API_KEY") + if not minimax_api_key: return "MINIMAX_API_KEY is not set" host = _minimax_host() - auth = f"Bearer {api_key}" + auth = f"Bearer {minimax_api_key}" body = {"model": os.getenv("MINIMAX_VIDEO_MODEL", "MiniMax-Hailuo-2.3"), "prompt": prompt} if reference_images: body["first_frame_image"] = _to_data_url(reference_images[0]) @@ -132,10 +132,10 @@ def _generate_video_minimax( def download(url: str, output_file: str) -> None: - api_key = os.getenv("GEMINI_API_KEY") - if not api_key: + gemini_api_key = os.getenv("GEMINI_API_KEY") + if not gemini_api_key: raise ValueError("GEMINI_API_KEY is not set") - response = requests.get(url, headers={"x-goog-api-key": api_key}, timeout=300) + response = requests.get(url, headers={"x-goog-api-key": gemini_api_key}, timeout=300) response.raise_for_status() _ensure_output_dir(output_file) with open(output_file, "wb") as f: @@ -143,10 +143,13 @@ def download(url: str, output_file: str) -> None: def _generate_video_gemini( - prompt: str, reference_images: list[str], output_file: str + prompt: str, reference_images: list[str], output_file: str, aspect_ratio: str = "16:9" ) -> str: reference_payload = [] - request_json = {"instances": [{"prompt": prompt}]} + request_json = { + "instances": [{"prompt": prompt}], + "parameters": {"aspectRatio": aspect_ratio}, + } for reference_image in reference_images: with open(reference_image, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") @@ -156,12 +159,12 @@ def _generate_video_gemini( ) if reference_payload: request_json["instances"][0]["referenceImages"] = reference_payload - api_key = os.getenv("GEMINI_API_KEY") - if not api_key: + gemini_api_key = os.getenv("GEMINI_API_KEY") + if not gemini_api_key: return "GEMINI_API_KEY is not set" response = requests.post( "https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview:predictLongRunning", - headers={"x-goog-api-key": api_key, "Content-Type": "application/json"}, + headers={"x-goog-api-key": gemini_api_key, "Content-Type": "application/json"}, json=request_json, timeout=60, ) @@ -171,7 +174,7 @@ def _generate_video_gemini( while True: response = requests.get( f"https://generativelanguage.googleapis.com/v1beta/{operation_name}", - headers={"x-goog-api-key": api_key}, + headers={"x-goog-api-key": gemini_api_key}, timeout=30, ) response.raise_for_status() @@ -199,7 +202,9 @@ def generate_video( # MiniMax video uses resolution/duration, not aspect_ratio; aspect_ratio ignored. return _generate_video_minimax(prompt, reference_images, output_file) if provider in ("gemini", "google"): - return _generate_video_gemini(prompt, reference_images, output_file) + return _generate_video_gemini( + prompt, reference_images, output_file, aspect_ratio + ) raise ValueError(f"Unknown video provider: {provider!r} (use 'gemini' or 'minimax')") diff --git a/tests/skills/test_video_generation.py b/tests/skills/test_video_generation.py index 07e2894e7..e016e1155 100644 --- a/tests/skills/test_video_generation.py +++ b/tests/skills/test_video_generation.py @@ -48,14 +48,17 @@ def test_minimax_full_flow(monkeypatch, tmp_path): def fake_post(url, headers=None, json=None, **kw): posts["url"] = url posts["json"] = json + assert headers["Authorization"] == "Bearer m" return FakeResp({"task_id": "T1", "base_resp": {"status_code": 0}}) def fake_get(url, headers=None, params=None, **kw): if url.endswith("/v1/query/video_generation"): + assert headers["Authorization"] == "Bearer m" assert params["task_id"] == "T1" return FakeResp({"status": "Success", "file_id": "F1", "base_resp": {"status_code": 0}}) if url.endswith("/v1/files/retrieve"): + assert headers["Authorization"] == "Bearer m" assert params["file_id"] == "F1" return FakeResp({"file": {"download_url": "https://dl/v.mp4"}, "base_resp": {"status_code": 0}}) @@ -166,6 +169,7 @@ def test_gemini_download_writes_nested_dir(monkeypatch, tmp_path): monkeypatch.setenv("GEMINI_API_KEY", "g") def fake_get(url, headers=None, **kw): + assert headers["x-goog-api-key"] == "g" return FakeResp(content=b"VIDEO") monkeypatch.setattr(vid.requests, "get", fake_get) @@ -185,3 +189,44 @@ def test_gemini_post_raises_on_http_error(monkeypatch, tmp_path): pf.write_text("a cat", encoding="utf-8") with pytest.raises(requests.HTTPError): vid.generate_video(str(pf), [], str(tmp_path / "v.mp4"), "16:9") + + +def test_gemini_forwards_aspect_ratio_to_predict_request(monkeypatch, tmp_path): + monkeypatch.setenv("GEMINI_API_KEY", "g") + captured = {} + + def fake_post(url, headers=None, json=None, **kw): + captured["url"] = url + captured["json"] = json + assert headers["x-goog-api-key"] == "g" + return FakeResp(json_data={"name": "operations/op"}) + + def fake_get(url, headers=None, **kw): + assert headers["x-goog-api-key"] == "g" + return FakeResp( + json_data={ + "done": True, + "response": { + "generateVideoResponse": { + "generatedSamples": [ + {"video": {"uri": "https://example.test/v.mp4"}} + ] + } + }, + }, + ) + + def fake_download(uri, output_file): + with open(output_file, "wb") as f: + f.write(b"video") + + monkeypatch.setattr(vid.requests, "post", fake_post) + monkeypatch.setattr(vid.requests, "get", fake_get) + monkeypatch.setattr(vid, "download", fake_download) + pf = tmp_path / "p.json" + pf.write_text("a tall cat", encoding="utf-8") + + vid.generate_video(str(pf), [], str(tmp_path / "v.mp4"), "9:16") + + assert "predictLongRunning" in captured["url"] + assert captured["json"]["parameters"]["aspectRatio"] == "9:16"