save code
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a FastAPI-based virtual try-on service that integrates with ComfyUI for AI-powered clothing try-on. The service accepts base64-encoded images (model photo, shirt, pants) and returns a generated try-on result.
|
||||||
|
|
||||||
|
## Running the Service
|
||||||
|
|
||||||
|
Start the server:
|
||||||
|
```bash
|
||||||
|
python service2.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The server runs on `http://0.0.0.0:47698` by default.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Required Python packages (install via pip):
|
||||||
|
- fastapi
|
||||||
|
- uvicorn
|
||||||
|
- httpx
|
||||||
|
- pydantic
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Request Flow
|
||||||
|
1. Client sends POST to `/try-on` with base64 images (model_image, shirt_image, pants_image)
|
||||||
|
2. Server decodes images and uploads to ComfyUI at `http://127.0.0.1:8188`
|
||||||
|
3. Server modifies workflow JSON (change2_1_ex.json) with uploaded image filenames
|
||||||
|
4. Workflow submitted to ComfyUI queue
|
||||||
|
5. Server polls ComfyUI history endpoint until completion (max 300s timeout)
|
||||||
|
6. Result image downloaded from ComfyUI and returned as base64
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
**service2.py**: Main FastAPI application
|
||||||
|
- `/try-on`: Main endpoint for virtual try-on
|
||||||
|
- `/health`: Health check endpoint
|
||||||
|
- `/`: Serves static frontend (index.html)
|
||||||
|
|
||||||
|
**change2_1_ex.json**: ComfyUI workflow configuration
|
||||||
|
- Node "11": Model image input (LoadImage)
|
||||||
|
- Node "10": Shirt image input (LoadImage)
|
||||||
|
- Node "33": Output image (SaveImage)
|
||||||
|
- Node "31": Text prompt for image editing
|
||||||
|
|
||||||
|
**static/index.html**: Frontend interface for testing
|
||||||
|
|
||||||
|
### ComfyUI Integration
|
||||||
|
|
||||||
|
The service expects ComfyUI to be running at `http://127.0.0.1:8188` with:
|
||||||
|
- FireRed-Image-Edit model loaded
|
||||||
|
- Qwen 2.5 VL CLIP model
|
||||||
|
- Required custom nodes for QwenEditConfigPreparer, QwenEditOutputExtractor
|
||||||
|
|
||||||
|
### Workflow Modification
|
||||||
|
|
||||||
|
When processing requests, the code dynamically updates these workflow nodes:
|
||||||
|
- `workflow["11"]["inputs"]["image"]`: Model photo filename
|
||||||
|
- `workflow["10"]["inputs"]["image"]`: Shirt photo filename
|
||||||
|
- `workflow["31"]["inputs"]["value"]`: Text prompt (currently hardcoded in Chinese)
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Images are uploaded with unique filenames (UUID prefix) to avoid caching issues
|
||||||
|
- The pants_image parameter is currently unused (commented out in code)
|
||||||
|
- Workflow uses 8 sampling steps with euler sampler and beta scheduler
|
||||||
|
- Output resolution is 768x1024 pixels
|
||||||
|
- CORS is enabled for all origins
|
||||||
+3
-9
@@ -44,7 +44,6 @@ app.add_middleware(
|
|||||||
class TryOnRequest(BaseModel):
|
class TryOnRequest(BaseModel):
|
||||||
model_image: str # base64 模特图片
|
model_image: str # base64 模特图片
|
||||||
shirt_image: str # base64 上衣图片
|
shirt_image: str # base64 上衣图片
|
||||||
pants_image: str # base64 裤子图片
|
|
||||||
|
|
||||||
|
|
||||||
class TryOnResponse(BaseModel):
|
class TryOnResponse(BaseModel):
|
||||||
@@ -113,7 +112,6 @@ async def try_on(req: TryOnRequest):
|
|||||||
换装接口
|
换装接口
|
||||||
- model_image: 模特图片 base64
|
- model_image: 模特图片 base64
|
||||||
- shirt_image: 上衣图片 base64
|
- shirt_image: 上衣图片 base64
|
||||||
- pants_image: 裤子图片 base64
|
|
||||||
返回 result_image: 换装结果 base64
|
返回 result_image: 换装结果 base64
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -124,11 +122,10 @@ async def try_on(req: TryOnRequest):
|
|||||||
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) 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)
|
||||||
# pants_bytes = decode_base64_image(req.pants_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}")
|
||||||
|
|
||||||
@@ -136,14 +133,12 @@ async def try_on(req: TryOnRequest):
|
|||||||
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"
|
|
||||||
|
|
||||||
# 并发上传三张图片
|
# 并发上传两张图片
|
||||||
try:
|
try:
|
||||||
model_name, shirt_name, pants_name = await asyncio.gather(
|
model_name, shirt_name = await asyncio.gather(
|
||||||
upload_image(client, model_bytes, model_fname),
|
upload_image(client, model_bytes, model_fname),
|
||||||
upload_image(client, shirt_bytes, shirt_fname),
|
upload_image(client, shirt_bytes, shirt_fname),
|
||||||
# upload_image(client, 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}")
|
||||||
@@ -151,7 +146,6 @@ async def try_on(req: TryOnRequest):
|
|||||||
# 修改工作流节点的图片输入
|
# 修改工作流节点的图片输入
|
||||||
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
|
|
||||||
|
|
||||||
# 提交工作流
|
# 提交工作流
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
.upload-grid {
|
.upload-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>AI 换装系统</h1>
|
<h1>AI 换装系统</h1>
|
||||||
<p class="subtitle">上传模特、上衣、裤子三张图片,AI 自动完成换装</p>
|
<p class="subtitle">上传模特和上衣两张图片,AI 自动完成换装</p>
|
||||||
|
|
||||||
<div class="upload-grid">
|
<div class="upload-grid">
|
||||||
<!-- 模特 -->
|
<!-- 模特 -->
|
||||||
@@ -224,15 +224,6 @@
|
|||||||
<div class="upload-hint">节点 10 · 上身服装</div>
|
<div class="upload-hint">节点 10 · 上身服装</div>
|
||||||
<img class="preview-img" id="preview-shirt" alt="上衣预览" />
|
<img class="preview-img" id="preview-shirt" alt="上衣预览" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 裤子 -->
|
|
||||||
<div class="upload-card" id="card-pants">
|
|
||||||
<input type="file" accept="image/*" id="input-pants" />
|
|
||||||
<div class="upload-icon">👖</div>
|
|
||||||
<div class="upload-label">裤子图片</div>
|
|
||||||
<div class="upload-hint">节点 4 · 下身服装</div>
|
|
||||||
<img class="preview-img" id="preview-pants" alt="裤子预览" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn-run" id="btn-run" disabled>开始换装</button>
|
<button class="btn-run" id="btn-run" disabled>开始换装</button>
|
||||||
@@ -245,7 +236,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const images = { model: null, shirt: null, pants: null };
|
const images = { model: null, shirt: null };
|
||||||
|
|
||||||
function setupUpload(inputId, previewId, cardId, key) {
|
function setupUpload(inputId, previewId, cardId, key) {
|
||||||
const input = document.getElementById(inputId);
|
const input = document.getElementById(inputId);
|
||||||
@@ -269,11 +260,10 @@
|
|||||||
|
|
||||||
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');
|
||||||
setupUpload('input-pants', 'preview-pants', 'card-pants', 'pants');
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(msg, cls = '') {
|
function setStatus(msg, cls = '') {
|
||||||
@@ -308,7 +298,6 @@
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model_image: images.model,
|
model_image: images.model,
|
||||||
shirt_image: images.shirt,
|
shirt_image: images.shirt,
|
||||||
// pants_image: images.pants,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user