diff --git a/examples/offline_inference/image_to_video/image_to_video.py b/examples/offline_inference/image_to_video/image_to_video.py index 28e6b7447..59644d1c1 100644 --- a/examples/offline_inference/image_to_video/image_to_video.py +++ b/examples/offline_inference/image_to_video/image_to_video.py @@ -61,7 +61,9 @@ from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args from vllm_omni.entrypoints.omni import Omni from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.model_extras import ( - adapt_image_to_video_prompt, + build_image_to_video_prompt as build_model_image_to_video_prompt, +) +from vllm_omni.model_extras import ( get_extra_body_params, get_model_class_name, ) @@ -542,7 +544,7 @@ def main(): negative_prompt=negative_prompt, media_inputs=media_inputs, ) - prompt_dict = adapt_image_to_video_prompt( + prompt_dict = build_model_image_to_video_prompt( model_class_name=model_class_name, prompt=prompt_dict, height=height, diff --git a/examples/offline_inference/text_to_image/text_to_image.py b/examples/offline_inference/text_to_image/text_to_image.py index fad89b085..e6f26e7b9 100644 --- a/examples/offline_inference/text_to_image/text_to_image.py +++ b/examples/offline_inference/text_to_image/text_to_image.py @@ -21,7 +21,9 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.lora.request import LoRARequest from vllm_omni.lora.utils import stable_lora_int_id from vllm_omni.model_extras import ( - adapt_text_to_image_prompt, + build_text_to_image_prompt as build_model_text_to_image_prompt, +) +from vllm_omni.model_extras import ( get_extra_body_params, get_model_class_name, should_init_extra_args_for_non_diffusion_stages, @@ -519,7 +521,7 @@ def main(): prompt=args.prompt, negative_prompt=args.negative_prompt, ) - prompt_dict = adapt_text_to_image_prompt( + prompt_dict = build_model_text_to_image_prompt( model_class_name=model_class_name, prompt=prompt_dict, height=args.height, diff --git a/examples/online_serving/image_to_video/README.md b/examples/online_serving/image_to_video/README.md index f18cea1c8..8f5b65061 100644 --- a/examples/online_serving/image_to_video/README.md +++ b/examples/online_serving/image_to_video/README.md @@ -1,6 +1,9 @@ # Image-To-Video -This example demonstrates how to deploy the Wan2.2 image-to-video model for online video generation using vLLM-Omni. +This shared example demonstrates online image-to-video generation with +vLLM-Omni. The startup script defaults to Wan2.2, but `MODEL` can select any +supported image-to-video model. The client sends the canonical Videos API +request; the server side owns model-specific prompt processing. ## Start Server @@ -20,9 +23,7 @@ bash run_server.sh The script allows overriding: - `MODEL` (default: `Wan-AI/Wan2.2-I2V-A14B-Diffusers`) -- `PORT` (default: `8091`) -- `BOUNDARY_RATIO` (default: `0.875`) -- `FLOW_SHIFT` (default: `12.0`) +- `PORT` (default: `8099`) - `CACHE_BACKEND` (default: `none`) - `ENABLE_CACHE_DIT_SUMMARY` (default: `0`) diff --git a/examples/online_serving/image_to_video/run_curl_image_to_video.sh b/examples/online_serving/image_to_video/run_curl_image_to_video.sh index 6f6a6f96d..40ac9bc6b 100644 --- a/examples/online_serving/image_to_video/run_curl_image_to_video.sh +++ b/examples/online_serving/image_to_video/run_curl_image_to_video.sh @@ -1,14 +1,24 @@ #!/bin/bash -# Wan2.2 image-to-video curl example using the async video job API. +# Shared image-to-video curl example using the async video job API. set -euo pipefail INPUT_IMAGE="${INPUT_IMAGE:-../../offline_inference/image_to_video/qwen-bear.png}" BASE_URL="${BASE_URL:-http://localhost:8099}" -OUTPUT_PATH="${OUTPUT_PATH:-wan22_i2v_output.mp4}" +OUTPUT_PATH="${OUTPUT_PATH:-image_to_video_output.mp4}" +PROMPT="${PROMPT:-A bear playing with yarn, smooth motion}" NEGATIVE_PROMPT="${NEGATIVE_PROMPT:-}" SAMPLE_SOLVER="${SAMPLE_SOLVER:-}" POLL_INTERVAL="${POLL_INTERVAL:-2}" +VIDEO_SECONDS="${VIDEO_SECONDS:-}" +SIZE="${SIZE:-}" +FPS="${FPS:-}" +NUM_INFERENCE_STEPS="${NUM_INFERENCE_STEPS:-}" +GUIDANCE_SCALE="${GUIDANCE_SCALE:-}" +GUIDANCE_SCALE_2="${GUIDANCE_SCALE_2:-}" +BOUNDARY_RATIO="${BOUNDARY_RATIO:-}" +FLOW_SHIFT="${FLOW_SHIFT:-}" +SEED="${SEED:-}" if [ ! -f "$INPUT_IMAGE" ]; then echo "Input image not found: $INPUT_IMAGE" @@ -18,19 +28,28 @@ fi create_cmd=( curl -sS -X POST "${BASE_URL}/v1/videos" -H "Accept: application/json" - -F "prompt=A bear playing with yarn, smooth motion" + -F "prompt=${PROMPT}" -F "input_reference=@${INPUT_IMAGE}" - -F "seconds=2" - -F "size=832x480" - -F "fps=16" - -F "num_inference_steps=40" - -F "guidance_scale=1.0" - -F "guidance_scale_2=1.0" - -F "boundary_ratio=0.875" - -F "flow_shift=12.0" - -F "seed=42" ) +append_optional_field() { + local name="$1" + local value="$2" + if [ -n "${value}" ]; then + create_cmd+=(-F "${name}=${value}") + fi +} + +append_optional_field "seconds" "${VIDEO_SECONDS}" +append_optional_field "size" "${SIZE}" +append_optional_field "fps" "${FPS}" +append_optional_field "num_inference_steps" "${NUM_INFERENCE_STEPS}" +append_optional_field "guidance_scale" "${GUIDANCE_SCALE}" +append_optional_field "guidance_scale_2" "${GUIDANCE_SCALE_2}" +append_optional_field "boundary_ratio" "${BOUNDARY_RATIO}" +append_optional_field "flow_shift" "${FLOW_SHIFT}" +append_optional_field "seed" "${SEED}" + if [ -n "${NEGATIVE_PROMPT}" ]; then create_cmd+=(-F "negative_prompt=${NEGATIVE_PROMPT}") fi diff --git a/examples/online_serving/image_to_video/run_server.sh b/examples/online_serving/image_to_video/run_server.sh index a488f5340..2cb24d841 100644 --- a/examples/online_serving/image_to_video/run_server.sh +++ b/examples/online_serving/image_to_video/run_server.sh @@ -1,12 +1,12 @@ #!/bin/bash -# Wan2.2 image-to-video server start script +# Shared image-to-video server start script MODEL="${MODEL:-Wan-AI/Wan2.2-I2V-A14B-Diffusers}" PORT="${PORT:-8099}" CACHE_BACKEND="${CACHE_BACKEND:-none}" ENABLE_CACHE_DIT_SUMMARY="${ENABLE_CACHE_DIT_SUMMARY:-0}" -echo "Starting Wan2.2 I2V server..." +echo "Starting image-to-video server..." echo "Model: $MODEL" echo "Port: $PORT" echo "Cache backend: $CACHE_BACKEND" diff --git a/examples/online_serving/text_to_image/README.md b/examples/online_serving/text_to_image/README.md index 17d377ea3..2c9bc2cb6 100644 --- a/examples/online_serving/text_to_image/README.md +++ b/examples/online_serving/text_to_image/README.md @@ -1,6 +1,9 @@ # Text-To-Image -This example demonstrates how to deploy Qwen-Image model for online image generation service using vLLM-Omni. +This shared example demonstrates online text-to-image generation with vLLM-Omni. +The startup script defaults to Qwen-Image, but `MODEL` can select any supported +text-to-image model. Clients send the canonical Images API request; the server +side owns model-specific prompt processing. ## Start Server @@ -226,8 +229,8 @@ count, use `size` and `n` rather than `height`, `width`, or | `height` | int | None | Image height in pixels | | `width` | int | None | Image width in pixels | | `size` | str | None | Image size (e.g., "1024x1024") | -| `num_inference_steps` | int | 50 | Number of denoising steps | -| `true_cfg_scale` | float | 4.0 | Qwen-Image CFG scale | +| `num_inference_steps` | int | model | Number of denoising steps | +| `true_cfg_scale` | float | model | True CFG scale when supported | | `seed` | int | None | Random seed (reproducible) | | `negative_prompt` | str | None | Negative prompt | | `num_outputs_per_prompt` | int | 1 | Number of images to generate | diff --git a/examples/online_serving/text_to_image/gradio_demo.py b/examples/online_serving/text_to_image/gradio_demo.py index c8329c02a..e42be80ea 100644 --- a/examples/online_serving/text_to_image/gradio_demo.py +++ b/examples/online_serving/text_to_image/gradio_demo.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Qwen-Image Gradio Demo for online serving. +Shared text-to-image Gradio demo for online serving. Usage: python gradio_demo.py [--server http://localhost:8091] [--port 7860] @@ -77,9 +77,9 @@ def generate_image( def create_demo(server_url: str): """Create Gradio demo interface.""" - with gr.Blocks(title="Qwen-Image Demo") as demo: - gr.Markdown("# Qwen-Image Online Generation") - gr.Markdown("Generate images using Qwen-Image model") + with gr.Blocks(title="Text-to-Image Demo") as demo: + gr.Markdown("# Text-to-Image Online Generation") + gr.Markdown("Generate images using the model configured on the server") with gr.Row(): with gr.Column(scale=1): @@ -115,7 +115,6 @@ def create_demo(server_url: str): label="Inference Steps", minimum=10, maximum=100, - # Default steps aligned with run_curl_text_to_image.sh to 100 value=100, step=5, ) @@ -173,7 +172,7 @@ def create_demo(server_url: str): def main(): - parser = argparse.ArgumentParser(description="Qwen-Image Gradio Demo") + parser = argparse.ArgumentParser(description="Shared text-to-image Gradio demo") parser.add_argument("--server", default="http://localhost:8091", help="Server URL") parser.add_argument("--port", type=int, default=7860, help="Gradio port") parser.add_argument("--share", action="store_true", help="Create public link") diff --git a/examples/online_serving/text_to_image/openai_chat_client.py b/examples/online_serving/text_to_image/openai_chat_client.py index f3c43086a..f70faae78 100644 --- a/examples/online_serving/text_to_image/openai_chat_client.py +++ b/examples/online_serving/text_to_image/openai_chat_client.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Qwen-Image OpenAI-compatible image generation client. +Shared OpenAI-compatible text-to-image generation client. Usage: python openai_chat_client.py --prompt "A beautiful landscape" --output output.png @@ -39,7 +39,7 @@ def generate_image( height: Image height in pixels width: Image width in pixels steps: Number of diffusion steps - true_cfg_scale: Qwen-Image CFG scale + true_cfg_scale: True CFG scale for models that support it seed: Random seed negative_prompt: Negative prompt num_outputs_per_prompt: Number of images to generate @@ -70,7 +70,7 @@ def generate_image( payload["num_inference_steps"] = steps if true_cfg_scale is not None: payload["true_cfg_scale"] = true_cfg_scale - if negative_prompt: + if negative_prompt is not None: payload["negative_prompt"] = negative_prompt if seed is not None: payload["seed"] = seed @@ -115,15 +115,15 @@ def generate_image( def main(): - parser = argparse.ArgumentParser(description="Qwen-Image chat client") + parser = argparse.ArgumentParser(description="Shared text-to-image client") parser.add_argument("--prompt", "-p", default="a cup of coffee on the table", help="Text prompt") - parser.add_argument("--output", "-o", default="qwen_image_output.png", help="Output file") + parser.add_argument("--output", "-o", default="text_to_image_output.png", help="Output file") parser.add_argument("--server", "-s", default="http://localhost:8091", help="Server URL") - parser.add_argument("--height", type=int, default=1024, help="Image height") - parser.add_argument("--width", type=int, default=1024, help="Image width") - parser.add_argument("--steps", type=int, default=50, help="Inference steps") - parser.add_argument("--cfg-scale", type=float, default=4.0, help="True CFG scale") - parser.add_argument("--seed", type=int, default=0, help="Random seed") + parser.add_argument("--height", type=int, help="Image height; defaults to the model setting") + parser.add_argument("--width", type=int, help="Image width; defaults to the model setting") + parser.add_argument("--steps", type=int, help="Inference steps; defaults to the model setting") + parser.add_argument("--cfg-scale", type=float, help="True CFG scale when supported") + parser.add_argument("--seed", type=int, help="Random seed") parser.add_argument("--negative", help="Negative prompt") parser.add_argument("--lora-path", default=None, help="Server-local LoRA adapter folder (PEFT format)") diff --git a/examples/online_serving/text_to_image/run_curl_text_to_image.sh b/examples/online_serving/text_to_image/run_curl_text_to_image.sh index 151df8948..a3961e66e 100755 --- a/examples/online_serving/text_to_image/run_curl_text_to_image.sh +++ b/examples/online_serving/text_to_image/run_curl_text_to_image.sh @@ -1,10 +1,21 @@ #!/bin/bash -# Qwen-Image text-to-image curl example +# Shared text-to-image curl example -curl -X POST http://localhost:8091/v1/images/generations \ +set -euo pipefail + +BASE_URL="${BASE_URL:-http://localhost:8091}" +PROMPT="${PROMPT:-a dragon laying over the spine of the Green Mountains of Vermont}" +SIZE="${SIZE:-}" +SEED="${SEED:-}" +OUTPUT_PATH="${OUTPUT_PATH:-text_to_image_output.png}" + +curl -sS -X POST "${BASE_URL}/v1/images/generations" \ -H "Content-Type: application/json" \ - -d '{ - "prompt": "a dragon laying over the spine of the Green Mountains of Vermont", - "size": "1024x1024", - "seed": 42 - }' | jq -r '.data[0].b64_json' | base64 -d > dragon.png + -d "$(jq -n --arg prompt "${PROMPT}" --arg size "${SIZE}" --arg seed "${SEED}" \ + '{prompt: $prompt} + + (if $size == "" then {} else {size: $size} end) + + (if $seed == "" then {} else {seed: ($seed | tonumber)} end)')" \ + | jq -r '.data[0].b64_json' \ + | base64 -d > "${OUTPUT_PATH}" + +echo "Saved image to ${OUTPUT_PATH}" diff --git a/examples/online_serving/text_to_image/run_server.sh b/examples/online_serving/text_to_image/run_server.sh index b25337a0e..84f63e24e 100755 --- a/examples/online_serving/text_to_image/run_server.sh +++ b/examples/online_serving/text_to_image/run_server.sh @@ -1,10 +1,10 @@ #!/bin/bash -# Qwen-Image online serving startup script +# Shared text-to-image online serving startup script MODEL="${MODEL:-Qwen/Qwen-Image}" PORT="${PORT:-8091}" -echo "Starting Qwen-Image server..." +echo "Starting text-to-image server..." echo "Model: $MODEL" echo "Port: $PORT" diff --git a/tests/entrypoints/openai_api/test_video_server.py b/tests/entrypoints/openai_api/test_video_server.py index f876f2921..23e9d5b8d 100644 --- a/tests/entrypoints/openai_api/test_video_server.py +++ b/tests/entrypoints/openai_api/test_video_server.py @@ -433,7 +433,7 @@ def test_i2v_video_generation_form(test_client, mocker: MockerFixture): assert input_image.size == (48, 32) -def test_i2v_online_applies_model_specific_prompt_adapter() -> None: +def test_i2v_online_applies_model_specific_prompt_builder() -> None: engine = FakeAsyncOmni() engine.model_class_name = "WanVACEPipeline" handler = OmniOpenAIServingVideo.for_diffusion( @@ -451,7 +451,7 @@ def test_i2v_online_applies_model_specific_prompt_adapter() -> None: height=16, num_frames=5, ), - "vace-online-adapter", + "vace-online-builder", reference_image=ReferenceImage(image), ) ) diff --git a/tests/model_extras/test_model_extras.py b/tests/model_extras/test_model_extras.py index 3ca5ccbc2..fde3ea878 100644 --- a/tests/model_extras/test_model_extras.py +++ b/tests/model_extras/test_model_extras.py @@ -11,9 +11,9 @@ from PIL import Image from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.model_extras import ( - adapt_image_to_video_prompt, - adapt_text_to_image_prompt, build_image_to_image_prompt, + build_image_to_video_prompt, + build_text_to_image_prompt, build_x_to_text_prompt, get_extra_body_params, get_extra_output_params, @@ -281,7 +281,7 @@ def test_ming_flash_omni_declared_extra_args_route_into_sampling_params() -> Non @pytest.mark.core_model @pytest.mark.cpu def test_ming_flash_omni_text_to_image_prompt_builder() -> None: - assert adapt_text_to_image_prompt( + assert build_text_to_image_prompt( "MingImagePipeline", { "prompt": "Please draw a cute cat.", @@ -302,7 +302,7 @@ def test_ming_flash_omni_text_to_image_prompt_builder() -> None: } # target_h/w are omitted when height/width are not supplied so the # pipeline's 1024x1024 default still applies. - assert adapt_text_to_image_prompt( + assert build_text_to_image_prompt( "MingImagePipeline", {"prompt": "Draw a poster.", "modalities": ["image"]}, ) == { @@ -424,7 +424,7 @@ def test_unknown_pipeline_has_empty_extra_registry() -> None: @pytest.mark.core_model @pytest.mark.cpu def test_bagel_text_to_image_prompt_builder() -> None: - assert adapt_text_to_image_prompt( + assert build_text_to_image_prompt( "BagelPipeline", { "prompt": "a cat", @@ -470,7 +470,7 @@ def test_bagel_image_to_image_prompt_builder() -> None: def test_unknown_pipeline_preserves_canonical_text_to_image_prompt() -> None: canonical_prompt = {"prompt": "a cat", "modalities": ["image"]} assert ( - adapt_text_to_image_prompt( + build_text_to_image_prompt( "UnknownPipeline", canonical_prompt, height=512, @@ -497,7 +497,7 @@ def test_unknown_pipeline_uses_default_image_to_image_prompt() -> None: def _build_vace_prompt(media_inputs: dict[str, Any], *, num_frames: int = 5) -> dict: - return adapt_image_to_video_prompt( + return build_image_to_video_prompt( "WanVACEPipeline", { "prompt": "a bird flying", @@ -591,7 +591,7 @@ def test_mammothmoda2_extra_registry_declares_request_and_response_params() -> N assert get_extra_output_params(model_class_name) == frozenset() assert should_init_extra_args_for_non_diffusion_stages(model_class_name) is True - wrapper_prompt = adapt_text_to_image_prompt( + wrapper_prompt = build_text_to_image_prompt( "MammothModa2ForConditionalGeneration", {"prompt": "a cat", "modalities": ["image"]}, height=256, @@ -605,7 +605,7 @@ def test_mammothmoda2_extra_registry_declares_request_and_response_params() -> N def test_mammothmoda2_text_to_image_prompt_builder() -> None: # Image dims are converted to the AR grid (width/16 x height/16); the negative # prompt is ignored (MammothModa2 t2i uses CFG, not an explicit negative path). - assert adapt_text_to_image_prompt( + assert build_text_to_image_prompt( "MammothModa2DiTPipeline", { "prompt": "a cat", diff --git a/vllm_omni/entrypoints/openai/serving_video.py b/vllm_omni/entrypoints/openai/serving_video.py index a86205360..033df7078 100644 --- a/vllm_omni/entrypoints/openai/serving_video.py +++ b/vllm_omni/entrypoints/openai/serving_video.py @@ -30,7 +30,7 @@ from vllm_omni.entrypoints.openai.stage_params import ( from vllm_omni.entrypoints.openai.utils import get_stage_type, parse_lora_request from vllm_omni.entrypoints.openai.video_api_utils import _encode_video_bytes, encode_video_base64 from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt -from vllm_omni.model_extras import adapt_image_to_video_prompt +from vllm_omni.model_extras import build_image_to_video_prompt from vllm_omni.outputs.output_metadata import ( DiffusionMetadataMapping, DiffusionMultimodalOutput, @@ -259,7 +259,7 @@ class OmniOpenAIServingVideo: model_class_name = getattr(self._get_diffusion_od_config(), "model_class_name", None) prompt = cast( OmniTextPrompt, - adapt_image_to_video_prompt( + build_image_to_video_prompt( model_class_name=model_class_name, prompt=prompt, height=gen_params.height, diff --git a/vllm_omni/model_extras/__init__.py b/vllm_omni/model_extras/__init__.py index 6ea807475..1a3d71870 100644 --- a/vllm_omni/model_extras/__init__.py +++ b/vllm_omni/model_extras/__init__.py @@ -2,9 +2,9 @@ # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from vllm_omni.model_extras.registry import ( - adapt_image_to_video_prompt, - adapt_text_to_image_prompt, build_image_to_image_prompt, + build_image_to_video_prompt, + build_text_to_image_prompt, build_x_to_text_prompt, get_extra_body_params, get_extra_output_params, @@ -14,9 +14,9 @@ from vllm_omni.model_extras.registry import ( ) __all__ = [ - "adapt_image_to_video_prompt", - "adapt_text_to_image_prompt", "build_image_to_image_prompt", + "build_image_to_video_prompt", + "build_text_to_image_prompt", "build_x_to_text_prompt", "get_extra_body_params", "get_extra_output_params", diff --git a/vllm_omni/model_extras/registry.py b/vllm_omni/model_extras/registry.py index 486550c9b..4c41c5920 100644 --- a/vllm_omni/model_extras/registry.py +++ b/vllm_omni/model_extras/registry.py @@ -227,7 +227,7 @@ _EXTRA_SPECS: dict[str, dict[str, Any]] = { } # Multi-stage discovery reports the top-level wrapper rather than its DiT -# submodule, so both names must resolve to the same request adapters. +# submodule, so both names must resolve to the same request builders. _EXTRA_SPECS["MammothModa2ForConditionalGeneration"] = _EXTRA_SPECS["MammothModa2DiTPipeline"] _EXTRA_SPECS["Mammothmoda2Model"] = _EXTRA_SPECS["MammothModa2DiTPipeline"] @@ -267,13 +267,13 @@ def should_init_extra_args_for_non_diffusion_stages(model_class_name: str | None return bool(spec and spec.get("init_extra_args_for_non_diffusion_stages", False)) -def adapt_text_to_image_prompt( +def build_text_to_image_prompt( model_class_name: str | None, prompt: dict[str, Any], height: int | None = None, width: int | None = None, ) -> dict[str, Any]: - """Apply only model-specific changes to an example-owned T2I prompt.""" + """Build a model-specific T2I prompt from an example-owned envelope.""" spec = _get_spec(model_class_name) builder: TextToImagePromptBuilder | None = spec.get("text_to_image_prompt_builder") if spec else None if builder is None: @@ -303,14 +303,14 @@ def build_image_to_image_prompt( return builder(prompt, negative_prompt, input_image, height, width) -def adapt_image_to_video_prompt( +def build_image_to_video_prompt( model_class_name: str | None, prompt: dict[str, Any], height: int | None = None, width: int | None = None, num_frames: int | None = None, ) -> dict[str, Any]: - """Apply only model-specific changes to an example-owned I2V prompt.""" + """Build a model-specific I2V prompt from an example-owned envelope.""" spec = _get_spec(model_class_name) builder: ImageToVideoPromptBuilder | None = spec.get("image_to_video_prompt_builder") if spec else None if builder is None: