Signed-off-by: Nick Cao <ncao@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
"""
|
|
End-to-end tests for Flux2 Klein inpainting in online serving mode.
|
|
|
|
Uses /v1/images/edits endpoint which is the correct API for image inpainting.
|
|
"""
|
|
|
|
import base64
|
|
from io import BytesIO
|
|
|
|
import httpx
|
|
import pytest
|
|
from PIL import Image, ImageDraw
|
|
|
|
from tests.helpers.runtime import OmniServer, OmniServerParams
|
|
|
|
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
|
|
|
|
MODEL = "black-forest-labs/FLUX.2-klein-4B"
|
|
|
|
_HEIGHT = 512
|
|
_WIDTH = 512
|
|
_NUM_INFERENCE_STEPS = 4
|
|
|
|
|
|
def _get_diffusion_feature_cases(model: str):
|
|
return [
|
|
pytest.param(
|
|
OmniServerParams(
|
|
model=model,
|
|
server_args=["--tensor-parallel-size", "2", "--enable-cpu-offload"],
|
|
),
|
|
id="tp2_cpu_offload",
|
|
),
|
|
pytest.param(
|
|
OmniServerParams(
|
|
model=model,
|
|
server_args=["--tensor-parallel-size", "2"],
|
|
),
|
|
id="tp2_basic",
|
|
),
|
|
]
|
|
|
|
|
|
def _image_to_base64_jpeg(image: Image.Image) -> str:
|
|
buffer = BytesIO()
|
|
image.save(buffer, format="JPEG")
|
|
buffer.seek(0)
|
|
return base64.b64encode(buffer.read()).decode("utf-8")
|
|
|
|
|
|
def _create_test_mask_base64(width: int = _WIDTH, height: int = _HEIGHT) -> str:
|
|
mask = Image.new("L", (width, height), 0)
|
|
draw = ImageDraw.Draw(mask)
|
|
draw.rectangle([width // 4, height // 4, width * 3 // 4, height * 3 // 4], fill=255)
|
|
return _image_to_base64_jpeg(mask)
|
|
|
|
|
|
def _compare_images(img1: Image.Image, img2: Image.Image) -> bool:
|
|
return list(img1.getdata()) == list(img2.getdata())
|
|
|
|
|
|
def _send_edit_request(host: str, port: int, model: str, image_b64: str, mask_b64: str, prompt: str, **kwargs):
|
|
url = f"http://{host}:{port}/v1/images/edits"
|
|
files = {
|
|
"image": ("image.jpg", base64.b64decode(image_b64), "image/jpeg"),
|
|
"mask_image": ("mask.jpg", base64.b64decode(mask_b64), "image/jpeg"),
|
|
}
|
|
data = {"prompt": prompt, "model": model, **kwargs}
|
|
with httpx.Client(timeout=60.0) as client:
|
|
response = client.post(url, files=files, data=data)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
|
|
@pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True)
|
|
def test_flux2_klein_inpaint_basic(omni_server: OmniServer):
|
|
input_image_b64 = _image_to_base64_jpeg(Image.new("RGB", (_WIDTH, _HEIGHT), (128, 128, 128)))
|
|
mask_b64 = _create_test_mask_base64()
|
|
|
|
result = _send_edit_request(
|
|
host=omni_server.host,
|
|
port=omni_server.port,
|
|
model=MODEL,
|
|
image_b64=input_image_b64,
|
|
mask_b64=mask_b64,
|
|
prompt="Fill in the masked area with a beautiful garden",
|
|
guidance_scale=1.0,
|
|
num_inference_steps=_NUM_INFERENCE_STEPS,
|
|
n=1,
|
|
seed=42,
|
|
)
|
|
|
|
assert "data" in result and len(result["data"]) == 1
|
|
img_data = result["data"][0].get("b64_json") or result["data"][0].get("url", "").split(",")[-1]
|
|
img = Image.open(BytesIO(base64.b64decode(img_data)))
|
|
assert img.size == (_WIDTH, _HEIGHT)
|
|
|
|
|
|
@pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True)
|
|
def test_flux2_klein_inpaint_deterministic(omni_server: OmniServer):
|
|
input_image_b64 = _image_to_base64_jpeg(Image.new("RGB", (_WIDTH, _HEIGHT), (128, 128, 128)))
|
|
mask_b64 = _create_test_mask_base64()
|
|
prompt = "A red flower in a field"
|
|
|
|
result1 = _send_edit_request(
|
|
host=omni_server.host,
|
|
port=omni_server.port,
|
|
model=MODEL,
|
|
image_b64=input_image_b64,
|
|
mask_b64=mask_b64,
|
|
prompt=prompt,
|
|
guidance_scale=1.0,
|
|
num_inference_steps=_NUM_INFERENCE_STEPS,
|
|
n=1,
|
|
seed=12345,
|
|
)
|
|
|
|
result2 = _send_edit_request(
|
|
host=omni_server.host,
|
|
port=omni_server.port,
|
|
model=MODEL,
|
|
image_b64=input_image_b64,
|
|
mask_b64=mask_b64,
|
|
prompt=prompt,
|
|
guidance_scale=1.0,
|
|
num_inference_steps=_NUM_INFERENCE_STEPS,
|
|
n=1,
|
|
seed=12345,
|
|
)
|
|
|
|
img1_data = result1["data"][0].get("b64_json") or result1["data"][0].get("url", "").split(",")[-1]
|
|
img2_data = result2["data"][0].get("b64_json") or result2["data"][0].get("url", "").split(",")[-1]
|
|
|
|
img1 = Image.open(BytesIO(base64.b64decode(img1_data)))
|
|
img2 = Image.open(BytesIO(base64.b64decode(img2_data)))
|
|
|
|
assert _compare_images(img1, img2), (
|
|
"Same input with same seed should produce identical output. This is critical for offline/online consistency."
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True)
|
|
def test_flux2_klein_inpaint_multiple_outputs(omni_server: OmniServer):
|
|
input_image_b64 = _image_to_base64_jpeg(Image.new("RGB", (_WIDTH, _HEIGHT), (128, 128, 128)))
|
|
mask_b64 = _create_test_mask_base64()
|
|
|
|
result = _send_edit_request(
|
|
host=omni_server.host,
|
|
port=omni_server.port,
|
|
model=MODEL,
|
|
image_b64=input_image_b64,
|
|
mask_b64=mask_b64,
|
|
prompt="A beautiful landscape",
|
|
guidance_scale=1.0,
|
|
num_inference_steps=_NUM_INFERENCE_STEPS,
|
|
n=2,
|
|
seed=42,
|
|
)
|
|
|
|
assert "data" in result and len(result["data"]) == 2
|