[Feature] HunyuanImage-3.0 IT2I: multi-image input + prompt API cleanup (#3444)
Signed-off-by: TaffyOfficial <2324465096@qq.com> Signed-off-by: TaffyOfficial <wu15922848573@outlook.com> Signed-off-by: skf1999 <13234016272@163.com> Signed-off-by: zuiho <2324465096@qq.com> Signed-off-by: Claude Code <noreply@anthropic.com> Signed-off-by: zuiho <wu15922848573@outlook.com> Signed-off-by: TaffyOfficial <2587297563@qq.com> Co-authored-by: TaffyOfficial <2324465096@qq.com> Co-authored-by: TaffyOfficial <wu15922848573@outlook.com> Co-authored-by: skf1999 <13234016272@163.com>
This commit is contained in:
17 files changed
+1559
-534
No files matched your search
@@ -1700,7 +1700,10 @@ async def edit_images(
|
||||
# vllm-omni extension for layered models (e.g., Qwen-Image-Layered)
|
||||
layers: int | None = Form(None),
|
||||
resolution: int | None = Form(None), # See SUPPORTED_LAYERED_RESOLUTIONS
|
||||
# /v1/images/edits is always IT2I; only the prompting knobs are exposed.
|
||||
bot_task: str | None = Form(None),
|
||||
sys_type: str | None = Form(None),
|
||||
system_prompt: str | None = Form(None),
|
||||
) -> ImageGenerationResponse:
|
||||
"""
|
||||
OpenAI-compatible image edit endpoint.
|
||||
@@ -1751,16 +1754,20 @@ async def edit_images(
|
||||
status_code=HTTPStatus.BAD_REQUEST.value,
|
||||
detail=detail,
|
||||
)
|
||||
pil_images = await _load_input_images(input_images_list)
|
||||
# Match the offline path: RGB normalize when the caller opts into
|
||||
# Hunyuan-aware behavior. RGBA/P uploads otherwise diverge from offline.
|
||||
normalize_edit_images_rgb = bot_task is not None or sys_type is not None
|
||||
pil_images = await _load_input_images(input_images_list, normalize_rgb=normalize_edit_images_rgb)
|
||||
prompt["multi_modal_data"] = {}
|
||||
prompt["multi_modal_data"]["image"] = pil_images
|
||||
|
||||
if mask_image is not None:
|
||||
loaded = await _load_input_images([mask_image])
|
||||
# Mask role is different (alpha channel matters); never normalize.
|
||||
loaded = await _load_input_images([mask_image], normalize_rgb=False)
|
||||
prompt["multi_modal_data"]["mask_image"] = loaded[0]
|
||||
|
||||
if reference_image is not None:
|
||||
loaded = await _load_input_images([reference_image])
|
||||
loaded = await _load_input_images([reference_image], normalize_rgb=normalize_edit_images_rgb)
|
||||
prompt["multi_modal_data"]["reference_image"] = loaded[0]
|
||||
|
||||
# 3 Build sample params
|
||||
@@ -1811,7 +1818,8 @@ async def edit_images(
|
||||
|
||||
# 3.3 Parse and add size if provided
|
||||
width, height = None, None
|
||||
if size.lower() == "auto":
|
||||
size_was_auto = size.lower() == "auto"
|
||||
if size_was_auto:
|
||||
if resolution is None:
|
||||
# No resolution specified, use input image size
|
||||
width, height = pil_images[0].size
|
||||
@@ -1882,10 +1890,13 @@ async def edit_images(
|
||||
"seed": effective_seed,
|
||||
"num_outputs_per_prompt": n,
|
||||
}
|
||||
if width is not None:
|
||||
extra_body["width"] = width
|
||||
if height is not None:
|
||||
extra_body["height"] = height
|
||||
# size="auto" resolves width/height from input image; forwarding
|
||||
# those would override AR-driven `<img_ratio_*>` token selection.
|
||||
if not size_was_auto:
|
||||
if width is not None:
|
||||
extra_body["width"] = width
|
||||
if height is not None:
|
||||
extra_body["height"] = height
|
||||
if negative_prompt is not None:
|
||||
extra_body["negative_prompt"] = negative_prompt
|
||||
if num_inference_steps is not None:
|
||||
@@ -1907,6 +1918,10 @@ async def edit_images(
|
||||
extra_body["lora"] = lora_dict
|
||||
if bot_task is not None:
|
||||
extra_body["bot_task"] = bot_task
|
||||
if sys_type is not None:
|
||||
extra_body["sys_type"] = sys_type
|
||||
if system_prompt is not None:
|
||||
extra_body["system_prompt"] = system_prompt
|
||||
|
||||
prompt_text = prompt.get("prompt", "")
|
||||
generation_result = await chat_handler.generate_diffusion_images(
|
||||
@@ -2187,6 +2202,8 @@ def _extract_images_from_result(result: Any) -> list[Any]:
|
||||
|
||||
async def _load_input_images(
|
||||
inputs: list[str],
|
||||
*,
|
||||
normalize_rgb: bool = True,
|
||||
) -> list[Image.Image]:
|
||||
"""
|
||||
convert to PIL.Image.Image list
|
||||
@@ -2233,7 +2250,18 @@ async def _load_input_images(
|
||||
if not images:
|
||||
raise ValueError("No valid input images found")
|
||||
|
||||
return images
|
||||
if not normalize_rgb:
|
||||
return images
|
||||
|
||||
# Match the offline HunyuanImage3 image-edit example path, which eagerly
|
||||
# normalizes input files with ``Image.open(...).convert("RGB")`` before
|
||||
# they reach the AR stage. Keeping uploads as RGBA/P PIL objects makes
|
||||
# online IT2I observe a different visual input than offline (for example
|
||||
# transparent-logo uploads alpha-composited over white instead of black),
|
||||
# which is enough for HunyuanImage3 AR recaption to diverge before DiT
|
||||
# sees the request -- root cause of the "online 3 magnets vs offline 1
|
||||
# magnet" systematic semantic mismatch.
|
||||
return [img.convert("RGB") for img in images]
|
||||
|
||||
|
||||
def _choose_output_format(output_format: str | None, background: str | None) -> str:
|
||||
|
||||
Reference in new issue
Block a user