mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
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:
parent
a22c6169b3
commit
a5e99ab0c4
@ -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"
|
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:
|
def _write(path: Path, text: str) -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(text, encoding="utf-8")
|
path.write_text(text, encoding="utf-8")
|
||||||
|
|||||||
@ -140,6 +140,9 @@ After generation:
|
|||||||
|
|
||||||
## Providers (Gemini / MiniMax)
|
## 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):
|
Auto-selected by environment variables (CLI unchanged):
|
||||||
|
|
||||||
- `GEMINI_API_KEY` set → Gemini Veo (default, unchanged).
|
- `GEMINI_API_KEY` set → Gemini Veo (default, unchanged).
|
||||||
|
|||||||
@ -107,11 +107,11 @@ def _download(url: str, output_file: str) -> None:
|
|||||||
def _generate_video_minimax(
|
def _generate_video_minimax(
|
||||||
prompt: str, reference_images: list[str], output_file: str
|
prompt: str, reference_images: list[str], output_file: str
|
||||||
) -> str:
|
) -> str:
|
||||||
api_key = os.getenv("MINIMAX_API_KEY")
|
minimax_api_key = os.getenv("MINIMAX_API_KEY")
|
||||||
if not api_key:
|
if not minimax_api_key:
|
||||||
return "MINIMAX_API_KEY is not set"
|
return "MINIMAX_API_KEY is not set"
|
||||||
host = _minimax_host()
|
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}
|
body = {"model": os.getenv("MINIMAX_VIDEO_MODEL", "MiniMax-Hailuo-2.3"), "prompt": prompt}
|
||||||
if reference_images:
|
if reference_images:
|
||||||
body["first_frame_image"] = _to_data_url(reference_images[0])
|
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:
|
def download(url: str, output_file: str) -> None:
|
||||||
api_key = os.getenv("GEMINI_API_KEY")
|
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
||||||
if not api_key:
|
if not gemini_api_key:
|
||||||
raise ValueError("GEMINI_API_KEY is not set")
|
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()
|
response.raise_for_status()
|
||||||
_ensure_output_dir(output_file)
|
_ensure_output_dir(output_file)
|
||||||
with open(output_file, "wb") as f:
|
with open(output_file, "wb") as f:
|
||||||
@ -143,10 +143,13 @@ def download(url: str, output_file: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _generate_video_gemini(
|
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:
|
) -> str:
|
||||||
reference_payload = []
|
reference_payload = []
|
||||||
request_json = {"instances": [{"prompt": prompt}]}
|
request_json = {
|
||||||
|
"instances": [{"prompt": prompt}],
|
||||||
|
"parameters": {"aspectRatio": aspect_ratio},
|
||||||
|
}
|
||||||
for reference_image in reference_images:
|
for reference_image in reference_images:
|
||||||
with open(reference_image, "rb") as f:
|
with open(reference_image, "rb") as f:
|
||||||
image_b64 = base64.b64encode(f.read()).decode("utf-8")
|
image_b64 = base64.b64encode(f.read()).decode("utf-8")
|
||||||
@ -156,12 +159,12 @@ def _generate_video_gemini(
|
|||||||
)
|
)
|
||||||
if reference_payload:
|
if reference_payload:
|
||||||
request_json["instances"][0]["referenceImages"] = reference_payload
|
request_json["instances"][0]["referenceImages"] = reference_payload
|
||||||
api_key = os.getenv("GEMINI_API_KEY")
|
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
||||||
if not api_key:
|
if not gemini_api_key:
|
||||||
return "GEMINI_API_KEY is not set"
|
return "GEMINI_API_KEY is not set"
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview:predictLongRunning",
|
"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,
|
json=request_json,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
)
|
)
|
||||||
@ -171,7 +174,7 @@ def _generate_video_gemini(
|
|||||||
while True:
|
while True:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"https://generativelanguage.googleapis.com/v1beta/{operation_name}",
|
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,
|
timeout=30,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@ -199,7 +202,9 @@ def generate_video(
|
|||||||
# MiniMax video uses resolution/duration, not aspect_ratio; aspect_ratio ignored.
|
# MiniMax video uses resolution/duration, not aspect_ratio; aspect_ratio ignored.
|
||||||
return _generate_video_minimax(prompt, reference_images, output_file)
|
return _generate_video_minimax(prompt, reference_images, output_file)
|
||||||
if provider in ("gemini", "google"):
|
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')")
|
raise ValueError(f"Unknown video provider: {provider!r} (use 'gemini' or 'minimax')")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -48,14 +48,17 @@ def test_minimax_full_flow(monkeypatch, tmp_path):
|
|||||||
def fake_post(url, headers=None, json=None, **kw):
|
def fake_post(url, headers=None, json=None, **kw):
|
||||||
posts["url"] = url
|
posts["url"] = url
|
||||||
posts["json"] = json
|
posts["json"] = json
|
||||||
|
assert headers["Authorization"] == "Bearer m"
|
||||||
return FakeResp({"task_id": "T1", "base_resp": {"status_code": 0}})
|
return FakeResp({"task_id": "T1", "base_resp": {"status_code": 0}})
|
||||||
|
|
||||||
def fake_get(url, headers=None, params=None, **kw):
|
def fake_get(url, headers=None, params=None, **kw):
|
||||||
if url.endswith("/v1/query/video_generation"):
|
if url.endswith("/v1/query/video_generation"):
|
||||||
|
assert headers["Authorization"] == "Bearer m"
|
||||||
assert params["task_id"] == "T1"
|
assert params["task_id"] == "T1"
|
||||||
return FakeResp({"status": "Success", "file_id": "F1",
|
return FakeResp({"status": "Success", "file_id": "F1",
|
||||||
"base_resp": {"status_code": 0}})
|
"base_resp": {"status_code": 0}})
|
||||||
if url.endswith("/v1/files/retrieve"):
|
if url.endswith("/v1/files/retrieve"):
|
||||||
|
assert headers["Authorization"] == "Bearer m"
|
||||||
assert params["file_id"] == "F1"
|
assert params["file_id"] == "F1"
|
||||||
return FakeResp({"file": {"download_url": "https://dl/v.mp4"},
|
return FakeResp({"file": {"download_url": "https://dl/v.mp4"},
|
||||||
"base_resp": {"status_code": 0}})
|
"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")
|
monkeypatch.setenv("GEMINI_API_KEY", "g")
|
||||||
|
|
||||||
def fake_get(url, headers=None, **kw):
|
def fake_get(url, headers=None, **kw):
|
||||||
|
assert headers["x-goog-api-key"] == "g"
|
||||||
return FakeResp(content=b"VIDEO")
|
return FakeResp(content=b"VIDEO")
|
||||||
|
|
||||||
monkeypatch.setattr(vid.requests, "get", fake_get)
|
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")
|
pf.write_text("a cat", encoding="utf-8")
|
||||||
with pytest.raises(requests.HTTPError):
|
with pytest.raises(requests.HTTPError):
|
||||||
vid.generate_video(str(pf), [], str(tmp_path / "v.mp4"), "16:9")
|
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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user