[Entrypoints] Support Qwen-Image-Edit 2509 online serving & update doc (#368)
Signed-off-by: David Chen <530634352@qq.com>
This commit is contained in:
1 parent
8939eb835d
commit
628c0a9217
14 files changed
+382
-37
No files matched your search
@@ -15,7 +15,7 @@ python image_edit.py \
|
||||
--cfg_scale 4.0 \
|
||||
```
|
||||
|
||||
### Multiple Image Editing (Qwen-Image-Edit-2509+)
|
||||
### Multiple Image Editing (Qwen-Image-Edit-2509)
|
||||
|
||||
For multiple image inputs, use `Qwen/Qwen-Image-Edit-2509` or later version:
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
This example demonstrates how to deploy Qwen-Image-Edit model for online image editing service using vLLM-Omni.
|
||||
|
||||
For **multi-image** input editing, use **Qwen-Image-Edit-2509** (QwenImageEditPlusPipeline) and send multiple images in the user message content.
|
||||
|
||||
## Start Server
|
||||
|
||||
### Basic Start
|
||||
@@ -10,6 +12,12 @@ This example demonstrates how to deploy Qwen-Image-Edit model for online image e
|
||||
vllm serve Qwen/Qwen-Image-Edit --omni --port 8092
|
||||
```
|
||||
|
||||
### Multi-Image Edit (Qwen-Image-Edit-2509)
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen-Image-Edit-2509 --omni --port 8092
|
||||
```
|
||||
|
||||
### Start with Parameters
|
||||
|
||||
|
||||
@@ -19,6 +27,12 @@ Or use the startup script:
|
||||
bash run_server.sh
|
||||
```
|
||||
|
||||
To serve Qwen-Image-Edit-2509 with the script:
|
||||
|
||||
```bash
|
||||
MODEL=Qwen/Qwen-Image-Edit-2509 bash run_server.sh
|
||||
```
|
||||
|
||||
## API Calls
|
||||
|
||||
### Method 1: Using curl (Image Editing)
|
||||
@@ -53,6 +67,9 @@ curl -s http://localhost:8092/v1/chat/completions \
|
||||
|
||||
```bash
|
||||
python openai_chat_client.py --input input.png --prompt "Convert to oil painting style" --output output.png
|
||||
|
||||
# Multi-image editing (Qwen-Image-Edit-2509 server required)
|
||||
python openai_chat_client.py --input input1.png input2.png --prompt "Combine these images into a single scene" --output output.png
|
||||
```
|
||||
|
||||
### Method 3: Using Gradio Demo
|
||||
@@ -121,6 +138,25 @@ Use `extra_body` to pass generation parameters:
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Image Editing (Qwen-Image-Edit-2509)
|
||||
|
||||
Provide multiple images in `content` (order matters):
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Combine these images into a single scene"},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."} },
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."} }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Generation Parameters (extra_body)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|
||||
@@ -15,8 +15,15 @@ import requests
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def _pil_to_b64_png(img: Image.Image) -> str:
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
|
||||
|
||||
def edit_image(
|
||||
input_image: Image.Image,
|
||||
extra_images: list[str] | None,
|
||||
prompt: str,
|
||||
steps: int,
|
||||
guidance_scale: float,
|
||||
@@ -28,19 +35,23 @@ def edit_image(
|
||||
if input_image is None:
|
||||
raise gr.Error("Please upload an image first")
|
||||
|
||||
# Convert image to base64
|
||||
buffer = BytesIO()
|
||||
input_image.save(buffer, format="PNG")
|
||||
image_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
images: list[Image.Image] = [input_image]
|
||||
if extra_images:
|
||||
for p in extra_images:
|
||||
try:
|
||||
images.append(Image.open(p).convert("RGB"))
|
||||
except Exception as e:
|
||||
raise gr.Error(f"Failed to open image: {p}. Error: {e}") from e
|
||||
|
||||
# Build user message with text and image
|
||||
content: list[dict[str, object]] = [{"type": "text", "text": prompt}]
|
||||
for img in images:
|
||||
content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{_pil_to_b64_png(img)}"}})
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_b64}"}},
|
||||
],
|
||||
"content": content,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -87,7 +98,10 @@ def create_demo(server_url: str):
|
||||
|
||||
with gr.Blocks(title="Qwen-Image-Edit Demo") as demo:
|
||||
gr.Markdown("# Qwen-Image-Edit Online Editing")
|
||||
gr.Markdown("Upload an image and describe the editing effect you want")
|
||||
gr.Markdown(
|
||||
"Upload an image and describe the editing effect you want. "
|
||||
"For multi-image editing, upload extra images (requires Qwen-Image-Edit-2509 server)."
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
@@ -95,6 +109,11 @@ def create_demo(server_url: str):
|
||||
label="Input Image",
|
||||
type="pil",
|
||||
)
|
||||
extra_images = gr.File(
|
||||
label="Additional Images (Optional)",
|
||||
file_count="multiple",
|
||||
type="filepath",
|
||||
)
|
||||
prompt = gr.Textbox(
|
||||
label="Edit Instruction",
|
||||
placeholder="Describe the editing effect you want...",
|
||||
@@ -152,13 +171,13 @@ def create_demo(server_url: str):
|
||||
inputs=[prompt],
|
||||
)
|
||||
|
||||
def process_edit(img, p, st, g, se, n):
|
||||
def process_edit(img, imgs, p, st, g, se, n):
|
||||
actual_seed = se if se >= 0 else None
|
||||
return edit_image(img, p, st, g, actual_seed, n, server_url)
|
||||
return edit_image(img, imgs, p, st, g, actual_seed, n, server_url)
|
||||
|
||||
edit_btn.click(
|
||||
fn=process_edit,
|
||||
inputs=[input_image, prompt, steps, guidance_scale, seed, negative_prompt],
|
||||
inputs=[input_image, extra_images, prompt, steps, guidance_scale, seed, negative_prompt],
|
||||
outputs=[output_image],
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Qwen-Image-Edit OpenAI-compatible chat client for image editing.
|
||||
Usage:
|
||||
python openai_chat_client.py --input qwen_image_output.png --prompt "Convert to watercolor style" --output output.png
|
||||
python openai_chat_client.py --input input.png --prompt "Convert to oil painting" --seed 42
|
||||
python openai_chat_client.py --input input1.png input2.png --prompt "Combine these images into a single scene"
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -16,8 +17,19 @@ import requests
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def _encode_image_as_data_url(input_path: Path) -> str:
|
||||
image_bytes = input_path.read_bytes()
|
||||
try:
|
||||
img = Image.open(BytesIO(image_bytes))
|
||||
mime_type = f"image/{img.format.lower()}" if img.format else "image/png"
|
||||
except Exception:
|
||||
mime_type = "image/png"
|
||||
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||
return f"data:{mime_type};base64,{image_b64}"
|
||||
|
||||
|
||||
def edit_image(
|
||||
input_image: str | Path,
|
||||
input_image: str | Path | list[str | Path],
|
||||
prompt: str,
|
||||
server_url: str = "http://localhost:8092",
|
||||
height: int | None = None,
|
||||
@@ -30,7 +42,7 @@ def edit_image(
|
||||
"""Edit an image using the chat completions API.
|
||||
|
||||
Args:
|
||||
input_image: Path to input image
|
||||
input_image: Path(s) to input image(s). For multi-image editing, pass multiple paths.
|
||||
prompt: Text description of the edit
|
||||
server_url: Server URL
|
||||
height: Output image height in pixels
|
||||
@@ -43,28 +55,22 @@ def edit_image(
|
||||
Returns:
|
||||
Edited image bytes or None if failed
|
||||
"""
|
||||
# Read and encode input image
|
||||
input_path = Path(input_image)
|
||||
if not input_path.exists():
|
||||
print(f"Error: Input image not found: {input_path}")
|
||||
return None
|
||||
|
||||
with open(input_path, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||
|
||||
# Detect image type
|
||||
img = Image.open(BytesIO(image_bytes))
|
||||
mime_type = f"image/{img.format.lower()}" if img.format else "image/png"
|
||||
input_images = input_image if isinstance(input_image, list) else [input_image]
|
||||
input_paths = [Path(p) for p in input_images]
|
||||
for p in input_paths:
|
||||
if not p.exists():
|
||||
print(f"Error: Input image not found: {p}")
|
||||
return None
|
||||
|
||||
# Build user message with text and image
|
||||
content: list[dict[str, object]] = [{"type": "text", "text": prompt}]
|
||||
for p in input_paths:
|
||||
content.append({"type": "image_url", "image_url": {"url": _encode_image_as_data_url(p)}})
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "image_url", "image_url": {"url": f"data:{mime_type};base64,{image_b64}"}},
|
||||
],
|
||||
"content": content,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -113,7 +119,7 @@ def edit_image(
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Qwen-Image-Edit chat client")
|
||||
parser.add_argument("--input", "-i", required=True, help="Input image path")
|
||||
parser.add_argument("--input", "-i", required=True, nargs="+", help="Input image path(s)")
|
||||
parser.add_argument("--prompt", "-p", required=True, help="Edit prompt")
|
||||
parser.add_argument("--output", "-o", default="output.png", help="Output file")
|
||||
parser.add_argument("--server", "-s", default="http://localhost:8092", help="Server URL")
|
||||
@@ -126,7 +132,10 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Input: {args.input}")
|
||||
if len(args.input) == 1:
|
||||
print(f"Input: {args.input[0]}")
|
||||
else:
|
||||
print(f"Inputs ({len(args.input)}): {', '.join(args.input)}")
|
||||
print(f"Prompt: {args.prompt}")
|
||||
|
||||
image_bytes = edit_image(
|
||||
|
||||
Reference in new issue
Block a user