From 47ff49dc192b633c77133ad303309ea1afdd02e9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 00:12:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8D=95=E5=BC=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=8F=98=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 70 ++++--- change1/change1.json | 400 ++++++++++++++++++++++++++++++++++++++ change1/service1.py | 261 +++++++++++++++++++++++++ change1/static/index.html | 390 +++++++++++++++++++++++++++++++++++++ change_app.py | 12 +- start_services.sh | 8 +- static/index.html | 17 +- 7 files changed, 1119 insertions(+), 39 deletions(-) create mode 100644 change1/change1.json create mode 100644 change1/service1.py create mode 100644 change1/static/index.html diff --git a/CLAUDE.md b/CLAUDE.md index ff43049..b753047 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: ```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 -# Shirt + pants try-on service (port 8889) +# Shirt + pants try-on service (port 12223) python change3/service3.py # Gateway router (port 28888) — requires Flask 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 ```bash -pip install fastapi uvicorn httpx pydantic flask requests +pip install fastapi uvicorn httpx pydantic flask requests oss2 pillow ``` ## 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 - └─► change_app.py (Flask, port 28888) ← Gateway router - ├─► change2/service2.py (FastAPI, port 8888) ← shirt-only - └─► change3/service3.py (FastAPI, port 8889) ← shirt + pants - └─► ComfyUI (port 8188) + └─► change_app.py (Flask, port 28888) ← Gateway router + ├─► change1/service1.py (FastAPI, 12221) ← single image (model only) + ├─► change2/service2.py (FastAPI, 12222) ← model + shirt + └─► 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": "" }`. The gateway wraps that into `{ "ret": 0, "state": 0, "msg": "success", "data": }`. -**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 -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 | -|---------|---------|----------|----------| -| `"11"` | Model/person image | ✓ | ✓ | -| `"10"` | Shirt image | ✓ | ✓ | -| `"4"` | Pants image | — | ✓ | -| `"33"` | Output image | ✓ | ✓ | +| Node ID | Purpose | service1 | service2 | service3 | +|---------|---------|----------|----------|----------| +| `"11"` | Model/person image (LoadImage) | ✓ | ✓ | ✓ | +| `"10"` | Shirt image (LoadImage) | — | ✓ | ✓ | +| `"4"` | Pants image (LoadImage) | — | — | ✓ | +| `"31"` | Text prompt (PrimitiveStringMultiline, overridden when `change_desc` is non-empty) | ✓ | ✓ | ✓ | +| `"33"` | Output image (SaveImage) | ✓ | ✓ | ✓ | The request flow within each service: -1. Decode base64 images -2. Upload images to ComfyUI (`/upload/image`) with UUID-prefixed filenames to avoid cache collisions -3. Inject filenames into workflow node inputs -4. Submit workflow to ComfyUI queue (`/prompt`) -5. Poll `/history/{prompt_id}` until `status.completed` is true (max 300s timeout, 2s poll interval) -6. Download result from `/view` and return as base64 +1. Pick a healthy ComfyUI host (primary, fallback to backup) +2. Decode base64 images +3. Upload images to ComfyUI (`/upload/image`) with UUID-prefixed filenames to avoid cache collisions +4. Inject filenames into workflow node inputs; if `change_desc` is non-empty, replace node `31` inputs with `{"value": change_desc}` +5. Submit workflow to ComfyUI queue (`/prompt`) +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 -Each service has its own test UI at `/` (served from `/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 `/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. diff --git a/change1/change1.json b/change1/change1.json new file mode 100644 index 0000000..3c55ca3 --- /dev/null +++ b/change1/change1.json @@ -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)" + } + } +} \ No newline at end of file diff --git a/change1/service1.py b/change1/service1.py new file mode 100644 index 0000000..236824d --- /dev/null +++ b/change1/service1.py @@ -0,0 +1,261 @@ +#!/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" + +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 服务器是否可用""" + try: + async with httpx.AsyncClient(timeout=timeout) as client: + resp = await client.get(f"{base_url}/system_stats") + return resp.status_code == 200 + 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) 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) 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) 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") diff --git a/change1/static/index.html b/change1/static/index.html new file mode 100644 index 0000000..c527e14 --- /dev/null +++ b/change1/static/index.html @@ -0,0 +1,390 @@ + + + + + + AI 换装系统 + + + +

AI 换装系统

+

上传一张图片,AI 自动按提示词进行编辑

+ +
+ +
+ +
🧍
+
模特图片
+
节点 11 · 人物全身照
+ 模特预览 +
+
+ +
+
提示词(change_desc)
+ + + +
+ + +
+ +
+
生成结果
+ 生成结果 + +
+ + + + diff --git a/change_app.py b/change_app.py index 5e2033f..52ea3b2 100644 --- a/change_app.py +++ b/change_app.py @@ -26,8 +26,8 @@ def change_cloth_base64(): human_img = data.get('human_img') cloth_img = data.get('cloth_img') - if not human_img or not cloth_img: - return jsonify({"ret": -1, "state": -1, "msg": "Both human_img and cloth_img are required"}), 400 + if not human_img: + return jsonify({"ret": -1, "state": -1, "msg": "human_img is required"}), 400 kuzi_img = data.get('kuzi_img') @@ -46,13 +46,19 @@ def change_cloth_base64(): 'change_desc': change_desc, } response = requests.post('http://127.0.0.1:12223/try-on', json=payload, timeout=360) - else: + elif cloth_img: payload = { 'model_image': human_img, 'shirt_image': cloth_img, 'change_desc': change_desc, } response = requests.post('http://127.0.0.1:12222/try-on', json=payload, timeout=360) + else: + payload = { + 'model_image': human_img, + 'change_desc': change_desc, + } + response = requests.post('http://127.0.0.1:12221/try-on', json=payload, timeout=360) response.raise_for_status() result = response.json() diff --git a/start_services.sh b/start_services.sh index a7f2872..6cfa17b 100755 --- a/start_services.sh +++ b/start_services.sh @@ -1,6 +1,6 @@ #!/bin/bash -# 启动本机换装网关与 service2 / service3。 -# ComfyUI 在其它机器运行;service2/service3 内已配置远端 COMFYUI_URL。 +# 启动本机换装网关与 service1 / service2 / service3。 +# ComfyUI 在其它机器运行;service1/service2/service3 内已配置远端 COMFYUI_URL。 ROOT="/home/ubuntu/change_cloth_server" LOG_DIR="/home/ubuntu/log" @@ -9,6 +9,7 @@ mkdir -p "$LOG_DIR" echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始停止旧进程..." # 按命令行中的脚本绝对路径结束旧实例(与下方 nohup 路径一致) +pkill -f "${ROOT}/change1/service1.py" 2>/dev/null && echo " 已停止 service1" || echo " service1 未在运行" pkill -f "${ROOT}/change2/service2.py" 2>/dev/null && echo " 已停止 service2" || echo " service2 未在运行" pkill -f "${ROOT}/change3/service3.py" 2>/dev/null && echo " 已停止 service3" || echo " service3 未在运行" pkill -f "${ROOT}/change_app.py" 2>/dev/null && echo " 已停止 change_app" || echo " change_app 未在运行" @@ -19,6 +20,9 @@ echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始启动服务..." export PATH="/home/ubuntu/.local/bin:$PATH" +nohup python3 "${ROOT}/change1/service1.py" >> "$LOG_DIR/service1.log" 2>&1 & +echo " service1 已启动 (PID: $!)" + nohup python3 "${ROOT}/change2/service2.py" >> "$LOG_DIR/service2.log" 2>&1 & echo " service2 已启动 (PID: $!)" diff --git a/static/index.html b/static/index.html index 9d1216e..b55d537 100644 --- a/static/index.html +++ b/static/index.html @@ -249,7 +249,7 @@

AI 换装测试

-

上传模特和衣服图片,调用 /change_cloth_base64 接口测试

+

上传图片调用 /change_cloth_base64 接口;只传模特→单图编辑,加上衣→换装,再加裤子→换装+换裤

@@ -262,13 +262,14 @@ 模特预览
- -
+ +
👕
上衣图片
-
cloth_img · 必填
+
cloth_img · 可选
+ 无则走 service1,有则走 service2 上衣预览
@@ -279,7 +280,7 @@
👖
裤子图片
kuzi_img · 可选
- 有则走 8888 服务,无则走 8889 + 填了则走 service3 裤子预览
@@ -365,7 +366,7 @@ } function updateRunButton() { - document.getElementById('btn-run').disabled = !(images.human && images.cloth); + document.getElementById('btn-run').disabled = !images.human; } function setStatus(msg, cls = '') { @@ -397,10 +398,10 @@ const body = { 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 { const resp = await fetch('/change_cloth_base64', {