Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
975ac3de5a | ||
|
|
bbb74606c2 | ||
|
|
de828b777a | ||
|
|
862f9e57d1 | ||
|
|
60123abfdb | ||
|
|
47ff49dc19 | ||
|
|
0b9b5d21ff | ||
|
|
79d8c6d17f | ||
|
|
ecdef627d4 | ||
|
|
a044943db7 | ||
|
|
7e89edaa47 | ||
|
|
71221ce1e3 | ||
|
|
ef8bbad157 |
@@ -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.
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
@@ -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>
|
||||||
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -437,4 +437,5 @@
|
|||||||
"title": "Image Comparer (rgthree)"
|
"title": "Image Comparer (rgthree)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 [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)
|
|
||||||
静态文件目录: /root/change_cloth_server/change2/static
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fed977afcf238470ca7418061517bcd14.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730545&Signature=CAO%2B7BQg22JrmiSJ%2B%2FqiZPPaJV8%3D
|
|
||||||
INFO: 127.0.0.1:34020 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
+85
-61
@@ -11,16 +11,11 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# 添加父目录到路径以导入config
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
import config
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
@@ -31,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 换装服务")
|
||||||
|
|
||||||
@@ -85,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):
|
||||||
@@ -98,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:
|
||||||
@@ -137,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
|
||||||
|
|
||||||
@@ -155,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} 的输出")
|
||||||
@@ -210,38 +235,24 @@ 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_)
|
|
||||||
|
|
||||||
# 根据配置转换图片格式并上传到 OSS
|
# 缩放到 768x1024
|
||||||
output_format = config.OUTPUT_FORMAT.lower()
|
# img = Image.open(io.BytesIO(result_bytes))
|
||||||
file_ext = '.jpg' if output_format == 'jpg' else '.png'
|
# img = img.resize((768, 1024), Image.LANCZOS)
|
||||||
object_name = f"tryon/{uuid.uuid4().hex}{file_ext}"
|
# 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()
|
||||||
|
|
||||||
# 使用PIL处理图片
|
# 将图片保存到临时文件,上传到 OSS
|
||||||
img = Image.open(io.BytesIO(result_bytes))
|
suffix = Path(filename).suffix or ".png"
|
||||||
|
object_name = f"tryon/{uuid.uuid4().hex}{suffix}"
|
||||||
if output_format == 'jpg':
|
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
||||||
# JPG格式:转换为RGB模式
|
tmp.write(result_bytes)
|
||||||
if img.mode in ('RGBA', 'LA', 'P'):
|
tmp_path = tmp.name
|
||||||
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
|
|
||||||
if img.mode == 'P':
|
|
||||||
img = img.convert('RGBA')
|
|
||||||
rgb_img.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
|
||||||
img = rgb_img
|
|
||||||
elif img.mode != 'RGB':
|
|
||||||
img = img.convert('RGB')
|
|
||||||
|
|
||||||
# 保存为JPG
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
|
|
||||||
img.save(tmp, format='JPEG', quality=config.JPG_QUALITY)
|
|
||||||
tmp_path = tmp.name
|
|
||||||
else:
|
|
||||||
# PNG格式:保持原样
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
|
||||||
img.save(tmp, format='PNG')
|
|
||||||
tmp_path = tmp.name
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result_url = upload_to_oss(tmp_path, object_name)
|
result_url = upload_to_oss(tmp_path, object_name)
|
||||||
@@ -257,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},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 挂载静态文件(前端页面)
|
# 挂载静态文件(前端页面)
|
||||||
|
|||||||
@@ -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(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -491,4 +491,4 @@
|
|||||||
"title": "Image Comparer (rgthree)"
|
"title": "Image Comparer (rgthree)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,97 +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
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F6b801985147c47e48a6ad89b4c0cd38d.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730384&Signature=PL28YpFYPBBTSkctaxdvQu%2F%2FefQ%3D
|
|
||||||
INFO: 127.0.0.1:44510 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fba82fba2546a4db494b57afe11b7a94e.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730474&Signature=6nEr1IdPNILxYlOq5A0HhwThgB4%3D
|
|
||||||
INFO: 127.0.0.1:58438 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fdef87868715448de972942060caeb7ff.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730502&Signature=lSklN83c3u5nFYmvUIrWkho0JmA%3D
|
|
||||||
INFO: 127.0.0.1:40984 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F48012034e9ec4fbcbc301bde9ea2833c.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730617&Signature=fcu%2BZDgr033DQojMCQj6h4gnds4%3D
|
|
||||||
INFO: 127.0.0.1:60844 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F081b8dfef8354b1c99e4e4ee11331292.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730641&Signature=gfdUA13vEyxRLdS%2BuxL%2FIKIntbU%3D
|
|
||||||
INFO: 127.0.0.1:45998 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Ff60f41ea7cf6433ca231ff20f5b30e23.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730672&Signature=GU0ILZWnxUaNDJQ3iWdlqTdQJfk%3D
|
|
||||||
INFO: 127.0.0.1:41238 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F9f09e890bc2e4575aca8d393c9abba7a.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730696&Signature=PXS1wcx709%2FTZ%2Bxivv665u3UqFs%3D
|
|
||||||
INFO: 127.0.0.1:55808 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F9df0b0ad8722441fa935ff7fe8fb1e75.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730727&Signature=E%2BFspFCXNNWPC4bJ7AwG99sQCVA%3D
|
|
||||||
INFO: 127.0.0.1:55810 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fec3e4af4c75945109f42047f87951c89.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730758&Signature=XaCCU2X2RlBHl9PYqcJ2rXEIZpI%3D
|
|
||||||
INFO: 127.0.0.1:55908 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F56324fe2dd0a40a3bf54150c147c3aea.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730789&Signature=e2VXdJK9m6cDqW18RkLTVEfOtV8%3D
|
|
||||||
INFO: 127.0.0.1:40940 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F099d57b365444a4e8ac36052d99d387d.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730819&Signature=FgIniNm8joNa0TK1WsEa7D2u%2BNU%3D
|
|
||||||
INFO: 127.0.0.1:49448 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F7c8cfa4ac1214d84843589e8ea255bcf.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730850&Signature=2goSdjNp7haPHXS%2BJ%2BAlLaFCR4Y%3D
|
|
||||||
INFO: 127.0.0.1:51658 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F64264e7412c0456b845ae8b7d6be49c7.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730881&Signature=JEjE%2FqkQgQkdjy4yU%2BUG%2F6TLHAM%3D
|
|
||||||
INFO: 127.0.0.1:52690 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fbffee82977644843af3f8934a4dfc6f6.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730912&Signature=M8yFTC4HzUdialZSniwqJhxVDX8%3D
|
|
||||||
INFO: 127.0.0.1:52570 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F175eceeea0294f23883ced29955d8c24.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730943&Signature=G8KPeNFe0cc02jW5LUsainkwY4s%3D
|
|
||||||
INFO: 127.0.0.1:32838 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fceb407de7f7c48ccb35bf220aa3b974d.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088730973&Signature=%2FQZVQVZ%2BJcvYkIf3ikTOIL6jmNY%3D
|
|
||||||
INFO: 127.0.0.1:59962 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F26ea45ae412842a6ab681d836625c4da.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731004&Signature=rvipO3wVQ3202yRWOeiOJNY28Dg%3D
|
|
||||||
INFO: 127.0.0.1:40790 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F8fb321a9a86b49c3a2386f04b6874e6f.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731030&Signature=1TS3LGwQa3vZ8YHQBMl7wu62vTc%3D
|
|
||||||
INFO: 127.0.0.1:35550 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F61e5865a46d649409ca6e34111aeb644.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731059&Signature=lbstfeJxP8zjiSiMY6gQBz4zajw%3D
|
|
||||||
INFO: 127.0.0.1:39236 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F5fda63d3db4b4d368cdf749ff6f53f09.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731090&Signature=6fFxh9tL2ks8jaDVgeT3rlZzbBQ%3D
|
|
||||||
INFO: 127.0.0.1:40194 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Ff4ab353137fc40f5874674df347a1c49.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731120&Signature=W8RQMPZgm8c4R4es8MPKkAulgxM%3D
|
|
||||||
INFO: 127.0.0.1:38234 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F2ec4c18d401245fc980645fd4847080b.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731151&Signature=OlnjmSM4nIKux7tApMTKkK%2FSbFY%3D
|
|
||||||
INFO: 127.0.0.1:43854 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F21c7139693994b3f83233dccb138dd6d.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731184&Signature=KQgU2K1IcmInb7XVnYO335q9IVk%3D
|
|
||||||
INFO: 127.0.0.1:56958 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F01b2301697624f1189aa86faca8a77f7.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731217&Signature=zuwm%2Bp2NGTJc4jC0k4qZyT583S0%3D
|
|
||||||
INFO: 127.0.0.1:45370 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Ffe17b4da3615421a90e4221e8eea46b4.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731248&Signature=9coHZI%2F3IEJCoERdMOq5CVTatNw%3D
|
|
||||||
INFO: 127.0.0.1:43270 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2Fbd9cc49fb54d4c51a910b63eedea1502.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731279&Signature=aACepaPGN0zeXDigrzRUN%2BCItlQ%3D
|
|
||||||
INFO: 127.0.0.1:33848 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F4814bf67eeac46dbbfc2b4b79c0f2adb.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731369&Signature=XksUlhVY7ii8jevDLvg9n8qNXhg%3D
|
|
||||||
INFO: 127.0.0.1:42736 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F0ea64d04225941e583493149e2e662ad.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731580&Signature=xTTQsyRZXx5kddb1C4j%2FEDAVyko%3D
|
|
||||||
INFO: 127.0.0.1:60680 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
接收到换装请求,正在处理...
|
|
||||||
文件上传成功,URL: http://xiangsilian.oss-cn-beijing.aliyuncs.com/tryon%2F08deeffce6c74498952b8202d2763340.png?OSSAccessKeyId=LTAI5tGp1sLzedqxihcNC1eb&Expires=2088731705&Signature=EYVRhvk4zecJIwxqTybdATVpKLI%3D
|
|
||||||
INFO: 127.0.0.1:46922 - "POST /try-on HTTP/1.1" 200 OK
|
|
||||||
+87
-61
@@ -11,16 +11,11 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# 添加父目录到路径以导入config
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
import config
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import oss2
|
import oss2
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -30,15 +25,24 @@ 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 换装服务")
|
||||||
|
|
||||||
@@ -76,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):
|
||||||
@@ -89,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:
|
||||||
@@ -128,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
|
||||||
|
|
||||||
@@ -146,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)
|
||||||
@@ -158,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} 的输出")
|
||||||
@@ -203,38 +230,24 @@ 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_)
|
|
||||||
|
|
||||||
# 根据配置转换图片格式并上传到 OSS
|
# 缩放到 768x1024
|
||||||
output_format = config.OUTPUT_FORMAT.lower()
|
# img = Image.open(io.BytesIO(result_bytes))
|
||||||
file_ext = '.jpg' if output_format == 'jpg' else '.png'
|
# img = img.resize((768, 1024), Image.LANCZOS)
|
||||||
object_name = f"tryon/{uuid.uuid4().hex}{file_ext}"
|
# 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()
|
||||||
|
|
||||||
# 使用PIL处理图片
|
# 将图片保存到临时文件,上传到 OSS
|
||||||
img = Image.open(io.BytesIO(result_bytes))
|
suffix = Path(filename).suffix or ".png"
|
||||||
|
object_name = f"tryon/{uuid.uuid4().hex}{suffix}"
|
||||||
if output_format == 'jpg':
|
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
||||||
# JPG格式:转换为RGB模式
|
tmp.write(result_bytes)
|
||||||
if img.mode in ('RGBA', 'LA', 'P'):
|
tmp_path = tmp.name
|
||||||
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
|
|
||||||
if img.mode == 'P':
|
|
||||||
img = img.convert('RGBA')
|
|
||||||
rgb_img.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
|
||||||
img = rgb_img
|
|
||||||
elif img.mode != 'RGB':
|
|
||||||
img = img.convert('RGB')
|
|
||||||
|
|
||||||
# 保存为JPG
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
|
|
||||||
img.save(tmp, format='JPEG', quality=config.JPG_QUALITY)
|
|
||||||
tmp_path = tmp.name
|
|
||||||
else:
|
|
||||||
# PNG格式:保持原样
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
|
|
||||||
img.save(tmp, format='PNG')
|
|
||||||
tmp_path = tmp.name
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result_url = upload_to_oss(tmp_path, object_name)
|
result_url = upload_to_oss(tmp_path, object_name)
|
||||||
@@ -250,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},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 挂载静态文件(前端页面)
|
# 挂载静态文件(前端页面)
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
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:
|
try:
|
||||||
if kuzi_img:
|
result_url = upload_to_oss(tmp_path, object_name)
|
||||||
payload = {
|
finally:
|
||||||
'model_image': human_img,
|
os.unlink(tmp_path)
|
||||||
'shirt_image': cloth_img,
|
|
||||||
'pants_image': kuzi_img,
|
|
||||||
}
|
|
||||||
response = requests.post('http://127.0.0.1:12223/try-on', json=payload, timeout=360)
|
|
||||||
else:
|
|
||||||
payload = {
|
|
||||||
'model_image': human_img,
|
|
||||||
'shirt_image': cloth_img,
|
|
||||||
}
|
|
||||||
response = requests.post('http://127.0.0.1:12222/try-on', json=payload, timeout=360)
|
|
||||||
|
|
||||||
response.raise_for_status()
|
if not result_url:
|
||||||
result = response.json()
|
raise HTTPException(status_code=500, detail="上传结果图片到 OSS 失败")
|
||||||
result_image = result.get('result_url')
|
|
||||||
|
|
||||||
if not result_image:
|
return result_url
|
||||||
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.run(host="0.0.0.0", port=28888, debug=False)
|
# 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:
|
||||||
|
result_url = await run_workflow(
|
||||||
|
WORKFLOW1,
|
||||||
|
{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},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory=str(_root / "static")), name="static")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 启动入口
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
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")
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
"""
|
|
||||||
换装服务配置文件
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 输出图片格式:'jpg' 或 'png'
|
|
||||||
OUTPUT_FORMAT = 'jpg'
|
|
||||||
|
|
||||||
# JPG压缩质量 (1-100),仅在OUTPUT_FORMAT='jpg'时生效
|
|
||||||
JPG_QUALITY = 85
|
|
||||||
@@ -1,43 +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
|
|
||||||
[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on all addresses (0.0.0.0)
|
|
||||||
* Running on http://127.0.0.1:28888
|
|
||||||
* Running on http://10.60.190.115:28888
|
|
||||||
[33mPress CTRL+C to quit[0m
|
|
||||||
1.202.76.38 - - [13/Mar/2026 02:51:46] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:53:04] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:53:59] "GET /static/index.html HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:53:59] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:54:34] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:55:02] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:55:45] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:56:57] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:57:21] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:57:52] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:58:16] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
180.163.30.27 - - [13/Mar/2026 02:58:21] "GET / HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:58:47] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:59:18] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 02:59:49] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:00:19] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:00:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:01:21] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:01:52] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:02:23] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:02:53] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:03:24] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:03:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:04:19] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:04:50] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:05:20] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:05:51] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:06:24] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:06:57] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:07:28] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:07:59] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:09:29] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:13:00] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
113.65.31.176 - - [13/Mar/2026 03:15:05] "POST /change_cloth_base64 HTTP/1.1" 200 -
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
your888password
|
||||||
+497
@@ -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 -
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
10266
|
||||||
+118
@@ -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
@@ -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"
|
||||||
@@ -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
@@ -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
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Let's Encrypt 证书续期脚本
|
||||||
|
# 流程:停服 → certbot renew(standalone)→ 修正证书权限 → 重启服务
|
||||||
|
# 由 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"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
httpx
|
||||||
|
pydantic
|
||||||
|
flask
|
||||||
|
requests
|
||||||
|
Pillow>=9.0,<11
|
||||||
|
oss2
|
||||||
Executable
+27
@@ -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_app(HTTPS 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"
|
||||||
+79
-8
@@ -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,9 +398,10 @@
|
|||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
human_img: images.human,
|
human_img: images.human,
|
||||||
cloth_img: images.cloth,
|
change_desc: getChangeDesc(),
|
||||||
};
|
};
|
||||||
if (images.kuzi) body.kuzi_img = images.kuzi;
|
if (images.cloth) body.cloth_img = images.cloth;
|
||||||
|
if (images.kuzi) body.kuzi_img = images.kuzi;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/change_cloth_base64', {
|
const resp = await fetch('/change_cloth_base64', {
|
||||||
|
|||||||
Reference in New Issue
Block a user