feat(skills): expose MiniMax resolution/duration in video-generation (#7912)

* 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 <willem.jiang@gmail.com>
This commit is contained in:
VicFei 2026-11-07 15:52:35 +08:00 committed by GitHub
parent ab168befae
commit a3d21e5e4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 98 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {}

View File

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

View File

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