"""Unit tests for InputValidationStage.preprocess_condition_image resolution logic.""" import unittest from unittest.mock import MagicMock, patch import numpy as np import torch from diffusers.pipelines.flux2.image_processor import Flux2ImageProcessor from PIL import Image from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType from sglang.multimodal_gen.configs.pipeline_configs.flux import Flux2PipelineConfig from sglang.multimodal_gen.configs.pipeline_configs.wan import ( WanI2V480PConfig, WanI2V720PConfig, ) from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams from sglang.multimodal_gen.runtime.pipelines.flux_2 import Flux2Pipeline from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req from sglang.multimodal_gen.runtime.pipelines_core.stages.input_validation import ( InputValidationStage, ) def _make_batch(condition_image: Image.Image, width=None, height=None) -> Req: """Create a minimal Req with a condition image and optional user dimensions.""" sp = SamplingParams( seed=42, num_outputs_per_prompt=1, width=width, height=height, ) batch = Req(sampling_params=sp, condition_image=condition_image) return batch def _make_server_args(pipeline_config): """Create a mock ServerArgs with the given pipeline config.""" sa = MagicMock() sa.pipeline_config = pipeline_config return sa class _DummyTI2IConfig: task_type = ModelTaskType.TI2I def __init__(self): self.vae_config = MagicMock() self.vae_config.get_vae_scale_factor.return_value = 8 def preprocess_vae_image(self, batch, vae_image_processor): return None def calculate_condition_image_size(self, image, width, height): return None def preprocess_condition_image( self, image, target_width, target_height, vae_image_processor ): return image, (target_width, target_height) def prepare_calculated_size(self, image): return image.size class TestCalculateDimensionsFromArea(unittest.TestCase): """Tests for InputValidationStage._calculate_dimensions_from_area.""" def test_square_aspect_ratio(self): # area=921600, aspect=1.0, mod=16 → sqrt(921600)=~960 w, h = InputValidationStage._calculate_dimensions_from_area(921600, 1.0, 16) self.assertEqual(w % 16, 0) self.assertEqual(h % 16, 0) self.assertEqual((w, h), (960, 960)) def test_16_9_aspect_ratio(self): # aspect = 720/1280 = 0.5625 w, h = InputValidationStage._calculate_dimensions_from_area(921600, 9 / 16, 16) self.assertEqual(w % 16, 0) self.assertEqual(h % 16, 0) self.assertEqual((w, h), (1280, 720)) def test_9_16_aspect_ratio(self): w, h = InputValidationStage._calculate_dimensions_from_area(921600, 16 / 9, 16) self.assertEqual(w % 16, 0) self.assertEqual(h % 16, 0) self.assertEqual((w, h), (720, 1280)) def test_mod_alignment(self): # Ensure dimensions are always multiples of mod_value w, h = InputValidationStage._calculate_dimensions_from_area(500000, 1.3, 16) self.assertEqual(w % 16, 0) self.assertEqual(h % 16, 0) class TestPreprocessConditionImageResolution(unittest.TestCase): """Tests for the WanI2V480PConfig branch of preprocess_condition_image. Verifies that: - Aspect ratio always comes from the condition image - User-specified width/height controls target area (scale) - Output is clamped to max_area when user dimensions exceed it - Dimensions are always mod-aligned """ def setUp(self): self.stage = InputValidationStage() def _run(self, config, img_w, img_h, user_w=None, user_h=None): """Run preprocess_condition_image and return (batch.width, batch.height).""" img = Image.new("RGB", (img_w, img_h), color="red") batch = _make_batch(img, width=user_w, height=user_h) server_args = _make_server_args(config) self.stage.preprocess_condition_image(batch, server_args, img_w, img_h) return batch.width, batch.height def test_720p_no_user_dims_16_9_image(self): """16:9 image, no user dims → 1280×720.""" w, h = self._run(WanI2V720PConfig(), 1920, 1080) self.assertEqual((w, h), (1280, 720)) def test_720p_no_user_dims_9_16_image(self): """9:16 image, no user dims → 720×1280.""" w, h = self._run(WanI2V720PConfig(), 1080, 1920) self.assertEqual((w, h), (720, 1280)) def test_720p_no_user_dims_square_image(self): """Square image, no user dims → ~960×960 (max_area=921600, sqrt≈960).""" w, h = self._run(WanI2V720PConfig(), 1024, 1024) self.assertEqual((w, h), (960, 960)) self.assertEqual(w % 16, 0) def test_720p_user_dims_equal_max_area_16_9_image(self): """16:9 image + user 1280×720 (=max_area) → 1280×720.""" w, h = self._run(WanI2V720PConfig(), 1920, 1080, 1280, 720) self.assertEqual((w, h), (1280, 720)) def test_720p_user_dims_equal_max_area_square_image(self): """Square image + user 1280×720 → still square (~960×960) because aspect ratio comes from image, not from user dimensions.""" w, h = self._run(WanI2V720PConfig(), 1024, 1024, 1280, 720) self.assertEqual((w, h), (960, 960)) def test_720p_user_dims_smaller_area(self): """Square image + user 832×480 → smaller square (target_area=399360).""" w, h = self._run(WanI2V720PConfig(), 1024, 1024, 832, 480) self.assertEqual((w, h), (624, 624)) self.assertEqual(w % 16, 0) def test_720p_user_dims_exceed_max_area(self): """4K request clamped to max_area.""" w, h = self._run(WanI2V720PConfig(), 1920, 1080, 3840, 2160) self.assertEqual(w % 16, 0) self.assertEqual(h % 16, 0) self.assertEqual((w, h), (1280, 720)) def test_480p_no_user_dims_16_9_image(self): """480p config, 16:9 image → area-based calc from max_area=399360.""" w, h = self._run(WanI2V480PConfig(), 1920, 1080) # max_area=480*832=399360, aspect=9/16 → (832, 464) due to rounding self.assertEqual(w % 16, 0) self.assertEqual(h % 16, 0) self.assertEqual((w, h), (832, 464)) def test_condition_image_resized_to_output_dims(self): """Condition image is resized to match output dimensions.""" img = Image.new("RGB", (1920, 1080), color="blue") batch = _make_batch(img) server_args = _make_server_args(WanI2V720PConfig()) self.stage.preprocess_condition_image(batch, server_args, 1920, 1080) self.assertEqual(batch.condition_image.size, (batch.width, batch.height)) def test_list_condition_image_takes_first(self): """List of condition images → uses first one.""" img1 = Image.new("RGB", (1920, 1080), color="red") img2 = Image.new("RGB", (800, 600), color="green") batch = _make_batch(img1) batch.condition_image = [img1, img2] server_args = _make_server_args(WanI2V720PConfig()) self.stage.preprocess_condition_image(batch, server_args, 1920, 1080) self.assertIsInstance(batch.condition_image, Image.Image) self.assertEqual((batch.width, batch.height), (1280, 720)) class TestFlux2ConditionImagePreprocess(unittest.TestCase): def test_matches_official_flux2_image_processor(self): config = Flux2PipelineConfig() config.vae_config.arch_config.vae_scale_factor = 8 processor = Flux2ImageProcessor(vae_scale_factor=16) image = Image.fromarray( np.arange(1792 * 1216 * 3, dtype=np.uint8).reshape(1216, 1792, 3), mode="RGB", ) size = config.calculate_condition_image_size(image, image.width, image.height) self.assertEqual(size, (1232, 832)) processed, processed_size = config.preprocess_condition_image( image, size[0], size[1], processor ) official_image = processor._resize_to_target_area(image, 1024 * 1024) expected_width = (official_image.width // 16) * 16 expected_height = (official_image.height // 16) * 16 expected = processor.preprocess( official_image, height=expected_height, width=expected_width, resize_mode="crop", ) self.assertEqual(processed_size, (expected_width, expected_height)) self.assertTrue(torch.equal(processed, expected)) @patch.object(Flux2Pipeline, "add_standard_ti2i_stages") def test_runtime_pipeline_uses_flux2_image_processor(self, mock_add_stages): pipeline = object.__new__(Flux2Pipeline) server_args = MagicMock() server_args.pipeline_config.vae_config.arch_config.vae_scale_factor = 8 Flux2Pipeline.create_pipeline_stages(pipeline, server_args) processor = mock_add_stages.call_args.kwargs["vae_image_processor"] self.assertIsInstance(processor, Flux2ImageProcessor) self.assertIs( processor, mock_add_stages.call_args.kwargs["image_vae_stage_kwargs"][ "vae_image_processor" ], ) class TestFlux2TI2ISizeResolution(unittest.TestCase): def setUp(self): self.stage = InputValidationStage() self.config = _DummyTI2IConfig() def test_uses_condition_image_size_when_width_height_not_explicit(self): image = Image.new("RGB", (1255, 833), color="red") batch = _make_batch(image) batch.extra = {} self.stage.preprocess_condition_image( batch, _make_server_args(self.config), image.width, image.height, ) self.assertEqual((batch.width, batch.height), (1248, 832)) def test_preserves_explicit_width_height_for_ti2i(self): image = Image.new("RGB", (1255, 833), color="red") batch = _make_batch(image, width=768, height=512) batch.extra = {"explicit_fields": ["width", "height"]} self.stage.preprocess_condition_image( batch, _make_server_args(self.config), image.width, image.height, ) self.assertEqual((batch.width, batch.height), (768, 512)) if __name__ == "__main__": unittest.main()