mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-07 19:20:34 +00:00
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:
parent
ab168befae
commit
a3d21e5e4d
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user