Spaces:
Sleeping
Sleeping
Align Prompt Enhancer with try-on safetensors inference
Browse files- README.md +16 -0
- app.py +10 -5
- inference.py +186 -5
- requirements.txt +1 -0
README.md
CHANGED
|
@@ -61,9 +61,23 @@ JOY_TRANSFORMER_PATH=transformer
|
|
| 61 |
JOY_DIT_CKPT_TYPE=safetensor
|
| 62 |
JOY_MODEL_PATH=FireRed-Image-Edit-1.0
|
| 63 |
JOY_VAE_PATH=FireRed-Image-Edit-1.0/vae/diffusion_pytorch_model.safetensors
|
|
|
|
|
|
|
| 64 |
HF_HOME=/data/.huggingface
|
| 65 |
```
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
## Expected Model Repository Layout
|
| 68 |
|
| 69 |
This wrapper downloads `MODEL_ID` with `huggingface_hub.snapshot_download`.
|
|
@@ -108,6 +122,8 @@ prompt = <image><image> + try-on instruction
|
|
| 108 |
output = one generated PIL image
|
| 109 |
```
|
| 110 |
|
|
|
|
|
|
|
| 111 |
If multiple garment images are uploaded, the app uses `primary_garment_index` to select one primary garment. It does not pretend to perform multi-reference fusion.
|
| 112 |
|
| 113 |
## Deployment Steps
|
|
|
|
| 61 |
JOY_DIT_CKPT_TYPE=safetensor
|
| 62 |
JOY_MODEL_PATH=FireRed-Image-Edit-1.0
|
| 63 |
JOY_VAE_PATH=FireRed-Image-Edit-1.0/vae/diffusion_pytorch_model.safetensors
|
| 64 |
+
QWEN235_CHAT_URL=http://ai-api.jdcloud.com/v1/chat/completions
|
| 65 |
+
QWEN235_MODEL=qwen3-vl-235
|
| 66 |
HF_HOME=/data/.huggingface
|
| 67 |
```
|
| 68 |
|
| 69 |
+
For Prompt Enhancer, add one of these as a Space Secret:
|
| 70 |
+
|
| 71 |
+
```text
|
| 72 |
+
QWEN235_API_KEY=your_api_key
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
or:
|
| 76 |
+
|
| 77 |
+
```text
|
| 78 |
+
OPENAI_API_KEY=your_api_key
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
## Expected Model Repository Layout
|
| 82 |
|
| 83 |
This wrapper downloads `MODEL_ID` with `huggingface_hub.snapshot_download`.
|
|
|
|
| 122 |
output = one generated PIL image
|
| 123 |
```
|
| 124 |
|
| 125 |
+
Prompt Enhancer is enabled by default in the UI. It sends the garment image and person image to the configured Qwen3-VL-235 endpoint, then displays the enhanced prompt used for generation.
|
| 126 |
+
|
| 127 |
If multiple garment images are uploaded, the app uses `primary_garment_index` to select one primary garment. It does not pretend to perform multi-reference fusion.
|
| 128 |
|
| 129 |
## Deployment Steps
|
app.py
CHANGED
|
@@ -29,6 +29,7 @@ def predict(
|
|
| 29 |
garment_type: str,
|
| 30 |
primary_garment_index: int,
|
| 31 |
prompt: str,
|
|
|
|
| 32 |
negative_prompt: str,
|
| 33 |
seed: int,
|
| 34 |
randomize_seed: bool,
|
|
@@ -45,7 +46,7 @@ def predict(
|
|
| 45 |
person = resize_for_demo(person_image, max_side=1024)
|
| 46 |
garments = [resize_for_demo(img, max_side=1024) for img in garment_images]
|
| 47 |
|
| 48 |
-
result, used_index,
|
| 49 |
person_image=person,
|
| 50 |
garment_images=garments,
|
| 51 |
garment_type=garment_type,
|
|
@@ -55,10 +56,11 @@ def predict(
|
|
| 55 |
guidance_scale=float(guidance_scale),
|
| 56 |
auto_crop=bool(crop_and_paste),
|
| 57 |
prompt=prompt,
|
|
|
|
| 58 |
negative_prompt=negative_prompt,
|
| 59 |
)
|
| 60 |
|
| 61 |
-
return result, int(seed), used_index,
|
| 62 |
|
| 63 |
except gr.Error:
|
| 64 |
raise
|
|
@@ -108,11 +110,12 @@ with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
|
|
| 108 |
)
|
| 109 |
|
| 110 |
prompt = gr.Textbox(
|
| 111 |
-
label="Try-On Prompt",
|
| 112 |
value="",
|
| 113 |
lines=3,
|
| 114 |
placeholder="Leave empty to use the default prompt for the selected garment type.",
|
| 115 |
)
|
|
|
|
| 116 |
|
| 117 |
with gr.Accordion("Advanced Settings", open=False):
|
| 118 |
seed = gr.Number(value=42, precision=0, label="Seed")
|
|
@@ -148,7 +151,8 @@ with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
|
|
| 148 |
output_image = gr.Image(label="Try-On Result", type="pil")
|
| 149 |
used_seed = gr.Number(label="Used Seed", precision=0)
|
| 150 |
used_garment_index = gr.Number(label="Used Garment Index", precision=0)
|
| 151 |
-
|
|
|
|
| 152 |
status = gr.Textbox(label="Status", interactive=False)
|
| 153 |
|
| 154 |
run_button.click(
|
|
@@ -159,6 +163,7 @@ with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
|
|
| 159 |
garment_type,
|
| 160 |
primary_garment_index,
|
| 161 |
prompt,
|
|
|
|
| 162 |
negative_prompt,
|
| 163 |
seed,
|
| 164 |
randomize_seed,
|
|
@@ -166,7 +171,7 @@ with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
|
|
| 166 |
guidance_scale,
|
| 167 |
crop_and_paste,
|
| 168 |
],
|
| 169 |
-
outputs=[output_image, used_seed, used_garment_index,
|
| 170 |
api_name="tryon",
|
| 171 |
)
|
| 172 |
|
|
|
|
| 29 |
garment_type: str,
|
| 30 |
primary_garment_index: int,
|
| 31 |
prompt: str,
|
| 32 |
+
prompt_enhancer: bool,
|
| 33 |
negative_prompt: str,
|
| 34 |
seed: int,
|
| 35 |
randomize_seed: bool,
|
|
|
|
| 46 |
person = resize_for_demo(person_image, max_side=1024)
|
| 47 |
garments = [resize_for_demo(img, max_side=1024) for img in garment_images]
|
| 48 |
|
| 49 |
+
result, used_index, enhanced_prompt, pe_status, status = run_tryon(
|
| 50 |
person_image=person,
|
| 51 |
garment_images=garments,
|
| 52 |
garment_type=garment_type,
|
|
|
|
| 56 |
guidance_scale=float(guidance_scale),
|
| 57 |
auto_crop=bool(crop_and_paste),
|
| 58 |
prompt=prompt,
|
| 59 |
+
prompt_enhancer=bool(prompt_enhancer),
|
| 60 |
negative_prompt=negative_prompt,
|
| 61 |
)
|
| 62 |
|
| 63 |
+
return result, int(seed), used_index, enhanced_prompt, pe_status, status
|
| 64 |
|
| 65 |
except gr.Error:
|
| 66 |
raise
|
|
|
|
| 110 |
)
|
| 111 |
|
| 112 |
prompt = gr.Textbox(
|
| 113 |
+
label="Original Try-On Prompt",
|
| 114 |
value="",
|
| 115 |
lines=3,
|
| 116 |
placeholder="Leave empty to use the default prompt for the selected garment type.",
|
| 117 |
)
|
| 118 |
+
prompt_enhancer = gr.Checkbox(value=True, label="Prompt Enhancer")
|
| 119 |
|
| 120 |
with gr.Accordion("Advanced Settings", open=False):
|
| 121 |
seed = gr.Number(value=42, precision=0, label="Seed")
|
|
|
|
| 151 |
output_image = gr.Image(label="Try-On Result", type="pil")
|
| 152 |
used_seed = gr.Number(label="Used Seed", precision=0)
|
| 153 |
used_garment_index = gr.Number(label="Used Garment Index", precision=0)
|
| 154 |
+
enhanced_prompt = gr.Textbox(label="Enhanced Prompt Used", interactive=False, lines=5)
|
| 155 |
+
pe_status = gr.Textbox(label="Prompt Enhancer Status", interactive=False)
|
| 156 |
status = gr.Textbox(label="Status", interactive=False)
|
| 157 |
|
| 158 |
run_button.click(
|
|
|
|
| 163 |
garment_type,
|
| 164 |
primary_garment_index,
|
| 165 |
prompt,
|
| 166 |
+
prompt_enhancer,
|
| 167 |
negative_prompt,
|
| 168 |
seed,
|
| 169 |
randomize_seed,
|
|
|
|
| 171 |
guidance_scale,
|
| 172 |
crop_and_paste,
|
| 173 |
],
|
| 174 |
+
outputs=[output_image, used_seed, used_garment_index, enhanced_prompt, pe_status, status],
|
| 175 |
api_name="tryon",
|
| 176 |
)
|
| 177 |
|
inference.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import math
|
| 4 |
import os
|
| 5 |
import random
|
|
|
|
| 6 |
import sys
|
| 7 |
import contextlib
|
|
|
|
| 8 |
from dataclasses import dataclass
|
| 9 |
from pathlib import Path
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
import numpy as np
|
|
|
|
| 13 |
import torch
|
| 14 |
import torchvision.transforms.functional as TF
|
| 15 |
from huggingface_hub import snapshot_download
|
|
@@ -32,6 +39,28 @@ DEFAULT_PROMPTS = {
|
|
| 32 |
"full_body": "Make the person in Picture 2 wear the clothing from Picture 1. Keep the pose, body shape, background, and lighting from Picture 2.",
|
| 33 |
}
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
@dataclass
|
| 37 |
class LoadedTryOnModel:
|
|
@@ -43,6 +72,7 @@ class LoadedTryOnModel:
|
|
| 43 |
|
| 44 |
_MODEL: LoadedTryOnModel | None = None
|
| 45 |
_MODEL_DIR: Path | None = None
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def _get_bool_env(name: str, default: bool = False) -> bool:
|
|
@@ -70,6 +100,155 @@ def set_seed(seed: int) -> torch.Generator:
|
|
| 70 |
return torch.Generator(device="cuda").manual_seed(seed)
|
| 71 |
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
def _download_model_repo() -> Path:
|
| 74 |
model_id = os.getenv("MODEL_ID")
|
| 75 |
if not model_id:
|
|
@@ -504,7 +683,8 @@ def run_tryon(
|
|
| 504 |
auto_crop: bool = False,
|
| 505 |
prompt: str | None = None,
|
| 506 |
negative_prompt: str = DEFAULT_NEG_PROMPT,
|
| 507 |
-
|
|
|
|
| 508 |
if person_image is None:
|
| 509 |
raise ValueError("person_image is required.")
|
| 510 |
if not garment_images:
|
|
@@ -517,6 +697,7 @@ def run_tryon(
|
|
| 517 |
person = person_image.convert("RGB")
|
| 518 |
garment = primary_garment.convert("RGB")
|
| 519 |
effective_prompt = _build_prompt(prompt, garment_type)
|
|
|
|
| 520 |
effective_negative = (negative_prompt or DEFAULT_NEG_PROMPT).strip()
|
| 521 |
|
| 522 |
if auto_crop:
|
|
@@ -524,7 +705,7 @@ def run_tryon(
|
|
| 524 |
model,
|
| 525 |
person,
|
| 526 |
garment,
|
| 527 |
-
|
| 528 |
num_inference_steps,
|
| 529 |
guidance_scale,
|
| 530 |
seed,
|
|
@@ -533,7 +714,7 @@ def run_tryon(
|
|
| 533 |
if cropped_result is not None:
|
| 534 |
image, path_status = cropped_result
|
| 535 |
status = f"Success. Used garment index {used_index}. {path_status}."
|
| 536 |
-
return image, used_index,
|
| 537 |
|
| 538 |
target_h, target_w = get_bucket_size(person)
|
| 539 |
input_person = resize_center_crop(person, (target_h, target_w))
|
|
@@ -541,7 +722,7 @@ def run_tryon(
|
|
| 541 |
output = _run_pipeline(
|
| 542 |
model,
|
| 543 |
[input_garment, input_person],
|
| 544 |
-
|
| 545 |
target_h,
|
| 546 |
target_w,
|
| 547 |
num_inference_steps,
|
|
@@ -550,7 +731,7 @@ def run_tryon(
|
|
| 550 |
effective_negative,
|
| 551 |
)
|
| 552 |
status = f"Success. Used garment index {used_index}. direct path."
|
| 553 |
-
return output, used_index,
|
| 554 |
|
| 555 |
|
| 556 |
def clear_model_cache() -> None:
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import base64
|
| 4 |
+
import hashlib
|
| 5 |
+
import io
|
| 6 |
+
import json
|
| 7 |
import math
|
| 8 |
import os
|
| 9 |
import random
|
| 10 |
+
import re
|
| 11 |
import sys
|
| 12 |
import contextlib
|
| 13 |
+
import time
|
| 14 |
from dataclasses import dataclass
|
| 15 |
from pathlib import Path
|
| 16 |
from typing import Any
|
| 17 |
|
| 18 |
import numpy as np
|
| 19 |
+
import requests
|
| 20 |
import torch
|
| 21 |
import torchvision.transforms.functional as TF
|
| 22 |
from huggingface_hub import snapshot_download
|
|
|
|
| 39 |
"full_body": "Make the person in Picture 2 wear the clothing from Picture 1. Keep the pose, body shape, background, and lighting from Picture 2.",
|
| 40 |
}
|
| 41 |
|
| 42 |
+
QWEN235_MODEL = os.getenv("QWEN235_MODEL", "qwen3-vl-235")
|
| 43 |
+
QWEN235_CHAT_URL = os.getenv("QWEN235_CHAT_URL", "http://ai-api.jdcloud.com/v1/chat/completions")
|
| 44 |
+
PE_SYSTEM_ROLE = "You are an expert vision AI prompt engineer."
|
| 45 |
+
PE_TEMPLATE_DEFAULT = (
|
| 46 |
+
"# Multi-Image Edit Instruction Rewriter\n\n"
|
| 47 |
+
"You are an expert at refining MULTI-IMAGE editing instructions. Given the "
|
| 48 |
+
"user's original prompt and the uploaded reference images (in order: Image 1, "
|
| 49 |
+
"Image 2, Image 3, Image 4), produce a single improved English editing "
|
| 50 |
+
"caption that a diffusion-based multi-image editing model can follow precisely.\n\n"
|
| 51 |
+
"## Core Rules\n"
|
| 52 |
+
"1. Preserve intent. Never invent new operations. Only clarify, enrich and correct what the user already asked for.\n"
|
| 53 |
+
"2. Always refer to the inputs as Image 1 / Image 2 / Image 3 / Image 4.\n"
|
| 54 |
+
"3. For every visual element in the output, state explicitly which reference image it comes from.\n"
|
| 55 |
+
"4. Output the rewritten English caption only. No preamble, no JSON, no markdown fences, no explanations.\n\n"
|
| 56 |
+
"## Replacement / Virtual Try-On Rules\n"
|
| 57 |
+
"For try-on, consistency is more important than creativity. Do not add cinematic effects, extra props, "
|
| 58 |
+
"new lighting, new camera angles, or creative embellishments. Preserve everything in the person image "
|
| 59 |
+
"that is not explicitly replaced: identity, face, hair, skin tone, body shape, pose, expression, "
|
| 60 |
+
"background, lighting, color tone, camera angle, and composition. Replace only the garment, preserving "
|
| 61 |
+
"the garment reference's color, pattern, texture, logo, silhouette, and natural fabric drape."
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
|
| 65 |
@dataclass
|
| 66 |
class LoadedTryOnModel:
|
|
|
|
| 72 |
|
| 73 |
_MODEL: LoadedTryOnModel | None = None
|
| 74 |
_MODEL_DIR: Path | None = None
|
| 75 |
+
_PE_CACHE: dict[str, str] = {}
|
| 76 |
|
| 77 |
|
| 78 |
def _get_bool_env(name: str, default: bool = False) -> bool:
|
|
|
|
| 100 |
return torch.Generator(device="cuda").manual_seed(seed)
|
| 101 |
|
| 102 |
|
| 103 |
+
def _encode_image_base64_png(pil_image: Image.Image) -> str:
|
| 104 |
+
buf = io.BytesIO()
|
| 105 |
+
pil_image.convert("RGB").save(buf, format="PNG")
|
| 106 |
+
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _remove_think_block(text: str) -> str:
|
| 110 |
+
return re.sub(r"<think>.*?</think>", "", text or "", flags=re.IGNORECASE | re.DOTALL).strip()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _normalize_chat_message_content(content: Any) -> str:
|
| 114 |
+
if content is None:
|
| 115 |
+
return ""
|
| 116 |
+
if isinstance(content, str):
|
| 117 |
+
return content.strip()
|
| 118 |
+
|
| 119 |
+
texts: list[str] = []
|
| 120 |
+
|
| 121 |
+
def walk(value: Any) -> None:
|
| 122 |
+
if value is None:
|
| 123 |
+
return
|
| 124 |
+
if isinstance(value, str):
|
| 125 |
+
if value.strip():
|
| 126 |
+
texts.append(value.strip())
|
| 127 |
+
return
|
| 128 |
+
if isinstance(value, dict):
|
| 129 |
+
text = value.get("text")
|
| 130 |
+
if isinstance(text, str) and text.strip():
|
| 131 |
+
texts.append(text.strip())
|
| 132 |
+
for child in value.values():
|
| 133 |
+
if isinstance(child, (dict, list)):
|
| 134 |
+
walk(child)
|
| 135 |
+
return
|
| 136 |
+
if isinstance(value, list):
|
| 137 |
+
for item in value:
|
| 138 |
+
walk(item)
|
| 139 |
+
|
| 140 |
+
walk(content)
|
| 141 |
+
return "\n".join(texts).strip()
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _extract_caption_text(text: str) -> str:
|
| 145 |
+
clean = _remove_think_block((text or "").strip())
|
| 146 |
+
if not clean:
|
| 147 |
+
return ""
|
| 148 |
+
clean = clean.replace("```json", "").replace("```", "").strip()
|
| 149 |
+
try:
|
| 150 |
+
obj = json.loads(clean)
|
| 151 |
+
if isinstance(obj, dict) and isinstance(obj.get("Rewritten"), str):
|
| 152 |
+
return obj["Rewritten"].strip().replace("\n", " ")
|
| 153 |
+
except Exception:
|
| 154 |
+
pass
|
| 155 |
+
return clean
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _image_signature(image: Image.Image | None) -> str:
|
| 159 |
+
if image is None:
|
| 160 |
+
return "none"
|
| 161 |
+
try:
|
| 162 |
+
buf = io.BytesIO()
|
| 163 |
+
image.convert("RGB").save(buf, format="PNG")
|
| 164 |
+
data = buf.getvalue()
|
| 165 |
+
digest = hashlib.md5(data[:65536]).hexdigest()
|
| 166 |
+
return f"{image.size[0]}x{image.size[1]}_{digest}"
|
| 167 |
+
except Exception:
|
| 168 |
+
return f"{image.size[0]}x{image.size[1]}"
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _build_pe_prompt_text(original_prompt: str) -> str:
|
| 172 |
+
original_prompt = str(original_prompt or "").strip()
|
| 173 |
+
if not original_prompt:
|
| 174 |
+
return PE_TEMPLATE_DEFAULT
|
| 175 |
+
return (
|
| 176 |
+
f"Based on the uploaded image(s) and the prompt: '{original_prompt}', "
|
| 177 |
+
"please follow the instructions below to produce the rewritten caption.\n\n"
|
| 178 |
+
f"{PE_TEMPLATE_DEFAULT}"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _rewrite_prompt_qwen3vl235_chatapi(prompt: str, images: list[Image.Image]) -> str:
|
| 183 |
+
prompt = str(prompt or "").strip()
|
| 184 |
+
if not prompt:
|
| 185 |
+
return prompt
|
| 186 |
+
|
| 187 |
+
api_key = os.getenv("QWEN235_API_KEY") or os.getenv("OPENAI_API_KEY")
|
| 188 |
+
if not api_key:
|
| 189 |
+
raise EnvironmentError("QWEN235_API_KEY or OPENAI_API_KEY is not set")
|
| 190 |
+
|
| 191 |
+
user_content: list[dict[str, Any]] = []
|
| 192 |
+
for image in images:
|
| 193 |
+
user_content.append(
|
| 194 |
+
{
|
| 195 |
+
"type": "image_url",
|
| 196 |
+
"image_url": {"url": f"data:image/png;base64,{_encode_image_base64_png(image)}"},
|
| 197 |
+
}
|
| 198 |
+
)
|
| 199 |
+
user_content.append({"type": "text", "text": _build_pe_prompt_text(prompt)})
|
| 200 |
+
|
| 201 |
+
payload = {
|
| 202 |
+
"model": QWEN235_MODEL,
|
| 203 |
+
"messages": [
|
| 204 |
+
{"role": "system", "content": PE_SYSTEM_ROLE},
|
| 205 |
+
{"role": "user", "content": user_content},
|
| 206 |
+
],
|
| 207 |
+
"max_tokens": int(os.getenv("QWEN235_MAX_TOKENS", "1024")),
|
| 208 |
+
"stream": False,
|
| 209 |
+
}
|
| 210 |
+
headers = {
|
| 211 |
+
"Authorization": f"Bearer {api_key}",
|
| 212 |
+
"Content-Type": "application/json",
|
| 213 |
+
"Trace-Id": f"tryon-space-qwen235-{int(time.time() * 1000)}",
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
last_err: Exception | None = None
|
| 217 |
+
for attempt in range(int(os.getenv("QWEN235_MAX_RETRIES", "3"))):
|
| 218 |
+
try:
|
| 219 |
+
resp = requests.post(QWEN235_CHAT_URL, headers=headers, json=payload, timeout=float(os.getenv("QWEN235_TIMEOUT", "60")))
|
| 220 |
+
if resp.status_code != 200:
|
| 221 |
+
raise RuntimeError(f"Qwen3-VL-235 API failed: status={resp.status_code}, text={resp.text[:2000]}")
|
| 222 |
+
data = resp.json()
|
| 223 |
+
content = (((data.get("choices") or [{}])[0].get("message") or {}).get("content"))
|
| 224 |
+
caption = _extract_caption_text(_normalize_chat_message_content(content))
|
| 225 |
+
return caption or prompt
|
| 226 |
+
except Exception as exc:
|
| 227 |
+
last_err = exc
|
| 228 |
+
time.sleep(0.5 * (2**attempt))
|
| 229 |
+
|
| 230 |
+
raise RuntimeError(f"Prompt Enhancer failed: {last_err}")
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def rewrite_prompt_if_enabled(prompt: str, images: list[Image.Image], enabled: bool) -> tuple[str, str]:
|
| 234 |
+
prompt = str(prompt or "")
|
| 235 |
+
if not enabled:
|
| 236 |
+
return prompt, "Prompt Enhancer disabled."
|
| 237 |
+
if not images:
|
| 238 |
+
return prompt, "Prompt Enhancer skipped: no reference images."
|
| 239 |
+
|
| 240 |
+
cache_key = f"qwen235|p={prompt.strip()}|imgs={'|'.join(_image_signature(img) for img in images)}"
|
| 241 |
+
if cache_key in _PE_CACHE:
|
| 242 |
+
return _PE_CACHE[cache_key], "Prompt Enhancer cache hit."
|
| 243 |
+
|
| 244 |
+
try:
|
| 245 |
+
rewritten = _rewrite_prompt_qwen3vl235_chatapi(prompt, [img.convert("RGB") for img in images])
|
| 246 |
+
_PE_CACHE[cache_key] = rewritten
|
| 247 |
+
return rewritten, "Prompt Enhancer applied."
|
| 248 |
+
except Exception as exc:
|
| 249 |
+
return prompt, f"Prompt Enhancer failed, using original prompt: {exc}"
|
| 250 |
+
|
| 251 |
+
|
| 252 |
def _download_model_repo() -> Path:
|
| 253 |
model_id = os.getenv("MODEL_ID")
|
| 254 |
if not model_id:
|
|
|
|
| 683 |
auto_crop: bool = False,
|
| 684 |
prompt: str | None = None,
|
| 685 |
negative_prompt: str = DEFAULT_NEG_PROMPT,
|
| 686 |
+
prompt_enhancer: bool = True,
|
| 687 |
+
) -> tuple[Image.Image, int, str, str, str]:
|
| 688 |
if person_image is None:
|
| 689 |
raise ValueError("person_image is required.")
|
| 690 |
if not garment_images:
|
|
|
|
| 697 |
person = person_image.convert("RGB")
|
| 698 |
garment = primary_garment.convert("RGB")
|
| 699 |
effective_prompt = _build_prompt(prompt, garment_type)
|
| 700 |
+
enhanced_prompt, pe_status = rewrite_prompt_if_enabled(effective_prompt, [garment, person], prompt_enhancer)
|
| 701 |
effective_negative = (negative_prompt or DEFAULT_NEG_PROMPT).strip()
|
| 702 |
|
| 703 |
if auto_crop:
|
|
|
|
| 705 |
model,
|
| 706 |
person,
|
| 707 |
garment,
|
| 708 |
+
enhanced_prompt,
|
| 709 |
num_inference_steps,
|
| 710 |
guidance_scale,
|
| 711 |
seed,
|
|
|
|
| 714 |
if cropped_result is not None:
|
| 715 |
image, path_status = cropped_result
|
| 716 |
status = f"Success. Used garment index {used_index}. {path_status}."
|
| 717 |
+
return image, used_index, enhanced_prompt, pe_status, status
|
| 718 |
|
| 719 |
target_h, target_w = get_bucket_size(person)
|
| 720 |
input_person = resize_center_crop(person, (target_h, target_w))
|
|
|
|
| 722 |
output = _run_pipeline(
|
| 723 |
model,
|
| 724 |
[input_garment, input_person],
|
| 725 |
+
enhanced_prompt,
|
| 726 |
target_h,
|
| 727 |
target_w,
|
| 728 |
num_inference_steps,
|
|
|
|
| 731 |
effective_negative,
|
| 732 |
)
|
| 733 |
status = f"Success. Used garment index {used_index}. direct path."
|
| 734 |
+
return output, used_index, enhanced_prompt, pe_status, status
|
| 735 |
|
| 736 |
|
| 737 |
def clear_model_cache() -> None:
|
requirements.txt
CHANGED
|
@@ -9,6 +9,7 @@ transformers
|
|
| 9 |
safetensors
|
| 10 |
pillow
|
| 11 |
opencv-python-headless
|
|
|
|
| 12 |
numpy
|
| 13 |
scipy
|
| 14 |
einops
|
|
|
|
| 9 |
safetensors
|
| 10 |
pillow
|
| 11 |
opencv-python-headless
|
| 12 |
+
requests
|
| 13 |
numpy
|
| 14 |
scipy
|
| 15 |
einops
|