Compare commits

13 Commits
Author SHA1 Message Date
Ubuntu 975ac3de5a save code 2026-06-04 22:34:46 +08:00
xsl bbb74606c2 save code 2026-06-04 22:10:48 +08:00
xsl de828b777a save code 2026-06-04 20:53:19 +08:00
Ubuntu 862f9e57d1 完成https 服务 2026-05-16 23:27:38 +08:00
Your Name 60123abfdb 添加密码 2026-05-10 23:26:42 +08:00
Your Name 47ff49dc19 添加单张图片变换 2026-05-06 00:12:55 +08:00
Your Name 0b9b5d21ff save code 2026-05-05 23:02:59 +08:00
Your Name 79d8c6d17f save code 2026-04-06 17:47:45 +08:00
Your Name ecdef627d4 save code 2026-04-06 15:30:35 +08:00
Your Name a044943db7 修改为https 服务器 2026-03-20 09:46:39 +08:00
Your Name 7e89edaa47 save code 2026-03-19 23:51:21 +08:00
Your Name 71221ce1e3 save code 2026-03-19 16:38:16 +08:00
Your Name ef8bbad157 save code 2026-03-14 06:03:53 +00:00
33 changed files with 7989 additions and 174 deletions
+43 -25
View File
@@ -7,61 +7,79 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Each service runs independently and must be started separately: Each service runs independently and must be started separately:
```bash ```bash
# Shirt-only try-on service (port 8888) # Single-image edit service (port 12221)
python change1/service1.py
# Shirt-only try-on service (port 12222)
python change2/service2.py python change2/service2.py
# Shirt + pants try-on service (port 8889) # Shirt + pants try-on service (port 12223)
python change3/service3.py python change3/service3.py
# Gateway router (port 28888) — requires Flask # Gateway router (port 28888) — requires Flask
python change_app.py python change_app.py
``` ```
ComfyUI must be running at `http://127.0.0.1:8188` before starting the services. ComfyUI must be reachable at the URL configured in each service (currently `http://112.126.94.241:28188`, with `:38188` as backup).
## Dependencies ## Dependencies
```bash ```bash
pip install fastapi uvicorn httpx pydantic flask requests pip install fastapi uvicorn httpx pydantic flask requests oss2 pillow
``` ```
## Architecture ## Architecture
This is a three-tier AI virtual try-on system built around ComfyUI. This is an AI virtual try-on / image-edit system built around ComfyUI, fronted by three FastAPI workers and a Flask gateway.
``` ```
Client Client
└─► change_app.py (Flask, port 28888) ← Gateway router └─► change_app.py (Flask, port 28888) ← Gateway router
├─► change2/service2.py (FastAPI, port 8888) ← shirt-only ├─► change1/service1.py (FastAPI, 12221) ← single image (model only)
─► change3/service3.py (FastAPI, port 8889) ← shirt + pants ─► change2/service2.py (FastAPI, 12222) ← model + shirt
└─► ComfyUI (port 8188) └─► change3/service3.py (FastAPI, 12223) ← model + shirt + pants
└─► ComfyUI
``` ```
**Gateway** (`change_app.py`): Accepts `POST /change_cloth_base64` with `human_img`, `cloth_img`, and optional `kuzi_img` (pants). Routes to `service3` when `kuzi_img` is present, otherwise to `service2`. Both backends are called at `/do_change_cloth`. **Gateway** (`change_app.py`): Accepts `POST /change_cloth_base64` with `human_img` (required) and optional `cloth_img` / `kuzi_img` / `change_desc`. Routing rules:
- `kuzi_img` present → `service3` (12223)
- `cloth_img` present (no `kuzi_img`) → `service2` (12222)
- only `human_img``service1` (12221)
**service2** (`change2/service2.py`): Handles shirt-only try-on. Exposes `POST /try-on` with `model_image` + `shirt_image`. Uses ComfyUI workflow `change2_1_ex.json`. All backends expose `POST /try-on` and return `{ "result_url": "<oss url>" }`. The gateway wraps that into `{ "ret": 0, "state": 0, "msg": "success", "data": <oss url> }`.
**service3** (`change3/service3.py`): Handles shirt + pants try-on. Exposes `POST /try-on` with `model_image` + `shirt_image` + `pants_image`. Uses ComfyUI workflow `change2_2_0308.json`. **service1** (`change1/service1.py`): Single-image edit (e.g. background replacement, full-body generation from a reference). Exposes `POST /try-on` with `model_image` + optional `change_desc`. Uses ComfyUI workflow `change1.json`.
**service2** (`change2/service2.py`): Shirt-only try-on. Exposes `POST /try-on` with `model_image` + `shirt_image` + optional `change_desc`. Uses ComfyUI workflow `change2_1_ex.json`.
**service3** (`change3/service3.py`): Shirt + pants try-on. Exposes `POST /try-on` with `model_image` + `shirt_image` + `pants_image` + optional `change_desc`. Uses ComfyUI workflow `change2_2_0308.json`.
### ComfyUI Workflow Integration ### ComfyUI Workflow Integration
Both services dynamically modify a JSON workflow template before submission: Each service dynamically modifies a JSON workflow template before submission:
| Node ID | Purpose | service2 | service3 | | Node ID | Purpose | service1 | service2 | service3 |
|---------|---------|----------|----------| |---------|---------|----------|----------|----------|
| `"11"` | Model/person image | ✓ | ✓ | | `"11"` | Model/person image (LoadImage) | ✓ | ✓ | ✓ |
| `"10"` | Shirt image | ✓ | ✓ | | `"10"` | Shirt image (LoadImage) | — | ✓ | ✓ |
| `"4"` | Pants image | — | ✓ | | `"4"` | Pants image (LoadImage) | — | — | ✓ |
| `"33"` | Output image | ✓ | ✓ | | `"31"` | Text prompt (PrimitiveStringMultiline, overridden when `change_desc` is non-empty) | ✓ | ✓ | ✓ |
| `"33"` | Output image (SaveImage) | ✓ | ✓ | ✓ |
The request flow within each service: The request flow within each service:
1. Decode base64 images 1. Pick a healthy ComfyUI host (primary, fallback to backup)
2. Upload images to ComfyUI (`/upload/image`) with UUID-prefixed filenames to avoid cache collisions 2. Decode base64 images
3. Inject filenames into workflow node inputs 3. Upload images to ComfyUI (`/upload/image`) with UUID-prefixed filenames to avoid cache collisions
4. Submit workflow to ComfyUI queue (`/prompt`) 4. Inject filenames into workflow node inputs; if `change_desc` is non-empty, replace node `31` inputs with `{"value": change_desc}`
5. Poll `/history/{prompt_id}` until `status.completed` is true (max 300s timeout, 2s poll interval) 5. Submit workflow to ComfyUI queue (`/prompt`)
6. Download result from `/view` and return as base64 6. Poll `/history/{prompt_id}` until `status.completed` is true (max 300s timeout, 2s poll interval)
7. Download the result from `/view`, push it to Aliyun OSS, and return the signed URL
### Frontend ### Frontend
Each service has its own test UI at `/` (served from `<service_dir>/static/index.html`). The `change2` UI accepts 2 images; `change3` accepts 3 images. Both POST directly to the service's `/try-on` endpoint. Each service has its own test UI at `/` (served from `<service_dir>/static/index.html`):
- `change1` UI: 1 upload card (model) + prompt panel
- `change2` UI: 2 upload cards (model, shirt) + prompt panel
- `change3` UI: 3 upload cards (model, shirt, pants) + prompt panel
All three POST directly to the service's own `/try-on` endpoint.
+400
View File
@@ -0,0 +1,400 @@
{
"1": {
"inputs": {
"samples": [
"22",
0
],
"vae": [
"13",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"2": {
"inputs": {
"to_ref": true,
"ref_main_image": false,
"ref_longest_edge": 1024,
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"11",
0
],
"configs": [
"8",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"6": {
"inputs": {
"conditioning": [
"15",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "条件零化"
}
},
"7": {
"inputs": {
"custom_output": [
"15",
2
]
},
"class_type": "QwenEditOutputExtractor",
"_meta": {
"title": "Qwen Edit Output Extractor"
}
},
"8": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"9",
0
],
"ref_crop": "pad",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"11",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"9": {
"inputs": {
"max_size": 2048,
"image": [
"11",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"11": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161212.795.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"13": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"14": {
"inputs": {
"prompt": "",
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"18",
0
],
"vae": [
"13",
0
],
"configs": [
"2",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"15": {
"inputs": {
"prompt": [
"31",
0
],
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"18",
0
],
"vae": [
"13",
0
],
"configs": [
"2",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"16": {
"inputs": {
"gguf_name": "FireRed-Image-Edit-1.1-transformer-q4_k_m.gguf"
},
"class_type": "LoaderGGUF",
"_meta": {
"title": "GGUF Loader"
}
},
"17": {
"inputs": {
"lora_name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors",
"strength_model": 1,
"model": [
"16",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoRA加载器(仅模型)"
}
},
"18": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "ClipLoaderGGUF",
"_meta": {
"title": "GGUF CLIP Loader"
}
},
"19": {
"inputs": {
"image": [
"1",
0
],
"pad_info": [
"7",
0
]
},
"class_type": "CropWithPadInfo",
"_meta": {
"title": "Crop With Pad Info"
}
},
"21": {
"inputs": {
"samples1": [
"15",
1
],
"samples2": [
"14",
1
]
},
"class_type": "LatentAdd",
"_meta": {
"title": "Latent相加"
}
},
"22": {
"inputs": {
"seed": 814498275732948,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"17",
0
],
"positive": [
"15",
0
],
"negative": [
"14",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"24": {
"inputs": {
"upscale_method": "lanczos",
"scale_by": [
"19",
1
],
"image": [
"19",
0
]
},
"class_type": "ImageScaleBy",
"_meta": {
"title": "缩放图像(比例)"
}
},
"28": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "disabled",
"samples": [
"21",
0
]
},
"class_type": "LatentUpscale",
"_meta": {
"title": "缩放Latent"
}
},
"29": {
"inputs": {
"Number": "768"
},
"class_type": "Int",
"_meta": {
"title": "宽度"
}
},
"30": {
"inputs": {
"Number": "1024"
},
"class_type": "Int",
"_meta": {
"title": "高度"
}
},
"31": {
"inputs": {
"value": "图1的背景改成纯色深灰色,生成人物全身站立图。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "字符串(多行)"
}
},
"32": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "center",
"image": [
"24",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"33": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"32",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"36": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_pfeet_00001_.png&type=temp&subfolder=&rand=0.983916958744797"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_pfeet_00002_.png&type=temp&subfolder=&rand=0.7859180062182538"
}
]
},
"image_a": [
"32",
0
],
"image_b": [
"11",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
}
}
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""
ComfyUI 单图编辑服务
- 接受 1 个 base64 图片(模特/原图)
- 上传到 ComfyUI,运行工作流 change1.json
- 返回生成结果 OSS URL
"""
import asyncio
import base64
import io
import json
import os
import tempfile
import time
import uuid
from pathlib import Path
import httpx
import oss2
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
# COMFYUI_URL = "http://127.0.0.1:8188"
COMFYUI_URL = "http://112.126.94.241:28188"
COMFYUI_URL_BACKUP = "http://112.126.94.241:38188"
WORKFLOW_PATH = Path(__file__).parent / "change1.json"
_cred_dir = Path(__file__).parent.parent
COMFYUI_USER = (_cred_dir / "user.txt").read_text(encoding="utf-8").strip()
COMFYUI_PASS = (_cred_dir / "password.txt").read_text(encoding="utf-8").strip()
COMFYUI_AUTH = (COMFYUI_USER, COMFYUI_PASS)
NODE_MODEL = "11" # 输入图(LoadImage
NODE_OUTPUT = "33" # 输出(SaveImage
NODE_PROMPT = "31" # 文本提示(PrimitiveStringMultiline
app = FastAPI(title="ComfyUI 单图编辑服务")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
def upload_to_oss(image_path, object_name=None):
access_key_id = 'LTAI5tGp1sLzedqxihcNC1eb'
access_key_secret = 'IFZE1b8YYreCP6zfA6GaZ9uBT678qO'
endpoint = 'oss-cn-beijing.aliyuncs.com'
bucket_name = 'xiangsilian'
auth = oss2.Auth(access_key_id, access_key_secret)
bucket = oss2.Bucket(auth, endpoint, bucket_name)
if object_name is None:
object_name = image_path.split('/')[-1]
try:
bucket.put_object_from_file(object_name, image_path)
url = bucket.sign_url('GET', object_name, 3600 * 24 * 365 * 10)
print(f"文件上传成功,URL: {url}")
return url
except Exception as e:
print(f"上传失败: {str(e)}")
return None
class TryOnRequest(BaseModel):
model_image: str # base64 输入图片
change_desc: str = "" # 编辑描述,可空;非空时替换节点 31 的 inputs
class TryOnResponse(BaseModel):
result_url: str # OSS 图片 URL
def decode_base64_image(b64_str: str) -> bytes:
"""解码 base64 图片,自动处理 data:image/... 前缀"""
if "," in b64_str:
b64_str = b64_str.split(",", 1)[1]
return base64.b64decode(b64_str)
async def check_comfyui_alive(base_url: str, timeout: float = 3.0) -> bool:
"""检查指定的 ComfyUI 服务器是否可用(能连通且非 5xx 即视为可用)"""
try:
async with httpx.AsyncClient(timeout=timeout, auth=COMFYUI_AUTH) as client:
resp = await client.get(f"{base_url}/system_stats")
return resp.status_code < 500
except Exception as e:
print(f"ComfyUI 健康检查失败 {base_url}: {e}")
return False
async def pick_comfyui_url() -> str:
"""优先返回正式服务器,不可用时切换到备份服务器"""
if await check_comfyui_alive(COMFYUI_URL):
print(f"使用正式服务器: {COMFYUI_URL}")
return COMFYUI_URL
print(f"正式服务器不可用,尝试切换到备份服务器: {COMFYUI_URL_BACKUP}")
if await check_comfyui_alive(COMFYUI_URL_BACKUP):
print(f"使用备份服务器: {COMFYUI_URL_BACKUP}")
return COMFYUI_URL_BACKUP
raise HTTPException(status_code=503, detail="正式与备份 ComfyUI 服务器均不可用")
async def upload_image(client: httpx.AsyncClient, base_url: str, image_bytes: bytes, filename: str) -> str:
"""上传图片到 ComfyUI,返回服务器文件名"""
files = {
"image": (filename, io.BytesIO(image_bytes), "image/jpeg"),
}
data = {"overwrite": "true"}
resp = await client.post(f"{base_url}/upload/image", files=files, data=data)
resp.raise_for_status()
result = resp.json()
return result["name"]
async def queue_prompt(client: httpx.AsyncClient, base_url: str, workflow: dict) -> str:
"""提交工作流到队列,返回 prompt_id"""
payload = {"prompt": workflow, "client_id": str(uuid.uuid4())}
resp = await client.post(f"{base_url}/prompt", json=payload)
resp.raise_for_status()
return resp.json()["prompt_id"]
async def wait_for_result(client: httpx.AsyncClient, base_url: str, prompt_id: str, timeout: int = 300) -> dict:
"""轮询历史记录,等待任务完成,返回输出节点数据"""
deadline = time.time() + timeout
while time.time() < deadline:
resp = await client.get(f"{base_url}/history/{prompt_id}")
resp.raise_for_status()
history = resp.json()
if prompt_id in history:
entry = history[prompt_id]
status = entry.get("status", {})
if status.get("completed"):
return entry.get("outputs", {})
if status.get("status_str") == "error":
messages = status.get("messages", [])
raise HTTPException(status_code=500, detail=f"ComfyUI 工作流执行出错: {messages}")
await asyncio.sleep(2)
raise HTTPException(status_code=504, detail="等待 ComfyUI 超时(300秒)")
async def fetch_image_bytes(client: httpx.AsyncClient, base_url: str, filename: str, subfolder: str = "", type_: str = "output") -> bytes:
"""从 ComfyUI 下载图片,返回原始字节"""
params = {"filename": filename, "subfolder": subfolder, "type": type_}
resp = await client.get(f"{base_url}/view", params=params)
resp.raise_for_status()
return resp.content
@app.post("/try-on", response_model=TryOnResponse)
async def try_on(req: TryOnRequest):
"""
单图编辑接口
- model_image: 输入图片 base64
- change_desc: 可选的编辑描述(非空时覆盖工作流节点 31)
返回 result_url: 生成结果的 OSS URL
"""
print(f"接收到单图编辑请求,正在处理...")
comfyui_url = await pick_comfyui_url()
workflow = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8"))
desc = (req.change_desc or "").strip()
if desc:
workflow[NODE_PROMPT]["inputs"] = {"value": desc}
async with httpx.AsyncClient(timeout=60.0, auth=COMFYUI_AUTH) as client:
try:
model_bytes = decode_base64_image(req.model_image)
except Exception as e:
raise HTTPException(status_code=400, detail=f"图片解码失败: {e}")
uid = uuid.uuid4().hex[:8]
model_fname = f"model_{uid}.jpg"
try:
model_name = await upload_image(client, comfyui_url, model_bytes, model_fname)
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"上传图片到 ComfyUI 失败: {e}")
workflow[NODE_MODEL]["inputs"]["image"] = model_name
try:
prompt_id = await queue_prompt(client, comfyui_url, workflow)
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"提交工作流失败: {e}")
async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as poll_client:
outputs = await wait_for_result(poll_client, comfyui_url, prompt_id, timeout=300)
node_output = outputs.get(NODE_OUTPUT)
if not node_output:
raise HTTPException(status_code=500, detail=f"工作流未返回节点 {NODE_OUTPUT} 的输出")
images = node_output.get("images", [])
if not images:
raise HTTPException(status_code=500, detail="输出节点没有图片")
img_info = images[0]
filename = img_info["filename"]
subfolder = img_info.get("subfolder", "")
type_ = img_info.get("type", "output")
async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as dl_client:
result_bytes = await fetch_image_bytes(dl_client, comfyui_url, filename, subfolder, type_)
suffix = Path(filename).suffix or ".png"
object_name = f"tryon/{uuid.uuid4().hex}{suffix}"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(result_bytes)
tmp_path = tmp.name
try:
result_url = upload_to_oss(tmp_path, object_name)
finally:
os.unlink(tmp_path)
if not result_url:
raise HTTPException(status_code=500, detail="上传结果图片到 OSS 失败")
return TryOnResponse(result_url=result_url)
@app.get("/health")
async def health():
"""健康检查"""
primary_ok = await check_comfyui_alive(COMFYUI_URL)
backup_ok = await check_comfyui_alive(COMFYUI_URL_BACKUP)
if primary_ok:
active = COMFYUI_URL
elif backup_ok:
active = COMFYUI_URL_BACKUP
else:
active = None
return {
"status": "ok" if active else "degraded",
"comfyui_active": active,
"comfyui_primary": {"url": COMFYUI_URL, "alive": primary_ok},
"comfyui_backup": {"url": COMFYUI_URL_BACKUP, "alive": backup_ok},
}
static_dir = Path(__file__).parent / "static"
static_dir.mkdir(exist_ok=True)
print(f"静态文件目录: {static_dir}")
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
@app.get("/")
async def index():
return FileResponse(str(static_dir / "index.html"))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=12221, log_level="info")
+390
View File
@@ -0,0 +1,390 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI 换装系统</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: #0f0f13;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 16px 64px;
}
h1 {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #a78bfa, #60a5fa, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 0.9rem;
color: #888;
margin-bottom: 40px;
text-align: center;
}
.upload-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
width: 100%;
max-width: 460px;
margin-bottom: 32px;
}
.upload-card {
background: #1a1a24;
border: 2px dashed #333;
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
position: relative;
min-height: 280px;
}
.upload-card:hover {
border-color: #7c3aed;
background: #1e1e2e;
}
.upload-card.has-image {
border-style: solid;
border-color: #6d28d9;
}
.upload-card input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
.upload-icon {
font-size: 2.5rem;
line-height: 1;
}
.upload-label {
font-size: 1rem;
font-weight: 600;
color: #c4b5fd;
}
.upload-hint {
font-size: 0.78rem;
color: #666;
text-align: center;
}
.preview-img {
width: 100%;
max-height: 200px;
object-fit: contain;
border-radius: 10px;
display: none;
}
.preview-img.visible {
display: block;
}
.btn-run {
padding: 14px 56px;
font-size: 1.05rem;
font-weight: 700;
border: none;
border-radius: 50px;
background: linear-gradient(135deg, #7c3aed, #2563eb);
color: #fff;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
letter-spacing: 0.5px;
}
.btn-run:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-run:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.status-bar {
margin-top: 20px;
font-size: 0.9rem;
color: #888;
min-height: 24px;
text-align: center;
}
.status-bar.running { color: #60a5fa; }
.status-bar.success { color: #34d399; }
.status-bar.error { color: #f87171; }
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #60a5fa44;
border-top-color: #60a5fa;
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.result-section {
margin-top: 40px;
width: 100%;
max-width: 500px;
display: none;
flex-direction: column;
align-items: center;
gap: 16px;
}
.result-section.visible { display: flex; }
.result-title {
font-size: 1.1rem;
font-weight: 600;
color: #a78bfa;
}
.result-img {
width: 100%;
border-radius: 16px;
box-shadow: 0 0 40px #7c3aed44;
}
.btn-download {
padding: 10px 36px;
border: 2px solid #7c3aed;
border-radius: 50px;
background: transparent;
color: #a78bfa;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-download:hover {
background: #7c3aed22;
}
.prompt-panel {
width: 100%;
max-width: 460px;
margin-bottom: 24px;
background: #1a1a24;
border-radius: 16px;
padding: 20px;
border: 1px solid #2a2a3a;
}
.prompt-panel .prompt-title {
font-size: 0.85rem;
font-weight: 600;
color: #a78bfa;
margin-bottom: 12px;
}
.radio-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
font-size: 0.9rem;
color: #ccc;
}
.radio-row input { margin-top: 3px; accent-color: #7c3aed; flex-shrink: 0; }
.change-desc-input {
width: 100%;
margin-top: 4px;
padding: 12px;
border-radius: 10px;
border: 1px solid #333;
background: #12121a;
color: #e0e0e0;
font-family: inherit;
font-size: 0.88rem;
resize: vertical;
min-height: 88px;
}
.change-desc-input:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 640px) {
h1 { font-size: 1.5rem; }
}
</style>
</head>
<body>
<h1>AI 换装系统</h1>
<p class="subtitle">上传一张图片,AI 自动按提示词进行编辑</p>
<div class="upload-grid">
<!-- 模特/原图 -->
<div class="upload-card" id="card-model">
<input type="file" accept="image/*" id="input-model" />
<div class="upload-icon">🧍</div>
<div class="upload-label">模特图片</div>
<div class="upload-hint">节点 11 · 人物全身照</div>
<img class="preview-img" id="preview-model" alt="模特预览" />
</div>
</div>
<div class="prompt-panel" id="prompt-panel">
<div class="prompt-title">提示词(change_desc</div>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-default" value="default" checked />
<span>使用默认(传空,不覆盖工作流节点 31)</span>
</label>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-custom" value="custom" />
<span>自定义提示词</span>
</label>
<textarea id="change-desc" class="change-desc-input" placeholder="输入编辑描述…" rows="4" disabled></textarea>
</div>
<button class="btn-run" id="btn-run" disabled>开始生成</button>
<div class="status-bar" id="status-bar"></div>
<div class="result-section" id="result-section">
<div class="result-title">生成结果</div>
<img class="result-img" id="result-img" alt="生成结果" />
<button class="btn-download" id="btn-download">下载图片</button>
</div>
<script>
const images = { model: null };
function setupUpload(inputId, previewId, cardId, key) {
const input = document.getElementById(inputId);
const preview = document.getElementById(previewId);
const card = document.getElementById(cardId);
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
images[key] = ev.target.result; // data:image/...;base64,...
preview.src = ev.target.result;
preview.classList.add('visible');
card.classList.add('has-image');
updateRunButton();
};
reader.readAsDataURL(file);
});
}
setupUpload('input-model', 'preview-model', 'card-model', 'model');
const promptDefault = document.getElementById('prompt-default');
const promptCustom = document.getElementById('prompt-custom');
const changeDescEl = document.getElementById('change-desc');
function syncPromptUi() {
changeDescEl.disabled = !promptCustom.checked;
}
promptDefault.addEventListener('change', syncPromptUi);
promptCustom.addEventListener('change', syncPromptUi);
function getChangeDesc() {
return promptCustom.checked ? changeDescEl.value.trim() : '';
}
function updateRunButton() {
const btn = document.getElementById('btn-run');
btn.disabled = !images.model;
}
function setStatus(msg, cls = '') {
const el = document.getElementById('status-bar');
el.className = 'status-bar ' + cls;
el.innerHTML = msg;
}
function elapsed(start) {
const s = Math.round((Date.now() - start) / 1000);
return `${s}`;
}
document.getElementById('btn-run').addEventListener('click', async () => {
const btn = document.getElementById('btn-run');
const resultSection = document.getElementById('result-section');
const resultImg = document.getElementById('result-img');
btn.disabled = true;
resultSection.classList.remove('visible');
const start = Date.now();
let timer = setInterval(() => {
setStatus(`<span class="spinner"></span>正在生成中,请稍候… 已等待 ${elapsed(start)}`, 'running');
}, 1000);
setStatus('<span class="spinner"></span>正在上传图片并提交工作流…', 'running');
try {
const resp = await fetch('/try-on', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_image: images.model,
change_desc: getChangeDesc(),
}),
});
clearInterval(timer);
if (!resp.ok) {
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
throw new Error(err.detail || '请求失败');
}
const data = await resp.json();
resultImg.src = data.result_url;
resultSection.classList.add('visible');
setStatus(`✅ 生成完成!耗时 ${elapsed(start)}`, 'success');
document.getElementById('btn-download').onclick = () => {
const a = document.createElement('a');
a.href = data.result_url;
a.download = `tryon_${Date.now()}.png`;
a.target = '_blank';
a.click();
};
} catch (err) {
clearInterval(timer);
setStatus(`❌ 错误:${err.message}`, 'error');
} finally {
updateRunButton();
}
});
</script>
</body>
</html>
+440
View File
@@ -0,0 +1,440 @@
{
"1": {
"inputs": {
"samples": [
"22",
0
],
"vae": [
"13",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"2": {
"inputs": {
"to_ref": true,
"ref_main_image": false,
"ref_longest_edge": [
"20",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"10",
0
],
"configs": [
"8",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"6": {
"inputs": {
"conditioning": [
"15",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "条件零化"
}
},
"7": {
"inputs": {
"custom_output": [
"15",
2
]
},
"class_type": "QwenEditOutputExtractor",
"_meta": {
"title": "Qwen Edit Output Extractor"
}
},
"8": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"9",
0
],
"ref_crop": "pad",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"11",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"9": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"11",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"10": {
"inputs": {
"image": "5d22eef9-c44c-437c-ab8b-a19c9137a5a1.webp"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"11": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161212.795.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"13": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"14": {
"inputs": {
"prompt": "",
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"18",
0
],
"vae": [
"13",
0
],
"configs": [
"2",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"15": {
"inputs": {
"prompt": [
"31",
0
],
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"18",
0
],
"vae": [
"13",
0
],
"configs": [
"2",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"16": {
"inputs": {
"gguf_name": "FireRed-Image-Edit-1.1-transformer-q4_k_m.gguf"
},
"class_type": "LoaderGGUF",
"_meta": {
"title": "GGUF Loader"
}
},
"17": {
"inputs": {
"lora_name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors",
"strength_model": 1,
"model": [
"16",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoRA加载器(仅模型)"
}
},
"18": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "ClipLoaderGGUF",
"_meta": {
"title": "GGUF CLIP Loader"
}
},
"19": {
"inputs": {
"image": [
"1",
0
],
"pad_info": [
"7",
0
]
},
"class_type": "CropWithPadInfo",
"_meta": {
"title": "Crop With Pad Info"
}
},
"20": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"10",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"21": {
"inputs": {
"samples1": [
"15",
1
],
"samples2": [
"14",
1
]
},
"class_type": "LatentAdd",
"_meta": {
"title": "Latent相加"
}
},
"22": {
"inputs": {
"seed": 11489974198335,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"17",
0
],
"positive": [
"15",
0
],
"negative": [
"14",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"24": {
"inputs": {
"upscale_method": "lanczos",
"scale_by": [
"19",
1
],
"image": [
"19",
0
]
},
"class_type": "ImageScaleBy",
"_meta": {
"title": "缩放图像(比例)"
}
},
"25": {
"inputs": {
"Number": "1536"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"28": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "disabled",
"samples": [
"21",
0
]
},
"class_type": "LatentUpscale",
"_meta": {
"title": "缩放Latent"
}
},
"29": {
"inputs": {
"Number": "768"
},
"class_type": "Int",
"_meta": {
"title": "宽度"
}
},
"30": {
"inputs": {
"Number": "1024"
},
"class_type": "Int",
"_meta": {
"title": "高度"
}
},
"31": {
"inputs": {
"value": "图1的背景改成纯色深灰色,生成人物全身站立图,穿图2的衣服。维持衣服材质。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "字符串(多行)"
}
},
"32": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "center",
"image": [
"24",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"33": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"32",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"36": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_tldsp_00001_.png&type=temp&subfolder=&rand=0.59726273806135"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_tldsp_00002_.png&type=temp&subfolder=&rand=0.5669098706160076"
}
]
},
"image_a": [
"32",
0
],
"image_b": [
"11",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
}
}
+1
View File
@@ -438,3 +438,4 @@
} }
} }
} }
+411
View File
@@ -0,0 +1,411 @@
{
"1": {
"inputs": {
"samples": [
"22",
0
],
"vae": [
"13",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"2": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"20",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"10",
0
],
"configs": [
"8",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"7": {
"inputs": {
"custom_output": [
"15",
2
]
},
"class_type": "QwenEditOutputExtractor",
"_meta": {
"title": "Qwen Edit Output Extractor"
}
},
"8": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"9",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"11",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"9": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"11",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"10": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161212.795.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"11": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161206.244.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"13": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"14": {
"inputs": {
"prompt": "",
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"38",
0
],
"vae": [
"13",
0
],
"configs": [
"2",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"15": {
"inputs": {
"prompt": [
"31",
0
],
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"38",
0
],
"vae": [
"13",
0
],
"configs": [
"2",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"16": {
"inputs": {
"gguf_name": "FireRed-Image-Edit-1.1-Q4_0.gguf"
},
"class_type": "LoaderGGUF",
"_meta": {
"title": "GGUF Loader"
}
},
"17": {
"inputs": {
"lora_name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors",
"strength_model": 1,
"model": [
"16",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoRA加载器(仅模型)"
}
},
"19": {
"inputs": {
"image": [
"1",
0
],
"pad_info": [
"7",
0
]
},
"class_type": "CropWithPadInfo",
"_meta": {
"title": "Crop With Pad Info"
}
},
"20": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"10",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"21": {
"inputs": {
"samples1": [
"15",
1
],
"samples2": [
"14",
1
]
},
"class_type": "LatentAdd",
"_meta": {
"title": "Latent相加"
}
},
"22": {
"inputs": {
"seed": 1121572473225362,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"17",
0
],
"positive": [
"15",
0
],
"negative": [
"14",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"25": {
"inputs": {
"Number": "1300"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"28": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "disabled",
"samples": [
"21",
0
]
},
"class_type": "LatentUpscale",
"_meta": {
"title": "缩放Latent"
}
},
"29": {
"inputs": {
"Number": "768"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"30": {
"inputs": {
"Number": "1024"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"31": {
"inputs": {
"value": "图1的背景改成深灰色。并穿上图2的上衣和短裤。保持图1人物的姿势,人脸,配饰不变,构图不变。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "字符串(多行)"
}
},
"32": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "center",
"image": [
"19",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"33": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"32",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"36": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_olfct_00001_.png&type=temp&subfolder=&rand=0.5054960464066687"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_olfct_00002_.png&type=temp&subfolder=&rand=0.03696578228441516"
}
]
},
"image_a": [
"32",
0
],
"image_b": [
"11",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"38": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "加载CLIP"
}
}
}
-6
View File
@@ -1,6 +0,0 @@
/usr/local/miniconda3/envs/py312/lib/python3.12/site-packages/requests/__init__.py:113: RequestsDependencyWarning: urllib3 (2.6.3) or chardet (7.0.1)/charset_normalizer (3.4.5) doesn't match a supported version!
warnings.warn(
INFO: Started server process [9669]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:12222 (Press CTRL+C to quit)
+81 -29
View File
@@ -17,6 +17,7 @@ import uuid
from pathlib import Path from pathlib import Path
import httpx import httpx
from PIL import Image
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
@@ -25,15 +26,24 @@ from pydantic import BaseModel
import oss2 import oss2
COMFYUI_URL = "http://127.0.0.1:8188" # COMFYUI_URL = "http://127.0.0.1:8188"
# COMFYUI_URL = "http://117.50.80.187:47697" COMFYUI_URL = "http://112.126.94.241:28188"
COMFYUI_URL_BACKUP = "http://112.126.94.241:38188"
WORKFLOW_PATH = Path(__file__).parent / "change2_1_ex.json" WORKFLOW_PATH = Path(__file__).parent / "change2_1_ex.json"
_cred_dir = Path(__file__).parent.parent
COMFYUI_USER = (_cred_dir / "user.txt").read_text(encoding="utf-8").strip()
COMFYUI_PASS = (_cred_dir / "password.txt").read_text(encoding="utf-8").strip()
COMFYUI_AUTH = (COMFYUI_USER, COMFYUI_PASS)
# WORKFLOW_PATH = Path(__file__).parent / "change_fast2.json"
# 节点 ID 映射 # 节点 ID 映射
NODE_MODEL = "11" # 模特 NODE_MODEL = "11" # 模特
NODE_SHIRT = "10" # 上衣 NODE_SHIRT = "10" # 上衣
# NODE_PANTS = "4" # 裤子 # NODE_PANTS = "4" # 裤子
NODE_OUTPUT = "33" # 输出 NODE_OUTPUT = "33" # 输出
NODE_PROMPT = "31" # 文本提示(PrimitiveStringMultiline
app = FastAPI(title="ComfyUI 换装服务") app = FastAPI(title="ComfyUI 换装服务")
@@ -79,6 +89,7 @@ def upload_to_oss(image_path, object_name=None):
class TryOnRequest(BaseModel): class TryOnRequest(BaseModel):
model_image: str # base64 模特图片 model_image: str # base64 模特图片
shirt_image: str # base64 上衣图片 shirt_image: str # base64 上衣图片
change_desc: str = "" # 换装/编辑描述,可空;非空时替换节点 31 的 inputs
class TryOnResponse(BaseModel): class TryOnResponse(BaseModel):
@@ -92,31 +103,54 @@ def decode_base64_image(b64_str: str) -> bytes:
return base64.b64decode(b64_str) return base64.b64decode(b64_str)
async def upload_image(client: httpx.AsyncClient, image_bytes: bytes, filename: str) -> str: async def check_comfyui_alive(base_url: str, timeout: float = 3.0) -> bool:
"""检查指定的 ComfyUI 服务器是否可用(能连通且非 5xx 即视为可用)"""
try:
async with httpx.AsyncClient(timeout=timeout, auth=COMFYUI_AUTH) as client:
resp = await client.get(f"{base_url}/system_stats")
return resp.status_code < 500
except Exception as e:
print(f"ComfyUI 健康检查失败 {base_url}: {e}")
return False
async def pick_comfyui_url() -> str:
"""优先返回正式服务器,不可用时切换到备份服务器"""
if await check_comfyui_alive(COMFYUI_URL):
print(f"使用正式服务器: {COMFYUI_URL}")
return COMFYUI_URL
print(f"正式服务器不可用,尝试切换到备份服务器: {COMFYUI_URL_BACKUP}")
if await check_comfyui_alive(COMFYUI_URL_BACKUP):
print(f"使用备份服务器: {COMFYUI_URL_BACKUP}")
return COMFYUI_URL_BACKUP
raise HTTPException(status_code=503, detail="正式与备份 ComfyUI 服务器均不可用")
async def upload_image(client: httpx.AsyncClient, base_url: str, image_bytes: bytes, filename: str) -> str:
"""上传图片到 ComfyUI,返回服务器文件名""" """上传图片到 ComfyUI,返回服务器文件名"""
files = { files = {
"image": (filename, io.BytesIO(image_bytes), "image/jpeg"), "image": (filename, io.BytesIO(image_bytes), "image/jpeg"),
} }
data = {"overwrite": "true"} data = {"overwrite": "true"}
resp = await client.post(f"{COMFYUI_URL}/upload/image", files=files, data=data) resp = await client.post(f"{base_url}/upload/image", files=files, data=data)
resp.raise_for_status() resp.raise_for_status()
result = resp.json() result = resp.json()
return result["name"] return result["name"]
async def queue_prompt(client: httpx.AsyncClient, workflow: dict) -> str: async def queue_prompt(client: httpx.AsyncClient, base_url: str, workflow: dict) -> str:
"""提交工作流到队列,返回 prompt_id""" """提交工作流到队列,返回 prompt_id"""
payload = {"prompt": workflow, "client_id": str(uuid.uuid4())} payload = {"prompt": workflow, "client_id": str(uuid.uuid4())}
resp = await client.post(f"{COMFYUI_URL}/prompt", json=payload) resp = await client.post(f"{base_url}/prompt", json=payload)
resp.raise_for_status() resp.raise_for_status()
return resp.json()["prompt_id"] return resp.json()["prompt_id"]
async def wait_for_result(client: httpx.AsyncClient, prompt_id: str, timeout: int = 300) -> dict: async def wait_for_result(client: httpx.AsyncClient, base_url: str, prompt_id: str, timeout: int = 300) -> dict:
"""轮询历史记录,等待任务完成,返回输出节点数据""" """轮询历史记录,等待任务完成,返回输出节点数据"""
deadline = time.time() + timeout deadline = time.time() + timeout
while time.time() < deadline: while time.time() < deadline:
resp = await client.get(f"{COMFYUI_URL}/history/{prompt_id}") resp = await client.get(f"{base_url}/history/{prompt_id}")
resp.raise_for_status() resp.raise_for_status()
history = resp.json() history = resp.json()
if prompt_id in history: if prompt_id in history:
@@ -131,10 +165,10 @@ async def wait_for_result(client: httpx.AsyncClient, prompt_id: str, timeout: in
raise HTTPException(status_code=504, detail="等待 ComfyUI 超时(300秒)") raise HTTPException(status_code=504, detail="等待 ComfyUI 超时(300秒)")
async def fetch_image_bytes(client: httpx.AsyncClient, filename: str, subfolder: str = "", type_: str = "output") -> bytes: async def fetch_image_bytes(client: httpx.AsyncClient, base_url: str, filename: str, subfolder: str = "", type_: str = "output") -> bytes:
"""从 ComfyUI 下载图片,返回原始字节""" """从 ComfyUI 下载图片,返回原始字节"""
params = {"filename": filename, "subfolder": subfolder, "type": type_} params = {"filename": filename, "subfolder": subfolder, "type": type_}
resp = await client.get(f"{COMFYUI_URL}/view", params=params) resp = await client.get(f"{base_url}/view", params=params)
resp.raise_for_status() resp.raise_for_status()
return resp.content return resp.content
@@ -149,48 +183,45 @@ async def try_on(req: TryOnRequest):
""" """
print(f"接收到换装请求,正在处理...") print(f"接收到换装请求,正在处理...")
# 加载工作流模板
comfyui_url = await pick_comfyui_url()
workflow = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8")) workflow = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8"))
desc = (req.change_desc or "").strip()
if desc:
workflow[NODE_PROMPT]["inputs"] = {"value": desc}
async with httpx.AsyncClient(timeout=60.0, auth=COMFYUI_AUTH) as client:
async with httpx.AsyncClient(timeout=60.0) as client:
# 解码两张图片
try: try:
model_bytes = decode_base64_image(req.model_image) model_bytes = decode_base64_image(req.model_image)
shirt_bytes = decode_base64_image(req.shirt_image) shirt_bytes = decode_base64_image(req.shirt_image)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"图片解码失败: {e}") raise HTTPException(status_code=400, detail=f"图片解码失败: {e}")
# 生成唯一文件名,避免缓存冲突
uid = uuid.uuid4().hex[:8] uid = uuid.uuid4().hex[:8]
model_fname = f"model_{uid}.jpg" model_fname = f"model_{uid}.jpg"
shirt_fname = f"shirt_{uid}.jpg" shirt_fname = f"shirt_{uid}.jpg"
# 并发上传两张图片
try: try:
model_name, shirt_name = await asyncio.gather( model_name, shirt_name = await asyncio.gather(
upload_image(client, model_bytes, model_fname), upload_image(client, comfyui_url, model_bytes, model_fname),
upload_image(client, shirt_bytes, shirt_fname), upload_image(client, comfyui_url, shirt_bytes, shirt_fname),
) )
except httpx.HTTPError as e: except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"上传图片到 ComfyUI 失败: {e}") raise HTTPException(status_code=502, detail=f"上传图片到 ComfyUI 失败: {e}")
# 修改工作流节点的图片输入
workflow[NODE_MODEL]["inputs"]["image"] = model_name workflow[NODE_MODEL]["inputs"]["image"] = model_name
workflow[NODE_SHIRT]["inputs"]["image"] = shirt_name workflow[NODE_SHIRT]["inputs"]["image"] = shirt_name
# 提交工作流
try: try:
prompt_id = await queue_prompt(client, workflow) prompt_id = await queue_prompt(client, comfyui_url, workflow)
except httpx.HTTPError as e: except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"提交工作流失败: {e}") raise HTTPException(status_code=502, detail=f"提交工作流失败: {e}")
# 等待结果(最多5分钟) async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as poll_client:
async with httpx.AsyncClient(timeout=30.0) as poll_client: outputs = await wait_for_result(poll_client, comfyui_url, prompt_id, timeout=300)
outputs = await wait_for_result(poll_client, prompt_id, timeout=300)
# 从节点33的输出获取图片
node_output = outputs.get(NODE_OUTPUT) node_output = outputs.get(NODE_OUTPUT)
if not node_output: if not node_output:
raise HTTPException(status_code=500, detail=f"工作流未返回节点 {NODE_OUTPUT} 的输出") raise HTTPException(status_code=500, detail=f"工作流未返回节点 {NODE_OUTPUT} 的输出")
@@ -204,9 +235,17 @@ async def try_on(req: TryOnRequest):
subfolder = img_info.get("subfolder", "") subfolder = img_info.get("subfolder", "")
type_ = img_info.get("type", "output") type_ = img_info.get("type", "output")
# 下载结果图片 async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as dl_client:
async with httpx.AsyncClient(timeout=30.0) as dl_client: result_bytes = await fetch_image_bytes(dl_client, comfyui_url, filename, subfolder, type_)
result_bytes = await fetch_image_bytes(dl_client, filename, subfolder, type_)
# 缩放到 768x1024
# img = Image.open(io.BytesIO(result_bytes))
# img = img.resize((768, 1024), Image.LANCZOS)
# buf = io.BytesIO()
# fmt = (Path(filename).suffix.lstrip(".") or "png").upper()
# fmt = "JPEG" if fmt in ("JPG", "JPEG") else fmt
# img.save(buf, format=fmt)
# result_bytes = buf.getvalue()
# 将图片保存到临时文件,上传到 OSS # 将图片保存到临时文件,上传到 OSS
suffix = Path(filename).suffix or ".png" suffix = Path(filename).suffix or ".png"
@@ -229,7 +268,20 @@ async def try_on(req: TryOnRequest):
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""健康检查""" """健康检查"""
return {"status": "ok", "comfyui": COMFYUI_URL} primary_ok = await check_comfyui_alive(COMFYUI_URL)
backup_ok = await check_comfyui_alive(COMFYUI_URL_BACKUP)
if primary_ok:
active = COMFYUI_URL
elif backup_ok:
active = COMFYUI_URL_BACKUP
else:
active = None
return {
"status": "ok" if active else "degraded",
"comfyui_active": active,
"comfyui_primary": {"url": COMFYUI_URL, "alive": primary_ok},
"comfyui_backup": {"url": COMFYUI_URL_BACKUP, "alive": backup_ok},
}
# 挂载静态文件(前端页面) # 挂载静态文件(前端页面)
+70
View File
@@ -196,6 +196,49 @@
background: #7c3aed22; background: #7c3aed22;
} }
.prompt-panel {
width: 100%;
max-width: 900px;
margin-bottom: 24px;
background: #1a1a24;
border-radius: 16px;
padding: 20px;
border: 1px solid #2a2a3a;
}
.prompt-panel .prompt-title {
font-size: 0.85rem;
font-weight: 600;
color: #a78bfa;
margin-bottom: 12px;
}
.radio-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
font-size: 0.9rem;
color: #ccc;
}
.radio-row input { margin-top: 3px; accent-color: #7c3aed; flex-shrink: 0; }
.change-desc-input {
width: 100%;
margin-top: 4px;
padding: 12px;
border-radius: 10px;
border: 1px solid #333;
background: #12121a;
color: #e0e0e0;
font-family: inherit;
font-size: 0.88rem;
resize: vertical;
min-height: 88px;
}
.change-desc-input:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 640px) { @media (max-width: 640px) {
.upload-grid { grid-template-columns: 1fr; } .upload-grid { grid-template-columns: 1fr; }
h1 { font-size: 1.5rem; } h1 { font-size: 1.5rem; }
@@ -226,6 +269,19 @@
</div> </div>
</div> </div>
<div class="prompt-panel" id="prompt-panel">
<div class="prompt-title">提示词(change_desc</div>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-default" value="default" checked />
<span>使用默认(传空,不覆盖工作流节点 31)</span>
</label>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-custom" value="custom" />
<span>自定义提示词</span>
</label>
<textarea id="change-desc" class="change-desc-input" placeholder="输入换装/编辑描述…" rows="4" disabled></textarea>
</div>
<button class="btn-run" id="btn-run" disabled>开始换装</button> <button class="btn-run" id="btn-run" disabled>开始换装</button>
<div class="status-bar" id="status-bar"></div> <div class="status-bar" id="status-bar"></div>
@@ -261,6 +317,19 @@
setupUpload('input-model', 'preview-model', 'card-model', 'model'); setupUpload('input-model', 'preview-model', 'card-model', 'model');
setupUpload('input-shirt', 'preview-shirt', 'card-shirt', 'shirt'); setupUpload('input-shirt', 'preview-shirt', 'card-shirt', 'shirt');
const promptDefault = document.getElementById('prompt-default');
const promptCustom = document.getElementById('prompt-custom');
const changeDescEl = document.getElementById('change-desc');
function syncPromptUi() {
changeDescEl.disabled = !promptCustom.checked;
}
promptDefault.addEventListener('change', syncPromptUi);
promptCustom.addEventListener('change', syncPromptUi);
function getChangeDesc() {
return promptCustom.checked ? changeDescEl.value.trim() : '';
}
function updateRunButton() { function updateRunButton() {
const btn = document.getElementById('btn-run'); const btn = document.getElementById('btn-run');
btn.disabled = !(images.model && images.shirt); btn.disabled = !(images.model && images.shirt);
@@ -298,6 +367,7 @@
body: JSON.stringify({ body: JSON.stringify({
model_image: images.model, model_image: images.model,
shirt_image: images.shirt, shirt_image: images.shirt,
change_desc: getChangeDesc(),
}), }),
}); });
+465
View File
@@ -0,0 +1,465 @@
{
"1": {
"inputs": {
"samples": [
"22",
0
],
"vae": [
"13",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"2": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"20",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"10",
0
],
"configs": [
"8",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"3": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"4",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"4": {
"inputs": {
"image": "man_xiazhuang_100.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"5": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"3",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"4",
0
],
"configs": [
"2",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"7": {
"inputs": {
"custom_output": [
"15",
2
]
},
"class_type": "QwenEditOutputExtractor",
"_meta": {
"title": "Qwen Edit Output Extractor"
}
},
"8": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"9",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"11",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"9": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"11",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"10": {
"inputs": {
"image": "微信图片_20260602170848_649_2470.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"11": {
"inputs": {
"image": "cc356b953b6b14ea330d0b509457bb06.png"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"13": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"14": {
"inputs": {
"prompt": "",
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"38",
0
],
"vae": [
"13",
0
],
"configs": [
"5",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"15": {
"inputs": {
"prompt": [
"31",
0
],
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"38",
0
],
"vae": [
"13",
0
],
"configs": [
"5",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"16": {
"inputs": {
"gguf_name": "FireRed-Image-Edit-1.1-transformer-q4_k_m.gguf"
},
"class_type": "LoaderGGUF",
"_meta": {
"title": "GGUF Loader"
}
},
"17": {
"inputs": {
"lora_name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors",
"strength_model": 1,
"model": [
"16",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoRA加载器(仅模型)"
}
},
"19": {
"inputs": {
"image": [
"1",
0
],
"pad_info": [
"7",
0
]
},
"class_type": "CropWithPadInfo",
"_meta": {
"title": "Crop With Pad Info"
}
},
"20": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"10",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"21": {
"inputs": {
"samples1": [
"15",
1
],
"samples2": [
"14",
1
]
},
"class_type": "LatentAdd",
"_meta": {
"title": "Latent相加"
}
},
"22": {
"inputs": {
"seed": 175444883678744,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"17",
0
],
"positive": [
"15",
0
],
"negative": [
"14",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"25": {
"inputs": {
"Number": "1300"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"28": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "disabled",
"samples": [
"21",
0
]
},
"class_type": "LatentUpscale",
"_meta": {
"title": "缩放Latent"
}
},
"29": {
"inputs": {
"Number": "768"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"30": {
"inputs": {
"Number": "1024"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"31": {
"inputs": {
"value": "图1的背景改成纯色深灰色,生成人物全身站立图,穿图2的衣服,穿图3的裤子。维持衣服材质。保持全身人像完整的头部和身体。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "字符串(多行)"
}
},
"32": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "center",
"image": [
"19",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"33": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"32",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"36": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_ltmdy_00071_.png&type=temp&subfolder=&rand=0.9521985052609522"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_ltmdy_00072_.png&type=temp&subfolder=&rand=0.17309370496973586"
}
]
},
"image_a": [
"32",
0
],
"image_b": [
"11",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"38": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "加载CLIP"
}
}
}
+494
View File
@@ -0,0 +1,494 @@
{
"1": {
"inputs": {
"samples": [
"22",
0
],
"vae": [
"13",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"2": {
"inputs": {
"to_ref": true,
"ref_main_image": false,
"ref_longest_edge": [
"20",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"10",
0
],
"configs": [
"8",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"3": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"4",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"4": {
"inputs": {
"image": "82aaadb9-1c86-4521-86f9-3583184f6e0f.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"5": {
"inputs": {
"to_ref": true,
"ref_main_image": false,
"ref_longest_edge": [
"3",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"4",
0
],
"configs": [
"2",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"6": {
"inputs": {
"conditioning": [
"15",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "条件零化"
}
},
"7": {
"inputs": {
"custom_output": [
"15",
2
]
},
"class_type": "QwenEditOutputExtractor",
"_meta": {
"title": "Qwen Edit Output Extractor"
}
},
"8": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"9",
0
],
"ref_crop": "pad",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "center",
"vl_upscale": "bicubic",
"image": [
"11",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"9": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"11",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"10": {
"inputs": {
"image": "5d22eef9-c44c-437c-ab8b-a19c9137a5a1.webp"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"11": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161212.795.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"13": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"14": {
"inputs": {
"prompt": "",
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"18",
0
],
"vae": [
"13",
0
],
"configs": [
"5",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"15": {
"inputs": {
"prompt": [
"31",
0
],
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"18",
0
],
"vae": [
"13",
0
],
"configs": [
"5",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"16": {
"inputs": {
"gguf_name": "FireRed-Image-Edit-1.1-transformer-q4_k_m.gguf"
},
"class_type": "LoaderGGUF",
"_meta": {
"title": "GGUF Loader"
}
},
"17": {
"inputs": {
"lora_name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors",
"strength_model": 1,
"model": [
"16",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoRA加载器(仅模型)"
}
},
"18": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "ClipLoaderGGUF",
"_meta": {
"title": "GGUF CLIP Loader"
}
},
"19": {
"inputs": {
"image": [
"1",
0
],
"pad_info": [
"7",
0
]
},
"class_type": "CropWithPadInfo",
"_meta": {
"title": "Crop With Pad Info"
}
},
"20": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"10",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"21": {
"inputs": {
"samples1": [
"15",
1
],
"samples2": [
"14",
1
]
},
"class_type": "LatentAdd",
"_meta": {
"title": "Latent相加"
}
},
"22": {
"inputs": {
"seed": 42692485840454,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "beta",
"denoise": 1,
"model": [
"17",
0
],
"positive": [
"15",
0
],
"negative": [
"14",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"24": {
"inputs": {
"upscale_method": "lanczos",
"scale_by": [
"19",
1
],
"image": [
"19",
0
]
},
"class_type": "ImageScaleBy",
"_meta": {
"title": "缩放图像(比例)"
}
},
"25": {
"inputs": {
"Number": "1536"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"28": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "disabled",
"samples": [
"21",
0
]
},
"class_type": "LatentUpscale",
"_meta": {
"title": "缩放Latent"
}
},
"29": {
"inputs": {
"Number": "768"
},
"class_type": "Int",
"_meta": {
"title": "宽度"
}
},
"30": {
"inputs": {
"Number": "1024"
},
"class_type": "Int",
"_meta": {
"title": "高度"
}
},
"31": {
"inputs": {
"value": "图1的背景改成纯色深灰色,生成人物全身站立图,穿图2的衣服,穿图3的衣服。维持衣服材质。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "字符串(多行)"
}
},
"32": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "center",
"image": [
"24",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"33": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"32",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"36": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_mpazx_00077_.png&type=temp&subfolder=&rand=0.7490888740442448"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_mpazx_00078_.png&type=temp&subfolder=&rand=0.37548041933644327"
}
]
},
"image_a": [
"32",
0
],
"image_b": [
"11",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
}
}
+465
View File
@@ -0,0 +1,465 @@
{
"1": {
"inputs": {
"samples": [
"22",
0
],
"vae": [
"13",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE解码"
}
},
"2": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"20",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"10",
0
],
"configs": [
"8",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"3": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"4",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"4": {
"inputs": {
"image": "70474cfbc4f51e083610b18f441396db.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"5": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"3",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"4",
0
],
"configs": [
"2",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"7": {
"inputs": {
"custom_output": [
"15",
2
]
},
"class_type": "QwenEditOutputExtractor",
"_meta": {
"title": "Qwen Edit Output Extractor"
}
},
"8": {
"inputs": {
"to_ref": true,
"ref_main_image": true,
"ref_longest_edge": [
"9",
0
],
"ref_crop": "center",
"ref_upscale": "lanczos",
"to_vl": true,
"vl_resize": true,
"vl_target_size": 384,
"vl_crop": "disabled",
"vl_upscale": "bicubic",
"image": [
"11",
0
]
},
"class_type": "QwenEditConfigPreparer",
"_meta": {
"title": "Qwen Edit Config Preparer"
}
},
"9": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"11",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"10": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161212.795.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"11": {
"inputs": {
"image": "cq5dam.web.hE7E3DA.1800.1800 - 2024-04-29T161206.244.jpg"
},
"class_type": "LoadImage",
"_meta": {
"title": "加载图像"
}
},
"13": {
"inputs": {
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "加载VAE"
}
},
"14": {
"inputs": {
"prompt": "",
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"38",
0
],
"vae": [
"13",
0
],
"configs": [
"5",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"15": {
"inputs": {
"prompt": [
"31",
0
],
"return_full_refs_cond": true,
"instruction": "Describe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.",
"clip": [
"38",
0
],
"vae": [
"13",
0
],
"configs": [
"5",
0
]
},
"class_type": "TextEncodeQwenImageEditPlusCustom_lrzjason",
"_meta": {
"title": "TextEncodeQwenImageEditPlusCustom lrzjason"
}
},
"16": {
"inputs": {
"gguf_name": "FireRed-Image-Edit-1.1-Q4_0.gguf"
},
"class_type": "LoaderGGUF",
"_meta": {
"title": "GGUF Loader"
}
},
"17": {
"inputs": {
"lora_name": "FireRed-Image-Edit-1.0-Lightning-8steps-v1.0.safetensors",
"strength_model": 1,
"model": [
"16",
0
]
},
"class_type": "LoraLoaderModelOnly",
"_meta": {
"title": "LoRA加载器(仅模型)"
}
},
"19": {
"inputs": {
"image": [
"1",
0
],
"pad_info": [
"7",
0
]
},
"class_type": "CropWithPadInfo",
"_meta": {
"title": "Crop With Pad Info"
}
},
"20": {
"inputs": {
"max_size": [
"25",
0
],
"image": [
"10",
0
]
},
"class_type": "QwenEditAdaptiveLongestEdge",
"_meta": {
"title": "Qwen Edit Adaptive Longest Edge"
}
},
"21": {
"inputs": {
"samples1": [
"15",
1
],
"samples2": [
"14",
1
]
},
"class_type": "LatentAdd",
"_meta": {
"title": "Latent相加"
}
},
"22": {
"inputs": {
"seed": 1121572473225362,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"17",
0
],
"positive": [
"15",
0
],
"negative": [
"14",
0
],
"latent_image": [
"28",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "K采样器"
}
},
"25": {
"inputs": {
"Number": "1300"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"28": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "disabled",
"samples": [
"21",
0
]
},
"class_type": "LatentUpscale",
"_meta": {
"title": "缩放Latent"
}
},
"29": {
"inputs": {
"Number": "768"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"30": {
"inputs": {
"Number": "1024"
},
"class_type": "Int",
"_meta": {
"title": "Int"
}
},
"31": {
"inputs": {
"value": "图1的背景改成纯色深灰色,生成人物全身站立图,穿图2的衣服,穿图3的衣服。维持衣服材质。"
},
"class_type": "PrimitiveStringMultiline",
"_meta": {
"title": "字符串(多行)"
}
},
"32": {
"inputs": {
"upscale_method": "nearest-exact",
"width": [
"29",
0
],
"height": [
"30",
0
],
"crop": "center",
"image": [
"19",
0
]
},
"class_type": "ImageScale",
"_meta": {
"title": "缩放图像"
}
},
"33": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"32",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "保存图像"
}
},
"36": {
"inputs": {
"rgthree_comparer": {
"images": [
{
"name": "A",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_olfct_00001_.png&type=temp&subfolder=&rand=0.5054960464066687"
},
{
"name": "B",
"selected": true,
"url": "/api/view?filename=rgthree.compare._temp_olfct_00002_.png&type=temp&subfolder=&rand=0.03696578228441516"
}
]
},
"image_a": [
"32",
0
],
"image_b": [
"11",
0
]
},
"class_type": "Image Comparer (rgthree)",
"_meta": {
"title": "Image Comparer (rgthree)"
}
},
"38": {
"inputs": {
"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors",
"type": "qwen_image",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "加载CLIP"
}
}
}
File diff suppressed because it is too large Load Diff
-10
View File
@@ -1,10 +0,0 @@
/usr/local/miniconda3/envs/py312/lib/python3.12/site-packages/requests/__init__.py:113: RequestsDependencyWarning: urllib3 (2.6.3) or chardet (7.0.1)/charset_normalizer (3.4.5) doesn't match a supported version!
warnings.warn(
INFO: Started server process [9612]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:12223 (Press CTRL+C to quit)
静态文件目录: /root/change_cloth_server/change3/static
接收到换装请求,正在处理...
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F627bcee434384a7b847faf2dbbce90ae.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730306&Signature=aATPXbjJCE3ArJnYfRyvSVSI8EE%3D
INFO: 127.0.0.1:39178 - "POST /try-on HTTP/1.1" 200 OK
+83 -29
View File
@@ -18,21 +18,31 @@ from pathlib import Path
import httpx import httpx
import oss2 import oss2
from PIL import Image
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
COMFYUI_URL = "http://127.0.0.1:8188" # COMFYUI_URL = "http://127.0.0.1:8188"
# COMFYUI_URL = "http://117.50.80.187:47697" COMFYUI_URL = "http://112.126.94.241:28188"
COMFYUI_URL_BACKUP = "http://112.126.94.241:38188"
WORKFLOW_PATH = Path(__file__).parent / "change2_2_0308.json" WORKFLOW_PATH = Path(__file__).parent / "change2_2_0308.json"
_cred_dir = Path(__file__).parent.parent
COMFYUI_USER = (_cred_dir / "user.txt").read_text(encoding="utf-8").strip()
COMFYUI_PASS = (_cred_dir / "password.txt").read_text(encoding="utf-8").strip()
COMFYUI_AUTH = (COMFYUI_USER, COMFYUI_PASS)
# WORKFLOW_PATH = Path(__file__).parent / "change_fast3.json"
# 节点 ID 映射 # 节点 ID 映射
NODE_MODEL = "11" # 模特 NODE_MODEL = "11" # 模特
NODE_SHIRT = "10" # 上衣 NODE_SHIRT = "10" # 上衣
NODE_PANTS = "4" # 裤子 NODE_PANTS = "4" # 裤子
NODE_OUTPUT = "33" # 输出 NODE_OUTPUT = "33" # 输出
NODE_PROMPT = "31" # 文本提示(PrimitiveStringMultiline
app = FastAPI(title="ComfyUI 换装服务") app = FastAPI(title="ComfyUI 换装服务")
@@ -70,6 +80,7 @@ class TryOnRequest(BaseModel):
model_image: str # base64 模特图片 model_image: str # base64 模特图片
shirt_image: str # base64 上衣图片 shirt_image: str # base64 上衣图片
pants_image: str # base64 裤子图片 pants_image: str # base64 裤子图片
change_desc: str = "" # 换装/编辑描述,可空;非空时替换节点 31 的 inputs
class TryOnResponse(BaseModel): class TryOnResponse(BaseModel):
@@ -83,31 +94,54 @@ def decode_base64_image(b64_str: str) -> bytes:
return base64.b64decode(b64_str) return base64.b64decode(b64_str)
async def upload_image(client: httpx.AsyncClient, image_bytes: bytes, filename: str) -> str: async def check_comfyui_alive(base_url: str, timeout: float = 3.0) -> bool:
"""检查指定的 ComfyUI 服务器是否可用(能连通且非 5xx 即视为可用)"""
try:
async with httpx.AsyncClient(timeout=timeout, auth=COMFYUI_AUTH) as client:
resp = await client.get(f"{base_url}/system_stats")
return resp.status_code < 500
except Exception as e:
print(f"ComfyUI 健康检查失败 {base_url}: {e}")
return False
async def pick_comfyui_url() -> str:
"""优先返回正式服务器,不可用时切换到备份服务器"""
if await check_comfyui_alive(COMFYUI_URL):
print(f"使用正式服务器: {COMFYUI_URL}")
return COMFYUI_URL
print(f"正式服务器不可用,尝试切换到备份服务器: {COMFYUI_URL_BACKUP}")
if await check_comfyui_alive(COMFYUI_URL_BACKUP):
print(f"使用备份服务器: {COMFYUI_URL_BACKUP}")
return COMFYUI_URL_BACKUP
raise HTTPException(status_code=503, detail="正式与备份 ComfyUI 服务器均不可用")
async def upload_image(client: httpx.AsyncClient, base_url: str, image_bytes: bytes, filename: str) -> str:
"""上传图片到 ComfyUI,返回服务器文件名""" """上传图片到 ComfyUI,返回服务器文件名"""
files = { files = {
"image": (filename, io.BytesIO(image_bytes), "image/jpeg"), "image": (filename, io.BytesIO(image_bytes), "image/jpeg"),
} }
data = {"overwrite": "true"} data = {"overwrite": "true"}
resp = await client.post(f"{COMFYUI_URL}/upload/image", files=files, data=data) resp = await client.post(f"{base_url}/upload/image", files=files, data=data)
resp.raise_for_status() resp.raise_for_status()
result = resp.json() result = resp.json()
return result["name"] return result["name"]
async def queue_prompt(client: httpx.AsyncClient, workflow: dict) -> str: async def queue_prompt(client: httpx.AsyncClient, base_url: str, workflow: dict) -> str:
"""提交工作流到队列,返回 prompt_id""" """提交工作流到队列,返回 prompt_id"""
payload = {"prompt": workflow, "client_id": str(uuid.uuid4())} payload = {"prompt": workflow, "client_id": str(uuid.uuid4())}
resp = await client.post(f"{COMFYUI_URL}/prompt", json=payload) resp = await client.post(f"{base_url}/prompt", json=payload)
resp.raise_for_status() resp.raise_for_status()
return resp.json()["prompt_id"] return resp.json()["prompt_id"]
async def wait_for_result(client: httpx.AsyncClient, prompt_id: str, timeout: int = 300) -> dict: async def wait_for_result(client: httpx.AsyncClient, base_url: str, prompt_id: str, timeout: int = 300) -> dict:
"""轮询历史记录,等待任务完成,返回输出节点数据""" """轮询历史记录,等待任务完成,返回输出节点数据"""
deadline = time.time() + timeout deadline = time.time() + timeout
while time.time() < deadline: while time.time() < deadline:
resp = await client.get(f"{COMFYUI_URL}/history/{prompt_id}") resp = await client.get(f"{base_url}/history/{prompt_id}")
resp.raise_for_status() resp.raise_for_status()
history = resp.json() history = resp.json()
if prompt_id in history: if prompt_id in history:
@@ -122,10 +156,10 @@ async def wait_for_result(client: httpx.AsyncClient, prompt_id: str, timeout: in
raise HTTPException(status_code=504, detail="等待 ComfyUI 超时(300秒)") raise HTTPException(status_code=504, detail="等待 ComfyUI 超时(300秒)")
async def fetch_image_bytes(client: httpx.AsyncClient, filename: str, subfolder: str = "", type_: str = "output") -> bytes: async def fetch_image_bytes(client: httpx.AsyncClient, base_url: str, filename: str, subfolder: str = "", type_: str = "output") -> bytes:
"""从 ComfyUI 下载图片,返回原始字节""" """从 ComfyUI 下载图片,返回原始字节"""
params = {"filename": filename, "subfolder": subfolder, "type": type_} params = {"filename": filename, "subfolder": subfolder, "type": type_}
resp = await client.get(f"{COMFYUI_URL}/view", params=params) resp = await client.get(f"{base_url}/view", params=params)
resp.raise_for_status() resp.raise_for_status()
return resp.content return resp.content
@@ -140,11 +174,16 @@ async def try_on(req: TryOnRequest):
返回 result_image: 换装结果 base64 返回 result_image: 换装结果 base64
""" """
print(f"接收到换装请求,正在处理...") print(f"接收到换装请求,正在处理...")
# 加载工作流模板
comfyui_url = await pick_comfyui_url()
workflow = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8")) workflow = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8"))
async with httpx.AsyncClient(timeout=60.0) as client: desc = (req.change_desc or "").strip()
# 解码三张图片 if desc:
workflow[NODE_PROMPT]["inputs"] = {"value": desc}
async with httpx.AsyncClient(timeout=60.0, auth=COMFYUI_AUTH) as client:
try: try:
model_bytes = decode_base64_image(req.model_image) model_bytes = decode_base64_image(req.model_image)
shirt_bytes = decode_base64_image(req.shirt_image) shirt_bytes = decode_base64_image(req.shirt_image)
@@ -152,38 +191,32 @@ async def try_on(req: TryOnRequest):
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"图片解码失败: {e}") raise HTTPException(status_code=400, detail=f"图片解码失败: {e}")
# 生成唯一文件名,避免缓存冲突
uid = uuid.uuid4().hex[:8] uid = uuid.uuid4().hex[:8]
model_fname = f"model_{uid}.jpg" model_fname = f"model_{uid}.jpg"
shirt_fname = f"shirt_{uid}.jpg" shirt_fname = f"shirt_{uid}.jpg"
pants_fname = f"pants_{uid}.jpg" pants_fname = f"pants_{uid}.jpg"
# 并发上传三张图片
try: try:
model_name, shirt_name, pants_name = await asyncio.gather( model_name, shirt_name, pants_name = await asyncio.gather(
upload_image(client, model_bytes, model_fname), upload_image(client, comfyui_url, model_bytes, model_fname),
upload_image(client, shirt_bytes, shirt_fname), upload_image(client, comfyui_url, shirt_bytes, shirt_fname),
upload_image(client, pants_bytes, pants_fname), upload_image(client, comfyui_url, pants_bytes, pants_fname),
) )
except httpx.HTTPError as e: except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"上传图片到 ComfyUI 失败: {e}") raise HTTPException(status_code=502, detail=f"上传图片到 ComfyUI 失败: {e}")
# 修改工作流节点的图片输入
workflow[NODE_MODEL]["inputs"]["image"] = model_name workflow[NODE_MODEL]["inputs"]["image"] = model_name
workflow[NODE_SHIRT]["inputs"]["image"] = shirt_name workflow[NODE_SHIRT]["inputs"]["image"] = shirt_name
workflow[NODE_PANTS]["inputs"]["image"] = pants_name workflow[NODE_PANTS]["inputs"]["image"] = pants_name
# 提交工作流
try: try:
prompt_id = await queue_prompt(client, workflow) prompt_id = await queue_prompt(client, comfyui_url, workflow)
except httpx.HTTPError as e: except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"提交工作流失败: {e}") raise HTTPException(status_code=502, detail=f"提交工作流失败: {e}")
# 等待结果(最多5分钟) async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as poll_client:
async with httpx.AsyncClient(timeout=30.0) as poll_client: outputs = await wait_for_result(poll_client, comfyui_url, prompt_id, timeout=300)
outputs = await wait_for_result(poll_client, prompt_id, timeout=300)
# 从节点33的输出获取图片
node_output = outputs.get(NODE_OUTPUT) node_output = outputs.get(NODE_OUTPUT)
if not node_output: if not node_output:
raise HTTPException(status_code=500, detail=f"工作流未返回节点 {NODE_OUTPUT} 的输出") raise HTTPException(status_code=500, detail=f"工作流未返回节点 {NODE_OUTPUT} 的输出")
@@ -197,9 +230,17 @@ async def try_on(req: TryOnRequest):
subfolder = img_info.get("subfolder", "") subfolder = img_info.get("subfolder", "")
type_ = img_info.get("type", "output") type_ = img_info.get("type", "output")
# 下载结果图片 async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as dl_client:
async with httpx.AsyncClient(timeout=30.0) as dl_client: result_bytes = await fetch_image_bytes(dl_client, comfyui_url, filename, subfolder, type_)
result_bytes = await fetch_image_bytes(dl_client, filename, subfolder, type_)
# 缩放到 768x1024
# img = Image.open(io.BytesIO(result_bytes))
# img = img.resize((768, 1024), Image.LANCZOS)
# buf = io.BytesIO()
# fmt = (Path(filename).suffix.lstrip(".") or "png").upper()
# fmt = "JPEG" if fmt in ("JPG", "JPEG") else fmt
# img.save(buf, format=fmt)
# result_bytes = buf.getvalue()
# 将图片保存到临时文件,上传到 OSS # 将图片保存到临时文件,上传到 OSS
suffix = Path(filename).suffix or ".png" suffix = Path(filename).suffix or ".png"
@@ -222,7 +263,20 @@ async def try_on(req: TryOnRequest):
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""健康检查""" """健康检查"""
return {"status": "ok", "comfyui": COMFYUI_URL} primary_ok = await check_comfyui_alive(COMFYUI_URL)
backup_ok = await check_comfyui_alive(COMFYUI_URL_BACKUP)
if primary_ok:
active = COMFYUI_URL
elif backup_ok:
active = COMFYUI_URL_BACKUP
else:
active = None
return {
"status": "ok" if active else "degraded",
"comfyui_active": active,
"comfyui_primary": {"url": COMFYUI_URL, "alive": primary_ok},
"comfyui_backup": {"url": COMFYUI_URL_BACKUP, "alive": backup_ok},
}
# 挂载静态文件(前端页面) # 挂载静态文件(前端页面)
+70
View File
@@ -196,6 +196,49 @@
background: #7c3aed22; background: #7c3aed22;
} }
.prompt-panel {
width: 100%;
max-width: 900px;
margin-bottom: 24px;
background: #1a1a24;
border-radius: 16px;
padding: 20px;
border: 1px solid #2a2a3a;
}
.prompt-panel .prompt-title {
font-size: 0.85rem;
font-weight: 600;
color: #a78bfa;
margin-bottom: 12px;
}
.radio-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
font-size: 0.9rem;
color: #ccc;
}
.radio-row input { margin-top: 3px; accent-color: #7c3aed; flex-shrink: 0; }
.change-desc-input {
width: 100%;
margin-top: 4px;
padding: 12px;
border-radius: 10px;
border: 1px solid #333;
background: #12121a;
color: #e0e0e0;
font-family: inherit;
font-size: 0.88rem;
resize: vertical;
min-height: 88px;
}
.change-desc-input:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 640px) { @media (max-width: 640px) {
.upload-grid { grid-template-columns: 1fr; } .upload-grid { grid-template-columns: 1fr; }
h1 { font-size: 1.5rem; } h1 { font-size: 1.5rem; }
@@ -235,6 +278,19 @@
</div> </div>
</div> </div>
<div class="prompt-panel" id="prompt-panel">
<div class="prompt-title">提示词(change_desc</div>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-default" value="default" checked />
<span>使用默认(传空,不覆盖工作流节点 31)</span>
</label>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-custom" value="custom" />
<span>自定义提示词</span>
</label>
<textarea id="change-desc" class="change-desc-input" placeholder="输入换装/编辑描述…" rows="4" disabled></textarea>
</div>
<button class="btn-run" id="btn-run" disabled>开始换装</button> <button class="btn-run" id="btn-run" disabled>开始换装</button>
<div class="status-bar" id="status-bar"></div> <div class="status-bar" id="status-bar"></div>
@@ -271,6 +327,19 @@
setupUpload('input-shirt', 'preview-shirt', 'card-shirt', 'shirt'); setupUpload('input-shirt', 'preview-shirt', 'card-shirt', 'shirt');
setupUpload('input-pants', 'preview-pants', 'card-pants', 'pants'); setupUpload('input-pants', 'preview-pants', 'card-pants', 'pants');
const promptDefault = document.getElementById('prompt-default');
const promptCustom = document.getElementById('prompt-custom');
const changeDescEl = document.getElementById('change-desc');
function syncPromptUi() {
changeDescEl.disabled = !promptCustom.checked;
}
promptDefault.addEventListener('change', syncPromptUi);
promptCustom.addEventListener('change', syncPromptUi);
function getChangeDesc() {
return promptCustom.checked ? changeDescEl.value.trim() : '';
}
function updateRunButton() { function updateRunButton() {
const btn = document.getElementById('btn-run'); const btn = document.getElementById('btn-run');
btn.disabled = !(images.model && images.shirt && images.pants); btn.disabled = !(images.model && images.shirt && images.pants);
@@ -309,6 +378,7 @@
model_image: images.model, model_image: images.model,
shirt_image: images.shirt, shirt_image: images.shirt,
pants_image: images.pants, pants_image: images.pants,
change_desc: getChangeDesc(),
}), }),
}); });
+340 -54
View File
@@ -1,71 +1,357 @@
import requests #!/usr/bin/env python3
from flask import Flask, jsonify, request, send_from_directory """
虚拟换装网关 —— 统一 FastAPI 服务(HTTPS / 443
app = Flask(__name__, static_folder='static') 路由规则(与原 change_app.py 保持一致):
kuzi_img 存在 → service3 逻辑(模特 + 上衣 + 裤子)
cloth_img 存在(无裤子)→ service2 逻辑(模特 + 上衣)
仅 human_img → service1 逻辑(单图编辑)
"""
import asyncio
import base64
import io
import json
import os
import tempfile
import time
import uuid
from pathlib import Path
import httpx
import oss2
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
# ---------------------------------------------------------------------------
# ComfyUI 配置
# ---------------------------------------------------------------------------
COMFYUI_URL = "http://112.126.94.241:28188"
COMFYUI_URL_BACKUP = "http://112.126.94.241:38188"
_root = Path(__file__).parent
COMFYUI_USER = (_root / "user.txt").read_text(encoding="utf-8").strip()
COMFYUI_PASS = (_root / "password.txt").read_text(encoding="utf-8").strip()
COMFYUI_AUTH = (COMFYUI_USER, COMFYUI_PASS)
# ---------------------------------------------------------------------------
# 工作流路径
# ---------------------------------------------------------------------------
WORKFLOW1 = _root / "change1" / "change1.json" # 单图编辑
WORKFLOW2 = _root / "change2" / "change2_1_ex.json" # 模特 + 上衣
WORKFLOW3 = _root / "change3" / "0604-change_fast3.json" # 模特 + 上衣 + 裤子
# ---------------------------------------------------------------------------
# 工作流节点 ID
# ---------------------------------------------------------------------------
NODE_MODEL = "11" # 模特图(LoadImage
NODE_SHIRT = "10" # 上衣图(LoadImage
NODE_PANTS = "4" # 裤子图(LoadImage
NODE_OUTPUT = "33" # 输出(SaveImage
NODE_PROMPT = "31" # 文本提示(PrimitiveStringMultiline
# ---------------------------------------------------------------------------
# OSS 配置
# ---------------------------------------------------------------------------
OSS_KEY_ID = "LTAI5tGp1sLzedqxihcNC1eb"
OSS_KEY_SECRET = "IFZE1b8YYreCP6zfA6GaZ9uBT678qO"
OSS_ENDPOINT = "oss-cn-beijing.aliyuncs.com"
OSS_BUCKET = "xiangsilian"
# ---------------------------------------------------------------------------
# HTTPS 证书路径
# ---------------------------------------------------------------------------
SSL_CERT = "/etc/letsencrypt/live/xiangsilian.com/fullchain.pem"
SSL_KEY = "/etc/letsencrypt/live/xiangsilian.com/privkey.pem"
# ---------------------------------------------------------------------------
# FastAPI 应用
# ---------------------------------------------------------------------------
app = FastAPI(title="虚拟换装服务")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.route('/') @app.exception_handler(HTTPException)
def index(): async def http_exception_handler(request: Request, exc: HTTPException):
return send_from_directory(app.static_folder, 'index.html') """将 HTTPException 统一转换为业务错误格式"""
return JSONResponse(
status_code=exc.status_code,
content={"ret": -1, "state": -1, "msg": exc.detail},
)
@app.route('/static/<path:filename>') # ---------------------------------------------------------------------------
def static_files(filename): # OSS 工具
return send_from_directory(app.static_folder, filename) # ---------------------------------------------------------------------------
def upload_to_oss(image_path: str, object_name: str) -> str | None:
auth = oss2.Auth(OSS_KEY_ID, OSS_KEY_SECRET)
bucket = oss2.Bucket(auth, OSS_ENDPOINT, OSS_BUCKET)
try:
bucket.put_object_from_file(object_name, image_path)
url = bucket.sign_url("GET", object_name, 3600 * 24 * 365 * 10)
print(f"OSS 上传成功: {url}")
return url
except Exception as e:
print(f"OSS 上传失败: {e}")
return None
@app.route('/change_cloth_base64', methods=['POST']) # ---------------------------------------------------------------------------
def change_cloth_base64(): # ComfyUI 工具函数
# ---------------------------------------------------------------------------
def decode_base64_image(b64_str: str) -> bytes:
"""解码 base64 图片,自动去除 data:image/... 前缀"""
if "," in b64_str:
b64_str = b64_str.split(",", 1)[1]
return base64.b64decode(b64_str)
data = request.get_json()
print(f"change_cloth_base64 input data keys:{list(data.keys()) if data else None}")
if not data:
return jsonify({"ret": -1, "state": -1, "msg": "No JSON data provided"}), 400
human_img = data.get('human_img') async def check_comfyui_alive(base_url: str, timeout: float = 3.0) -> bool:
cloth_img = data.get('cloth_img') try:
async with httpx.AsyncClient(timeout=timeout, auth=COMFYUI_AUTH) as client:
resp = await client.get(f"{base_url}/system_stats")
return resp.status_code < 500
except Exception as e:
print(f"ComfyUI 健康检查失败 {base_url}: {e}")
return False
if not human_img or not cloth_img:
return jsonify({"ret": -1, "state": -1, "msg": "Both human_img and cloth_img are required"}), 400
kuzi_img = data.get('kuzi_img') async def pick_comfyui_url() -> str:
if await check_comfyui_alive(COMFYUI_URL):
print(f"使用正式服务器: {COMFYUI_URL}")
return COMFYUI_URL
print(f"正式服务器不可用,切换备份: {COMFYUI_URL_BACKUP}")
if await check_comfyui_alive(COMFYUI_URL_BACKUP):
print(f"使用备份服务器: {COMFYUI_URL_BACKUP}")
return COMFYUI_URL_BACKUP
raise HTTPException(status_code=503, detail="正式与备份 ComfyUI 服务器均不可用")
async def upload_image(client: httpx.AsyncClient, base_url: str, image_bytes: bytes, filename: str) -> str:
files = {"image": (filename, io.BytesIO(image_bytes), "image/jpeg")}
resp = await client.post(f"{base_url}/upload/image", files=files, data={"overwrite": "true"})
resp.raise_for_status()
return resp.json()["name"]
async def queue_prompt(client: httpx.AsyncClient, base_url: str, workflow: dict) -> str:
payload = {"prompt": workflow, "client_id": str(uuid.uuid4())}
resp = await client.post(f"{base_url}/prompt", json=payload)
resp.raise_for_status()
return resp.json()["prompt_id"]
async def wait_for_result(
client: httpx.AsyncClient, base_url: str, prompt_id: str, timeout: int = 300
) -> dict:
deadline = time.time() + timeout
while time.time() < deadline:
resp = await client.get(f"{base_url}/history/{prompt_id}")
resp.raise_for_status()
history = resp.json()
if prompt_id in history:
entry = history[prompt_id]
status = entry.get("status", {})
if status.get("completed"):
return entry.get("outputs", {})
if status.get("status_str") == "error":
messages = status.get("messages", [])
raise HTTPException(status_code=500, detail=f"ComfyUI 工作流执行出错: {messages}")
await asyncio.sleep(2)
raise HTTPException(status_code=504, detail="等待 ComfyUI 超时(300秒)")
async def fetch_image_bytes(
client: httpx.AsyncClient, base_url: str, filename: str,
subfolder: str = "", type_: str = "output"
) -> bytes:
params = {"filename": filename, "subfolder": subfolder, "type": type_}
resp = await client.get(f"{base_url}/view", params=params)
resp.raise_for_status()
return resp.content
# ---------------------------------------------------------------------------
# 通用工作流执行
# ---------------------------------------------------------------------------
async def run_workflow(
workflow_path: Path,
node_images: dict[str, bytes],
change_desc: str = "",
) -> str:
"""
执行 ComfyUI 工作流并返回 OSS URL。
node_images: {节点ID: 图片字节},按顺序并发上传并注入工作流。
"""
comfyui_url = await pick_comfyui_url()
workflow = json.loads(workflow_path.read_text(encoding="utf-8"))
desc = (change_desc or "").strip()
if desc:
workflow[NODE_PROMPT]["inputs"] = {"value": desc}
uid = uuid.uuid4().hex[:8]
async with httpx.AsyncClient(timeout=60.0, auth=COMFYUI_AUTH) as client:
node_ids = list(node_images.keys())
upload_tasks = [
upload_image(client, comfyui_url, node_images[nid], f"{nid}_{uid}.jpg")
for nid in node_ids
]
try:
names = await asyncio.gather(*upload_tasks)
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"上传图片到 ComfyUI 失败: {e}")
for node_id, name in zip(node_ids, names):
workflow[node_id]["inputs"]["image"] = name
try: try:
if kuzi_img: prompt_id = await queue_prompt(client, comfyui_url, workflow)
payload = { except httpx.HTTPError as e:
'model_image': human_img, raise HTTPException(status_code=502, detail=f"提交工作流失败: {e}")
'shirt_image': cloth_img,
'pants_image': kuzi_img, async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as poll_client:
} outputs = await wait_for_result(poll_client, comfyui_url, prompt_id, timeout=300)
response = requests.post('http://127.0.0.1:12223/try-on', json=payload, timeout=360)
node_output = outputs.get(NODE_OUTPUT)
if not node_output:
raise HTTPException(status_code=500, detail=f"工作流未返回节点 {NODE_OUTPUT} 的输出")
images = node_output.get("images", [])
if not images:
raise HTTPException(status_code=500, detail="输出节点没有图片")
img_info = images[0]
filename = img_info["filename"]
subfolder = img_info.get("subfolder", "")
type_ = img_info.get("type", "output")
async with httpx.AsyncClient(timeout=30.0, auth=COMFYUI_AUTH) as dl_client:
result_bytes = await fetch_image_bytes(dl_client, comfyui_url, filename, subfolder, type_)
suffix = Path(filename).suffix or ".png"
object_name = f"tryon/{uuid.uuid4().hex}{suffix}"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(result_bytes)
tmp_path = tmp.name
try:
result_url = upload_to_oss(tmp_path, object_name)
finally:
os.unlink(tmp_path)
if not result_url:
raise HTTPException(status_code=500, detail="上传结果图片到 OSS 失败")
return result_url
# ---------------------------------------------------------------------------
# API 模型
# ---------------------------------------------------------------------------
class ChangeClothRequest(BaseModel):
human_img: str
cloth_img: str = ""
kuzi_img: str = ""
change_desc: str = ""
# ---------------------------------------------------------------------------
# 接口
# ---------------------------------------------------------------------------
@app.post("/change_cloth_base64")
async def change_cloth_base64(req: ChangeClothRequest):
print(f"收到请求,字段: human_img={'' if req.human_img else ''}, "
f"cloth_img={'' if req.cloth_img else ''}, "
f"kuzi_img={'' if req.kuzi_img else ''}")
if not req.human_img:
raise HTTPException(status_code=400, detail="human_img is required")
try:
model_bytes = decode_base64_image(req.human_img)
except Exception as e:
raise HTTPException(status_code=400, detail=f"human_img 解码失败: {e}")
if req.kuzi_img:
try:
shirt_bytes = decode_base64_image(req.cloth_img)
pants_bytes = decode_base64_image(req.kuzi_img)
except Exception as e:
raise HTTPException(status_code=400, detail=f"图片解码失败: {e}")
result_url = await run_workflow(
WORKFLOW3,
{NODE_MODEL: model_bytes, NODE_SHIRT: shirt_bytes, NODE_PANTS: pants_bytes},
req.change_desc,
)
elif req.cloth_img:
try:
shirt_bytes = decode_base64_image(req.cloth_img)
except Exception as e:
raise HTTPException(status_code=400, detail=f"cloth_img 解码失败: {e}")
result_url = await run_workflow(
WORKFLOW2,
{NODE_MODEL: model_bytes, NODE_SHIRT: shirt_bytes},
req.change_desc,
)
else: else:
payload = { result_url = await run_workflow(
'model_image': human_img, WORKFLOW1,
'shirt_image': cloth_img, {NODE_MODEL: model_bytes},
req.change_desc,
)
return {"ret": 0, "state": 0, "msg": "success", "data": result_url}
@app.get("/")
async def root():
return FileResponse(str(_root / "static" / "index.html"))
@app.get("/health")
async def health():
primary_ok = await check_comfyui_alive(COMFYUI_URL)
backup_ok = await check_comfyui_alive(COMFYUI_URL_BACKUP)
active = COMFYUI_URL if primary_ok else (COMFYUI_URL_BACKUP if backup_ok else None)
return {
"status": "ok" if active else "degraded",
"comfyui_active": active,
"comfyui_primary": {"url": COMFYUI_URL, "alive": primary_ok},
"comfyui_backup": {"url": COMFYUI_URL_BACKUP, "alive": backup_ok},
} }
response = requests.post('http://127.0.0.1:12222/try-on', json=payload, timeout=360)
response.raise_for_status()
result = response.json()
result_image = result.get('result_url')
if not result_image:
return jsonify({"ret": -1, "state": -1, "msg": "Backend returned no image"}), 500
except requests.exceptions.Timeout:
return jsonify({"ret": -1, "state": -1, "msg": "Backend service timeout"}), 504
except requests.exceptions.ConnectionError:
return jsonify({"ret": -1, "state": -1, "msg": "Cannot connect to backend service"}), 502
except requests.exceptions.HTTPError as e:
return jsonify({"ret": -1, "state": -1, "msg": f"Backend error: {e}"}), 502
return jsonify({
"ret": 0,
"state": 0,
"msg": "success",
"data": result_image,
})
if __name__ == '__main__': app.mount("/static", StaticFiles(directory=str(_root / "static")), name="static")
app.run(host="0.0.0.0", port=28888, debug=False)
# ---------------------------------------------------------------------------
# 启动入口
# ---------------------------------------------------------------------------
if __name__ == "__main__":
use_https = os.path.exists(SSL_CERT) and os.path.exists(SSL_KEY)
if use_https:
print(f"启动 HTTPS 服务,端口 443,证书: {SSL_CERT}")
uvicorn.run(
app,
host="0.0.0.0",
port=443,
ssl_certfile=SSL_CERT,
ssl_keyfile=SSL_KEY,
log_level="info",
)
else:
print("⚠ 证书文件不存在,降级为 HTTP,端口 28888")
uvicorn.run(app, host="0.0.0.0", port=28888, log_level="info")
-10
View File
@@ -1,10 +0,0 @@
/usr/local/miniconda3/envs/py312/lib/python3.12/site-packages/requests/__init__.py:113: RequestsDependencyWarning: urllib3 (2.6.3) or chardet (7.0.1)/charset_normalizer (3.4.5) doesn't match a supported version!
warnings.warn(
* Serving Flask app 'change_app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:28888
* Running on http://10.60.190.115:28888
Press CTRL+C to quit
1.202.76.38 - - [13/Mar/2026 02:51:46] "POST /change_cloth_base64 HTTP/1.1" 200 -
+2
View File
@@ -0,0 +1,2 @@
sudo apt install certbot -y
sudo certbot certonly --standalone -d change.xiangsilian.com --email youmiss@163.com --agree-tos --non-interactive
+1
View File
@@ -0,0 +1 @@
your888password
+497
View File
@@ -0,0 +1,497 @@
[startup] worker-0 started → http://117.50.89.68:28889
[startup] worker-1 started → http://117.50.226.22:28889
[startup] proxy listening on 0.0.0.0:28888
[startup] queue-0 → http://117.50.89.68:28889
[startup] queue-1 → http://117.50.226.22:28889
* Serving Flask app 'proxy'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:28888
* Running on http://10.60.190.115:28888
Press CTRL+C to quit
221.218.70.61 - - [19/Mar/2026 22:33:30] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:33:32] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:35:17] "GET / HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:35:17] "GET /favicon.ico HTTP/1.1" 404 -
221.218.70.61 - - [19/Mar/2026 22:40:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:41:01] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:41:26] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:41:49] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:42:14] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:42:37] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:42:58] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:43:23] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:43:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:44:14] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:44:49] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:45:10] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:45:36] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:45:55] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:46:26] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:46:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:47:14] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:47:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:47:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 22:48:15] "POST /change_cloth_base64 HTTP/1.1" 200 -
[startup] worker-0 started → http://117.50.89.68:28889
[startup] worker-1 started → http://117.50.226.22:28889
[startup] proxy listening on 0.0.0.0:28888
[startup] queue-0 → http://117.50.89.68:28889
[startup] queue-1 → http://117.50.226.22:28889
* Serving Flask app 'proxy'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:28888
* Running on http://10.60.190.115:28888
Press CTRL+C to quit
[startup] worker-0 started → http://117.50.89.68:28889
[startup] worker-1 started → http://117.50.226.22:28889
[startup] proxy listening on 0.0.0.0:28888
[startup] queue-0 → http://117.50.89.68:28889
[startup] queue-1 → http://117.50.226.22:28889
* Serving Flask app 'proxy'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:28888
* Running on http://10.60.190.115:28888
Press CTRL+C to quit
221.218.70.61 - - [19/Mar/2026 23:00:07] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:00:15] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:00:18] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:00:33] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:00:42] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:00:57] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:01:01] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:01:17] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:01:25] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:01:38] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:01:45] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:01:59] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:02:06] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:02:20] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:02:22] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:02:43] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:02:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:03:03] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:03:16] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:05:19] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:05:36] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:06:01] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:06:15] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:06:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:06:34] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:06:57] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:07:00] "POST /change_cloth_base64 HTTP/1.1" 200 -
[proxy] 2026-03-19 22:59:53 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[proxy] 2026-03-19 22:59:53 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 22:59:53 end=2026-03-19 23:00:07 | 14.45s | status=200
[proxy] 2026-03-19 23:00:07 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 22:59:53 end=2026-03-19 23:00:15 | 22.45s | status=200
[proxy] 2026-03-19 23:00:16 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:00:07 end=2026-03-19 23:00:18 | 10.49s | status=200
[proxy] 2026-03-19 23:00:18 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:00:18 end=2026-03-19 23:00:33 | 14.40s | status=200
[proxy] 2026-03-19 23:00:33 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:00:16 end=2026-03-19 23:00:42 | 26.47s | status=200
[proxy] 2026-03-19 23:00:42 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:00:42 end=2026-03-19 23:00:57 | 14.54s | status=200
[proxy] 2026-03-19 23:00:57 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:00:42 end=2026-03-19 23:01:01 | 18.41s | status=200
[proxy] 2026-03-19 23:01:01 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:01:01 end=2026-03-19 23:01:17 | 16.44s | status=200
[proxy] 2026-03-19 23:01:17 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:01:01 end=2026-03-19 23:01:25 | 24.45s | status=200
[proxy] 2026-03-19 23:01:26 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:01:17 end=2026-03-19 23:01:38 | 20.39s | status=200
[proxy] 2026-03-19 23:01:38 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:01:26 end=2026-03-19 23:01:45 | 18.63s | status=200
[proxy] 2026-03-19 23:01:45 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:01:38 end=2026-03-19 23:01:59 | 20.43s | status=200
[proxy] 2026-03-19 23:01:59 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:01:45 end=2026-03-19 23:02:06 | 20.54s | status=200
[proxy] 2026-03-19 23:02:06 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:01:59 end=2026-03-19 23:02:20 | 20.43s | status=200
[proxy] 2026-03-19 23:02:20 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:02:06 end=2026-03-19 23:02:22 | 16.47s | status=200
[proxy] 2026-03-19 23:02:23 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:02:20 end=2026-03-19 23:02:43 | 22.45s | status=200
[proxy] 2026-03-19 23:02:43 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:02:23 end=2026-03-19 23:02:50 | 26.49s | status=200
[proxy] 2026-03-19 23:02:51 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:02:43 end=2026-03-19 23:03:03 | 20.44s | status=200
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:02:51 end=2026-03-19 23:03:16 | 24.55s | status=200
[proxy] 2026-03-19 23:04:58 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:04:58 end=2026-03-19 23:05:19 | 20.39s | status=200
[proxy] 2026-03-19 23:05:19 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:05:19 end=2026-03-19 23:05:36 | 16.50s | status=200
[proxy] 2026-03-19 23:05:36 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[proxy] 2026-03-19 23:06:00 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:05:36 end=2026-03-19 23:06:01 | 24.47s | status=200
[proxy] 2026-03-19 23:06:01 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:06:00 end=2026-03-19 23:06:15 | 14.39s | status=200
[proxy] 2026-03-19 23:06:15 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:06:01 end=2026-03-19 23:06:31 | 30.47s | status=200
[proxy] 2026-03-19 23:06:32 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:06:15 end=2026-03-19 23:06:34 | 18.45s | status=200
[proxy] 2026-03-19 23:06:34 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:06:34 end=2026-03-19 23:06:57 | 22.47s | status=200
[proxy] 2026-03-19 23:06:57 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:06:32 end=2026-03-19 23:07:00 | 28.45s | status=200
[proxy] 2026-03-19 23:07:01 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
221.218.70.61 - - [19/Mar/2026 23:07:27] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:07:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:07:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:07:58] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:08:10] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:08:23] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:08:35] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:08:54] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:09:03] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:09:11] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:09:29] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:09:44] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:09:45] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:09:58] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:10:15] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:10:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:10:38] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:10:58] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:11:00] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:11:21] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:11:27] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:11:48] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:11:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:12:06] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:12:10] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:12:29] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:12:29] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:12:56] "POST /change_cloth_base64 HTTP/1.1" 200 -
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:07:00 end=2026-03-19 23:07:27 | 26.41s | status=200
[proxy] 2026-03-19 23:07:27 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:07:01 end=2026-03-19 23:07:31 | 30.41s | status=200
[proxy] 2026-03-19 23:07:31 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:07:27 end=2026-03-19 23:07:47 | 20.43s | status=200
[proxy] 2026-03-19 23:07:48 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:07:31 end=2026-03-19 23:07:58 | 26.42s | status=200
[proxy] 2026-03-19 23:07:59 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:07:48 end=2026-03-19 23:08:10 | 22.47s | status=200
[proxy] 2026-03-19 23:08:10 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:07:59 end=2026-03-19 23:08:23 | 24.58s | status=200
[proxy] 2026-03-19 23:08:24 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:08:10 end=2026-03-19 23:08:35 | 24.40s | status=200
[proxy] 2026-03-19 23:08:35 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:08:24 end=2026-03-19 23:08:54 | 30.44s | status=200
[proxy] 2026-03-19 23:08:55 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:08:35 end=2026-03-19 23:09:03 | 28.44s | status=200
[proxy] 2026-03-19 23:09:04 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:08:55 end=2026-03-19 23:09:11 | 16.36s | status=200
[proxy] 2026-03-19 23:09:12 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:09:04 end=2026-03-19 23:09:29 | 24.67s | status=200
[proxy] 2026-03-19 23:09:29 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:09:12 end=2026-03-19 23:09:44 | 32.45s | status=200
[proxy] 2026-03-19 23:09:44 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:09:29 end=2026-03-19 23:09:45 | 16.42s | status=200
[proxy] 2026-03-19 23:09:46 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:09:46 end=2026-03-19 23:09:58 | 12.41s | status=200
[proxy] 2026-03-19 23:09:58 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:09:44 end=2026-03-19 23:10:15 | 30.57s | status=200
[proxy] 2026-03-19 23:10:15 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:10:15 end=2026-03-19 23:10:31 | 16.40s | status=200
[proxy] 2026-03-19 23:10:32 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:10:15 end=2026-03-19 23:10:38 | 22.48s | status=200
[proxy] 2026-03-19 23:10:38 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:10:38 end=2026-03-19 23:10:58 | 20.40s | status=200
[proxy] 2026-03-19 23:10:59 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:10:32 end=2026-03-19 23:11:00 | 28.41s | status=200
[proxy] 2026-03-19 23:11:01 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:11:00 end=2026-03-19 23:11:21 | 20.44s | status=200
[proxy] 2026-03-19 23:11:21 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:11:01 end=2026-03-19 23:11:27 | 26.47s | status=200
[proxy] 2026-03-19 23:11:28 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:11:28 end=2026-03-19 23:11:48 | 20.41s | status=200
[proxy] 2026-03-19 23:11:48 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:11:21 end=2026-03-19 23:11:50 | 28.45s | status=200
[proxy] 2026-03-19 23:11:50 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:11:50 end=2026-03-19 23:12:06 | 16.41s | status=200
[proxy] 2026-03-19 23:12:06 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:11:50 end=2026-03-19 23:12:10 | 20.44s | status=200
[proxy] 2026-03-19 23:12:11 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:12:06 end=2026-03-19 23:12:29 | 22.45s | status=200
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:12:11 end=2026-03-19 23:12:29 | 18.38s | status=200
[proxy] 2026-03-19 23:12:29 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[proxy] 2026-03-19 23:12:30 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:12:29 end=2026-03-19 23:12:56 | 26.45s | status=200
221.218.70.61 - - [19/Mar/2026 23:13:03] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:13:17] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:13:34] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:13:48] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:13:52] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:14:03] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:14:11] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:14:22] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:14:32] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:14:45] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:15:03] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:15:07] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:15:22] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:15:26] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:15:40] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:15:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:16:02] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:16:09] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:16:38] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:17:07] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:17:36] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:26:15] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:26:36] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:26:59] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:27:30] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:27:53] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:28:12] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:28:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
[proxy] 2026-03-19 23:12:57 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:12:30 end=2026-03-19 23:13:03 | 32.68s | status=200
[proxy] 2026-03-19 23:13:03 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:12:57 end=2026-03-19 23:13:17 | 20.43s | status=200
[proxy] 2026-03-19 23:13:18 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:13:03 end=2026-03-19 23:13:34 | 30.68s | status=200
[proxy] 2026-03-19 23:13:34 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:13:18 end=2026-03-19 23:13:48 | 30.51s | status=200
[proxy] 2026-03-19 23:13:49 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:13:34 end=2026-03-19 23:13:52 | 18.38s | status=200
[proxy] 2026-03-19 23:13:53 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:13:49 end=2026-03-19 23:14:03 | 14.38s | status=200
[proxy] 2026-03-19 23:14:03 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:13:53 end=2026-03-19 23:14:11 | 18.41s | status=200
[proxy] 2026-03-19 23:14:11 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:14:03 end=2026-03-19 23:14:22 | 18.49s | status=200
[proxy] 2026-03-19 23:14:22 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:14:11 end=2026-03-19 23:14:32 | 20.40s | status=200
[proxy] 2026-03-19 23:14:32 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:14:22 end=2026-03-19 23:14:45 | 22.42s | status=200
[proxy] 2026-03-19 23:14:45 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:14:32 end=2026-03-19 23:15:03 | 30.46s | status=200
[proxy] 2026-03-19 23:15:03 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:14:45 end=2026-03-19 23:15:07 | 22.46s | status=200
[proxy] 2026-03-19 23:15:08 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:15:03 end=2026-03-19 23:15:21 | 18.42s | status=200
[proxy] 2026-03-19 23:15:22 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:15:08 end=2026-03-19 23:15:26 | 18.39s | status=200
[proxy] 2026-03-19 23:15:27 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:15:22 end=2026-03-19 23:15:40 | 18.40s | status=200
[proxy] 2026-03-19 23:15:41 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:15:27 end=2026-03-19 23:15:47 | 20.57s | status=200
[proxy] 2026-03-19 23:15:48 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:15:48 end=2026-03-19 23:16:02 | 14.36s | status=200
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:15:41 end=2026-03-19 23:16:09 | 28.43s | status=200
[proxy] 2026-03-19 23:16:10 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:16:10 end=2026-03-19 23:16:38 | 28.43s | status=200
[proxy] 2026-03-19 23:16:38 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:16:38 end=2026-03-19 23:17:07 | 28.48s | status=200
[proxy] 2026-03-19 23:17:07 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:17:07 end=2026-03-19 23:17:36 | 28.45s | status=200
[proxy] 2026-03-19 23:25:53 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:25:53 end=2026-03-19 23:26:15 | 22.48s | status=200
[proxy] 2026-03-19 23:26:16 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:26:16 end=2026-03-19 23:26:36 | 20.49s | status=200
[proxy] 2026-03-19 23:26:37 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:26:37 end=2026-03-19 23:26:59 | 22.46s | status=200
[proxy] 2026-03-19 23:27:00 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:27:00 end=2026-03-19 23:27:30 | 30.47s | status=200
[proxy] 2026-03-19 23:27:30 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:27:30 end=2026-03-19 23:27:53 | 22.42s | status=200
[proxy] 2026-03-19 23:27:53 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:27:53 end=2026-03-19 23:28:12 | 18.50s | status=200
[proxy] 2026-03-19 23:28:12 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:28:12 end=2026-03-19 23:28:31 | 18.41s | status=200
221.218.70.61 - - [19/Mar/2026 23:28:52] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:29:12] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:29:29] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:30:00] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:30:17] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:30:40] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:31:09] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:31:30] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:31:51] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:32:18] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:32:40] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:33:11] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:33:38] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:33:40] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:34:01] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:34:09] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:34:22] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:34:38] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:34:43] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:35:04] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:35:05] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:35:26] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:35:36] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:35:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:35:51] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:36:07] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:36:12] "POST /change_cloth_base64 HTTP/1.1" 200 -
[proxy] 2026-03-19 23:28:31 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:28:31 end=2026-03-19 23:28:52 | 20.46s | status=200
[proxy] 2026-03-19 23:28:52 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:28:52 end=2026-03-19 23:29:12 | 20.42s | status=200
[proxy] 2026-03-19 23:29:13 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:29:13 end=2026-03-19 23:29:29 | 16.64s | status=200
[proxy] 2026-03-19 23:29:30 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:29:30 end=2026-03-19 23:30:00 | 30.65s | status=200
[proxy] 2026-03-19 23:30:01 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:30:01 end=2026-03-19 23:30:17 | 16.60s | status=200
[proxy] 2026-03-19 23:30:18 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:30:18 end=2026-03-19 23:30:40 | 22.70s | status=200
[proxy] 2026-03-19 23:30:41 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:30:41 end=2026-03-19 23:31:09 | 28.41s | status=200
[proxy] 2026-03-19 23:31:10 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:31:10 end=2026-03-19 23:31:30 | 20.44s | status=200
[proxy] 2026-03-19 23:31:31 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:31:31 end=2026-03-19 23:31:51 | 20.47s | status=200
[proxy] 2026-03-19 23:31:51 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:31:51 end=2026-03-19 23:32:18 | 26.45s | status=200
[proxy] 2026-03-19 23:32:18 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:32:18 end=2026-03-19 23:32:40 | 22.43s | status=200
[proxy] 2026-03-19 23:32:41 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:32:41 end=2026-03-19 23:33:11 | 30.61s | status=200
[proxy] 2026-03-19 23:33:12 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[proxy] 2026-03-19 23:33:17 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:33:17 end=2026-03-19 23:33:38 | 20.47s | status=200
[proxy] 2026-03-19 23:33:38 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:33:12 end=2026-03-19 23:33:40 | 28.47s | status=200
[proxy] 2026-03-19 23:33:41 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:33:40 end=2026-03-19 23:34:01 | 20.43s | status=200
[proxy] 2026-03-19 23:34:01 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:33:41 end=2026-03-19 23:34:09 | 28.40s | status=200
[proxy] 2026-03-19 23:34:10 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:34:01 end=2026-03-19 23:34:22 | 20.46s | status=200
[proxy] 2026-03-19 23:34:22 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:34:10 end=2026-03-19 23:34:38 | 28.51s | status=200
[proxy] 2026-03-19 23:34:39 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:34:22 end=2026-03-19 23:34:43 | 20.45s | status=200
[proxy] 2026-03-19 23:34:43 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:34:43 end=2026-03-19 23:35:04 | 20.44s | status=200
[proxy] 2026-03-19 23:35:04 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:34:39 end=2026-03-19 23:35:05 | 26.44s | status=200
[proxy] 2026-03-19 23:35:06 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:35:05 end=2026-03-19 23:35:26 | 20.49s | status=200
[proxy] 2026-03-19 23:35:26 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:35:06 end=2026-03-19 23:35:36 | 30.70s | status=200
[proxy] 2026-03-19 23:35:37 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:35:26 end=2026-03-19 23:35:47 | 20.41s | status=200
[proxy] 2026-03-19 23:35:47 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:35:37 end=2026-03-19 23:35:51 | 14.38s | status=200
[proxy] 2026-03-19 23:35:51 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:35:47 end=2026-03-19 23:36:07 | 20.44s | status=200
[proxy] 2026-03-19 23:36:08 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:35:51 end=2026-03-19 23:36:12 | 20.41s | status=200
[proxy] 2026-03-19 23:36:12 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
221.218.70.61 - - [19/Mar/2026 23:36:28] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:36:41] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:36:49] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:36:57] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:37:10] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:37:18] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:37:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:37:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:37:52] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:38:13] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:38:35] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:38:56] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:39:17] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:39:38] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:39:59] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:40:20] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:40:41] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:41:02] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:41:23] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:41:44] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:42:05] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:42:26] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:42:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:43:08] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:43:28] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:43:49] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:44:10] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:44:31] "POST /change_cloth_base64 HTTP/1.1" 200 -
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:36:08 end=2026-03-19 23:36:28 | 20.43s | status=200
[proxy] 2026-03-19 23:36:29 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:36:12 end=2026-03-19 23:36:41 | 28.43s | status=200
[proxy] 2026-03-19 23:36:41 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:36:29 end=2026-03-19 23:36:49 | 20.43s | status=200
[proxy] 2026-03-19 23:36:50 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:36:41 end=2026-03-19 23:36:57 | 16.42s | status=200
[proxy] 2026-03-19 23:36:58 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:36:50 end=2026-03-19 23:37:10 | 20.55s | status=200
[proxy] 2026-03-19 23:37:11 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:36:58 end=2026-03-19 23:37:18 | 20.44s | status=200
[proxy] 2026-03-19 23:37:19 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:37:11 end=2026-03-19 23:37:31 | 20.45s | status=200
[proxy] 2026-03-19 23:37:31 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:37:19 end=2026-03-19 23:37:47 | 28.46s | status=200
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:37:31 end=2026-03-19 23:37:52 | 20.45s | status=200
[proxy] 2026-03-19 23:37:52 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:37:52 end=2026-03-19 23:38:13 | 20.68s | status=200
[proxy] 2026-03-19 23:38:14 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:38:14 end=2026-03-19 23:38:35 | 20.49s | status=200
[proxy] 2026-03-19 23:38:35 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:38:35 end=2026-03-19 23:38:56 | 20.50s | status=200
[proxy] 2026-03-19 23:38:56 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:38:56 end=2026-03-19 23:39:17 | 20.68s | status=200
[proxy] 2026-03-19 23:39:18 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:39:18 end=2026-03-19 23:39:38 | 20.60s | status=200
[proxy] 2026-03-19 23:39:39 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:39:39 end=2026-03-19 23:39:59 | 20.47s | status=200
[proxy] 2026-03-19 23:40:00 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:40:00 end=2026-03-19 23:40:20 | 20.49s | status=200
[proxy] 2026-03-19 23:40:20 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:40:20 end=2026-03-19 23:40:41 | 20.44s | status=200
[proxy] 2026-03-19 23:40:41 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:40:41 end=2026-03-19 23:41:02 | 20.47s | status=200
[proxy] 2026-03-19 23:41:02 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:41:02 end=2026-03-19 23:41:23 | 20.47s | status=200
[proxy] 2026-03-19 23:41:23 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:41:23 end=2026-03-19 23:41:44 | 20.57s | status=200
[proxy] 2026-03-19 23:41:44 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:41:44 end=2026-03-19 23:42:05 | 20.44s | status=200
[proxy] 2026-03-19 23:42:05 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:42:05 end=2026-03-19 23:42:26 | 20.44s | status=200
[proxy] 2026-03-19 23:42:26 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:42:26 end=2026-03-19 23:42:47 | 20.45s | status=200
[proxy] 2026-03-19 23:42:47 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:42:47 end=2026-03-19 23:43:08 | 20.45s | status=200
[proxy] 2026-03-19 23:43:08 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:43:08 end=2026-03-19 23:43:28 | 20.46s | status=200
[proxy] 2026-03-19 23:43:29 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:43:29 end=2026-03-19 23:43:49 | 20.43s | status=200
[proxy] 2026-03-19 23:43:50 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.89.68:28889/change_cloth_base64 | backend=http://117.50.89.68:28889 | start=2026-03-19 23:43:50 end=2026-03-19 23:44:10 | 20.56s | status=200
[proxy] 2026-03-19 23:44:11 | 221.218.70.61 | queued → queue-1 (http://117.50.226.22:28889) | POST /change_cloth_base64?
[proxy] 2026-03-19 23:44:15 | 221.218.70.61 | queued → queue-0 (http://117.50.89.68:28889) | POST /change_cloth_base64?
[worker] POST http://117.50.226.22:28889/change_cloth_base64 | backend=http://117.50.226.22:28889 | start=2026-03-19 23:44:11 end=2026-03-19 23:44:31 | 20.47s | status=200
221.218.70.61 - - [19/Mar/2026 23:44:34] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:44:52] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:45:04] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:45:13] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:45:25] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:45:34] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:45:48] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:45:55] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:46:13] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:46:16] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:46:37] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:46:41] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:46:56] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:47:02] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:47:20] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:47:23] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:47:43] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:47:44] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:48:05] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:48:10] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:48:30] "POST /change_cloth_base64 HTTP/1.1" 200 -
221.218.70.61 - - [19/Mar/2026 23:48:39] "POST /change_cloth_base64 HTTP/1.1" 200 -
+1
View File
@@ -0,0 +1 @@
10266
+118
View File
@@ -0,0 +1,118 @@
from flask import Flask, request, Response, send_from_directory
import itertools
import threading
import queue
import requests
import time
import os
app = Flask(__name__, static_folder=None)
BACKENDS = [
"http://117.50.89.68:28889",
"http://117.50.226.22:28889",
]
# One queue per backend, auto-sized from BACKENDS
queues = [queue.Queue() for _ in BACKENDS]
_rr = itertools.count()
EXCLUDED_REQ_HEADERS = {"host", "content-length", "transfer-encoding", "connection"}
EXCLUDED_RESP_HEADERS = {"content-encoding", "content-length", "transfer-encoding", "connection"}
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
def _now():
return time.strftime("%Y-%m-%d %H:%M:%S")
def worker(q: queue.Queue, backend_url: str):
"""Single worker thread per queue — processes requests one at a time."""
while True:
task = q.get()
if task is None:
break
method, url, headers, data, result_event, result_holder = task
t_start = time.time()
start_ts = _now()
try:
resp = requests.request(
method=method,
url=url,
headers=headers,
data=data,
allow_redirects=False,
timeout=60,
stream=True,
)
result_holder["response"] = resp
result_holder["error"] = None
except Exception as e:
result_holder["response"] = None
result_holder["error"] = str(e)
finally:
elapsed = time.time() - t_start
end_ts = _now()
status = resp.status_code if result_holder.get("response") else "ERR"
print(
f"[worker] {method} {url} | backend={backend_url} | "
f"start={start_ts} end={end_ts} | "
f"{elapsed:.2f}s | status={status}"
)
result_event.set()
q.task_done()
# Start one worker thread per queue
for i, backend in enumerate(BACKENDS):
t = threading.Thread(target=worker, args=(queues[i], backend), daemon=True, name=f"worker-{i}")
t.start()
print(f"[startup] worker-{i} started → {backend}")
@app.route("/", methods=["GET"])
def index():
return send_from_directory(STATIC_DIR, "index.html")
@app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"])
def proxy(path):
idx = next(_rr) % len(queues)
# Build the full target URL (preserves query string)
target_url = BACKENDS[idx] + request.full_path.rstrip("?")
# Filter hop-by-hop and host headers before forwarding
headers = {k: v for k, v in request.headers.items() if k.lower() not in EXCLUDED_REQ_HEADERS}
result_event = threading.Event()
result_holder: dict = {}
task = (request.method, target_url, headers, request.get_data(), result_event, result_holder)
queues[idx].put(task)
client_ip = request.remote_addr
print(f"[proxy] {_now()} | {client_ip} | queued → queue-{idx} ({BACKENDS[idx]}) | {request.method} {request.full_path}")
# Block until the worker finishes (or timeout)
if not result_event.wait(timeout=65):
print(f"[proxy] {_now()} | {client_ip} | TIMEOUT → queue-{idx} ({BACKENDS[idx]}) | {request.method} {request.full_path}")
return Response("Gateway Timeout", status=504)
if result_holder.get("error"):
print(f"[proxy] {_now()} | {client_ip} | ERROR → {result_holder['error']}")
return Response(f"Bad Gateway: {result_holder['error']}", status=502)
resp = result_holder["response"]
resp_headers = {k: v for k, v in resp.headers.items() if k.lower() not in EXCLUDED_RESP_HEADERS}
return Response(resp.content, status=resp.status_code, headers=resp_headers)
if __name__ == "__main__":
print(f"[startup] proxy listening on 0.0.0.0:28888")
for i, b in enumerate(BACKENDS):
print(f"[startup] queue-{i}{b}")
app.run(host="0.0.0.0", port=28888, threaded=True)
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
PID_FILE=proxy.pid
LOG_FILE=proxy.log
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "already running (pid $(cat "$PID_FILE"))"
exit 1
fi
nohup python3 proxy.py >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "started (pid $!) → log: $LOG_FILE"
+374
View File
@@ -0,0 +1,374 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI 换装测试</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
background: #0f0f13;
color: #e0e0e0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 16px 64px;
}
h1 {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #a78bfa, #60a5fa, #34d399);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 0.9rem;
color: #888;
margin-bottom: 40px;
text-align: center;
}
.upload-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
width: 100%;
max-width: 960px;
margin-bottom: 32px;
}
.upload-card {
background: #1a1a24;
border: 2px dashed #333;
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
position: relative;
min-height: 280px;
}
.upload-card:hover { border-color: #7c3aed; background: #1e1e2e; }
.upload-card.has-image { border-style: solid; border-color: #6d28d9; }
.upload-card.optional { border-color: #2a2a3a; }
.upload-card.optional .upload-label { color: #8b80b0; }
.upload-card input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
width: 100%;
height: 100%;
}
.upload-icon { font-size: 2.5rem; line-height: 1; }
.upload-label { font-size: 1rem; font-weight: 600; color: #c4b5fd; }
.upload-hint { font-size: 0.78rem; color: #666; text-align: center; }
.badge-optional {
font-size: 0.68rem;
background: #2a2a3a;
color: #777;
border-radius: 6px;
padding: 2px 8px;
letter-spacing: 0.5px;
}
.preview-img {
width: 100%;
max-height: 200px;
object-fit: contain;
border-radius: 10px;
display: none;
}
.preview-img.visible { display: block; }
.btn-clear {
position: absolute;
top: 10px;
right: 10px;
background: #3a1a1a;
border: none;
color: #f87171;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 0.8rem;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
z-index: 1;
}
.btn-clear.visible { display: flex; }
.btn-run {
padding: 14px 56px;
font-size: 1.05rem;
font-weight: 700;
border: none;
border-radius: 50px;
background: linear-gradient(135deg, #7c3aed, #2563eb);
color: #fff;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
}
.btn-run:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); }
.btn-run:disabled { opacity: 0.45; cursor: not-allowed; }
.status-bar {
margin-top: 20px;
font-size: 0.9rem;
color: #888;
min-height: 24px;
text-align: center;
}
.status-bar.running { color: #60a5fa; }
.status-bar.success { color: #34d399; }
.status-bar.error { color: #f87171; }
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid #60a5fa44;
border-top-color: #60a5fa;
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-right: 6px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.result-section {
margin-top: 40px;
width: 100%;
max-width: 500px;
display: none;
flex-direction: column;
align-items: center;
gap: 16px;
}
.result-section.visible { display: flex; }
.result-title { font-size: 1.1rem; font-weight: 600; color: #a78bfa; }
.result-img {
width: 100%;
border-radius: 16px;
box-shadow: 0 0 40px #7c3aed44;
}
.btn-download {
padding: 10px 36px;
border: 2px solid #7c3aed;
border-radius: 50px;
background: transparent;
color: #a78bfa;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-download:hover { background: #7c3aed22; }
.raw-resp {
width: 100%;
max-width: 960px;
margin-top: 32px;
background: #1a1a24;
border-radius: 12px;
padding: 16px;
font-size: 0.78rem;
color: #888;
word-break: break-all;
display: none;
}
.raw-resp.visible { display: block; }
.raw-resp pre { white-space: pre-wrap; }
@media (max-width: 640px) {
.upload-grid { grid-template-columns: 1fr; }
h1 { font-size: 1.5rem; }
}
</style>
</head>
<body>
<h1>AI 换装测试</h1>
<p class="subtitle">上传模特和衣服图片,调用 /change_cloth_base64 接口测试</p>
<div class="upload-grid">
<!-- 模特 -->
<div class="upload-card" id="card-human">
<button class="btn-clear" id="clear-human" title="清除"></button>
<input type="file" accept="image/*" id="input-human" />
<div class="upload-icon">🧍</div>
<div class="upload-label">模特图片</div>
<div class="upload-hint">human_img · 必填</div>
<img class="preview-img" id="preview-human" alt="模特预览" />
</div>
<!-- 上衣 -->
<div class="upload-card" id="card-cloth">
<button class="btn-clear" id="clear-cloth" title="清除"></button>
<input type="file" accept="image/*" id="input-cloth" />
<div class="upload-icon">👕</div>
<div class="upload-label">上衣图片</div>
<div class="upload-hint">cloth_img · 必填</div>
<img class="preview-img" id="preview-cloth" alt="上衣预览" />
</div>
<!-- 裤子(可选) -->
<div class="upload-card optional" id="card-kuzi">
<button class="btn-clear" id="clear-kuzi" title="清除"></button>
<input type="file" accept="image/*" id="input-kuzi" />
<div class="upload-icon">👖</div>
<div class="upload-label">裤子图片</div>
<div class="upload-hint">kuzi_img · 可选</div>
<span class="badge-optional">有则走 8888 服务,无则走 8889</span>
<img class="preview-img" id="preview-kuzi" alt="裤子预览" />
</div>
</div>
<button class="btn-run" id="btn-run" disabled>开始换装</button>
<div class="status-bar" id="status-bar"></div>
<div class="result-section" id="result-section">
<div class="result-title">换装结果</div>
<img class="result-img" id="result-img" alt="换装结果" />
<button class="btn-download" id="btn-download">下载图片</button>
</div>
<div class="raw-resp" id="raw-resp">
<div style="color:#555;margin-bottom:8px;font-size:0.72rem;">接口原始响应(data 字段已截断)</div>
<pre id="raw-text"></pre>
</div>
<script>
const images = { human: null, cloth: null, kuzi: null };
function setupUpload(inputId, previewId, cardId, clearId, key) {
const input = document.getElementById(inputId);
const preview = document.getElementById(previewId);
const card = document.getElementById(cardId);
const clearBtn= document.getElementById(clearId);
input.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
images[key] = ev.target.result;
preview.src = ev.target.result;
preview.classList.add('visible');
card.classList.add('has-image');
clearBtn.classList.add('visible');
updateRunButton();
};
reader.readAsDataURL(file);
});
clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
images[key] = null;
input.value = '';
preview.src = '';
preview.classList.remove('visible');
card.classList.remove('has-image');
clearBtn.classList.remove('visible');
updateRunButton();
});
}
setupUpload('input-human', 'preview-human', 'card-human', 'clear-human', 'human');
setupUpload('input-cloth', 'preview-cloth', 'card-cloth', 'clear-cloth', 'cloth');
setupUpload('input-kuzi', 'preview-kuzi', 'card-kuzi', 'clear-kuzi', 'kuzi');
function updateRunButton() {
document.getElementById('btn-run').disabled = !(images.human && images.cloth);
}
function setStatus(msg, cls = '') {
const el = document.getElementById('status-bar');
el.className = 'status-bar ' + cls;
el.innerHTML = msg;
}
function elapsed(start) {
return Math.round((Date.now() - start) / 1000) + '秒';
}
document.getElementById('btn-run').addEventListener('click', async () => {
const btn = document.getElementById('btn-run');
const resultSection = document.getElementById('result-section');
const resultImg = document.getElementById('result-img');
const rawResp = document.getElementById('raw-resp');
const rawText = document.getElementById('raw-text');
btn.disabled = true;
resultSection.classList.remove('visible');
rawResp.classList.remove('visible');
const start = Date.now();
let timer = setInterval(() => {
setStatus(`<span class="spinner"></span>正在换装中,请稍候… 已等待 ${elapsed(start)}`, 'running');
}, 1000);
setStatus('<span class="spinner"></span>正在提交请求…', 'running');
const body = {
human_img: images.human,
cloth_img: images.cloth,
};
if (images.kuzi) body.kuzi_img = images.kuzi;
try {
const resp = await fetch('/change_cloth_base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
clearInterval(timer);
const data = await resp.json();
// 显示原始响应
rawText.textContent = JSON.stringify(data, null, 2);
rawResp.classList.add('visible');
if (data.ret !== 0 || !data.data) {
throw new Error(data.msg || '接口返回失败');
}
resultImg.src = data.data;
resultSection.classList.add('visible');
setStatus(`✅ 换装完成!耗时 ${elapsed(start)}`, 'success');
document.getElementById('btn-download').onclick = () => {
const a = document.createElement('a');
a.href = data.data;
a.download = `tryon_${Date.now()}.png`;
a.target = '_blank';
a.click();
};
} catch (err) {
clearInterval(timer);
setStatus(`❌ 错误:${err.message}`, 'error');
} finally {
updateRunButton();
}
});
</script>
</body>
</html>
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
PID_FILE=proxy.pid
if [ ! -f "$PID_FILE" ]; then
echo "not running (no pid file)"
exit 1
fi
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
kill "$PID"
rm -f "$PID_FILE"
echo "stopped (pid $PID)"
else
rm -f "$PID_FILE"
echo "stale pid file removed (process $PID not found)"
fi
Executable
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# Let's Encrypt 证书续期脚本
# 流程:停服 → certbot renewstandalone)→ 修正证书权限 → 重启服务
# 由 crontab 每月执行一次(证书90天过期,提前30天续期)
ROOT="/home/ubuntu/change_cloth_server"
LOG="/home/ubuntu/log/renew_cert.log"
mkdir -p "$(dirname "$LOG")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] === 开始证书续期 ===" >> "$LOG"
# 停止服务,释放 443 端口(certbot standalone 需要 80 端口,实际上不需要停止443)
# 但 certbot --standalone 需要 80 端口空闲即可,无需停止 443 服务
# 保险起见仍记录当前状态
pkill -f "${ROOT}/change_app.py" 2>/dev/null
echo " 服务已停止" >> "$LOG"
sleep 2
# 续期(standalone 模式使用 80 端口验证)
certbot renew --standalone --non-interactive >> "$LOG" 2>&1
RENEW_EXIT=$?
if [ $RENEW_EXIT -eq 0 ]; then
echo " 证书续期成功" >> "$LOG"
else
echo " 证书续期失败,退出码: $RENEW_EXIT" >> "$LOG"
fi
# 修正证书文件权限(确保非 root 用户可读)
chmod 644 /etc/letsencrypt/archive/xiangsilian.com/privkey*.pem 2>/dev/null
chmod 755 /etc/letsencrypt/live /etc/letsencrypt/archive 2>/dev/null
# 重启服务
export PATH="/home/ubuntu/.local/bin:$PATH"
nohup python3 "${ROOT}/change_app.py" >> "/home/ubuntu/log/change_app.log" 2>&1 &
echo " 服务已重启 (PID: $!)" >> "$LOG"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] === 续期流程结束 ===" >> "$LOG"
+8
View File
@@ -0,0 +1,8 @@
fastapi
uvicorn[standard]
httpx
pydantic
flask
requests
Pillow>=9.0,<11
oss2
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# 启动虚拟换装服务(统一 FastAPI,HTTPS 443 端口)
# ComfyUI 在外部机器运行;change_app.py 直接调用 service1/2/3 逻辑,无需单独启动子服务。
ROOT="/home/ubuntu/change_cloth_server"
LOG_DIR="/home/ubuntu/log"
mkdir -p "$LOG_DIR"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 停止旧进程..."
pkill -f "${ROOT}/change_app.py" 2>/dev/null && echo " 已停止 change_app" || echo " change_app 未在运行"
# 兼容:若旧的子服务仍在运行,一并停止
pkill -f "${ROOT}/change1/service1.py" 2>/dev/null && echo " 已停止 service1(旧)" || true
pkill -f "${ROOT}/change2/service2.py" 2>/dev/null && echo " 已停止 service2(旧)" || true
pkill -f "${ROOT}/change3/service3.py" 2>/dev/null && echo " 已停止 service3(旧)" || true
sleep 2
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 启动 change_appHTTPS 443..."
export PATH="/home/ubuntu/.local/bin:$PATH"
nohup python3 "${ROOT}/change_app.py" >> "$LOG_DIR/change_app.log" 2>&1 &
echo " change_app 已启动 (PID: $!)"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 启动完毕。日志: $LOG_DIR/change_app.log"
+78 -7
View File
@@ -198,6 +198,49 @@
.raw-resp.visible { display: block; } .raw-resp.visible { display: block; }
.raw-resp pre { white-space: pre-wrap; } .raw-resp pre { white-space: pre-wrap; }
.prompt-panel {
width: 100%;
max-width: 960px;
margin-bottom: 24px;
background: #1a1a24;
border-radius: 16px;
padding: 20px;
border: 1px solid #2a2a3a;
}
.prompt-panel .prompt-title {
font-size: 0.85rem;
font-weight: 600;
color: #a78bfa;
margin-bottom: 12px;
}
.radio-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
cursor: pointer;
font-size: 0.9rem;
color: #ccc;
}
.radio-row input { margin-top: 3px; accent-color: #7c3aed; flex-shrink: 0; }
.change-desc-input {
width: 100%;
margin-top: 4px;
padding: 12px;
border-radius: 10px;
border: 1px solid #333;
background: #12121a;
color: #e0e0e0;
font-family: inherit;
font-size: 0.88rem;
resize: vertical;
min-height: 88px;
}
.change-desc-input:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 640px) { @media (max-width: 640px) {
.upload-grid { grid-template-columns: 1fr; } .upload-grid { grid-template-columns: 1fr; }
h1 { font-size: 1.5rem; } h1 { font-size: 1.5rem; }
@@ -206,7 +249,7 @@
</head> </head>
<body> <body>
<h1>AI 换装测试</h1> <h1>AI 换装测试</h1>
<p class="subtitle">上传模特和衣服图片调用 /change_cloth_base64 接口测试</p> <p class="subtitle">上传图片调用 /change_cloth_base64 接口;只传模特→单图编辑,加上衣→换装,再加裤子→换装+换裤</p>
<div class="upload-grid"> <div class="upload-grid">
<!-- 模特 --> <!-- 模特 -->
@@ -219,13 +262,14 @@
<img class="preview-img" id="preview-human" alt="模特预览" /> <img class="preview-img" id="preview-human" alt="模特预览" />
</div> </div>
<!-- 上衣 --> <!-- 上衣(可选) -->
<div class="upload-card" id="card-cloth"> <div class="upload-card optional" id="card-cloth">
<button class="btn-clear" id="clear-cloth" title="清除"></button> <button class="btn-clear" id="clear-cloth" title="清除"></button>
<input type="file" accept="image/*" id="input-cloth" /> <input type="file" accept="image/*" id="input-cloth" />
<div class="upload-icon">👕</div> <div class="upload-icon">👕</div>
<div class="upload-label">上衣图片</div> <div class="upload-label">上衣图片</div>
<div class="upload-hint">cloth_img · 必填</div> <div class="upload-hint">cloth_img · 可选</div>
<span class="badge-optional">无则走 service1,有则走 service2</span>
<img class="preview-img" id="preview-cloth" alt="上衣预览" /> <img class="preview-img" id="preview-cloth" alt="上衣预览" />
</div> </div>
@@ -236,11 +280,24 @@
<div class="upload-icon">👖</div> <div class="upload-icon">👖</div>
<div class="upload-label">裤子图片</div> <div class="upload-label">裤子图片</div>
<div class="upload-hint">kuzi_img · 可选</div> <div class="upload-hint">kuzi_img · 可选</div>
<span class="badge-optional">则走 8888 服务,无则走 8889</span> <span class="badge-optional">填了则走 service3</span>
<img class="preview-img" id="preview-kuzi" alt="裤子预览" /> <img class="preview-img" id="preview-kuzi" alt="裤子预览" />
</div> </div>
</div> </div>
<div class="prompt-panel" id="prompt-panel">
<div class="prompt-title">提示词(change_desc</div>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-default" value="default" checked />
<span>使用默认(请求中传空字符串,不覆盖工作流)</span>
</label>
<label class="radio-row">
<input type="radio" name="prompt_mode" id="prompt-custom" value="custom" />
<span>自定义提示词</span>
</label>
<textarea id="change-desc" class="change-desc-input" placeholder="输入换装/编辑描述…" rows="4" disabled></textarea>
</div>
<button class="btn-run" id="btn-run" disabled>开始换装</button> <button class="btn-run" id="btn-run" disabled>开始换装</button>
<div class="status-bar" id="status-bar"></div> <div class="status-bar" id="status-bar"></div>
@@ -295,8 +352,21 @@
setupUpload('input-cloth', 'preview-cloth', 'card-cloth', 'clear-cloth', 'cloth'); setupUpload('input-cloth', 'preview-cloth', 'card-cloth', 'clear-cloth', 'cloth');
setupUpload('input-kuzi', 'preview-kuzi', 'card-kuzi', 'clear-kuzi', 'kuzi'); setupUpload('input-kuzi', 'preview-kuzi', 'card-kuzi', 'clear-kuzi', 'kuzi');
const promptDefault = document.getElementById('prompt-default');
const promptCustom = document.getElementById('prompt-custom');
const changeDescEl = document.getElementById('change-desc');
function syncPromptUi() {
changeDescEl.disabled = !promptCustom.checked;
}
promptDefault.addEventListener('change', syncPromptUi);
promptCustom.addEventListener('change', syncPromptUi);
function getChangeDesc() {
return promptCustom.checked ? changeDescEl.value.trim() : '';
}
function updateRunButton() { function updateRunButton() {
document.getElementById('btn-run').disabled = !(images.human && images.cloth); document.getElementById('btn-run').disabled = !images.human;
} }
function setStatus(msg, cls = '') { function setStatus(msg, cls = '') {
@@ -328,8 +398,9 @@
const body = { const body = {
human_img: images.human, human_img: images.human,
cloth_img: images.cloth, change_desc: getChangeDesc(),
}; };
if (images.cloth) body.cloth_img = images.cloth;
if (images.kuzi) body.kuzi_img = images.kuzi; if (images.kuzi) body.kuzi_img = images.kuzi;
try { try {
+1
View File
@@ -0,0 +1 @@
admin