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"