From a3d21e5e4da5f6c2a26b4b09aa7ce58656c17f06 Mon Sep 17 00:00:00 2001 From: VicFei Date: Sat, 7 Nov 2026 15:52:35 +0800 Subject: [PATCH] feat(skills): expose MiniMax resolution/duration in video-generation (#7912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(skills): expose MiniMax resolution/duration in video-generation Hailuo 2.3 accepts explicit resolution (768P/1080P) and duration (6/10s) in the /v1/video_generation body, but the skill sent neither — and its SKILL.md told the caller --aspect-ratio is ignored for MiniMax without offering any alternative, leaving cost/quality/length uncontrollable. Read MINIMAX_VIDEO_RESOLUTION and MINIMAX_VIDEO_DURATION from the environment and pass them through when set (unset keeps the API defaults byte-for-byte); values are passed through unvalidated beyond the integer check so newer tiers (H3 per-second 4-15s) need no script change. Document both knobs in SKILL.md with a draft/final example. Fixes #7911. * fix(skills): address MiniMax video override review --------- Co-authored-by: vicfei <4058491+vicfei@users.noreply.github.com> Co-authored-by: Willem Jiang --- README.md | 5 +++ skills/public/video-generation/AGENTS.md | 5 +++ skills/public/video-generation/SKILL.md | 27 ++++++++++-- .../video-generation/scripts/generate.py | 16 +++++++ tests/skills/test_video_generation.py | 44 ++++++++++++++++--- tests/skills/test_video_generation_cli.py | 10 ++++- .../test_video_generation_reference_mime.py | 2 + 7 files changed, 98 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7c2a221c0d..4ef02eab5f 100644 --- a/README.md +++ b/README.md @@ -1877,6 +1877,11 @@ Skills are what make DeerFlow do *almost anything*. A standard Agent Skill is a structured capability module — a Markdown file that defines a workflow, best practices, and references to supporting resources. DeerFlow ships with built-in skills for research, report generation, slide creation, web pages, image and video generation, and more. But the real power is extensibility: add your own skills, replace the built-in ones, or combine them into compound workflows. +For MiniMax, the [video-generation skill](skills/public/video-generation/SKILL.md) +accepts `MINIMAX_VIDEO_RESOLUTION` and `MINIMAX_VIDEO_DURATION` (integer seconds). +Unset overrides use the provider defaults. Invalid duration strings fail before +the script submits a request. + The video-generation skill derives reference-image MIME metadata from `.png`, `.jpg`, `.jpeg`, `.webp`, and `.gif` filename extensions (case-insensitive). A submitted reference with a missing or unrecognized extension raises a clear error diff --git a/skills/public/video-generation/AGENTS.md b/skills/public/video-generation/AGENTS.md index cfa0e286c0..5a57ba6e5e 100644 --- a/skills/public/video-generation/AGENTS.md +++ b/skills/public/video-generation/AGENTS.md @@ -9,3 +9,8 @@ Keep provider format acceptance separate from MIME recognition. The public-skill reference-MIME regressions mock HTTP calls, verify request metadata and exact input bytes for both providers, and reject unknown extensions before submission. Preserve the layout of unrelated script code. + +Omit blank MiniMax resolution and duration overrides from the request. Report +invalid duration strings with a `ValueError` naming `MINIMAX_VIDEO_DURATION` +before submission. The provider checks model-specific resolution and duration +combinations. Clear both override variables in offline video test fixtures. diff --git a/skills/public/video-generation/SKILL.md b/skills/public/video-generation/SKILL.md index 7e1640e8c9..3b37fc31fb 100644 --- a/skills/public/video-generation/SKILL.md +++ b/skills/public/video-generation/SKILL.md @@ -53,7 +53,7 @@ Parameters: - `--prompt-file`: Absolute path to JSON prompt file (required) - `--reference-images`: Absolute paths to reference images (optional). Images used by the provider must have a recognized filename extension: `.png`, `.jpg`, `.jpeg`, `.webp`, or `.gif` (case-insensitive). Missing or other extensions are rejected before submission. Filenames must match their contents: the script neither validates image contents nor converts bytes. Recognizing an extension does not guarantee that the selected provider accepts that format. - `--output-file`: Absolute path to output image file (required) -- `--aspect-ratio`: Aspect ratio of the generated image (optional, default: 16:9) +- `--aspect-ratio` sets the video aspect ratio for Gemini (optional, defaults to 16:9). [!NOTE] Do NOT read the python file, instead just call it with the parameters. @@ -113,6 +113,9 @@ Step 3: Use the image-generation skill to generate the reference image Load the image-generation skill and generate a single reference image `narnia-farewell-scene-01.jpg` according to the skill. Step 4: Use the generate.py script to generate the video + +With Gemini, set the aspect ratio on the command line. + ```bash python /mnt/skills/public/video-generation/scripts/generate.py \ --prompt-file /mnt/user-data/workspace/narnia-farewell-scene.json \ @@ -120,6 +123,17 @@ python /mnt/skills/public/video-generation/scripts/generate.py \ --output-file /mnt/user-data/outputs/narnia-farewell-scene-01.mp4 \ --aspect-ratio 16:9 ``` + +With MiniMax, set resolution and duration on the same command that runs the script. + +```bash +VIDEO_GENERATION_PROVIDER=minimax MINIMAX_VIDEO_RESOLUTION=1080P MINIMAX_VIDEO_DURATION=6 \ + python /mnt/skills/public/video-generation/scripts/generate.py \ + --prompt-file /mnt/user-data/workspace/narnia-farewell-scene.json \ + --reference-images /mnt/user-data/outputs/narnia-farewell-scene-01.jpg \ + --output-file /mnt/user-data/outputs/narnia-farewell-scene-01.mp4 +``` + > Do NOT read the python file, just call it with the parameters. ## Output Handling @@ -153,6 +167,11 @@ Auto-selected by environment variables (CLI unchanged): - Only `MINIMAX_API_KEY` set → MiniMax video (`/v1/video_generation`, async 3-step poll/download). - Force with `VIDEO_GENERATION_PROVIDER=gemini|minimax`. -MiniMax overrides: `MINIMAX_API_HOST` (default `https://api.minimaxi.com`), -`MINIMAX_VIDEO_MODEL` (default `MiniMax-Hailuo-2.3`). The first reference image is used -as MiniMax `first_frame_image`. MiniMax ignores `--aspect-ratio` (it uses resolution/duration). +MiniMax uses `MINIMAX_API_HOST` (default `https://api.minimaxi.com`) and +`MINIMAX_VIDEO_MODEL` (default `MiniMax-Hailuo-2.3`). It uses the first reference +image as `first_frame_image`. + +Set `MINIMAX_VIDEO_RESOLUTION` and `MINIMAX_VIDEO_DURATION` (integer seconds) to +override the API defaults. With Hailuo 2.3, use `768P` at 6 or 10 seconds, or +`1080P` at 6 seconds. For a draft and final pass, start at `768P` and switch to +`1080P` for the final 6-second clip. diff --git a/skills/public/video-generation/scripts/generate.py b/skills/public/video-generation/scripts/generate.py index ff86873d38..1f4a2fa07f 100644 --- a/skills/public/video-generation/scripts/generate.py +++ b/skills/public/video-generation/scripts/generate.py @@ -145,6 +145,22 @@ def _generate_video_minimax(prompt: str, reference_images: list[str], output_fil host = _minimax_host() auth = f"Bearer {minimax_api_key}" body = {"model": os.getenv("MINIMAX_VIDEO_MODEL", "MiniMax-Hailuo-2.3"), "prompt": prompt} + # Hailuo models take explicit resolution/duration in the request body. Values pass + # through unvalidated (beyond the integer check) so newer tiers with different + # allowlists — e.g. H3's per-second 4-15s durations — stay usable without a script + # change; the API rejects invalid combinations via base_resp. + resolution = os.getenv("MINIMAX_VIDEO_RESOLUTION", "").strip() + if resolution: + body["resolution"] = resolution + duration = os.getenv("MINIMAX_VIDEO_DURATION", "").strip() + if duration: + error = f"MINIMAX_VIDEO_DURATION must be an integer number of seconds (e.g. 6 or 10), got {duration!r}" + if not duration.isdecimal(): + raise ValueError(error) + try: + body["duration"] = int(duration) + except ValueError as exc: + raise ValueError(error) from exc if reference_images: body["first_frame_image"] = _to_data_url(reference_images[0]) response = requests.post( diff --git a/tests/skills/test_video_generation.py b/tests/skills/test_video_generation.py index f5fc2665d0..e9bead103e 100644 --- a/tests/skills/test_video_generation.py +++ b/tests/skills/test_video_generation.py @@ -12,9 +12,16 @@ vid = load("video-generation") @pytest.fixture(autouse=True) def clean_env(monkeypatch): - for k in ["GEMINI_API_KEY", "MINIMAX_API_KEY", "VIDEO_GENERATION_PROVIDER", - "MINIMAX_API_HOST", "MINIMAX_VIDEO_MODEL"]: - monkeypatch.delenv(k, raising=False) + for key in ( + "GEMINI_API_KEY", + "MINIMAX_API_KEY", + "VIDEO_GENERATION_PROVIDER", + "MINIMAX_API_HOST", + "MINIMAX_VIDEO_MODEL", + "MINIMAX_VIDEO_RESOLUTION", + "MINIMAX_VIDEO_DURATION", + ): + monkeypatch.delenv(key, raising=False) monkeypatch.setattr(vid.time, "sleep", lambda *_: None) @@ -41,8 +48,15 @@ def test_unknown_provider_raises(monkeypatch, tmp_path): vid.generate_video(str(pf), [], str(tmp_path / "v.mp4"), "16:9") -def test_minimax_full_flow(monkeypatch, tmp_path): +@pytest.mark.parametrize( + "video_settings", + [{}, {"resolution": "1080P", "duration": 6}], + ids=["defaults", "overrides"], +) +def test_minimax_full_flow(monkeypatch, tmp_path, video_settings): monkeypatch.setenv("MINIMAX_API_KEY", "m") + for field, value in video_settings.items(): + monkeypatch.setenv(f"MINIMAX_VIDEO_{field.upper()}", str(value)) posts = {} def fake_post(url, headers=None, json=None, **kw): @@ -74,10 +88,30 @@ def test_minimax_full_flow(monkeypatch, tmp_path): assert out.read_bytes() == b"MP4DATA" assert posts["url"].endswith("/v1/video_generation") - assert posts["json"]["model"] == "MiniMax-Hailuo-2.3" + assert posts["json"] == {"model": "MiniMax-Hailuo-2.3", "prompt": "a cat runs", **video_settings} + if "duration" in video_settings: + assert type(posts["json"]["duration"]) is int assert "successfully" in msg.lower() +@pytest.mark.parametrize("duration", ["abc", "2.5", "²"]) +def test_minimax_rejects_invalid_duration(monkeypatch, tmp_path, duration): + monkeypatch.setenv("MINIMAX_API_KEY", "m") + monkeypatch.setenv("MINIMAX_VIDEO_DURATION", duration) + + def fail_if_called(*args, **kwargs): + pytest.fail("Invalid duration should be rejected before submission") + + monkeypatch.setattr(vid.requests, "post", fail_if_called) + prompt = tmp_path / "prompt.json" + prompt.write_text("a cat runs", encoding="utf-8") + + with pytest.raises( + ValueError, match="MINIMAX_VIDEO_DURATION must be an integer number of seconds" + ): + vid.generate_video(str(prompt), [], str(tmp_path / "v.mp4")) + + def test_minimax_reference_first_frame(monkeypatch, tmp_path): monkeypatch.setenv("MINIMAX_API_KEY", "m") posts = {} diff --git a/tests/skills/test_video_generation_cli.py b/tests/skills/test_video_generation_cli.py index 41a0406a03..b064096c37 100644 --- a/tests/skills/test_video_generation_cli.py +++ b/tests/skills/test_video_generation_cli.py @@ -13,8 +13,14 @@ SCRIPT = Path(__file__).resolve().parents[2] / "skills/public/video-generation/s @pytest.fixture(autouse=True) -def clean_credentials(monkeypatch): - for key in ("GEMINI_API_KEY", "MINIMAX_API_KEY", "VIDEO_GENERATION_PROVIDER"): +def clean_env(monkeypatch): + for key in ( + "GEMINI_API_KEY", + "MINIMAX_API_KEY", + "VIDEO_GENERATION_PROVIDER", + "MINIMAX_VIDEO_RESOLUTION", + "MINIMAX_VIDEO_DURATION", + ): monkeypatch.delenv(key, raising=False) diff --git a/tests/skills/test_video_generation_reference_mime.py b/tests/skills/test_video_generation_reference_mime.py index 0f11241947..e8cc783aad 100644 --- a/tests/skills/test_video_generation_reference_mime.py +++ b/tests/skills/test_video_generation_reference_mime.py @@ -17,6 +17,8 @@ def _mock_provider(monkeypatch, provider): monkeypatch.setenv("GEMINI_API_KEY", "offline-test-key") monkeypatch.setenv("MINIMAX_API_KEY", "offline-test-key") monkeypatch.setenv("MINIMAX_API_HOST", "https://api.example.test") + monkeypatch.delenv("MINIMAX_VIDEO_RESOLUTION", raising=False) + monkeypatch.delenv("MINIMAX_VIDEO_DURATION", raising=False) captured = {"posts": [], "gets": []} def fake_post(url, *, json, **kwargs):