diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ff43049 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Running the Services + +Each service runs independently and must be started separately: + +```bash +# Shirt-only try-on service (port 8888) +python change2/service2.py + +# Shirt + pants try-on service (port 8889) +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. + +## Dependencies + +```bash +pip install fastapi uvicorn httpx pydantic flask requests +``` + +## Architecture + +This is a three-tier AI virtual try-on system built around ComfyUI. + +``` +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) +``` + +**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`. + +**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`. + +**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`. + +### ComfyUI Workflow Integration + +Both services dynamically modify a JSON workflow template before submission: + +| Node ID | Purpose | service2 | service3 | +|---------|---------|----------|----------| +| `"11"` | Model/person image | ✓ | ✓ | +| `"10"` | Shirt image | ✓ | ✓ | +| `"4"` | Pants image | — | ✓ | +| `"33"` | Output image | ✓ | ✓ | + +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 + +### 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. diff --git a/change2/service2.py b/change2/service2.py index 31aba2c..1967534 100644 --- a/change2/service2.py +++ b/change2/service2.py @@ -198,4 +198,4 @@ async def index(): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=47698, log_level="info") + uvicorn.run(app, host="0.0.0.0", port=12222, log_level="info") diff --git a/change3/service3.py b/change3/service3.py index 1fc6c24..5c695e5 100644 --- a/change3/service3.py +++ b/change3/service3.py @@ -200,4 +200,4 @@ async def index(): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=47698, log_level="info") + uvicorn.run(app, host="0.0.0.0", port=12223, log_level="info") diff --git a/change_app.py b/change_app.py index ce8a97f..cf037a4 100644 --- a/change_app.py +++ b/change_app.py @@ -1,77 +1,71 @@ +import requests +from flask import Flask, jsonify, request, send_from_directory + +app = Flask(__name__, static_folder='static') + + +@app.route('/') +def index(): + return send_from_directory(app.static_folder, 'index.html') + + +@app.route('/static/') +def static_files(filename): + return send_from_directory(app.static_folder, filename) + @app.route('/change_cloth_base64', methods=['POST']) def change_cloth_base64(): - log_message("change_cloth_base64 called") - # 获取参数 data = request.get_json() - print(f"change_cloth_base64 input data:{data}") + 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 + return jsonify({"ret": -1, "state": -1, "msg": "No JSON data provided"}), 400 human_img = data.get('human_img') cloth_img = data.get('cloth_img') - output_format = data.get('output_format') - if not output_format: - return jsonify({"state":-1, "error": "Missing 'output_format' parameter"}), 500 - + if not human_img or not cloth_img: - return jsonify({"ret":-1, 'msg': 'Both human_img and cloth_img are required'}), 400 - - human_filename = save_base64_image(human_img, 'human') - if not human_filename: - return jsonify({"ret":-1, 'msg': 'Failed to save human image'}), 500 - data['human_img'] = None - - human_url = f"http://117.50.44.174:{base64_test_port}/static/imgs/{human_filename}" - - # 保存服装图片 - cloth_filename = save_base64_image(cloth_img, 'cloth') - if not cloth_filename: - return jsonify({"ret":-1, 'msg': 'Failed to save cloth image'}), 500 - data['cloth_img'] = None - - cloth_url = f"http://117.50.44.174:{base64_test_port}/static/imgs/{cloth_filename}" - - data["human_url"] = human_url - data["cloth_url"] = cloth_url + return jsonify({"ret": -1, "state": -1, "msg": "Both human_img and cloth_img are required"}), 400 kuzi_img = data.get('kuzi_img') - if kuzi_img: - kuzi_filename = save_base64_image(kuzi_img, 'kuzi') - # data['kuzi_img'] = None - kuzi_url = f"http://117.50.44.174:{base64_test_port}/static/imgs/{kuzi_filename}" - data['kuzi_url'] = kuzi_url - - no2 = data.get('no2') - if not no2: - data['no2'] = False - tuodi = data.get('tuodi') - if not tuodi: - data['tuodi'] = False - - - if not data.get('suit'): - data['suit'] = False - - if data.get('cloth_len'): - print(f'客户端传入了衣服长度: {data['cloth_len']}') - else: - print('客户端传入了衣服长度,需要ai 判断') - - - # 在内部调用第二个HTTP请求 try: - # 调用第二个API(可以是外部服务或自己的另一个端点) - response = requests.post(f'http://127.0.0.1:{base64_test_port}/do_change_cloth', json=data) - - return Response( - response=response.content, - status=response.status_code, - headers=dict(response.headers) - ) - except requests.exceptions.RequestException as e: - data['second_api_error'] = str(e) - - return jsonify("error"), 500 \ No newline at end of file + if kuzi_img: + payload = { + 'model_image': human_img, + '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() + result = response.json() + result_image = result.get('result_image') + + if not result_image: + return jsonify({"ret": -1, "state": -1, "msg": "Backend returned no image"}), 500 + + except requests.exceptions.Timeout: + return jsonify({"ret": -1, "state": -1, "msg": "Backend service timeout"}), 504 + except requests.exceptions.ConnectionError: + return jsonify({"ret": -1, "state": -1, "msg": "Cannot connect to backend service"}), 502 + except requests.exceptions.HTTPError as e: + return jsonify({"ret": -1, "state": -1, "msg": f"Backend error: {e}"}), 502 + + return jsonify({ + "ret": 0, + "state": 0, + "msg": "success", + "data": result_image, + }) + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=28888, debug=False) diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..1cfa4e4 --- /dev/null +++ b/static/index.html @@ -0,0 +1,377 @@ + + + + + + AI 换装测试 + + + +

AI 换装测试

+

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

+ +
+ +
+ + +
🧍
+
模特图片
+
human_img · 必填
+ 模特预览 +
+ + +
+ + +
👕
+
上衣图片
+
cloth_img · 必填
+ 上衣预览 +
+ + +
+ + +
👖
+
裤子图片
+
kuzi_img · 可选
+ 有则走 8888 服务,无则走 8889 + 裤子预览 +
+
+ + +
+ +
+
换装结果
+ 换装结果 + +
+ +
+
接口原始响应(data 字段已截断)
+

+  
+ + + +