# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """ Unit tests for OpenAI-compatible video generation endpoints. """ import io import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from PIL import Image from pydantic import ValidationError from pytest_mock import MockerFixture from vllm_omni.entrypoints.openai.api_server import router from vllm_omni.entrypoints.openai.protocol.videos import VideoGenerationRequest, VideoResponseFormat from vllm_omni.entrypoints.openai.serving_video import OmniOpenAIServingVideo pytestmark = [pytest.mark.core_model, pytest.mark.cpu] class MockVideoResult: def __init__(self, videos): self.multimodal_output = {"video": videos} class FakeAsyncOmni: def __init__(self): self.stage_list = ["diffusion"] self.captured_prompt = None self.captured_sampling_params_list = None async def generate(self, prompt, request_id, sampling_params_list): self.captured_prompt = prompt self.captured_sampling_params_list = sampling_params_list num_outputs = sampling_params_list[0].num_outputs_per_prompt videos = [object() for _ in range(num_outputs)] yield MockVideoResult(videos) @pytest.fixture def test_client(): app = FastAPI() app.include_router(router) app.state.openai_serving_video = OmniOpenAIServingVideo.for_diffusion( diffusion_engine=FakeAsyncOmni(), model_name="Wan-AI/Wan2.2-T2V-A14B-Diffusers", ) return TestClient(app) def _make_test_image_bytes(size=(64, 64)) -> bytes: image = Image.new("RGB", size, color="blue") buf = io.BytesIO() image.save(buf, format="PNG") return buf.getvalue() def test_t2v_video_generation_form(test_client, mocker: MockerFixture): fps_values = [] def _fake_encode(video, fps): fps_values.append(fps) return "Zg==" mocker.patch( "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", side_effect=_fake_encode, ) response = test_client.post( "/v1/videos", data={ "prompt": "A cat runs across the street.", "size": "640x360", "seconds": "2", "fps": "12", "n": "2", }, ) assert response.status_code == 200 data = response.json() assert "data" in data and len(data["data"]) == 2 assert all(item["b64_json"] == "Zg==" for item in data["data"]) engine = test_client.app.state.openai_serving_video._engine_client captured = engine.captured_sampling_params_list[0] assert captured.num_outputs_per_prompt == 2 assert captured.width == 640 assert captured.height == 360 assert captured.num_frames == 24 assert captured.fps == 12 assert fps_values == [12, 12] def test_i2v_video_generation_form(test_client, mocker: MockerFixture): image_bytes = _make_test_image_bytes((48, 32)) mocker.patch( "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", return_value="Zg==", ) response = test_client.post( "/v1/videos", data={"prompt": "A bear playing with yarn."}, files={"input_reference": ("input.png", image_bytes, "image/png")}, ) assert response.status_code == 200 data = response.json() assert "data" in data and len(data["data"]) == 1 assert data["data"][0]["b64_json"] == "Zg==" engine = test_client.app.state.openai_serving_video._engine_client prompt = engine.captured_prompt assert "multi_modal_data" in prompt assert "image" in prompt["multi_modal_data"] input_image = prompt["multi_modal_data"]["image"] assert isinstance(input_image, Image.Image) assert input_image.size == (48, 32) def test_seconds_defaults_fps_and_frames(test_client, mocker: MockerFixture): fps_values = [] def _fake_encode(video, fps): fps_values.append(fps) return "Zg==" mocker.patch( "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", side_effect=_fake_encode, ) response = test_client.post( "/v1/videos", data={ "prompt": "A bird flying.", "seconds": "3", }, ) assert response.status_code == 200 engine = test_client.app.state.openai_serving_video._engine_client captured = engine.captured_sampling_params_list[0] assert captured.num_frames == 72 assert captured.fps == 24 assert fps_values == [24] def test_size_param_sets_width_height(test_client, mocker: MockerFixture): mocker.patch( "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", return_value="Zg==", ) response = test_client.post( "/v1/videos", data={ "prompt": "size test", "size": "320x240", }, ) assert response.status_code == 200 engine = test_client.app.state.openai_serving_video._engine_client captured = engine.captured_sampling_params_list[0] assert captured.width == 320 assert captured.height == 240 def test_sampling_params_pass_through(test_client, mocker: MockerFixture): mocker.patch( "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", return_value="Zg==", ) response = test_client.post( "/v1/videos", data={ "prompt": "param pass", "num_inference_steps": "30", "guidance_scale": "6.5", "guidance_scale_2": "8.0", "true_cfg_scale": "4.0", "boundary_ratio": "0.7", "flow_shift": "0.25", }, ) assert response.status_code == 200 engine = test_client.app.state.openai_serving_video._engine_client captured = engine.captured_sampling_params_list[0] assert captured.num_inference_steps == 30 assert captured.guidance_scale == 6.5 assert captured.guidance_scale_2 == 8.0 assert captured.true_cfg_scale == 4.0 assert captured.boundary_ratio == 0.7 assert captured.extra_args["flow_shift"] == 0.25 def test_missing_handler_returns_503(): app = FastAPI() app.include_router(router) app.state.openai_serving_video = None client = TestClient(app) response = client.post( "/v1/videos", data={"prompt": "no handler"}, ) assert response.status_code == 503 assert "not initialized" in response.json()["detail"].lower() def test_missing_prompt_returns_422(test_client): response = test_client.post( "/v1/videos", data={"size": "320x240"}, ) assert response.status_code == 422 def test_invalid_size_format_raises_validation_error(test_client): with pytest.raises(ValidationError): test_client.post( "/v1/videos", data={"prompt": "bad size", "size": "invalid"}, ) def test_invalid_size_parse_returns_500(test_client): response = test_client.post( "/v1/videos", data={"prompt": "bad size", "size": "640x"}, ) assert response.status_code == 500 assert "video generation failed" in response.json()["detail"].lower() def test_invalid_response_format_raises_validation_error(test_client): with pytest.raises(ValidationError): test_client.post( "/v1/videos", data={"prompt": "bad format", "response_format": "url"}, ) def test_invalid_seconds_returns_422(test_client): response = test_client.post( "/v1/videos", data={"prompt": "bad seconds", "seconds": "abc"}, ) assert response.status_code == 422 def test_invalid_n_raises_validation_error(test_client): with pytest.raises(ValidationError): test_client.post( "/v1/videos", data={"prompt": "bad n", "n": "0"}, ) with pytest.raises(ValidationError): test_client.post( "/v1/videos", data={"prompt": "bad n", "n": "5"}, ) def test_negative_prompt_and_seed_pass_through(test_client, mocker: MockerFixture): mocker.patch( "vllm_omni.entrypoints.openai.serving_video.encode_video_base64", return_value="Zg==", ) response = test_client.post( "/v1/videos", data={ "prompt": "snowy mountain", "negative_prompt": "blurry", "seed": "123", }, ) assert response.status_code == 200 engine = test_client.app.state.openai_serving_video._engine_client captured_prompt = engine.captured_prompt captured_params = engine.captured_sampling_params_list[0] assert captured_prompt["negative_prompt"] == "blurry" assert captured_params.seed == 123 def test_invalid_lora_returns_400(test_client): response = test_client.post( "/v1/videos", data={ "prompt": "lora test", "lora": '{"name": "bad-lora"}', }, ) assert response.status_code == 400 assert "lora" in response.json()["detail"].lower() def test_video_request_validation(): req = VideoGenerationRequest(prompt="test") assert req.prompt == "test" assert req.n == 1 assert req.response_format == VideoResponseFormat.B64_JSON with pytest.raises(ValueError): VideoGenerationRequest(prompt="test", response_format="url") with pytest.raises(ValueError): VideoGenerationRequest(prompt="test", size="invalid") with pytest.raises(ValueError): VideoGenerationRequest(prompt="test", seconds="abc") with pytest.raises(ValueError): VideoGenerationRequest(prompt="test", n=0)