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 <willem.jiang@gmail.com>
This commit is contained in:
Chris Z 2026-09-13 18:09:15 +08:00 committed by GitHub
parent a22c6169b3
commit a5e99ab0c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 74 additions and 13 deletions

View File

@ -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")

View File

@ -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).

View File

@ -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')")

View File

@ -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"