[Feature] Align MiniMax H3 official input matrix (#5752)
Signed-off-by: david6666666 <530634352@qq.com>
This commit is contained in:
1 parent
6a604be610
commit
ae7bee0c43
21 files changed
+1887
-331
No files matched your search
@@ -17,7 +17,7 @@ from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
from pytest_mock import MockerFixture
|
||||
@@ -62,10 +62,14 @@ class FakeAsyncOmni:
|
||||
def __init__(self):
|
||||
self.stage_configs = [SimpleNamespace(stage_type="diffusion")]
|
||||
self.default_sampling_params_list = [OmniDiffusionSamplingParams()]
|
||||
self.model_class_name = "WanPipeline"
|
||||
self.captured_prompt = None
|
||||
self.captured_reference_video_bytes = None
|
||||
self.captured_sampling_params_list = None
|
||||
|
||||
def get_diffusion_od_config(self):
|
||||
return SimpleNamespace(model_class_name=self.model_class_name)
|
||||
|
||||
async def generate(self, prompt, request_id, sampling_params_list):
|
||||
self.captured_prompt = prompt
|
||||
self.captured_sampling_params_list = sampling_params_list
|
||||
@@ -487,7 +491,7 @@ def test_v2v_video_generation_form(test_client, mocker: MockerFixture):
|
||||
assert input_video[0].size == (32, 24)
|
||||
|
||||
|
||||
def test_v2v_video_generation_with_video_reference_form(test_client, mocker: MockerFixture):
|
||||
def test_r9_typed_video_reference_form(test_client, mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"fake-video",
|
||||
@@ -506,8 +510,53 @@ def test_v2v_video_generation_with_video_reference_form(test_client, mocker: Moc
|
||||
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
input_video = engine.captured_prompt["multi_modal_data"]["video"]
|
||||
assert len(input_video) == 2
|
||||
assert input_video[0].size == (32, 24)
|
||||
assert len(input_video) == 1
|
||||
assert all(isinstance(item, str) for item in input_video)
|
||||
assert engine.captured_reference_video_bytes is not None
|
||||
assert len(engine.captured_reference_video_bytes) == 1
|
||||
assert b"ftyp" in engine.captured_reference_video_bytes[0][:32]
|
||||
|
||||
|
||||
def test_r5_mixed_image_video_audio_references_and_fanout(test_client, mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"fake-video",
|
||||
)
|
||||
test_client.app.state.openai_serving_video._engine_client.model_class_name = "MiniMaxH3Pipeline"
|
||||
response = test_client.post(
|
||||
"/v1/videos",
|
||||
data={
|
||||
"prompt": "Use both references.",
|
||||
"image_reference": json.dumps(
|
||||
[
|
||||
{"image_url": _make_test_image_data_url((40, 24))},
|
||||
{"image_url": _make_test_image_data_url((40, 24))},
|
||||
]
|
||||
),
|
||||
"video_reference": json.dumps(
|
||||
{"video_url": _make_test_video_data_url((32, 24), 2)},
|
||||
),
|
||||
"audio_reference": json.dumps(
|
||||
[{"audio_url": "data:audio/mp3;base64,ZmFrZQ=="}],
|
||||
),
|
||||
"extra_params": json.dumps({"task": "ref2va", "frame_indices": [0, -1], "start_time_seconds": 1.5}),
|
||||
"num_outputs_per_prompt": "2",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
video_id = response.json()["id"]
|
||||
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
multi_modal_data = engine.captured_prompt["multi_modal_data"]
|
||||
assert len(multi_modal_data["image"]) == 2
|
||||
assert len(multi_modal_data["video"]) == 1
|
||||
assert all(isinstance(item, str) for item in multi_modal_data["video"])
|
||||
assert engine.captured_reference_video_bytes is not None
|
||||
assert len(engine.captured_reference_video_bytes) == 1
|
||||
assert b"ftyp" in engine.captured_reference_video_bytes[0][:32]
|
||||
assert isinstance(multi_modal_data["audio"], str)
|
||||
assert engine.captured_sampling_params_list[0].num_outputs_per_prompt == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("endpoint", ["/v1/videos", "/v1/videos/sync"])
|
||||
@@ -520,6 +569,7 @@ def test_multi_video_generation_preserves_uploaded_files_until_generation(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"fake-video",
|
||||
)
|
||||
test_client.app.state.openai_serving_video._engine_client.model_class_name = "MiniMaxH3Pipeline"
|
||||
response = test_client.post(
|
||||
endpoint,
|
||||
data={
|
||||
@@ -527,8 +577,10 @@ def test_multi_video_generation_preserves_uploaded_files_until_generation(
|
||||
"extra_params": json.dumps({"task": "ref2va", "duration": 15.0}),
|
||||
},
|
||||
files=[
|
||||
("input_references", ("subject.png", _make_test_image_bytes((40, 24)), "image/png")),
|
||||
("input_references", ("subject.mp4", b"subject-video", "video/mp4")),
|
||||
("input_references", ("background.mov", b"background-video", "video/quicktime")),
|
||||
("input_references", ("voice.wav", b"reference-audio", "audio/wav")),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -544,6 +596,8 @@ def test_multi_video_generation_preserves_uploaded_files_until_generation(
|
||||
assert engine.captured_reference_video_bytes == [b"subject-video", b"background-video"]
|
||||
assert len(input_videos) == 2
|
||||
assert all(not Path(path).exists() for path in input_videos)
|
||||
assert isinstance(engine.captured_prompt["multi_modal_data"]["image"], Image.Image)
|
||||
assert isinstance(engine.captured_prompt["multi_modal_data"]["audio"], str)
|
||||
assert engine.captured_sampling_params_list[0].extra_args["task"] == "ref2va"
|
||||
assert engine.captured_sampling_params_list[0].extra_args["duration"] == 15.0
|
||||
|
||||
@@ -1200,17 +1254,67 @@ def test_rejects_input_reference_and_image_reference_together(test_client):
|
||||
assert "only one of input_reference, image_reference, or video_reference" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_rejects_image_reference_and_video_reference_together(test_client):
|
||||
def test_r10_typed_image_and_video_reference_form(test_client, mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"fake-video",
|
||||
)
|
||||
test_client.app.state.openai_serving_video._engine_client.model_class_name = "MiniMaxH3Pipeline"
|
||||
response = test_client.post(
|
||||
"/v1/videos",
|
||||
data={
|
||||
"prompt": "bad refs",
|
||||
"image_reference": '{"image_url": "https://example.com/cat.png"}',
|
||||
"video_reference": '{"video_url": "https://example.com/cat.mp4"}',
|
||||
"prompt": "use both references",
|
||||
"image_reference": json.dumps({"image_url": _make_test_image_data_url((40, 24))}),
|
||||
"video_reference": json.dumps({"video_url": _make_test_video_data_url((32, 24), 2)}),
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
_wait_for_status(test_client, response.json()["id"], VideoGenerationStatus.COMPLETED.value)
|
||||
|
||||
|
||||
def test_generic_video_model_rejects_mixed_image_and_video_references(test_client):
|
||||
response = test_client.post(
|
||||
"/v1/videos/sync",
|
||||
data={
|
||||
"prompt": "use both references",
|
||||
"image_reference": json.dumps({"image_url": _make_test_image_data_url((40, 24))}),
|
||||
"video_reference": json.dumps({"video_url": _make_test_video_data_url((32, 24), 2)}),
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "only one of input_reference, image_reference, or video_reference" in response.json()["detail"].lower()
|
||||
assert "does not support mixed image and video" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
def test_h3_multipart_rejects_bmp_image_reference(test_client):
|
||||
image = Image.new("RGB", (64, 64), color="blue")
|
||||
image_buffer = io.BytesIO()
|
||||
image.save(image_buffer, format="BMP")
|
||||
|
||||
test_client.app.state.openai_serving_video._engine_client.model_class_name = "MiniMaxH3Pipeline"
|
||||
response = test_client.post(
|
||||
"/v1/videos/sync",
|
||||
data={"prompt": "reject unsupported image container", "extra_params": '{"task":"ref2va"}'},
|
||||
files=[("input_references", ("reference.bmp", image_buffer.getvalue(), "image/bmp"))],
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "must use jpg" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_h3_upload_limit_checks_declared_size_before_read():
|
||||
class OversizedUpload:
|
||||
size = api_server.MINIMAX_H3_MAX_REFERENCE_IMAGE_BYTES + 1
|
||||
|
||||
async def read(self, _size):
|
||||
raise AssertionError("the oversized upload must be rejected before reading")
|
||||
|
||||
with pytest.raises(HTTPException, match="size limit"):
|
||||
await api_server._read_upload_limited(
|
||||
OversizedUpload(),
|
||||
max_bytes=api_server.MINIMAX_H3_MAX_REFERENCE_IMAGE_BYTES,
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_seconds_returns_422(test_client):
|
||||
|
||||
Reference in new issue
Block a user